---
title: Manage display templates
slug: cms-13/docs/manage-display-templates
createdAt: 2026-10-13T18:03:00.000Z
---

Display templates let content managers style Visual Builder nodes without changing code, while developers control the available styles. Developers define the templates and their settings, and content managers choose among them in the editor. To learn how display templates fit into the layout and composition system, see [Configure Visual Builder](docId:_25oKPc2PruHIFr_QPEvc). For the editor perspective, see [Select Visual Builder styles](docId:6TsN4WhTUJgDtmxOKHe3r).

CMS gives you three ways to manage display templates:

- `IDisplayTemplateRepository` – Manages display templates in code from inside the CMS application. Use it to define templates in your solution and provision them at startup.
- **CMS REST API** – Manages display templates from outside the application, such as a deployment pipeline or an external tool, through the `displaytemplates` endpoints.
- **Mark (Optimizely Agent Platform)** – Manages display templates through conversational tools that wrap the same operations. See [Available tools](docId\:v4llE83TvCJ0kzb27dct2).

All three act on the same data. A template you create with one method is visible to the other two.

## Display template structure

A display template consists of the following:

- **Key** – The unique identifier of the template. The key must be two to 255 characters, start with a letter, and contain only letters, digits, and underscores.
- **Display name** – The name shown to content managers in the editor.
- **Scope** – The single type association that controls where the template is available. See [Scope a display template](docId:8aJVwSBmt_FLj2XTfW68F).
- **Default** – Whether CMS preselects the template for a node of the associated type. CMS uses the first template it finds with the default flag set. It seeds each setting with a default value (the first choice for a select setting, `false` for a checkbox setting).
- **Settings** – An ordered list of settings. Each setting has a key, a display name, an editor, a sort order, and, for select editors, a list of choices.

### Scope a display template

Associate each template with exactly one of the following. Saving a template with no association or with more than one association throws a `ValidationException`.

- **Base type** – Makes the template available to every content type that derives from that base type.
- **Content type** – Makes the template available to one specific content type.
- **Node type** – Makes the template available to layout nodes, which have no content type of their own. The values are `row` and `column`. In code, use the `CompositionNodeType.Row` and `CompositionNodeType.Column` constants in the `EPiServer.VisualBuilder` namespace.

:::hint{type="info"}
**Note**
The .NET API and the REST API use different names for the same base type. Components are the clearest case: the .NET base type is `Block`, and the REST API calls it `_component`. Validation fails when you send a REST value to the .NET API, or a .NET value to the REST API.
:::

The following table maps each base type to its .NET constant and its REST value.

:::Iframe{code="<thead ><tr ><th >Base type</th><th >.NET constant</th><th >REST value</th></tr></thead>"}

:::

:::Iframe{code="<tbody ><tr ><td >Experience</td><td ><code >VisualBuilderContentTypeBase.Experience</code></td><td ><code >_experience</code></td></tr><tr ><td >Section</td><td ><code >VisualBuilderContentTypeBase.Section</code></td><td ><code >_section</code></td></tr><tr ><td >Component (element)</td><td ><code >ContentTypeBase.Block</code></td><td ><code >_component</code></td></tr></tbody>"}

:::

`VisualBuilderContentTypeBase` is in the `EPiServer.VisualBuilder` namespace, and `ContentTypeBase` is in `EPiServer.DataAbstraction`. Elements extend blocks, so a template scoped to elements uses the block base type in code. The REST API also accepts other base types, such as `_page` and `_media`. Those content types have no Visual Builder layout to style.

### Setting editors

The `editor` value tells the editor UI how to render the setting. The `DisplaySettingEditor` class in `EPiServer.DataAbstraction` defines the supported values:

- `DisplaySettingEditor.Select` (`select`) – A drop-down list. The content manager picks one of the setting's choices.
- `DisplaySettingEditor.Checkbox` (`checkbox`) – A toggle that stores `true` or `false`. CMS ignores choices for this editor.

CMS validates the values a content manager selects against the template. The content reports a validation warning when a value matches no defined choice. A non-boolean value on a checkbox setting produces the same warning.

## Manage display templates in code

`AddCms()` registers `IDisplayTemplateRepository` in the service container. The interface is in the `EPiServer.DataAbstraction` namespace. Inject it like any other CMS service:

```csharp
using EPiServer.DataAbstraction;

public class SectionStyleService(IDisplayTemplateRepository displayTemplateRepository)
{
    // ...
}
```

The interface has the following members:

- `List()` – Returns every display template.
- `Load(int id)` – Returns the template with the given ID, or `null`.
- `Load(string key)` – Returns the template with the given key, or `null`. The key comparison ignores case.
- `Save(DisplayTemplate template)` – Creates or updates a template.
- `Delete(int id)` – Deletes the template with the given ID.
- `Delete(string key)` – Deletes the template with the given key.
- `FindByNodeType(string nodeType)` – Returns the templates associated with a node type, ordered by name.
- `FindByBaseType(ContentTypeBase baseType)` – Returns the templates associated with a base type, ordered by name.
- `FindByContentType(int contentTypeID)` – Returns the templates associated with a content type and with its base type, ordered by name.

### Create a display template

Set a key, a display name, and one type association, then call `Save`. The following hosted service creates a section template at application startup. The template has a color scheme setting and a highlight toggle.

```csharp
using EPiServer.DataAbstraction;
using EPiServer.VisualBuilder;
using Microsoft.Extensions.Hosting;

public class DisplayTemplateProvisioner(IDisplayTemplateRepository displayTemplateRepository)
    : IHostedService
{
    public Task StartAsync(CancellationToken cancellationToken)
    {
        if (displayTemplateRepository.Load("DefaultSection") is not null)
        {
            return Task.CompletedTask;
        }

        displayTemplateRepository.Save(new DisplayTemplate
        {
            Key = "DefaultSection",
            Name = "Default section",
            BaseType = VisualBuilderContentTypeBase.Section,
            IsDefault = true,
            Settings =
            [
                new DisplaySetting
                {
                    Key = "colorScheme",
                    Name = "Color scheme",
                    Editor = DisplaySettingEditor.Select,
                    SortOrder = 10,
                    Choices =
                    [
                        new DisplaySettingChoice { Key = "default", Name = "Default", SortOrder = 10 },
                        new DisplaySettingChoice { Key = "primary", Name = "Primary", SortOrder = 20 },
                        new DisplaySettingChoice { Key = "secondary", Name = "Secondary", SortOrder = 30 }
                    ]
                },
                new DisplaySetting
                {
                    Key = "highlight",
                    Name = "Highlighted",
                    Editor = DisplaySettingEditor.Checkbox,
                    SortOrder = 20
                }
            ]
        });

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
```

Register the hosted service after `AddCms()`:

```csharp
services.AddHostedService<DisplayTemplateProvisioner>();
```

CMS registers its own startup work as `IHostedLifecycleService`. The host completes every `StartingAsync` call before it runs any `StartAsync` call. As a result, the repository is ready when your `StartAsync` runs, regardless of registration order.

Treat templates, settings, and choices as abstractions. For example, name a choice `primary` rather than `red`. Abstract names stay meaningful across delivery channels and survive brand color changes.

### Update a display template

`Load` and `List` return read-only instances, which the repository caches. Call `CreateWritableClone` before you change anything, then save the clone. Saving a read-only instance throws an `ArgumentException`.

```csharp
var template = displayTemplateRepository.Load("DefaultSection");
if (template is null)
{
    return;
}

var writableTemplate = template.CreateWritableClone();
writableTemplate.Name = "Standard section";
writableTemplate.Settings.Add(new DisplaySetting
{
    Key = "verticalSpacing",
    Name = "Vertical spacing",
    Editor = DisplaySettingEditor.Select,
    SortOrder = 30,
    Choices =
    [
        new DisplaySettingChoice { Key = "compact", Name = "Compact", SortOrder = 10 },
        new DisplaySettingChoice { Key = "roomy", Name = "Roomy", SortOrder = 20 }
    ]
});

displayTemplateRepository.Save(writableTemplate);
```

Note the following about updates:

- `Save` writes the settings list as given. It deletes any existing setting that is missing from the list.
- `Save` skips the write when the template exists and nothing changed.
- Deleting a setting or a choice that content already uses does not rewrite that content. The stored value remains until someone saves the content again. The content reports a validation warning in the meantime.

### Find display templates

Retrieve templates with `List` or one of the `Find` methods:

```csharp
var allTemplates = displayTemplateRepository.List();
var rowTemplates = displayTemplateRepository.FindByNodeType(CompositionNodeType.Row);
var sectionTemplates = displayTemplateRepository.FindByBaseType(VisualBuilderContentTypeBase.Section);
var templatesForContentType = displayTemplateRepository.FindByContentType(contentType.ID);
```

`FindByContentType` returns templates scoped to the content type and to its base type. This set matches what the editor offers for a node of that content type.

### Delete a display template

Call `Delete` with the template key or ID:

```csharp
displayTemplateRepository.Delete("DefaultSection");
```

Both `Delete` overloads throw an `ArgumentException` when no matching template exists.

### Validation rules

`Save` throws a `ValidationException` when any of the following is true:

- The key is missing, shorter than two characters, longer than 255 characters, or does not match the pattern `^[A-Za-z][_0-9A-Za-z]+$`.
- The display name is missing or longer than 255 characters.
- The template has no type association, or more than one.
- `BaseType` does not reference an existing type, or `ContentTypeID` does not reference an existing content type.
- The key of a template being created is already in use.
- Two settings in the template share a key, or two choices in a setting share a key.

### React to display template changes

The repository publishes an event on every change. The events are in the `EPiServer.DataAbstraction` namespace, and each carries the affected `DisplayTemplate`:

- `DisplayTemplateCreatedEvent`
- `DisplayTemplateUpdatedEvent`
- `DisplayTemplateDeletedEvent`

All three derive from the abstract `DisplayTemplateEvent` record. Subscribe to the base type to handle every change with one subscriber:

```csharp
public class DisplayTemplateSubscriber : IEventSubscriber<DisplayTemplateEvent>
{
    public Task HandleAsync(
        DisplayTemplateEvent eventData,
        EventContext context,
        CancellationToken cancellationToken = default)
    {
        // Respond to the change, for example by clearing a rendering cache.
        return Task.CompletedTask;
    }
}
```

Register the subscriber in the service container:

```csharp
services.AddCmsEventSubscriber<DisplayTemplateEvent, DisplayTemplateSubscriber>();
```

For details about event subscribers, see [Send and receive custom events](docId\:bSjUhLQxeEnes-rOTiJ2N).

## Manage display templates with the REST API

The CMS REST API exposes display templates at `/_cms/v1/displaytemplates`. For the base address, OAuth client setup, and access tokens, see [Introduction to the CMS REST API](docId\:MM1lPFKxUy4D2kTAk9-T3) and [Authentication and authorization](docId\:VPNgx_fyJwFGtQ4QdFZ_Z).

The API applies the same validation rules as the repository.

### Create a template

Send a `POST` request with the template in the request body. A successful request returns `201 Created`. A key already in use returns `409 Conflict`.

```http
POST https://example.com/_cms/v1/displaytemplates
Content-Type: application/json

{
  "key": "DefaultSection",
  "displayName": "Default section",
  "baseType": "_section",
  "isDefault": true,
  "settings": {
    "colorScheme": {
      "displayName": "Color scheme",
      "editor": "select",
      "sortOrder": 10,
      "choices": {
        "default": { "displayName": "Default", "sortOrder": 10 },
        "primary": { "displayName": "Primary", "sortOrder": 20 },
        "secondary": { "displayName": "Secondary", "sortOrder": 30 }
      }
    },
    "highlight": {
      "displayName": "Highlighted",
      "editor": "checkbox",
      "sortOrder": 20
    }
  }
}
```

For the full request schema, see [Create display template](https://docs.optimizely.com/cms-13/api/create-display-template).

### List templates

Send a `GET` request. The response is a paged collection. Use the `pageIndex` and `pageSize` query parameters to page through the results.

```http
GET https://example.com/_cms/v1/displaytemplates?pageIndex=0&pageSize=50
```

For the full response schema, see [List display templates](https://docs.optimizely.com/cms-13/api/list-display-templates).

### Get a template

Send a `GET` request with the template key:

```http
GET https://example.com/_cms/v1/displaytemplates/DefaultSection
```

The endpoint supports the `If-None-Match` and `If-Modified-Since` request headers for conditional requests.

For the full response schema, see [Get display template](https://docs.optimizely.com/cms-13/api/get-display-template).

### Update a template

Send a `PATCH` request with the `application/merge-patch+json` content type and only the fields you want to change.

```http
PATCH https://example.com/_cms/v1/displaytemplates/DefaultSection
Content-Type: application/merge-patch+json

{
  "displayName": "Standard section",
  "settings": {
    "verticalSpacing": {
      "displayName": "Vertical spacing",
      "editor": "select",
      "sortOrder": 30,
      "choices": {
        "compact": { "displayName": "Compact", "sortOrder": 10 },
        "roomy": { "displayName": "Roomy", "sortOrder": 20 }
      }
    }
  }
}
```

Use the `If-Match` or `If-Unmodified-Since` request headers to avoid overwriting a concurrent change.

For the full request schema, see [Patch display template](https://docs.optimizely.com/cms-13/api/patch-display-template).

### Delete a template

Send a `DELETE` request with the template key:

```http
DELETE https://example.com/_cms/v1/displaytemplates/DefaultSection
```

The endpoint supports the `If-Match` and `If-Unmodified-Since` request headers.

For the full response details, see [Delete display template](https://docs.optimizely.com/cms-13/api/delete-display-template).

## Field names in code and in the REST API

The .NET model and the REST representation describe the same template but do not use the same names or shapes.

:::Iframe{code="<thead ><tr ><th >.NET</th><th >REST</th><th >Difference</th></tr></thead>"}

:::

:::Iframe{code="<tbody ><tr ><td ><code >Key</code></td><td ><code >key</code></td><td >Same value.</td></tr><tr ><td ><code >Name</code></td><td ><code >displayName</code></td><td >Different name for the same field.</td></tr><tr ><td ><code >BaseType</code></td><td ><code >baseType</code></td><td >A <code >ContentTypeBase</code> value in .NET, an underscore-prefixed string in the API. The names differ. See <undefined >Scope a display template</undefined>.</td></tr><tr ><td ><code >ContentTypeID</code></td><td ><code >contentType</code></td><td >The numeric content type ID in .NET, the content type key in the API.</td></tr><tr ><td ><code >Settings</code></td><td ><code >settings</code></td><td >A list of <code >DisplaySetting</code> objects carrying their own <code >Key</code> in .NET, an object keyed by setting key in the API.</td></tr><tr ><td ><code >Choices</code></td><td ><code >choices</code></td><td >A list of <code >DisplaySettingChoice</code> objects carrying their own <code >Key</code> in .NET, an object keyed by choice key in the API.</td></tr><tr ><td ><code >Created</code>, <code >CreatedBy</code></td><td ><code >created</code>, <code >createdBy</code></td><td >CMS sets these values. Read-only.</td></tr><tr ><td ><code >Saved</code>, <code >SavedBy</code></td><td ><code >lastModified</code>, <code >lastModifiedBy</code></td><td >Different names. CMS sets these values. Read-only.</td></tr></tbody>"}

:::

## How display templates reach the delivery site

Optimizely Graph indexes the applied template and the selected settings with the content. Every node in the composition carries a `displayTemplateKey` and a `displaySettings` array. The delivery site reads these values to render the content. For the indexed structure, see [Configure Visual Builder](docId:_25oKPc2PruHIFr_QPEvc).

Export packages include display templates, so a template you define in one environment transfers with an export and import.
