diff --git a/migrations/Init1697574747_down.sql b/migrations/Init1697574747_down.sql
index 0441b6b2d5c501794e4b3f545c910f90ff6f7c79..51616bf012cd71027a9d2fdef0d864a254f61450 100644
--- a/migrations/Init1697574747_down.sql
+++ b/migrations/Init1697574747_down.sql
@@ -1,7 +1,7 @@
-DROP TABLE IF EXISTS users;
-
 DROP TABLE IF EXISTS sessions;
 
 DROP TABLE IF EXISTS templates;
 
 DROP TABLE IF EXISTS template_sets;
+
+DROP TABLE IF EXISTS users;
\ No newline at end of file
diff --git a/migrations/Init1697574747_up.sql b/migrations/Init1697574747_up.sql
index 4c8df60d3f91917eb3849f4b4e7c2025cbb23311..91d6902d7458b9c587dd5bb02e42d88ffee7bdf2 100644
--- a/migrations/Init1697574747_up.sql
+++ b/migrations/Init1697574747_up.sql
@@ -37,7 +37,7 @@ CREATE TABLE templates
     type           VARCHAR(255) NOT NULL,
     name           VARCHAR(255) NOT NULL,
     version        VARCHAR(255) NOT NULL,
-    json           JSONB        NOT NULL,
+    config         JSONB        NOT NULL,
     created_by     UUID         NOT NULL REFERENCES users (id) ON DELETE CASCADE,
     created_at     TIMESTAMPTZ  NOT NULL DEFAULT current_timestamp,
     updated_at     TIMESTAMPTZ
diff --git a/public/assets/icons/refresh.svg b/public/assets/icons/refresh.svg
new file mode 100644
index 0000000000000000000000000000000000000000..b072eb097abd3c493a1371d82a6cca349ef63d41
--- /dev/null
+++ b/public/assets/icons/refresh.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
+  <path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
+  <path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
+</svg>
\ No newline at end of file
diff --git a/src/app/eiffel/parser.go b/src/app/eiffel/parser.go
index 1710137d486f777d95a417fcd46553a1a0be509b..0951d2c2cc66f0a02b576147111a3c7e68026717 100644
--- a/src/app/eiffel/parser.go
+++ b/src/app/eiffel/parser.go
@@ -8,7 +8,17 @@ import (
 
 const BasicTemplateName = "ebt"
 
-var ErrInvalidTemplate = errors.New("eiffel.parser.error.invalid-template")
+var (
+	ErrInvalidTemplate = errors.New("eiffel.parser.error.invalid-template")
+)
+
+type ParsingResult struct {
+	Template string
+	Variant  string
+	Errors   []error
+	Warnings []error
+	Notices  []error
+}
 
 type BasicParser struct {
 	Template      *BasicTemplate `hvalidate:"required,ruleReferences"`
@@ -58,6 +68,14 @@ type PreprocessorMissingError struct {
 
 type BasicPreprocessor func(string) (string, error)
 
+func (p *BasicParser) Parse(input string, variant string) (string, error) {
+	/*v, ok := p.Template.Variants[variant]
+	if !ok {
+		return "", errors.New("eiffel.parser.error.variant-not-found")
+	}*/
+	return "", nil
+}
+
 func (p *BasicParser) Validate(v validation.V) []error {
 	v.AddFunc("ruleReferences", RuleReferencesValidator)
 
diff --git a/src/app/template/template.go b/src/app/template/template.go
index 95a023708f750346499109583d295543dae9f378..6d1c582429474c967801f5dfcc67bb6d5e82e3e0 100644
--- a/src/app/template/template.go
+++ b/src/app/template/template.go
@@ -17,11 +17,11 @@ const (
 	SetRepositoryName = "SetRepository"
 )
 
-// ErrTemplateJsonMissingInfo is returned if the template's JSON does not contain the necessary information (name and version).
-var ErrTemplateJsonMissingInfo = errors.New("template json missing necessary information (check name and version)")
+// ErrTemplateConfigMissingInfo is returned if the template's config JSON does not contain the necessary information (name and version).
+var ErrTemplateConfigMissingInfo = errors.New("template's config json missing necessary information (check name and version)")
 
 // Template is the template entity that is saved in the database. It contains the template's metadata.
-// Each template belongs to a template set. Templates are versioned and the information about the template should always match the template's JSON.
+// Each template belongs to a template set. Templates are versioned and the information about the template should always match the template's config JSON.
 // Actually, Type, Name and Version are redundant, but they are used for easier querying.
 type Template struct {
 	ID          uuid.UUID
@@ -29,7 +29,7 @@ type Template struct {
 	Type        string
 	Name        string
 	Version     string
-	Json        string
+	Config      string
 	CreatedBy   uuid.UUID
 	CreatedAt   time.Time
 	UpdatedAt   *time.Time
@@ -39,7 +39,7 @@ type Template struct {
 type ToCreate struct {
 	TemplateSet uuid.UUID `hvalidate:"required"`
 	Type        string    `hvalidate:"required"`
-	Json        string    `hvalidate:"required"`
+	Config      string    `hvalidate:"required"`
 	CreatedBy   uuid.UUID `hvalidate:"required"`
 }
 
@@ -48,11 +48,11 @@ type ToUpdate struct {
 	ID          uuid.UUID `hvalidate:"required"`
 	TemplateSet uuid.UUID `hvalidate:"required"`
 	Type        string    `hvalidate:"required"`
-	Json        string    `hvalidate:"required"`
+	Config      string    `hvalidate:"required"`
 }
 
 // NecessaryInfo is the necessary information about a template. It is used to create a new template.
-// The template's JSON has to contain this information. It is extracted from the JSON and saved in the database.
+// The template's config JSON has to contain this information. It is extracted from the config JSON and saved in the database.
 type NecessaryInfo struct {
 	Name    string `json:"name"`
 	Version string `json:"version"`
@@ -106,12 +106,12 @@ type Repository interface {
 	// FindByTemplateSetID finds all templates by their template set id. It returns persistence.ErrNotFound if no templates could be found and persistence.ErrReadRow for any other error.
 	FindByTemplateSetID(ctx context.Context, templateSetID uuid.UUID) ([]*Template, error)
 	// Create creates a new template and returns it. It returns persistence.ErrInsert if the template could not be inserted.
-	// It also extracts the necessary information from the template's JSON and saves it in the database.
-	// If the JSON does not contain the necessary information, it returns ErrTemplateJsonMissingInfo.
+	// It also extracts the necessary information from the template's config JSON and saves it in the database.
+	// If the config JSON does not contain the necessary information, it returns ErrTemplateConfigMissingInfo.
 	Create(ctx context.Context, template *ToCreate) (*Template, error)
 	// Update updates an existing template and returns it. It returns persistence.ErrUpdate if the template could not be updated.
-	// It also extracts the necessary information from the template's JSON and saves it in the database.
-	// If the JSON does not contain the necessary information, it returns ErrTemplateJsonMissingInfo.
+	// It also extracts the necessary information from the template's config JSON and saves it in the database.
+	// If the config JSON does not contain the necessary information, it returns ErrTemplateConfigMissingInfo.
 	Update(ctx context.Context, template *ToUpdate) (*Template, error)
 	// Delete deletes an existing template by its id. It returns persistence.ErrDelete if the template could not be deleted.
 	Delete(ctx context.Context, id uuid.UUID) error
@@ -140,19 +140,19 @@ func (t *Template) ToUpdate() *ToUpdate {
 		ID:          t.ID,
 		TemplateSet: t.TemplateSet,
 		Type:        t.Type,
-		Json:        t.Json,
+		Config:      t.Config,
 	}
 }
 
 // NecessaryInfo returns the valid necessary information about a template from a Template.
-// It will return ErrTemplateJsonMissingInfo if the template's JSON does not contain the necessary information (name and version).
-// This method is used by Created and Update to extract the necessary information from the template's JSON.
+// It will return ErrTemplateConfigMissingInfo if the template's config JSON does not contain the necessary information (name and version).
+// This method is used by Created and Update to extract the necessary information from the template's config JSON.
 func (t *Template) NecessaryInfo() (*NecessaryInfo, error) {
 	info := &NecessaryInfo{}
-	err := json.Unmarshal([]byte(t.Json), info)
+	err := json.Unmarshal([]byte(t.Config), info)
 
 	if info.Name == "" || info.Version == "" {
-		return nil, ErrTemplateJsonMissingInfo
+		return nil, ErrTemplateConfigMissingInfo
 	}
 
 	return info, err
@@ -191,8 +191,8 @@ func (r *PGSetRepository) RepositoryName() string {
 // FindByID finds a template by its id. It returns persistence.ErrNotFound if the template could not be found and persistence.ErrReadRow for any other error.
 func (r *PGRepository) FindByID(ctx context.Context, id uuid.UUID) (*Template, error) {
 	t := &Template{}
-	err := r.db.QueryRow(ctx, "SELECT id, template_set, type, name, version, json, created_by, created_at, updated_at FROM templates WHERE id = $1", id).
-		Scan(&t.ID, &t.TemplateSet, &t.Type, &t.Name, &t.Version, &t.Json, &t.CreatedBy, &t.CreatedAt, &t.UpdatedAt)
+	err := r.db.QueryRow(ctx, "SELECT id, template_set, type, name, version, config, created_by, created_at, updated_at FROM templates WHERE id = $1", id).
+		Scan(&t.ID, &t.TemplateSet, &t.Type, &t.Name, &t.Version, &t.Config, &t.CreatedBy, &t.CreatedAt, &t.UpdatedAt)
 
 	if err != nil {
 		return nil, persistence.PGReadErr(err)
@@ -203,7 +203,7 @@ func (r *PGRepository) FindByID(ctx context.Context, id uuid.UUID) (*Template, e
 
 // FindByTemplateSetID finds all templates by their template set id. It returns persistence.ErrNotFound if no templates could be found and persistence.ErrReadRow for any other error.
 func (r *PGRepository) FindByTemplateSetID(ctx context.Context, templateSetID uuid.UUID) ([]*Template, error) {
-	rows, err := r.db.Query(ctx, "SELECT id, template_set, type, name, version, json, created_by, created_at, updated_at FROM templates WHERE template_set = $1", templateSetID)
+	rows, err := r.db.Query(ctx, "SELECT id, template_set, type, name, version, config, created_by, created_at, updated_at FROM templates WHERE template_set = $1", templateSetID)
 	if err != nil {
 		return nil, persistence.PGReadErr(err)
 	}
@@ -211,7 +211,7 @@ func (r *PGRepository) FindByTemplateSetID(ctx context.Context, templateSetID uu
 	var templates []*Template
 	for rows.Next() {
 		t := &Template{}
-		err := rows.Scan(&t.ID, &t.TemplateSet, &t.Type, &t.Name, &t.Version, &t.Json, &t.CreatedBy, &t.CreatedAt, &t.UpdatedAt)
+		err := rows.Scan(&t.ID, &t.TemplateSet, &t.Type, &t.Name, &t.Version, &t.Config, &t.CreatedBy, &t.CreatedAt, &t.UpdatedAt)
 		if err != nil {
 			return nil, persistence.PGReadErr(err)
 		}
@@ -223,14 +223,14 @@ func (r *PGRepository) FindByTemplateSetID(ctx context.Context, templateSetID uu
 }
 
 // Create creates a new template and returns it. It returns persistence.ErrInsert if the template could not be inserted.
-// It also checks if the template's JSON contains the necessary information (name and version).
-// If the JSON does not contain the necessary information, it returns ErrTemplateJsonMissingInfo.
+// It also checks if the template's config JSON contains the necessary information (name and version).
+// If the config JSON does not contain the necessary information, it returns ErrTemplateConfigMissingInfo.
 func (r *PGRepository) Create(ctx context.Context, toCreate *ToCreate) (*Template, error) {
 	newTemplate := &Template{
 		ID:          uuid.New(),
 		TemplateSet: toCreate.TemplateSet,
 		Type:        toCreate.Type,
-		Json:        toCreate.Json,
+		Config:      toCreate.Config,
 		CreatedBy:   toCreate.CreatedBy,
 		CreatedAt:   time.Now(),
 	}
@@ -245,8 +245,8 @@ func (r *PGRepository) Create(ctx context.Context, toCreate *ToCreate) (*Templat
 
 	_, err = r.db.Exec(
 		ctx,
-		"INSERT INTO templates (id, template_set, name, version, type, json, created_by, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
-		newTemplate.ID, newTemplate.TemplateSet, newTemplate.Name, newTemplate.Version, newTemplate.Type, newTemplate.Json, newTemplate.CreatedBy, newTemplate.CreatedAt,
+		"INSERT INTO templates (id, template_set, name, version, type, config, created_by, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
+		newTemplate.ID, newTemplate.TemplateSet, newTemplate.Name, newTemplate.Version, newTemplate.Type, newTemplate.Config, newTemplate.CreatedBy, newTemplate.CreatedAt,
 	)
 	if err != nil {
 		return nil, errors.Join(persistence.ErrInsert, err)
@@ -256,12 +256,12 @@ func (r *PGRepository) Create(ctx context.Context, toCreate *ToCreate) (*Templat
 }
 
 // Update updates an existing template and returns it. It returns persistence.ErrUpdate if the template could not be updated.
-// It also checks if the template's JSON contains the necessary information (name and version).
-// If the JSON does not contain the necessary information, it returns ErrTemplateJsonMissingInfo.
+// It also checks if the template's config JSON contains the necessary information (name and version).
+// If the config JSON does not contain the necessary information, it returns ErrTemplateConfigMissingInfo.
 func (r *PGRepository) Update(ctx context.Context, toUpdate *ToUpdate) (*Template, error) {
 	template := &Template{
-		ID:   toUpdate.ID,
-		Json: toUpdate.Json,
+		ID:     toUpdate.ID,
+		Config: toUpdate.Config,
 	}
 
 	tmplInfo, err := template.NecessaryInfo()
@@ -272,16 +272,16 @@ func (r *PGRepository) Update(ctx context.Context, toUpdate *ToUpdate) (*Templat
 	err = r.db.QueryRow(
 		ctx,
 		`UPDATE templates
-	 	SET template_set = $1, type = $2, name = $3, version = $4, json = $5, updated_at = NOW()
+	 	SET template_set = $1, type = $2, name = $3, version = $4, config = $5, updated_at = NOW()
 	 	WHERE id = $6
-	 	RETURNING template_set, type, name, version, json, created_by, created_at, updated_at`,
-		toUpdate.TemplateSet, toUpdate.Type, tmplInfo.Name, tmplInfo.Version, toUpdate.Json, toUpdate.ID,
+	 	RETURNING template_set, type, name, version, config, created_by, created_at, updated_at`,
+		toUpdate.TemplateSet, toUpdate.Type, tmplInfo.Name, tmplInfo.Version, toUpdate.Config, toUpdate.ID,
 	).Scan(
 		&template.TemplateSet,
 		&template.Type,
 		&template.Name,
 		&template.Version,
-		&template.Json,
+		&template.Config,
 		&template.CreatedBy,
 		&template.CreatedAt,
 		&template.UpdatedAt,
diff --git a/src/app/template/template_test.go b/src/app/template/template_test.go
index ee0b888326ce8ec04a4a53e6e6ab8172d4c04b0d..67e59e3a5d520439775430ab1bfaa468c86dada8 100644
--- a/src/app/template/template_test.go
+++ b/src/app/template/template_test.go
@@ -50,14 +50,14 @@ func TestPGRepository(t *testing.T) {
 		found, err := templateRepo.FindByID(ctx, tmpl.ID)
 		require.NoError(t, err)
 		require.NotNil(t, tmpl)
-		unifiedJsonEqual(t, tmpl.Json, found.Json)
+		unifiedConfigEqual(t, tmpl.Config, found.Config)
 		assert.Equal(t, tmplUnify(*tmpl), tmplUnify(*found))
 	})
 
 	t.Run("FindByTemplateSet", func(t *testing.T) {
 		tmplToCreate := &ToCreate{
 			Type: "ebt",
-			Json: `{
+			Config: `{
 			"name": "Baz",
 			"version": "1.0.0",
 			"authors": ["Qux Bar"],
@@ -80,7 +80,7 @@ func TestPGRepository(t *testing.T) {
 	t.Run("Create Template", func(t *testing.T) {
 		tmplToCreate := &ToCreate{
 			Type: "ebt",
-			Json: `{
+			Config: `{
 			"name": "Baz",
 			"version": "1.0.0",
 			"authors": ["Qux Bar"],
@@ -103,7 +103,7 @@ func TestPGRepository(t *testing.T) {
 		assert.Equal(t, tmpl.Version, "1.0.0")
 		assert.Equal(t, tmpl.TemplateSet, tmplSet.ID)
 		assert.Equal(t, tmpl.CreatedBy, u.ID)
-		unifiedJsonEqual(t, tmplToCreate.Json, tmpl.Json)
+		unifiedConfigEqual(t, tmplToCreate.Config, tmpl.Config)
 	})
 
 	t.Run("Update Template", func(t *testing.T) {
@@ -117,7 +117,7 @@ func TestPGRepository(t *testing.T) {
 
 		toUpdate := newTmpl.ToUpdate()
 		toUpdate.Type = "foo"
-		toUpdate.Json = `{
+		toUpdate.Config = `{
 			"name": "Bizzo",
 			"version": "2.0.0",
 			"authors": ["Qux Bar"],
@@ -133,7 +133,7 @@ func TestPGRepository(t *testing.T) {
 		assert.Equal(t, update.Type, "foo")
 		assert.Equal(t, update.Name, "Bizzo")
 		assert.Equal(t, update.Version, "2.0.0")
-		unifiedJsonEqual(t, toUpdate.Json, update.Json)
+		unifiedConfigEqual(t, toUpdate.Config, update.Config)
 	})
 
 	t.Run("Delete Template", func(t *testing.T) {
@@ -292,7 +292,7 @@ func fooToCreate() (*user.ToCreate, *SetToCreate, *ToCreate) {
 			Description: "Foo Bar",
 		}, &ToCreate{
 			Type: "ebt",
-			Json: `{
+			Config: `{
 				"name": "Foo",
 				"version": "1.0.0",
 				"authors": ["Foo Bar"],
@@ -303,10 +303,10 @@ func fooToCreate() (*user.ToCreate, *SetToCreate, *ToCreate) {
 }
 
 // tmplUnify unifies the template for comparison.
-// It sets the json to "{}" as different whitespaces may lead to different json strings while the content is identical.
+// It sets the config json to "{}" as different whitespaces may lead to different json strings while the content is identical.
 // It truncates the time to seconds as the database does not store milliseconds.
 func tmplUnify(tmpl Template) Template {
-	tmpl.Json = "{}"
+	tmpl.Config = "{}"
 	tmpl.CreatedAt = tmpl.CreatedAt.Truncate(time.Second)
 	if tmpl.UpdatedAt != nil {
 		*tmpl.UpdatedAt = tmpl.UpdatedAt.Truncate(time.Second)
@@ -315,18 +315,18 @@ func tmplUnify(tmpl Template) Template {
 	return tmpl
 }
 
-// unifiedJsonEqual compares two json strings by unmarshalling them into a map[string]any.
-// Even with different whitespaces the json strings are considered equal if the content is equal.
-func unifiedJsonEqual(t *testing.T, expected string, actual string) {
-	expectedJson := make(map[string]any)
-	actualJson := make(map[string]any)
+// unifiedConfigEqual compares two config json strings by unmarshalling them into a map[string]any.
+// Even with different whitespaces the config json strings are considered equal if the content is equal.
+func unifiedConfigEqual(t *testing.T, expectedJson string, actualJson string) {
+	expectedConfig := make(map[string]any)
+	actualConfig := make(map[string]any)
 
-	err := json.Unmarshal([]byte(expected), &expectedJson)
+	err := json.Unmarshal([]byte(expectedJson), &expectedConfig)
 	require.NoError(t, err)
-	err = json.Unmarshal([]byte(actual), &actualJson)
+	err = json.Unmarshal([]byte(actualJson), &actualConfig)
 	require.NoError(t, err)
 
-	assert.Equal(t, expectedJson, actualJson)
+	assert.Equal(t, expectedConfig, actualConfig)
 }
 
 // tmplSetUnify unifies the template set for comparison. It truncates the time to seconds as the database does not store milliseconds.
diff --git a/src/app/template/web.go b/src/app/template/web.go
index de7c9f7d34053ad171800fd280ce36f56374a2e9..5e84ac1c50692616f4ad8c6902c7da07754735c9 100644
--- a/src/app/template/web.go
+++ b/src/app/template/web.go
@@ -12,6 +12,12 @@ import (
 	"net/http"
 )
 
+var (
+	ErrInvalidTemplateSetID = errors.New("invalid template set id")
+	ErrTemplateSetNotFound  = errors.New("template set not found")
+	ErrUserNotPermitted     = errors.New("user not permitted")
+)
+
 func RegisterController(appCtx *hctx.AppCtx, webCtx *web.Ctx) {
 	registerNavigation(appCtx, webCtx)
 
@@ -23,6 +29,37 @@ func RegisterController(appCtx *hctx.AppCtx, webCtx *web.Ctx) {
 	router.Get("/template-set/edit/{id}", templateSetEditFormController(appCtx, webCtx).ServeHTTP)
 	router.Put("/template-set/{id}", templateSetEditController(appCtx, webCtx).ServeHTTP)
 	router.Delete("/template-set/{id}", templateSetDeleteController(appCtx, webCtx).ServeHTTP)
+
+	router.Get("/template-set/{id}/list", templateListController(appCtx, webCtx).ServeHTTP)
+	router.Get("/template-set/{id}/new", templateNewController(appCtx, webCtx).ServeHTTP)
+	router.Post("/template-set/{id}/new", templateNewSaveController(appCtx, webCtx).ServeHTTP)
+}
+
+// SetFromParams returns a template set from the given request parameters. It might return an error if
+// the template set id is invalid (ErrInvalidTemplateSetID), the template set is not found (ErrTemplateSetNotFound)
+// or the user is not permitted to access the template set (ErrUserNotPermitted).
+// In the latter case, the template set is still returned and the caller can decide whether to handle the user
+// not being permitted to access this template set as an error or not.
+func SetFromParams(io web.IO, repo SetRepository, param string) (*Set, error) {
+	ctx := io.Context()
+	u := user.MustCtxUser(ctx)
+
+	templateSetID := web.URLParam(io.Request(), param)
+	templateSetUUID, err := uuid.Parse(templateSetID)
+	if templateSetID == "" || err != nil {
+		return nil, ErrInvalidTemplateSetID
+	}
+
+	templateSet, err := repo.FindByID(ctx, templateSetUUID)
+	if err != nil {
+		return nil, errors.Join(ErrTemplateSetNotFound, err)
+	}
+
+	if templateSet.CreatedBy != u.ID {
+		return templateSet, ErrUserNotPermitted
+	}
+
+	return templateSet, nil
 }
 
 func registerNavigation(appCtx *hctx.AppCtx, webCtx *web.Ctx) {
@@ -92,25 +129,11 @@ func templateSetEditFormController(appCtx *hctx.AppCtx, webCtx *web.Ctx) http.Ha
 	templateSetRepository := util.UnwrapType[SetRepository](appCtx.Repository(SetRepositoryName))
 
 	return web.NewController(appCtx, webCtx, func(io web.IO) error {
-		ctx := io.Context()
-		u := user.MustCtxUser(ctx)
-
-		templateSetID := web.URLParam(io.Request(), "id")
-		templateSetUUID, err := uuid.Parse(templateSetID)
-		if templateSetID == "" || err != nil {
-			return io.InlineError(web.ErrInternal, fmt.Errorf("template set id %s invalid (during edit page)", templateSetID), err)
-		}
-
-		templateSet, err := templateSetRepository.FindByID(ctx, templateSetUUID)
+		templateSet, err := SetFromParams(io, templateSetRepository, "id")
 		if err != nil {
 			return io.InlineError(web.ErrInternal, err)
 		}
 
-		if templateSet.CreatedBy != u.ID {
-			appCtx.Info("user %s tried to edit template set %s without permission", u.ID.String(), templateSetID)
-			return io.InlineError(web.ErrInternal, fmt.Errorf("template set %s not found", templateSetID))
-		}
-
 		return io.RenderJoined(
 			web.NewFormData(templateSet.ToUpdate(), nil),
 			"template.set.edit.form",
@@ -124,24 +147,12 @@ func templateSetEditController(appCtx *hctx.AppCtx, webCtx *web.Ctx) http.Handle
 
 	return web.NewController(appCtx, webCtx, func(io web.IO) error {
 		ctx := io.Context()
-		u := user.MustCtxUser(ctx)
-
-		templateSetID := web.URLParam(io.Request(), "id")
-		templateSetUUID, err := uuid.Parse(templateSetID)
-		if templateSetID == "" || err != nil {
-			return io.InlineError(web.ErrInternal, fmt.Errorf("template set id %s invalid (during edit)", templateSetID), err)
-		}
 
-		templateSet, err := templateSetRepository.FindByID(ctx, templateSetUUID)
+		templateSet, err := SetFromParams(io, templateSetRepository, "id")
 		if err != nil {
 			return io.InlineError(web.ErrInternal, err)
 		}
 
-		if templateSet.CreatedBy != u.ID {
-			appCtx.Info("user %s tried to edit template set %s without permission", u.ID.String(), templateSetID)
-			return io.InlineError(web.ErrInternal, fmt.Errorf("template set %s not found", templateSetID))
-		}
-
 		toUpdate := templateSet.ToUpdate()
 		err, validationErrs := web.ReadForm(io.Request(), toUpdate, appCtx.Validator)
 		if err != nil {
@@ -168,23 +179,12 @@ func templateSetDeleteController(appCtx *hctx.AppCtx, webCtx *web.Ctx) http.Hand
 		ctx := io.Context()
 		u := user.MustCtxUser(ctx)
 
-		templateSetID := web.URLParam(io.Request(), "id")
-		templateSetUUID, err := uuid.Parse(templateSetID)
-		if templateSetID == "" || err != nil {
-			return io.InlineError(web.ErrInternal, fmt.Errorf("template set id %s invalid (during deletion)", templateSetID), err)
-		}
-
-		templateSet, err := templateSetRepository.FindByID(ctx, templateSetUUID)
+		templateSet, err := SetFromParams(io, templateSetRepository, "id")
 		if err != nil {
 			return io.InlineError(web.ErrInternal, err)
 		}
 
-		if templateSet.CreatedBy != u.ID {
-			appCtx.Info("user %s tried to delete template set %s without permission", u.ID.String(), templateSetID)
-			return io.InlineError(web.ErrInternal, fmt.Errorf("template set %s not found", templateSetID))
-		}
-
-		err = templateSetRepository.Delete(ctx, templateSetUUID)
+		err = templateSetRepository.Delete(ctx, templateSet.ID)
 		if err != nil {
 			return io.InlineError(web.ErrInternal, err)
 		}
@@ -197,3 +197,99 @@ func templateSetDeleteController(appCtx *hctx.AppCtx, webCtx *web.Ctx) http.Hand
 		return io.Render("template.set.list", "template/_list-set.go.html", templateSets)
 	})
 }
+
+func templateListController(appCtx *hctx.AppCtx, webCtx *web.Ctx) http.Handler {
+	templateSetRepository := util.UnwrapType[SetRepository](appCtx.Repository(SetRepositoryName))
+	templateRepository := util.UnwrapType[Repository](appCtx.Repository(RepositoryName))
+
+	return web.NewController(appCtx, webCtx, func(io web.IO) error {
+		ctx := io.Context()
+
+		templateSet, err := SetFromParams(io, templateSetRepository, "id")
+		if err != nil {
+			return io.Error(web.ErrInternal, err)
+		}
+
+		templates, err := templateRepository.FindByTemplateSetID(ctx, templateSet.ID)
+		if err != nil {
+			return io.Error(web.ErrInternal, err)
+		}
+
+		return io.RenderJoined(struct {
+			TemplateSet *Set
+			Templates   []*Template
+		}{
+			TemplateSet: templateSet,
+			Templates:   templates,
+		}, "template.list.page", "template/list-page.go.html", "template/_list.go.html")
+	})
+}
+
+func templateNewController(appCtx *hctx.AppCtx, webCtx *web.Ctx) http.Handler {
+	templateSetRepository := util.UnwrapType[SetRepository](appCtx.Repository(SetRepositoryName))
+
+	return web.NewController(appCtx, webCtx, func(io web.IO) error {
+		templateSet, err := SetFromParams(io, templateSetRepository, "id")
+		if err != nil {
+			return io.Error(web.ErrInternal, err)
+		}
+
+		return io.RenderJoined(
+			web.NewFormData(struct {
+				TemplateSet *Set
+				Template    *ToCreate
+			}{
+				TemplateSet: templateSet,
+				Template:    &ToCreate{TemplateSet: templateSet.ID},
+			}, nil),
+			"template.new.page",
+			"template/new-page.go.html",
+			"template/_form-new.go.html",
+		)
+	})
+}
+
+func templateNewSaveController(appCtx *hctx.AppCtx, webCtx *web.Ctx) http.Handler {
+	templateSetRepository := util.UnwrapType[SetRepository](appCtx.Repository(SetRepositoryName))
+	templateRepository := util.UnwrapType[Repository](appCtx.Repository(RepositoryName))
+
+	return web.NewController(appCtx, webCtx, func(io web.IO) error {
+		ctx := io.Context()
+		u := user.MustCtxUser(ctx)
+
+		templateSet, err := SetFromParams(io, templateSetRepository, "id")
+		if err != nil {
+			return io.Error(web.ErrInternal, err)
+		}
+
+		// todo add validation and transformation based on template type => e.g. EBT (EIFFEL Basic Template)
+
+		toCreate := &ToCreate{TemplateSet: templateSet.ID, CreatedBy: u.ID}
+		err, validationErrs := web.ReadForm(io.Request(), toCreate, appCtx.Validator)
+		if err != nil {
+			return io.Error(web.ErrInternal, err)
+		}
+
+		if validationErrs != nil {
+			return io.RenderJoined(
+				web.NewFormData(struct {
+					TemplateSet *Set
+					Template    *ToCreate
+				}{
+					TemplateSet: templateSet,
+					Template:    toCreate,
+				}, nil, validationErrs...),
+				"template.new.page",
+				"template/new-page.go.html",
+				"template/_form-new.go.html",
+			)
+		}
+
+		_, err = templateRepository.Create(ctx, toCreate)
+		if err != nil {
+			return io.Error(web.ErrInternal, err)
+		}
+
+		return io.Redirect(fmt.Sprintf("/template-set/%s/list", templateSet.ID), http.StatusFound)
+	})
+}
diff --git a/templates/template/_form-new.go.html b/templates/template/_form-new.go.html
new file mode 100644
index 0000000000000000000000000000000000000000..8322007b0761121acbfc772194bfb97f7c369f86
--- /dev/null
+++ b/templates/template/_form-new.go.html
@@ -0,0 +1,39 @@
+{{ define "template.new.form" }}
+    {{ printf "%+v" .Data }}
+    <div class="card template-new-form-card">
+        <div class="card-header">{{ t "template.new" }}</div>
+        <div class="card-body">
+            <form method="post" action="/template-set/{{ .Data.Form.TemplateSet.ID }}/new">
+                <fieldset class="template-new-fieldset">
+                    <div id="form-messages">
+                        {{ range $success := .Data.Successes }}
+                            <div class="alert alert-success">{{ t $success }}</div>
+                        {{ end }}
+                        {{ range $violation := .Data.WildcardViolations }}
+                            <div class="alert alert-danger">{{ t $violation.Error }}</div>
+                        {{ end }}
+                    </div>
+
+                    <div class="row">
+                        <div class="col-12">
+                            <label for="config" class="form-label">{{ t "template.config" }}</label>
+                            <textarea
+                                    rows="10"
+                                    id="config"
+                                    class="form-control {{ if .Data.HasViolations "Config" }}is-invalid{{ end }}"
+                                    name="Config"
+                                    placeholder="{{ t "template.config" }}"
+                            >{{ .Data.Form.Template.Config }}</textarea>
+                            {{ range $validation := .Data.ValidationErrors "Config" }}
+                                <div class="invalid-feedback">{{ t $validation.GenericErrorKey }}</div>
+                            {{ end }}
+                        </div>
+                        <div class="col mt-2">
+                            <button type="submit" class="btn btn-primary">{{ t "harmony.generic.create" }}</button>
+                        </div>
+                    </div>
+                </fieldset>
+            </form>
+        </div>
+    </div>
+{{ end }}
\ No newline at end of file
diff --git a/templates/template/_form-set-new.go.html b/templates/template/_form-set-new.go.html
index 601dce5bd82cbf4977a6e914031dbc1ee6affd52..2a0a330f0e3a9c2956be765351fdd2208d392317 100644
--- a/templates/template/_form-set-new.go.html
+++ b/templates/template/_form-set-new.go.html
@@ -57,7 +57,7 @@
                             {{ end }}
                         </div>
                         <div class="col mt-2">
-                            <button type="submit" class="btn btn-primary">Submit</button>
+                            <button type="submit" class="btn btn-primary">{{ t "harmony.generic.create" }}</button>
                         </div>
                     </div>
                 </fieldset>
diff --git a/templates/template/_list-set.go.html b/templates/template/_list-set.go.html
index 021353c43a1d1b03ba5f28b64d790f6d7ca41cbf..f4dca73933625ed7e9416d9aee555787032d1310 100644
--- a/templates/template/_list-set.go.html
+++ b/templates/template/_list-set.go.html
@@ -1,12 +1,20 @@
 {{ define "template.set.list" }}
     <div class="template-set-list">
         <div class="template-set-list-header row mb-5">
-            <div class="col-8">
+            <div class="col-7">
                 <h1>{{ "template.set.list" | t }}</h1>
             </div>
             <div class="col">
                 <a href="/template-set/new" hx-boost="true" hx-target="body" class="btn btn-secondary">{{ "template.set.new" | t }}</a>
             </div>
+            <div class="col">
+                <button hx-get="/template-set/list" hx-target="body" class="btn btn-secondary">
+                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
+                        <path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
+                        <path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
+                    </svg>
+                </button>
+            </div>
         </div>
         <table class="table">
             <thead>
@@ -25,7 +33,7 @@
 
                 {{ range .Data }}
                     <tr>
-                        <td><a class="template-set-view" href="/template-set/{{ .ID }}" hx-boost="true" hx-target="body">{{ .Name }}</a></td>
+                        <td><a class="template-set-view" href="/template-set/{{ .ID }}/list" hx-boost="true" hx-target="body">{{ .Name }}</a></td>
                         <td>{{ .Version }}</td>
                         <td>
                             {{/* edit button + modal */}}
diff --git a/templates/template/_list.go.html b/templates/template/_list.go.html
new file mode 100644
index 0000000000000000000000000000000000000000..25cf81afa8aef08ec30aa154a1592d1848ef9679
--- /dev/null
+++ b/templates/template/_list.go.html
@@ -0,0 +1,41 @@
+{{ define "template.list" }}
+    <div class="template-list">
+        <div class="template-set-list-header row mb-5">
+            <div class="col-7">
+                <h1>{{ tf "template.list" "name" .Data.TemplateSet.Name }}</h1>
+            </div>
+            <div class="col">
+                <a href="/template-set/{{ .Data.TemplateSet.ID }}/new" hx-boost="true" hx-target="body" class="btn btn-secondary">{{ "template.new" | t }}</a>
+            </div>
+            <div class="col">
+                <button hx-get="/template-set/{{ .Data.TemplateSet.ID }}/list" hx-target="body" class="btn btn-secondary">
+                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
+                        <path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
+                        <path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
+                    </svg>
+                </button>
+            </div>
+        </div>
+    </div>
+    <div class="template-list-set-information">
+        <div class="card">
+            <div class="card-header">{{ "template.set.information" | t }}</div>
+            <div class="card-body">
+                <dl class="row">
+                    <dt class="col-4">{{ "template.set.name" | t }}</dt>
+                    <dd class="col-8">{{ .Data.TemplateSet.Name }}</dd>
+                    <dt class="col-4">{{ "template.set.version" | t }}</dt>
+                    <dd class="col-8">{{ .Data.TemplateSet.Version }}</dd>
+                    <dt class="col-4">{{ "template.set.description" | t }}</dt>
+                    <dd class="col-8">{{ .Data.TemplateSet.Description }}</dd>
+                    <dt class="col-4">{{ "template.set.createdAt" | t }}</dt>
+                    <dd class="col-8">{{ .Data.TemplateSet.CreatedAt.Format "02.01.2006" }}</dd>
+                    {{ if .Data.TemplateSet.UpdatedAt }}
+                        <dt class="col-sm-4">{{ "template.set.updatedAt" | t }}</dt>
+                        <dd class="col-sm-8">{{ .Data.TemplateSet.UpdatedAt.Format "02.01.2006" }}</dd>
+                    {{ end }}
+                </dl>
+            </div>
+        </div>
+    </div>
+{{ end }}
\ No newline at end of file
diff --git a/templates/template/list-page.go.html b/templates/template/list-page.go.html
new file mode 100644
index 0000000000000000000000000000000000000000..1b59249e0c639d16cb270f1e77d5b66fc0537952
--- /dev/null
+++ b/templates/template/list-page.go.html
@@ -0,0 +1,7 @@
+{{ define "template.list.page" }}
+    {{ template "index" . }}
+{{ end }}
+
+{{ define "content" }}
+    {{ template "template.list" . }}
+{{ end }}
\ No newline at end of file
diff --git a/templates/template/new-page.go.html b/templates/template/new-page.go.html
new file mode 100644
index 0000000000000000000000000000000000000000..0e86defeaa064d3d962a2c7d3bff8165fed9a326
--- /dev/null
+++ b/templates/template/new-page.go.html
@@ -0,0 +1,7 @@
+{{ define "template.new.page" }}
+    {{ template "index" . }}
+{{ end }}
+
+{{ define "content" }}
+    {{ template "template.new.form" . }}
+{{ end }}
\ No newline at end of file
diff --git a/templates/user/_form-edit.go.html b/templates/user/_form-edit.go.html
index bb02fd8a98e20906afd894d62d8ce3e5cd94ca0f..23462b833b0a5b6dd622e14d4216b641e45903fb 100644
--- a/templates/user/_form-edit.go.html
+++ b/templates/user/_form-edit.go.html
@@ -52,7 +52,7 @@
                             {{ end }}
                         </div>
                         <div class="col mt-2">
-                            <button type="submit" class="btn btn-primary">Submit</button>
+                            <button type="submit" class="btn btn-primary">{{ t "harmony.generic.save" }}</button>
                         </div>
                     </div>
                 </fieldset>
diff --git a/translations/de.json b/translations/de.json
index 89dffccabc393940ee6046549bd3919bf814c72b..a1b187ef9cfd970ef6feb0cdcf2a20b75fc977df 100644
--- a/translations/de.json
+++ b/translations/de.json
@@ -25,9 +25,12 @@
       "list": "Schablonensätze Übersicht",
       "list.empty": "Es wurden noch keine Schablonensätze erstellt.",
       "new": "Neuen Schablonensatz erstellen",
+      "information": "Informationen",
       "name": "Name",
       "version": "Version",
       "description": "Beschreibung",
+      "createdAt": "Erstellt am",
+      "updatedAt": "Zuletzt aktualisiert am",
       "action": {
         "actions": "Aktionen",
         "edit": "Bearbeiten",
@@ -50,6 +53,17 @@
         "cancel": "Abbrechen",
         "updated": "Der Schablonensatz wurde aktualisiert."
       }
+    },
+    "list": "Schablonen Übersicht {{ .name }}",
+    "list.empty": "Es wurden noch keine Schablonen erstellt.",
+    "new": "Neue Schablone erstellen",
+    "name": "Name",
+    "version": "Version",
+    "config": "Konfiguration (JSON)",
+    "action": {
+      "actions": "Aktionen",
+      "edit": "Bearbeiten",
+      "delete": "Löschen"
     }
   },
   "eiffel": {
@@ -97,7 +111,10 @@
       }
     },
     "generic": {
-      "close": "Schließen"
+      "close": "Schließen",
+      "refresh": "Aktualisieren",
+      "save": "Speichern",
+      "create": "Erstellen"
     }
   }
 }
\ No newline at end of file