# Models

> Source: https://agilitycms.com/docs/javascript/management-sdk/models

The Management SDK exposes model operations through a `modelMethods` group on the client — `apiClient.modelMethods` in JavaScript, `client.modelMethods` in .NET. Use it to retrieve, create, update, and delete **content models** (the definitions behind content items) and **page models**, also called modules (the definitions behind page components).

## Method reference

All six methods exist in both SDKs.

| Operation | JavaScript | .NET |
| --- | --- | --- |
| Get a content model by ID | `getContentModel(modelId, guid)` | `Task<Model?> GetContentModel(int? id, string guid)` |
| Get a model by reference name | `getModelByReferenceName(referenceName, guid)` | `Task<Model?> GetModelByReferenceName(string? referenceName, string guid)` |
| List content models | `getContentModules(includeDefaults, guid, includeModules)` | `Task<List<Model?>> GetContentModules(bool includeDefaults, string guid, bool includeModules = false)` |
| List page models | `getPageModules(includeDefault, guid)` | `Task<List<Model?>> GetPageModules(string guid, bool includeDefault = false)` |
| Create or update a model | `saveModel(model, guid)` | `Task<Model?> SaveModel(Model model, string guid)` |
| Delete a model | `deleteModel(modelId, guid)` | `Task<string?> DeleteModel(int? id, string guid)` |

Argument order for the two list methods differs between the SDKs, so match the arguments by position rather than by name when porting code. Both SDKs call the same REST endpoints underneath, and the parameters those endpoints accept are `includeDefaults` and `includeModules`.

## Content models vs. page models

Content models define the fields of content items stored in containers. Page models (modules) define the fields of the components you place on a page. The two are retrieved with different methods but share the same `Model` shape, so the same field definitions apply to both.

---

## The shape of a model

This is worth reading before you write any code that creates a model, because the structure is narrower than it looks and the mistakes are silent.

A `Model` has these properties:

| Property | Type | Notes |
| --- | --- | --- |
| `id` | number | `-1` to create a new model. |
| `displayName` | string | Human-readable name. |
| `referenceName` | string | Unique identifier used by the API. |
| `description` | string | Optional. |
| `allowTagging` | boolean | Optional. |
| `fields` | `ModelField[]` | The field definitions. |
| `lastModifiedDate`, `lastModifiedBy`, `lastModifiedAuthorID` | — | Read-only. |
| `contentDefinitionTypeName`, `isPublished`, `wasUnpublished` | — | Read-only. |

And a `ModelField` — the part that surprises people — has exactly these:

| Property | Type | Notes |
| --- | --- | --- |
| `name` | string | **The field's programmatic name.** This is what you read from `fields.title` in the Fetch API. There is no separate `referenceName` on a field. |
| `label` | string | What editors see in the CMS. |
| `type` | string | The field type — see below. |
| `settings` | `{ [key: string]: string }` | Everything else about the field. **Values are strings.** |
| `description` | string | Help text. |
| `labelHelpDescription` | string | Tooltip help. |
| `itemOrder` | number | Position in the form. |
| `designerOnly`, `isDataField`, `editable`, `hiddenField` | boolean | Behaviour flags. |
| `fieldID` | string | Internal identifier. |

> **Three things a field does *not* have:** `referenceName` (it's `name`), `required` (it's a `settings` entry), and a top-level `defaultValue` (also a `settings` entry). Older examples used all three. Because the API accepts unknown properties without complaining, a model built that way saves “successfully” and then behaves nothing like you intended — unnamed fields, nothing marked required, no defaults.

> **`settings` values are strings.** Both the OpenAPI schema and the SDKs type `settings` as a string-to-string dictionary. Write `"200"`, not `200`; `"True"`, not `true`. There is nowhere to put an array or a nested object, so a setting that needs a list is serialized into a string.

### The reliable way to get a field definition right

Build the field once in the Agility UI, then read the model back and copy the shape. This takes a minute and beats any table, including this one — it gives you the exact keys your instance expects for that field type:

<div class="code-tabs" data-tabs="JavaScript,.NET">

```ts
const model = await apiClient.modelMethods.getModelByReferenceName('BlogPost', guid);
console.log(JSON.stringify(model.fields, null, 2));
```

```csharp
var model = await client.modelMethods.GetModelByReferenceName("BlogPost", guid);
Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(
    model?.Fields,
    new System.Text.Json.JsonSerializerOptions { WriteIndented = true }));
```

</div>

## Field types

Field types are **PascalCase strings**, and your instance publishes its own authoritative list:

```
GET /api/v1/instance/{guid}/model/field-types
```

That returns an array of every type name the instance accepts — always the final word, since the set grows over time. A companion endpoint, `GET /api/v1/instance/{guid}/model/used-field-types?includeDefaults=true&includeModules=false`, narrows it to the types your models already use.

These are the types you'll meet most often, with the `settings` keys that go with them:

| Type | Purpose | Typical `settings` keys |
| --- | --- | --- |
| `Text` | Single-line text | `Required`, `Length`, `DefaultValue`, `RegexValidationPattern`, `RegexValidationMessage` |
| `LongText` | Multi-line text | `Required`, `Length`, `DefaultValue` |
| `HTML` | Rich text | `Required` |
| `Date` | Date, optionally with time | `Required`, `ShowTime`, `DefaultValue` |
| `Boolean` | Checkbox | `CheckedByDefault` |
| `Number` | Numeric | `Required`, `DefaultValue` |
| `DropdownList` | Fixed choices | `Required`, `Choices`, `DefaultValue` |
| `Link` | URL with text and target | `Required` |
| `ImageAttachment` | Single image | `Required` |
| `FileAttachment` | Single file | `Required` |
| `LinkedContentDropdown` | Pick one linked item | `ContentModel`, `ContentView`, `RenderAs`, `DisplayColumn`, `SaveTextToField`, `SaveValueToField` |
| `LinkedContentNestedGrid` | Nested list of linked items | `ContentModel`, `RenderAs`, `IsNested`, `Sort`, `SortDirection`, `Columns` |
| `LinkedContentSearchListBox` | Multi-select linked items | `ContentModel`, `ContentView`, `RenderAs`, `SaveTextToField`, `SaveValueToField` |
| `Tab` | Groups following fields under a tab | — |
| `CustomSection` | Static instructional block | `CustomSectionValue` |

Every type also accepts the common settings: `Required`, `Unique`, `Hidden`, `CopyAcrossAllLanguages`, and `HideWhenFormula`.

> **Confirm the exact keys against your own instance.** The names above are taken from real models, but settings keys vary by field type and new ones get added. Read a model back (see above) before relying on any particular key — an unrecognized key is ignored silently rather than rejected, so a typo looks like a setting that doesn't work.

> **Linked-content fields need a matching pair of text/value fields.** `SaveTextToField` and `SaveValueToField` point at two other `Text` fields on the same model — e.g. a `Section` dropdown alongside `Section_TextField` and `Section_ValueField`. Create those fields too, or the dropdown has nowhere to store its selection.

---

## Retrieving models

### List content models

Returns model summaries rather than full field definitions.

<div class="code-tabs" data-tabs="JavaScript,.NET">

```ts
const contentModels = await apiClient.modelMethods.getContentModules(
  true,  // includeDefaults
  guid,  // instance GUID
  false  // includeModules
);
```

```csharp
// includeDefaults: include default system models
// includeModules: include page module models
var models = await client.modelMethods.GetContentModules(
    includeDefaults: true,
    guid: guid,
    includeModules: false
);

foreach (var model in models)
{
    Console.WriteLine($"{model?.ReferenceName} - {model?.DisplayName}");
}
```

</div>

This call maps to `GET /api/v1/instance/{guid}/model/list/{includeDefaults}`, which accepts these parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `includeDefaults` | boolean | Include Agility's default system models. Travels in the request path. |
| `includeModules` | boolean | Also return page module models alongside content models. Defaults to `false`. |
| `updatedSince` | date-time string | Return only the models changed on or after this timestamp. Use it for incremental syncs instead of re-reading every model. |

### List page models

<div class="code-tabs" data-tabs="JavaScript,.NET">

```ts
const pageModels = await apiClient.modelMethods.getPageModules(
  true,  // includeDefault
  guid   // instance GUID
);
```

```csharp
var pageModels = await client.modelMethods.GetPageModules(
    guid: guid,
    includeDefault: false
);

foreach (var model in pageModels)
{
    Console.WriteLine($"{model?.ReferenceName}");
}
```

</div>

This call maps to `GET /api/v1/instance/{guid}/model/list-page-modules/{includeDefault}`. It takes the instance GUID and the single boolean `includeDefault` (default `false`) — there are no other parameters.

### Get a model by ID

Returns the full model, including its field definitions and settings.

<div class="code-tabs" data-tabs="JavaScript,.NET">

```ts
const modelDetails = await apiClient.modelMethods.getContentModel(modelId, guid);

console.log('Model fields:', modelDetails.fields);
```

```csharp
var model = await client.modelMethods.GetContentModel(modelId, guid);
Console.WriteLine($"Model: {model?.DisplayName}");
Console.WriteLine($"Fields: {model?.Fields?.Count}");
```

</div>

### Get a model by reference name

This is the primary method for locating an existing model. It returns `null` when no model matches.

<div class="code-tabs" data-tabs="JavaScript,.NET">

```ts
const model = await apiClient.modelMethods.getModelByReferenceName('BlogPost', guid);

if (model) {
  console.log('Found model:', model.displayName);
} else {
  console.log('Model not found');
}
```

```csharp
var model = await client.modelMethods.GetModelByReferenceName("BlogPost", guid);

if (model != null)
{
    Console.WriteLine($"Found model: {model.DisplayName} (ID: {model.ID})");
}
else
{
    Console.WriteLine("Model not found");
}
```

</div>

---

## Creating a model

`saveModel` / `SaveModel` handles both creation and updates. Use an ID of `-1` to create a new model; pass an existing ID to update one.

<div class="code-tabs" data-tabs="JavaScript,.NET">

```ts
const blogPostModel = {
  id: -1, // -1 for new models
  displayName: 'Blog Post',
  referenceName: 'BlogPost',
  description: 'Blog post content model',
  fields: [
    {
      name: 'Title',
      label: 'Title',
      type: 'Text',
      settings: { Required: 'True', Length: '200' }
    },
    {
      name: 'Body',
      label: 'Body',
      type: 'HTML',
      settings: { Required: 'True' }
    },
    {
      name: 'FeaturedImage',
      label: 'Featured Image',
      type: 'ImageAttachment',
      settings: {}
    },
    {
      name: 'PublishDate',
      label: 'Publish Date',
      type: 'Date',
      settings: { Required: 'True', ShowTime: 'False' }
    }
  ]
};

const savedModel = await apiClient.modelMethods.saveModel(blogPostModel, guid);
console.log('Created model with ID:', savedModel.id);
```

```csharp
using agility.models;

var newModel = new Model
{
    ID = -1, // -1 for new models
    DisplayName = "Blog Post",
    ReferenceName = "BlogPost",
    Description = "Blog post content model",
    Fields = new List<ModelField>
    {
        new ModelField
        {
            Name = "Title",
            Label = "Title",
            Type = "Text",
            Settings = new Dictionary<string, string>
            {
                ["Required"] = "True",
                ["Length"] = "200"
            }
        },
        new ModelField
        {
            Name = "Body",
            Label = "Body",
            Type = "HTML",
            Settings = new Dictionary<string, string> { ["Required"] = "True" }
        },
        new ModelField
        {
            Name = "PublishDate",
            Label = "Publish Date",
            Type = "Date",
            Settings = new Dictionary<string, string>
            {
                ["Required"] = "True",
                ["ShowTime"] = "False"
            }
        }
    }
};

var saved = await client.modelMethods.SaveModel(newModel, guid);
Console.WriteLine($"Created model ID: {saved?.ID}");
```

</div>

### A model with a linked-content field

Linked content needs the dropdown plus the two fields that store its selection:

```ts
const articleModel = {
  id: -1,
  displayName: 'Article',
  referenceName: 'Article',
  fields: [
    { name: 'Title', label: 'Title', type: 'Text', settings: { Required: 'True' } },

    // The dropdown itself
    {
      name: 'Category',
      label: 'Category',
      type: 'LinkedContentDropdown',
      settings: {
        ContentModel: 'Category',
        ContentView: 'Categories',
        RenderAs: 'dropdown',
        DisplayColumn: 'Title',
        SaveTextToField: 'Category_TextField',
        SaveValueToField: 'Category_ValueField'
      }
    },

    // ...and the two fields it writes into
    { name: 'Category_TextField', label: 'Category_TextField', type: 'Text', settings: {} },
    { name: 'Category_ValueField', label: 'Category_ValueField', type: 'Text', settings: {} }
  ]
};
```

> Documented for JavaScript; the same structure applies in .NET using `ModelField` and a `Dictionary<string, string>` for `Settings`.

---

## Updating a model

Retrieve the model, mutate its field collection, then save it back. Always start from a retrieved model — saving a hand-built object drops any field you didn't include.

<div class="code-tabs" data-tabs="JavaScript,.NET">

```ts
const existingModel = await apiClient.modelMethods.getModelByReferenceName('BlogPost', guid);

if (existingModel) {
  existingModel.fields.push({
    name: 'Tags',
    label: 'Tags',
    type: 'Text',
    settings: { Length: '500' }
  });

  const updatedModel = await apiClient.modelMethods.saveModel(existingModel, guid);
  console.log('Updated model:', updatedModel.displayName);
}
```

```csharp
var existing = await client.modelMethods.GetModelByReferenceName("BlogPost", guid);

if (existing != null)
{
    existing.Fields.Add(new ModelField
    {
        Name = "Tags",
        Label = "Tags",
        Type = "Text",
        Settings = new Dictionary<string, string> { ["Length"] = "500" }
    });

    var updated = await client.modelMethods.SaveModel(existing, guid);
    Console.WriteLine($"Updated model: {updated?.DisplayName}");
}
```

</div>

### Modifying an existing field

Find the field by its `name`, change what you need, and save.

```ts
const model = await apiClient.modelMethods.getContentModel(modelId, guid);

const titleField = model.fields.find(f => f.name === 'Title');
if (titleField) {
  titleField.settings.Length = '300';
  titleField.settings.Required = 'True';
}

await apiClient.modelMethods.saveModel(model, guid);
```

> **Renaming a field's `name` is a breaking change.** It's the key every API response and every front-end template uses. Renaming it orphans the stored values on existing items. Change the `label` instead when you only want editors to see something different.

---

## Deleting a model

<div class="code-tabs" data-tabs="JavaScript,.NET">

```ts
await apiClient.modelMethods.deleteModel(modelId, guid);
console.log('Model deleted successfully');
```

```csharp
var result = await client.modelMethods.DeleteModel(modelId, guid);
Console.WriteLine($"Delete result: {result}");
```

</div>

> **Warning:** Deleting a model that has associated containers or content items may fail. Verify dependencies before deleting.

### Safe delete with validation

Check that the model exists and that no container depends on it before deleting. `getContainersByModel` answers the dependency question directly, and exists in both SDKs.

<div class="code-tabs" data-tabs="JavaScript,.NET">

```ts
async function safeDeleteModel(modelId: number, guid: string) {
  const model = await apiClient.modelMethods.getContentModel(modelId, guid);
  if (!model) {
    console.log('Model not found');
    return;
  }

  const dependents = await apiClient.containerMethods.getContainersByModel(modelId, guid);

  if (dependents.length > 0) {
    console.warn(
      'Cannot delete model - containers depend on it:',
      dependents.map(c => c.referenceName)
    );
    return;
  }

  await apiClient.modelMethods.deleteModel(modelId, guid);
  console.log('Model deleted successfully');
}
```

```csharp
var model = await client.modelMethods.GetContentModel(modelId, guid);
if (model == null)
{
    Console.WriteLine("Model not found");
    return;
}

var dependents = await client.containerMethods.GetContainersByModel(modelId, guid);

if (dependents.Count > 0)
{
    Console.WriteLine("Cannot delete model - containers depend on it:");
    foreach (var c in dependents) Console.WriteLine($"  {c?.ReferenceName}");
    return;
}

await client.modelMethods.DeleteModel(modelId, guid);
```

</div>

---

## Naming conventions

Field `name` values and model `referenceName` values are permanent in practice, so pick them deliberately.

- **Model reference names:** PascalCase, singular — `BlogPost`, `Article`, `ProductCatalog`. Agility's own models follow this.
- **Field names:** PascalCase — `Title`, `PublishDate`, `FeaturedImage`. The Fetch API lowercases the first letter on read, so `Title` arrives as `fields.title`.
- **Avoid hyphens** in reference names. They're accepted here but break GraphQL queries later — see [Containers & Lists](/docs/javascript/management-sdk/containers).
- Be descriptive. `Title` beats `T`; `BlogPost` beats `BP`.

### Validate before saving

Catch the structural mistakes — including the ones the API accepts silently.

```ts
function validateModel(model: any): string[] {
  const errors: string[] = [];

  if (!model.displayName) errors.push('displayName is required');
  if (!model.referenceName) errors.push('referenceName is required');
  if (!model.fields?.length) errors.push('At least one field is required');

  model.fields?.forEach((field: any, i: number) => {
    const at = `Field ${i + 1}`;
    if (!field.name) errors.push(`${at}: name is required`);
    if (!field.type) errors.push(`${at}: type is required`);

    // These are accepted and then ignored, which is worse than an error.
    if ('referenceName' in field) {
      errors.push(`${at}: fields use 'name', not 'referenceName'`);
    }
    if ('required' in field) {
      errors.push(`${at}: 'required' belongs in settings, as the string 'True'`);
    }

    // settings values must be strings
    Object.entries(field.settings ?? {}).forEach(([k, v]) => {
      if (typeof v !== 'string') {
        errors.push(`${at}: settings.${k} must be a string, got ${typeof v}`);
      }
    });
  });

  return errors;
}
```

---

## Error handling

<div class="code-tabs" data-tabs="JavaScript,.NET">

```ts
try {
  const model = await apiClient.modelMethods.getModelByReferenceName('BlogPost', guid);
} catch (error) {
  console.error('Error:', error);
}
```

```csharp
try
{
    var model = await client.modelMethods.GetModelByReferenceName("BlogPost", guid);
}
catch (ApplicationException ex)
{
    Console.Error.WriteLine($"Error: {ex.Message}");
}
```

</div>
