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).
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 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.
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'sname),required(it's asettingsentry), and a top-leveldefaultValue(also asettingsentry). 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.
settingsvalues are strings. Both the OpenAPI schema and the SDKs typesettingsas a string-to-string dictionary. Write"200", not200;"True", nottrue. There is nowhere to put an array or a nested object, so a setting that needs a list is serialized into a string.
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 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.
SaveTextToFieldandSaveValueToFieldpoint at two otherTextfields on the same model — e.g. aSectiondropdown alongsideSection_TextFieldandSection_ValueField. Create those fields too, or the dropdown has nowhere to store its selection.
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:
| 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. |
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.
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}");
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");
}
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}");
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
ModelFieldand aDictionary<string, string>forSettings.
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}");
}
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
nameis 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 thelabelinstead when you only want editors to see something different.
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.
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);
Field name values and model referenceName values are permanent in practice, so pick them deliberately.
BlogPost, Article, ProductCatalog. Agility's own models follow this.Title, PublishDate, FeaturedImage. The Fetch API lowercases the first letter on read, so Title arrives as fields.title.Title beats T; BlogPost beats BP.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;
}
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}");
}