Agility CMS documentationAgility CMS documentation
OverviewEditorsDevelopersOwners & AdminsTraining GuideApps
Sign inLet's Chat
Management SDK
Models

Models

Models

Retrieve, create, update, and delete content and page models with the Agility Management SDK, with paired JavaScript and .NET examples.

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.

OperationJavaScript.NET
Get a content model by IDgetContentModel(modelId, guid)Task<Model?> GetContentModel(int? id, string guid)
Get a model by reference namegetModelByReferenceName(referenceName, guid)Task<Model?> GetModelByReferenceName(string? referenceName, string guid)
List content modelsgetContentModules(includeDefaults, guid, includeModules)Task<List<Model?>> GetContentModules(bool includeDefaults, string guid, bool includeModules = false)
List page modelsgetPageModules(includeDefault, guid)Task<List<Model?>> GetPageModules(string guid, bool includeDefault = false)
Create or update a modelsaveModel(model, guid)Task<Model?> SaveModel(Model model, string guid)
Delete a modeldeleteModel(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:

PropertyTypeNotes
idnumber-1 to create a new model.
displayNamestringHuman-readable name.
referenceNamestringUnique identifier used by the API.
descriptionstringOptional.
allowTaggingbooleanOptional.
fieldsModelField[]The field definitions.
lastModifiedDate, lastModifiedBy, lastModifiedAuthorID—Read-only.
contentDefinitionTypeName, isPublished, wasUnpublished—Read-only.

And a ModelField — the part that surprises people — has exactly these:

PropertyTypeNotes
namestringThe field's programmatic name. This is what you read from fields.title in the Fetch API. There is no separate referenceName on a field.
labelstringWhat editors see in the CMS.
typestringThe field type — see below.
settings{ [key: string]: string }Everything else about the field. Values are strings.
descriptionstringHelp text.
labelHelpDescriptionstringTooltip help.
itemOrdernumberPosition in the form.
designerOnly, isDataField, editable, hiddenFieldbooleanBehaviour flags.
fieldIDstringInternal 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:

const model = await apiClient.modelMethods.getModelByReferenceName('BlogPost', guid);
console.log(JSON.stringify(model.fields, null, 2));
var model = await client.modelMethods.GetModelByReferenceName("BlogPost", guid);
Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(
    model?.Fields,
    new System.Text.Json.JsonSerializerOptions { WriteIndented = true }));

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:

TypePurposeTypical settings keys
TextSingle-line textRequired, Length, DefaultValue, RegexValidationPattern, RegexValidationMessage
LongTextMulti-line textRequired, Length, DefaultValue
HTMLRich textRequired
DateDate, optionally with timeRequired, ShowTime, DefaultValue
BooleanCheckboxCheckedByDefault
NumberNumericRequired, DefaultValue
DropdownListFixed choicesRequired, Choices, DefaultValue
LinkURL with text and targetRequired
ImageAttachmentSingle imageRequired
FileAttachmentSingle fileRequired
LinkedContentDropdownPick one linked itemContentModel, ContentView, RenderAs, DisplayColumn, SaveTextToField, SaveValueToField
LinkedContentNestedGridNested list of linked itemsContentModel, RenderAs, IsNested, Sort, SortDirection, Columns
LinkedContentSearchListBoxMulti-select linked itemsContentModel, ContentView, RenderAs, SaveTextToField, SaveValueToField
TabGroups following fields under a tab—
CustomSectionStatic instructional blockCustomSectionValue

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.

const contentModels = await apiClient.modelMethods.getContentModules(
  true,  // includeDefaults
  guid,  // instance GUID
  false  // includeModules
);
// 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}");
}

This call maps to GET /api/v1/instance/{guid}/model/list/{includeDefaults}, which accepts these parameters:

ParameterTypeDescription
includeDefaultsbooleanInclude Agility's default system models. Travels in the request path.
includeModulesbooleanAlso return page module models alongside content models. Defaults to false.
updatedSincedate-time stringReturn only the models changed on or after this timestamp. Use it for incremental syncs instead of re-reading every model.

List page models

const pageModels = await apiClient.modelMethods.getPageModules(
  true,  // includeDefault
  guid   // instance GUID
);
var pageModels = await client.modelMethods.GetPageModules(
    guid: guid,
    includeDefault: false
);

foreach (var model in pageModels)
{
    Console.WriteLine($"{model?.ReferenceName}");
}

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.

const modelDetails = await apiClient.modelMethods.getContentModel(modelId, guid);

console.log('Model fields:', modelDetails.fields);
var model = await client.modelMethods.GetContentModel(modelId, guid);
Console.WriteLine($"Model: {model?.DisplayName}");
Console.WriteLine($"Fields: {model?.Fields?.Count}");

Get a model by reference name

This is the primary method for locating an existing model. It returns null when no model matches.

const model = await apiClient.modelMethods.getModelByReferenceName('BlogPost', guid);

if (model) {
  console.log('Found model:', model.displayName);
} else {
  console.log('Model not found');
}
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");
}

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.

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);
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}");

A model with a linked-content field

Linked content needs the dropdown plus the two fields that store its selection:

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.

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);
}
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}");
}

Modifying an existing field

Find the field by its name, change what you need, and save.

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

await apiClient.modelMethods.deleteModel(modelId, guid);
console.log('Model deleted successfully');
var result = await client.modelMethods.DeleteModel(modelId, guid);
Console.WriteLine($"Delete result: {result}");

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.

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');
}
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);

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.
  • Be descriptive. Title beats T; BlogPost beats BP.

Validate before saving

Catch the structural mistakes — including the ones the API accepts silently.

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

try {
  const model = await apiClient.modelMethods.getModelByReferenceName('BlogPost', guid);
} catch (error) {
  console.error('Error:', error);
}
try
{
    var model = await client.modelMethods.GetModelByReferenceName("BlogPost", guid);
}
catch (ApplicationException ex)
{
    Console.Error.WriteLine($"Error: {ex.Message}");
}
On this page
Method referenceContent models vs. page modelsThe shape of a modelField typesRetrieving modelsCreating a modelUpdating a modelDeleting a modelNaming conventionsError handling
Agility CMS documentationAgility CMS documentation

Documentation for the CMS built for editors, developers, and AI agents.

Docs
  • Overview
  • Editors
  • Developers
  • Owners & Admins
  • Training Guide
  • Changelog
Resources
  • Get Support
  • MCP Server
  • System Status
  • llms.txt
Agility
  • agilitycms.com
  • Start Free Trial
  • Sign in
  • Blog
© 2026 Agility Inc. All rights reserved.
Privacy PolicyTerms of Service