What is your developer-dependent CMS actually costing you? Calculate your costs and get a full report
What is your developer-dependent CMS actually costing you? Calculate your costs and get a full report
Content
Create, read, update, publish, and delete Agility content items with the Management SDK, in JavaScript and .NET.
The content methods handle content items: creating, reading, updating, deleting, and moving them through the publishing and approval workflow. Every method is reached from the client instance you created in Getting Started (apiClient.contentMethods in JavaScript, client.Content in .NET), and every method takes the instance guid and a locale in addition to its own arguments.
The .NET examples on this page use
Agility.Management.SDK2.0. If you are on 1.x, see Migrating to 2.0 for the method names.
The JavaScript and .NET SDKs cover the same core operations but differ in two important ways:
Promise<number[]> (an array of content IDs). The .NET equivalents return a BatchResult: result.ItemId is the content ID for a single-item operation, and result.ItemIds holds the IDs from a bulk save. Single and batch saves are separate API endpoints, and a single save returns a single content ID — see Saving a Single Content Item.returnBatchId: true to return a batch ID without waiting, .NET takes waitForBatch: false.| Operation | JavaScript | .NET |
|---|---|---|
| Retrieve a content item by ID | getContentItem | GetContentItemAsync |
| List content items in a container | getContentItems (deprecated) | GetContentListAsync |
| List with advanced filtering | getContentList | GetContentListAsync |
| Save a single item (create or update) | saveContentItem | SaveContentItemAsync |
| Save multiple items in bulk | saveContentItems | SaveContentItemsAsync |
| Publish an item | publishContent | PublishContentItemAsync |
| Unpublish an item | unPublishContent | UnpublishContentItemAsync |
| Request approval | contentRequestApproval | RequestApprovalContentItemAsync |
| Approve an item | approveContent | ApproveContentItemAsync |
| Decline an item | declineContent | DeclineContentItemAsync |
| Delete an item | deleteContent | DeleteContentItemAsync |
| Bulk workflow on many items at once | batchWorkflowContent | BatchWorkflowContentItemsAsync |
| Retrieve item history | getContentHistory | GetContentItemHistoryAsync |
| Retrieve item comments | getContentComments | GetContentItemCommentsAsync |
When you are working with more than one content item, prefer the bulk methods. They accept arrays — pass all of your items or IDs in a single call rather than looping over the single-item methods.
| Single Item Method | Bulk Method | Use Case |
|---|---|---|
saveContentItem() / SaveContentItemAsync() | saveContentItems() / SaveContentItemsAsync() | Creating or updating multiple items |
publishContent() / PublishContentItemAsync() | batchWorkflowContent() / BatchWorkflowContentItemsAsync() | Publishing multiple items at once |
unPublishContent() / UnpublishContentItemAsync() | batchWorkflowContent() / BatchWorkflowContentItemsAsync() | Unpublishing multiple items at once |
Both SDKs return content IDs in the same order as the input array, so you can correlate each result with the item you sent.
Retrieves a specific content item by ID and locale.
| Parameter | Type | Required | Description |
|---|---|---|---|
| contentID | number / int | Yes | The ID of the content item to retrieve |
| guid | string | Yes | The website GUID |
| locale | string | Yes | The locale code (e.g. en-us) |
Returns the content item object — Promise<ContentItem> in JavaScript, Task<ContentItem> in .NET.
const contentItem = await apiClient.contentMethods.getContentItem(123, "your-guid", "en-us")
console.log(contentItem.fields.title)
var contentItem = await client.Content.GetContentItemAsync(guid, locale, 123);
Console.WriteLine(contentItem.Fields["title"]);
.NET signature: Task<ContentItem> GetContentItemAsync(string instanceGuid, string locale, int contentId, CancellationToken cancellationToken = default)
Lists content items from a container with pagination and basic filtering.
In JavaScript, getContentItems is deprecated — use getContentList instead. It takes a ListParams object holding take, skip, sortField, and sortDirection. In .NET, GetContentListAsync is the list method, and it takes the same options as a ContentListOptions object.
| Parameter | JavaScript | .NET |
|---|---|---|
| referenceName | Yes | Yes |
| guid | Yes | Yes |
| locale | Yes | Yes |
| Pagination / sorting | listParams: ListParams | ContentListOptions: Take, Skip, SortField, SortDirection |
| Filtering | via listParams | ContentListOptions: Filter (a ContentListFilterModel), Fields (comma-separated field names) |
Returns a ContentList containing the items and pagination info.
const listParams = {
take: 10,
skip: 0,
sortField: "title",
sortDirection: "asc",
}
const contentList = await apiClient.contentMethods.getContentItems("articles", "your-guid", "en-us", listParams)
using Agility.Management.Sdk.Clients;
var contentList = await client.Content.GetContentListAsync(
guid,
locale,
"blog-posts",
new ContentListOptions
{
SortDirection = "asc", // "asc" or "desc" (optional)
SortField = "title", // Field to sort by (optional)
Take = 50, // Page size (default: 50)
Skip = 0 // Record offset (default: 0)
});
Console.WriteLine($"Total: {contentList?.TotalCount}");
foreach (var item in contentList?.Items ?? [])
{
// Items are JSON nodes in the API's own shape
Console.WriteLine(item?["contentID"]);
}
.NET signature: Task<ContentList> GetContentListAsync(string instanceGuid, string locale, string referenceName, ContentListOptions options = null, CancellationToken cancellationToken = default)
getContentList retrieves content items using a POST request with a filter object, giving you more control than getContentItems.
| Parameter | Type | Required | Description |
|---|---|---|---|
| referenceName | string | Yes | The reference name of the content model |
| guid | string | Yes | The website GUID |
| locale | string | Yes | The locale code |
| listParams | ListParams | Yes | Pagination and filtering parameters |
| filterObject | ContentListFilterModel | No | Advanced filter criteria |
const listParams = {
take: 20,
skip: 0,
sortField: "dateCreated",
sortDirection: "desc",
showDeleted: false,
}
const filterObject = {
publishedState: "published",
searchText: "important",
}
const contentList = await apiClient.contentMethods.getContentList(
"articles",
"your-guid",
"en-us",
listParams,
filterObject,
)
take defaults to 50 when you omit it, and skip defaults to 0. To walk a whole container, keep requesting pages with an increasing skip until a page comes back shorter than your take — don't assume one large request captured everything. If you already know which items you want, GET /api/v1/instance/{guid}/{locale}/items?ids=… fetches those specific items by ID instead of listing a container.
In .NET,
GetContentListAsynccalls the same POST route. Pass the filter as aContentListFilterModelinContentListOptions.Filter, alongsideTake,SkipandShowDeleted.
Saving is not publishing. A save always writes to Staging. If the item was already Published it drops back to Staging, and your live site keeps serving the previous version until you call Publishing. A save with no publish is the most common reason a write appears to have done nothing.
Creates or updates a single content item. Use contentID: -1 (JavaScript also accepts 0) for new items, or an existing content ID to update.
| Parameter | Type | Required | Description |
|---|---|---|---|
| contentItem | ContentItem | Yes | The content item object to save |
| guid | string | Yes | The website GUID |
| locale | string | Yes | The locale code |
| returnBatchId | boolean | No | JavaScript: if true, returns the batch ID immediately without waiting. The .NET equivalent is waitForBatch: false. |
Saving one item and saving a batch are two distinct Management API endpoints:
POST /api/v1/instance/{guid}/{locale}/item saves one item.POST /api/v1/instance/{guid}/{locale}/item/multi is the batch endpoint, and is the one that deals in multiple items.Both respond with a batch ID, not a content ID. The write is queued, not performed, during your
request. saveContentItem returns the content ID because the SDK polls that batch for you and
hands back the result — see How writes complete.
Pass returnBatchId: true in JavaScript, or waitForBatch: false in .NET, to get the batch ID immediately and poll it yourself.
So a single save yields one content ID. .NET returns a BatchResult whose ItemId is that content ID. The JavaScript SDK resolves to that same content ID and may hand it back wrapped in a single-element array (as in the example below), but the authoritative API contract for a single save is one integer, not a list.
For bulk operations, use the batch save methods described below instead.
// Create a new content item
const newItem: ContentItem = {
contentID: -1, // -1 or 0 for new items
properties: {
definitionName: "BlogPost",
referenceName: "blogposts",
},
fields: {
title: "New Article",
content: "Article content...",
author: "John Doe",
publishDate: "2024-01-15",
},
seo: {
metaDescription: "A great new article",
},
}
const savedIds = await apiClient.contentMethods.saveContentItem(newItem, "your-guid", "en-us")
console.log("Created content ID:", savedIds[0])
// Update an existing content item
const existingItem = await apiClient.contentMethods.getContentItem(123, "your-guid", "en-us")
existingItem.fields.title = "Updated Title"
const updatedIds = await apiClient.contentMethods.saveContentItem(existingItem, "your-guid", "en-us")
using System.Text.Json.Nodes;
using Agility.Management.Sdk.Models;
// Create a new content item
var newItem = new ContentItem
{
ContentID = -1, // -1 for new items
Properties = new ContentItemProperties
{
DefinitionName = "BlogPost",
ReferenceName = "blog-posts"
},
Fields = new JsonObject
{
["title"] = "New Article",
["slug"] = "new-article",
["content"] = "Article content here..."
}
};
var saved = await client.Content.SaveContentItemAsync(guid, locale, newItem);
Console.WriteLine($"Created content ID: {saved.ItemId}");
// Update an existing item
var existing = await client.Content.GetContentItemAsync(guid, locale, 123);
existing.Fields["title"] = "Updated Title";
var updated = await client.Content.SaveContentItemAsync(guid, locale, existing);
.NET signature: Task<BatchResult> SaveContentItemAsync(string instanceGuid, string locale, ContentItem item, bool waitForBatch = true, CancellationToken cancellationToken = default)
In JavaScript, ContentItem is an interface:
interface ContentItem {
contentID: number // Use -1 or 0 for new items, existing ID for updates
properties: {
definitionName: string // The content model name
referenceName: string // The container reference name
itemOrder?: number // Optional ordering
releaseDate?: string // Optional scheduled release date
pullDate?: string // Optional scheduled pull date
}
fields: {
[key: string]: any // Field values matching the content model
}
seo?: {
// Optional SEO properties
metaDescription?: string
metaKeywords?: string
metaHTML?: string
menuVisible?: boolean
sitemapVisible?: boolean
}
scripts?: {
// Optional custom scripts
top?: string
bottom?: string
}
}
In .NET the equivalent type is ContentItem from the Agility.Management.Sdk.Models namespace, where Properties is a ContentItemProperties object (DefinitionName, ReferenceName) and Fields is a JsonObject.
Recommended for bulk operations. Saves multiple content items in a single batch call.
| Parameter | Type | Required | Description |
|---|---|---|---|
| contentItems | ContentItem[] / IReadOnlyList<ContentItem> | Yes | The content items to save |
| guid | string | Yes | The website GUID |
| locale | string | Yes | The locale code |
| returnBatchId | boolean | No | JavaScript: if true, returns the batch ID immediately without waiting. The .NET equivalent is waitForBatch: false. |
Notes that apply to both SDKs:
contentID: -1) and updates (an existing contentID) in the same batch.-1 in the returned array means that item failed to save. Log the failure and check the item manually — a precise per-item error message is not yet returned. In .NET, a failed item throws AgilityBatchException instead.JavaScript returns Promise<number[]>; .NET returns a BatchResult whose ItemIds holds the content IDs.
const contentItems: ContentItem[] = [
{
contentID: -1,
properties: {definitionName: "BlogPost", referenceName: "blogposts"},
fields: {title: "Article 1", slug: "article-1"},
},
{
contentID: -1,
properties: {definitionName: "BlogPost", referenceName: "blogposts"},
fields: {title: "Article 2", slug: "article-2"},
},
{
contentID: -1,
properties: {definitionName: "BlogPost", referenceName: "blogposts"},
fields: {title: "Article 3", slug: "article-3"},
},
]
const savedIds = await apiClient.contentMethods.saveContentItems(contentItems, "your-guid", "en-us")
// savedIds[0] corresponds to 'Article 1'
// savedIds[1] corresponds to 'Article 2'
// savedIds[2] corresponds to 'Article 3'
console.log("Created IDs:", savedIds)
var items = new List<ContentItem>
{
new ContentItem
{
ContentID = -1,
Properties = new ContentItemProperties { DefinitionName = "BlogPost", ReferenceName = "blog-posts" },
Fields = new JsonObject { ["title"] = "Article 1" }
},
new ContentItem
{
ContentID = -1,
Properties = new ContentItemProperties { DefinitionName = "BlogPost", ReferenceName = "blog-posts" },
Fields = new JsonObject { ["title"] = "Article 2" }
}
};
var result = await client.Content.SaveContentItemsAsync(guid, locale, items);
// result.ItemIds holds the content IDs, in input order
Console.WriteLine($"Saved IDs: {string.Join(", ", result.ItemIds)}");
.NET signature: Task<BatchResult> SaveContentItemsAsync(string instanceGuid, string locale, IReadOnlyList<ContentItem> items, bool waitForBatch = true, CancellationToken cancellationToken = default)
Because the returned IDs keep their input order, you can map them straight back onto your source records.
// Example: Import products and track their new IDs
const products = [
{sku: "SKU-001", name: "Product A", price: 29.99},
{sku: "SKU-002", name: "Product B", price: 49.99},
{sku: "SKU-003", name: "Product C", price: 19.99},
]
const contentItems: ContentItem[] = products.map((product) => ({
contentID: -1,
properties: {definitionName: "Product", referenceName: "products"},
fields: {
sku: product.sku,
name: product.name,
price: product.price,
},
}))
const savedIds = await apiClient.contentMethods.saveContentItems(contentItems, "your-guid", "en-us")
// Map the returned IDs back to original products (same order!)
const productsWithIds = products.map((product, index) => ({
...product,
contentID: savedIds[index],
}))
console.log(productsWithIds)
// [
// { sku: 'SKU-001', name: 'Product A', price: 29.99, contentID: 101 },
// { sku: 'SKU-002', name: 'Product B', price: 49.99, contentID: 102 },
// { sku: 'SKU-003', name: 'Product C', price: 19.99, contentID: 103 }
// ]
const contentItems: ContentItem[] = [
// New item
{
contentID: -1,
properties: {definitionName: "BlogPost", referenceName: "blogposts"},
fields: {title: "Brand New Post"},
},
// Update existing item
{
contentID: 456, // Existing content ID
properties: {definitionName: "BlogPost", referenceName: "blogposts"},
fields: {title: "Updated Existing Post"},
},
]
const savedIds = await apiClient.contentMethods.saveContentItems(contentItems, "your-guid", "en-us")
// savedIds[0] = new ID for 'Brand New Post'
// savedIds[1] = 456 (the updated item retains its ID)
Publishes a single content item through the batch workflow system.
| Parameter | Type | Required | Description |
|---|---|---|---|
| contentID | number / int | Yes | The ID of the content item to publish |
| guid | string | Yes | The website GUID |
| locale | string | Yes | The locale code |
| comments | string | No | Optional comments for the publish operation |
| returnBatchId | boolean | No | JavaScript: if true, returns the batch ID immediately without waiting. The .NET equivalent is waitForBatch: false. |
JavaScript returns Promise<number[]> containing the published content ID; .NET returns a BatchResult, whose ItemId is the content ID.
To publish more than one item, use batchWorkflowContent() in JavaScript or BatchWorkflowContentItemsAsync() in .NET with WorkflowOperationType.Publish rather than looping.
// Publish a single item
const publishedIds = await apiClient.contentMethods.publishContent(123, "your-guid", "en-us", "Publishing update")
console.log("Published ID:", publishedIds[0])
// Return batch ID immediately for custom polling
const batchId = await apiClient.contentMethods.publishContent(123, "your-guid", "en-us", null, true)
var published = await client.Content.PublishContentItemAsync(
guid,
locale,
123,
comments: "Publishing update" // optional
);
Console.WriteLine($"Published ID: {published.ItemId}");
// Return without waiting, for custom polling
var queued = await client.Content.PublishContentItemAsync(guid, locale, 123, waitForBatch: false);
Console.WriteLine($"Batch ID: {queued.BatchId}");
.NET signature: Task<BatchResult> PublishContentItemAsync(string instanceGuid, string locale, int contentId, string comments = null, bool waitForBatch = true, CancellationToken cancellationToken = default)
Unpublishes a single content item through the batch workflow system. Takes the same parameters as publishing.
To unpublish more than one item, use batchWorkflowContent() in JavaScript or BatchWorkflowContentItemsAsync() in .NET with WorkflowOperationType.Unpublish.
const unpublishedIds = await apiClient.contentMethods.unPublishContent(123, "your-guid", "en-us", "Temporary unpublish")
console.log("Unpublished ID:", unpublishedIds[0])
var unpublished = await client.Content.UnpublishContentItemAsync(
guid,
locale,
123,
comments: "Temporarily unpublishing"
);
.NET signature: Task<BatchResult> UnpublishContentItemAsync(string instanceGuid, string locale, int contentId, string comments = null, bool waitForBatch = true, CancellationToken cancellationToken = default)
Submits a content item for approval through the workflow system.
| Parameter | Type | Required | Description |
|---|---|---|---|
| contentID | number / int | Yes | The ID of the content item |
| guid | string | Yes | The website GUID |
| locale | string | Yes | The locale code |
| comments | string | No | Optional comments for the approval request |
| returnBatchId | boolean | No | JavaScript: if true, returns the batch ID immediately without waiting. The .NET equivalent is waitForBatch: false. |
const requestedIds = await apiClient.contentMethods.contentRequestApproval(123, "your-guid", "en-us", "Ready for review")
var requested = await client.Content.RequestApprovalContentItemAsync(guid, locale, 123, "Ready for review");
.NET signature: Task<BatchResult> RequestApprovalContentItemAsync(string instanceGuid, string locale, int contentId, string comments = null, bool waitForBatch = true, CancellationToken cancellationToken = default)
Approves a content item in the workflow system. Same parameters as requesting approval.
const approvedIds = await apiClient.contentMethods.approveContent(123, "your-guid", "en-us", "Approved for publication")
var approved = await client.Content.ApproveContentItemAsync(guid, locale, 123, "Approved for publication");
.NET signature: Task<BatchResult> ApproveContentItemAsync(string instanceGuid, string locale, int contentId, string comments = null, bool waitForBatch = true, CancellationToken cancellationToken = default)
Declines a content item in the workflow system. Same parameters as approving.
const declinedIds = await apiClient.contentMethods.declineContent(123, "your-guid", "en-us", "Needs revision")
var declined = await client.Content.DeclineContentItemAsync(guid, locale, 123, "Needs revision");
.NET signature: Task<BatchResult> DeclineContentItemAsync(string instanceGuid, string locale, int contentId, string comments = null, bool waitForBatch = true, CancellationToken cancellationToken = default)
Deletes a content item through the batch workflow system.
| Parameter | Type | Required | Description |
|---|---|---|---|
| contentID | number / int | Yes | The ID of the content item to delete |
| guid | string | Yes | The website GUID |
| locale | string | Yes | The locale code |
| comments | string | No | Optional comments for the deletion |
| returnBatchId | boolean | No | JavaScript: if true, returns the batch ID immediately without waiting. The .NET equivalent is waitForBatch: false. |
const deletedIds = await apiClient.contentMethods.deleteContent(123, "your-guid", "en-us", "Removing outdated content")
var deleted = await client.Content.DeleteContentItemAsync(
guid,
locale,
123,
comments: "Removing outdated content"
);
.NET signature: Task<BatchResult> DeleteContentItemAsync(string instanceGuid, string locale, int contentId, string comments = null, bool waitForBatch = true, CancellationToken cancellationToken = default)
batchWorkflowContent performs a workflow operation on multiple content items at once, and is the recommended approach for bulk publishing, unpublishing, and approvals. Pass all of your content IDs in a single call instead of looping over the single-item methods.
| Parameter | Type | Required | Description |
|---|---|---|---|
| contentIDs | number[] | Yes | Array of ALL content IDs to process (e.g. [101, 102, 103, 104, ...]) |
| guid | string | Yes | The website GUID |
| locale | string | Yes | The locale code |
| operation | WorkflowOperationType | Yes | The workflow operation to perform |
| returnBatchId | boolean | No | If true, returns the batch ID immediately without waiting. The .NET equivalent is waitForBatch: false. |
| Value | Description |
|---|---|
WorkflowOperationType.Publish | Publish content items |
WorkflowOperationType.Unpublish | Unpublish content items |
WorkflowOperationType.Approve | Approve content items |
WorkflowOperationType.Decline | Decline content items |
WorkflowOperationType.RequestApproval | Request approval for content items |
Returns Promise<number[]> — the processed content IDs, in the same order as the input array. All items are processed as a single atomic batch operation.
import {WorkflowOperationType} from "@agility/management-sdk"
// Pass ALL content IDs you want to publish in a single array
const contentIDsToPublish = [101, 102, 103, 104, 105, 106, 107, 108, 109, 110]
const publishedIds = await apiClient.contentMethods.batchWorkflowContent(
contentIDsToPublish, // All IDs in one call
"your-guid",
"en-us",
WorkflowOperationType.Publish,
)
// Returns all IDs in the same order: [101, 102, 103, 104, 105, 106, 107, 108, 109, 110]
console.log("Published:", publishedIds)
Unpublishing works the same way:
const contentIDs = [101, 102, 103]
const unpublishedIds = await apiClient.contentMethods.batchWorkflowContent(
contentIDs,
"your-guid",
"en-us",
WorkflowOperationType.Unpublish,
)
// unpublishedIds[0] = 101
// unpublishedIds[1] = 102
// unpublishedIds[2] = 103
A full approval process, batched at each step:
const contentIDs = [201, 202, 203]
// Step 1: Request approval for all items
const requestedIds = await apiClient.contentMethods.batchWorkflowContent(
contentIDs,
"your-guid",
"en-us",
WorkflowOperationType.RequestApproval,
)
console.log("Approval requested for:", requestedIds)
// Step 2: Approve all items (typically done by an approver)
const approvedIds = await apiClient.contentMethods.batchWorkflowContent(
contentIDs,
"your-guid",
"en-us",
WorkflowOperationType.Approve,
)
console.log("Approved:", approvedIds)
// Step 3: Publish approved items
const publishedIds = await apiClient.contentMethods.batchWorkflowContent(
approvedIds,
"your-guid",
"en-us",
WorkflowOperationType.Publish,
)
console.log("Published:", publishedIds)
Declining in bulk:
const contentIDs = [301, 302]
const declinedIds = await apiClient.contentMethods.batchWorkflowContent(
contentIDs,
"your-guid",
"en-us",
WorkflowOperationType.Decline,
)
console.log("Declined content IDs:", declinedIds)
In .NET the same operation is BatchWorkflowContentItemsAsync. It returns a BatchResult; pass waitForBatch: false to get the batch ID back without waiting.
using Agility.Management.Sdk.Models;
var result = await client.Content.BatchWorkflowContentItemsAsync(
guid, locale, [101, 102, 103], WorkflowOperationType.Publish);
.NET signature: Task<BatchResult> BatchWorkflowContentItemsAsync(string instanceGuid, string locale, IEnumerable<int> contentIds, WorkflowOperationType operation, bool waitForBatch = true, CancellationToken cancellationToken = default)
Retrieves the history of changes for a specific content item.
| Parameter | Type | Required | Description |
|---|---|---|---|
| locale | string | Yes | The locale code |
| guid | string | Yes | The website GUID |
| contentID | number | Yes | The ID of the content item |
| take | number | No | Number of history entries to retrieve (default: 50) |
| skip | number | No | Number of history entries to skip (default: 0) |
Returns Promise<ContentItemHistory> — the history entries with pagination.
const history = await apiClient.contentMethods.getContentHistory("en-us", "your-guid", 123, 25, 0)
console.log(history.items.length)
.NET:
await client.Content.GetContentItemHistoryAsync(guid, "en-us", 123, take: 25, skip: 0), which returns aContentItemHistoryResponse.
Retrieves comments for a specific content item.
| Parameter | Type | Required | Description |
|---|---|---|---|
| locale | string | Yes | The locale code |
| guid | string | Yes | The website GUID |
| contentID | number | Yes | The ID of the content item |
| take | number | No | Number of comments to retrieve (default: 50) |
| skip | number | No | Number of comments to skip (default: 0) |
Returns Promise<ItemComments> — the comments with pagination.
const comments = await apiClient.contentMethods.getContentComments("en-us", "your-guid", 123, 10, 0)
console.log(comments.items.length)
.NET:
await client.Content.GetContentItemCommentsAsync(guid, "en-us", 123, take: 10, skip: 0), which returns anItemCommentsResponse.
When importing content from an external system or syncing with a third-party data source, follow these guidelines.
batchWorkflowContent() in JavaScript or BatchWorkflowContentItemsAsync() in .NET to publish all saved items at once.Do not use parallel API calls to speed up imports. While it may seem faster, parallel requests can:
Instead, batch your updates in chunks and process the chunks sequentially in a single thread.
A recommended batch size is 50–100 items.
import {WorkflowOperationType} from "@agility/management-sdk"
const BATCH_SIZE = 50
const allSavedIds: number[] = []
for (let i = 0; i < contentItems.length; i += BATCH_SIZE) {
const batch = contentItems.slice(i, i + BATCH_SIZE)
const savedIds = await apiClient.contentMethods.saveContentItems(batch, guid, locale)
allSavedIds.push(...savedIds)
console.log(`Saved batch ${Math.floor(i / BATCH_SIZE) + 1}: ${savedIds.length} items`)
}
// Publish all saved items in batches
for (let i = 0; i < allSavedIds.length; i += BATCH_SIZE) {
const batch = allSavedIds.slice(i, i + BATCH_SIZE)
await apiClient.contentMethods.batchWorkflowContent(batch, guid, locale, WorkflowOperationType.Publish)
console.log(`Published batch ${Math.floor(i / BATCH_SIZE) + 1}: ${batch.length} items`)
}
using Agility.Management.Sdk.Models;
const int BATCH_SIZE = 50;
var allIds = new List<int>();
for (int i = 0; i < contentItems.Count; i += BATCH_SIZE)
{
var batch = contentItems.GetRange(i, Math.Min(BATCH_SIZE, contentItems.Count - i));
var saved = await client.Content.SaveContentItemsAsync(guid, locale, batch);
allIds.AddRange(saved.ItemIds);
Console.WriteLine($"Saved batch {i / BATCH_SIZE + 1}: {saved.ItemIds.Count} items");
}
// Publish all saved items in batches
for (int i = 0; i < allIds.Count; i += BATCH_SIZE)
{
var batch = allIds.GetRange(i, Math.Min(BATCH_SIZE, allIds.Count - i));
await client.Content.BatchWorkflowContentItemsAsync(guid, locale, batch, WorkflowOperationType.Publish);
Console.WriteLine($"Published batch {i / BATCH_SIZE + 1}: {batch.Count} items");
}
Page through the existing content list first, build a lookup map, then split your source records into creates and updates before batching. take defaults to 50, so keep requesting pages with an increasing skip until a short page tells you that you have reached the end.
import {WorkflowOperationType} from "@agility/management-sdk"
async function syncExternalData(externalItems: ExternalItem[]) {
const guid = "your-guid"
const locale = "en-us"
// Step 1: Page through the existing content list and build a lookup map.
// take defaults to 50, so keep paging with skip until a short page comes
// back — that way the map really does cover every existing item.
const PAGE_SIZE = 250
const existingMap = new Map<string, number>()
for (let skip = 0; ; skip += PAGE_SIZE) {
const page = await apiClient.contentMethods.getContentList("products", guid, locale, {
take: PAGE_SIZE,
skip,
})
// Build a map for fast duplicate checking (key = external ID or SKU)
for (const item of page.items) {
if (item.fields.externalId) {
existingMap.set(item.fields.externalId, item.contentID)
}
}
if (page.items.length < PAGE_SIZE) break
}
// Step 2: Separate items into creates vs updates
const itemsToCreate: ContentItem[] = []
const itemsToUpdate: ContentItem[] = []
for (const ext of externalItems) {
const existingId = existingMap.get(ext.externalId)
if (existingId) {
// Update existing item
itemsToUpdate.push({
contentID: existingId,
properties: {definitionName: "Product", referenceName: "products"},
fields: {name: ext.name, price: ext.price, externalId: ext.externalId},
})
} else {
// Create new item
itemsToCreate.push({
contentID: -1,
properties: {definitionName: "Product", referenceName: "products"},
fields: {name: ext.name, price: ext.price, externalId: ext.externalId},
})
}
}
// Step 3: Process in batches (recommended batch size: 50-100 items)
const BATCH_SIZE = 50
const allSavedIds: number[] = []
// Process creates in batches
for (let i = 0; i < itemsToCreate.length; i += BATCH_SIZE) {
const batch = itemsToCreate.slice(i, i + BATCH_SIZE)
const savedIds = await apiClient.contentMethods.saveContentItems(batch, guid, locale)
allSavedIds.push(...savedIds)
}
// Process updates in batches
for (let i = 0; i < itemsToUpdate.length; i += BATCH_SIZE) {
const batch = itemsToUpdate.slice(i, i + BATCH_SIZE)
const savedIds = await apiClient.contentMethods.saveContentItems(batch, guid, locale)
allSavedIds.push(...savedIds)
}
// Step 4: Publish all saved items in batches
for (let i = 0; i < allSavedIds.length; i += BATCH_SIZE) {
const batch = allSavedIds.slice(i, i + BATCH_SIZE)
await apiClient.contentMethods.batchWorkflowContent(batch, guid, locale, WorkflowOperationType.Publish)
}
return {
created: itemsToCreate.length,
updated: itemsToUpdate.length,
published: allSavedIds.length,
}
}
In .NET the same pattern uses
GetContentListAsync()for the lookup andBatchWorkflowContentItemsAsync()for the publishes.
For smaller datasets or one-time initial imports, page through the existing content list and do duplicate checking locally.
async function initialImport(sourceData: SourceItem[]) {
const guid = "your-guid"
const locale = "en-us"
// For initial imports or small lists: page through everything and check
// locally. take defaults to 50, so loop with skip until a short page
// signals the end — otherwise the duplicate check silently misses items.
const PAGE_SIZE = 250
// Build hashtable/map for O(1) duplicate lookups
const existingBySlug = new Map<string, number>()
for (let skip = 0; ; skip += PAGE_SIZE) {
const page = await apiClient.contentMethods.getContentList("articles", guid, locale, {take: PAGE_SIZE, skip})
for (const item of page.items) {
existingBySlug.set(item.fields.slug, item.contentID)
}
if (page.items.length < PAGE_SIZE) break
}
// Filter to only new items
const newItems = sourceData.filter((item) => !existingBySlug.has(item.slug))
if (newItems.length === 0) {
console.log("No new items to import")
return
}
// Transform and save all new items
const contentItems = newItems.map((item) => ({
contentID: -1,
properties: {definitionName: "Article", referenceName: "articles"},
fields: {title: item.title, slug: item.slug, body: item.body},
}))
const savedIds = await apiClient.contentMethods.saveContentItems(contentItems, guid, locale)
// Publish all at once
const publishedIds = await apiClient.contentMethods.batchWorkflowContent(
savedIds,
guid,
locale,
WorkflowOperationType.Publish,
)
console.log(`Imported and published ${publishedIds.length} new items`)
}
| Scenario | Strategy |
|---|---|
| Large ongoing sync | Page through existing → build map → batch saves → batch publishes |
| Small list / initial import | Page through existing → local duplicate check → single batch save → single batch publish |
| Any import | Always use sequential batching, never parallel requests |
This example creates, saves, and publishes content items in bulk while tracking the correlation between the source records and the resulting content IDs.
import agilityMgmt, {WorkflowOperationType} from "@agility/management-sdk"
async function bulkImportAndPublish() {
const apiClient = agilityMgmt.getApi({
location: "USA",
websiteId: "your-guid",
securityKey: "your-api-key",
})
const guid = "your-guid"
const locale = "en-us"
// Source data to import
const sourceData = [
{externalId: "ext-001", title: "First Article", body: "Content 1..."},
{externalId: "ext-002", title: "Second Article", body: "Content 2..."},
{externalId: "ext-003", title: "Third Article", body: "Content 3..."},
{externalId: "ext-004", title: "Fourth Article", body: "Content 4..."},
]
// Step 1: Transform source data into ContentItem objects
const contentItems = sourceData.map((item) => ({
contentID: -1, // New items
properties: {
definitionName: "Article",
referenceName: "articles",
},
fields: {
title: item.title,
body: item.body,
externalId: item.externalId, // Track original ID in a field
},
}))
// Step 2: Bulk save all items using saveContentItems()
const savedIds = await apiClient.contentMethods.saveContentItems(contentItems, guid, locale)
// savedIds are in the SAME ORDER as contentItems input
console.log("Saved content IDs:", savedIds)
// Step 3: Create a mapping of external IDs to Agility content IDs
const idMapping = sourceData.map((item, index) => ({
externalId: item.externalId,
title: item.title,
contentID: savedIds[index], // Same order guarantees correct mapping
}))
// Step 4: Bulk publish all saved items using batchWorkflowContent()
const publishedIds = await apiClient.contentMethods.batchWorkflowContent(
savedIds,
guid,
locale,
WorkflowOperationType.Publish,
)
// publishedIds are in the SAME ORDER as savedIds input
console.log("Published content IDs:", publishedIds)
return idMapping
}
bulkImportAndPublish()
.then((mapping) => console.log("Import complete!", mapping))
.catch((err) => console.error("Import failed:", err))
Expected output:
Saved content IDs: [1001, 1002, 1003, 1004]
Published content IDs: [1001, 1002, 1003, 1004]
Import complete!
The .NET equivalent of
batchWorkflowContent()isBatchWorkflowContentItemsAsync().
All methods throw on failure: Exception objects in JavaScript, AgilityManagementException in .NET (and AgilityBatchException when items in a batch fail).
try {
const contentItem = await apiClient.contentMethods.getContentItem(123, "your-guid", "en-us")
} catch (error) {
console.error("Failed to get content item:", error.message)
}
try
{
var item = await client.Content.GetContentItemAsync(guid, locale, 123);
}
catch (AgilityManagementException ex)
{
Console.Error.WriteLine($"Error ({ex.StatusCode}): {ex.ApiMessage ?? ex.Message}");
}