# Content Items

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

The `ContentMethods` class handles 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.contentMethods` in .NET — and every method takes the instance `guid` and a `locale` in addition to its own arguments.

The JavaScript and .NET SDKs cover the same core operations but differ in two important ways:

- **Return shapes.** JavaScript workflow methods return `Promise<number[]>` (an array of content IDs). The .NET equivalents return `Task<int?>` for single-item operations and `Task<List<object?>>` for bulk saves. Single and batch saves are separate API endpoints, and a single save returns a single content ID — see [Saving a Single Content Item](#saving-a-single-content-item).
- **Coverage.** Bulk workflow operations, advanced list filtering, history, and comments exist only in the JavaScript SDK today.

## Method Overview

| Operation | JavaScript | .NET |
| --- | --- | --- |
| Retrieve a content item by ID | `getContentItem` | `GetContentItem` |
| List content items in a container | `getContentItems` (deprecated) | `GetContentItems` |
| List with advanced filtering | `getContentList` | — |
| Save a single item (create or update) | `saveContentItem` | `SaveContentItem` |
| Save multiple items in bulk | `saveContentItems` | `SaveContentItems` |
| Publish an item | `publishContent` | `PublishContent` |
| Unpublish an item | `unPublishContent` | `UnPublishContent` |
| Request approval | `contentRequestApproval` | `ContentRequestApproval` |
| Approve an item | `approveContent` | `ApproveContent` |
| Decline an item | `declineContent` | `DeclineContent` |
| Delete an item | `deleteContent` | `DeleteContent` |
| Bulk workflow on many items at once | `batchWorkflowContent` | — |
| Retrieve item history | `getContentHistory` | — |
| Retrieve item comments | `getContentComments` | — |

## Bulk Operations Overview

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()` / `SaveContentItem()` | **`saveContentItems()` / `SaveContentItems()`** | Creating or updating multiple items |
| `publishContent()` | **`batchWorkflowContent()`** (JavaScript only) | Publishing multiple items at once |
| `unPublishContent()` | **`batchWorkflowContent()`** (JavaScript only) | 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. In .NET, bulk workflow operations are not available — save in batches, then publish each item individually.

---

## Retrieving a Content Item

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.

<div class="code-tabs" data-tabs="JavaScript,.NET">

```ts
const contentItem = await apiClient.contentMethods.getContentItem(123, "your-guid", "en-us")
console.log(contentItem.fields.title)
```

```csharp
var contentItem = await client.contentMethods.GetContentItem(123, guid, locale);
Console.WriteLine(contentItem.Fields["title"]);
```

</div>

.NET signature: `Task<ContentItem> GetContentItem(int? contentID, string guid, string locale)`

---

## Listing Content Items

Lists content items from a container with pagination and basic filtering.

In JavaScript, `getContentItems` is **deprecated** — use [`getContentList`](#listing-with-advanced-filtering) instead. It takes a `ListParams` object holding `take`, `skip`, `sortField`, and `sortDirection`. In .NET, `GetContentItems` is the supported list method and takes the filtering options as discrete optional arguments.

| Parameter | JavaScript | .NET |
| --- | --- | --- |
| referenceName | Yes | Yes |
| guid | Yes | Yes |
| locale | Yes | Yes |
| Pagination / sorting | `listParams: ListParams` | `take`, `skip`, `sortField`, `sortDirection` |
| Filtering | via `listParams` | `filter` (OData string), `fields` (comma-separated field names) |

Returns a `ContentList` containing the items and pagination info.

<div class="code-tabs" data-tabs="JavaScript,.NET">

```ts
const listParams = {
	take: 10,
	skip: 0,
	sortField: "title",
	sortDirection: "asc",
}

const contentList = await apiClient.contentMethods.getContentItems("articles", "your-guid", "en-us", listParams)
```

```csharp
var contentList = await client.contentMethods.GetContentItems(
    referenceName: "blog-posts",
    guid: guid,
    locale: locale,
    filter: null,           // OData filter string (optional)
    fields: null,           // Comma-separated field names (optional)
    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 ?? [])
{
    Console.WriteLine(item.ContentID);
}
```

</div>

.NET signature: `Task<ContentList?> GetContentItems(string? referenceName, string guid, string locale, string? filter = null, string? fields = null, string? sortDirection = null, string? sortField = null, int? take = 50, int? skip = 0)`

---

## Listing with Advanced Filtering

`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 |

```ts
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.

> Not available in the .NET SDK yet.

---

## Saving a Single Content Item

> **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](#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 only — if true, returns the batch ID immediately without waiting |

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](/docs/javascript/management-sdk/getting-started#how-writes-complete).
Pass `returnBatchId: true` (JavaScript only) to get the batch ID immediately and poll it yourself.

So a single save yields one content ID. .NET surfaces it directly as `Task<int>`. 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.

<div class="code-tabs" data-tabs="JavaScript,.NET">

```ts
// 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")
```

```csharp
using agility.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 Dictionary<string, object?>
    {
        { "title", "New Article" },
        { "slug", "new-article" },
        { "content", "Article content here..." }
    }
};

var newContentID = await client.contentMethods.SaveContentItem(newItem, guid, locale);
Console.WriteLine($"Created content ID: {newContentID}");

// Update an existing item
var existing = await client.contentMethods.GetContentItem(123, guid, locale);
existing.Fields["title"] = "Updated Title";
var updatedID = await client.contentMethods.SaveContentItem(existing, guid, locale);
```

</div>

.NET signature: `Task<int> SaveContentItem(ContentItem? contentItem, string guid, string locale)`

### The ContentItem Shape

In JavaScript, `ContentItem` is an interface:

```ts
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.models` namespace, where `Properties` is a `ContentItemProperties` object (`DefinitionName`, `ReferenceName`) and `Fields` is a `Dictionary<string, object?>`.

---

## Saving Multiple Content Items (Bulk)

**Recommended for bulk operations.** Saves multiple content items in a single batch call.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| contentItems | `ContentItem[]` / `List<ContentItem?>` | Yes | The content items to save |
| guid | string | Yes | The website GUID |
| locale | string | Yes | The locale code |
| returnBatchId | boolean | No | JavaScript only — if true, returns the batch ID immediately without waiting |

Notes that apply to both SDKs:

- **Order preservation:** the returned IDs are in the same order as the input, so you can correlate each returned ID with its input item.
- **Performance:** for 2+ items, always use the bulk method instead of looping over the single-item method.
- **Mixed operations:** you can mix new items (`contentID: -1`) and updates (an existing `contentID`) in the same batch.
- **Failures:** a `-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.

JavaScript returns `Promise<number[]>`; .NET returns `Task<List<object?>>` holding the content IDs.

<div class="code-tabs" data-tabs="JavaScript,.NET">

```ts
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)
```

```csharp
var items = new List<ContentItem?>
{
    new ContentItem
    {
        ContentID = -1,
        Properties = new ContentItemProperties { DefinitionName = "BlogPost", ReferenceName = "blog-posts" },
        Fields = new Dictionary<string, object?> { { "title", "Article 1" } }
    },
    new ContentItem
    {
        ContentID = -1,
        Properties = new ContentItemProperties { DefinitionName = "BlogPost", ReferenceName = "blog-posts" },
        Fields = new Dictionary<string, object?> { { "title", "Article 2" } }
    }
};

var results = await client.contentMethods.SaveContentItems(items, guid, locale);
// results is a List<object?> containing the content IDs
Console.WriteLine($"Saved IDs: {string.Join(", ", results)}");
```

</div>

.NET signature: `Task<List<object?>> SaveContentItems(List<ContentItem?> contentItems, string guid, string locale)`

### Correlating Results with Input Data

Because the returned IDs keep their input order, you can map them straight back onto your source records.

```ts
// 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 }
// ]
```

### Mixed Create and Update Operations

```ts
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)
```

---

## Publishing

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 only — if true, returns the batch ID immediately without waiting |

JavaScript returns `Promise<number[]>` containing the published content ID; .NET returns `Task<int?>`.

In JavaScript, to publish more than one item use [`batchWorkflowContent()`](#bulk-workflow-operations) with `WorkflowOperationType.Publish` rather than looping.

<div class="code-tabs" data-tabs="JavaScript,.NET">

```ts
// 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)
```

```csharp
var publishedID = await client.contentMethods.PublishContent(
    contentID: 123,
    guid: guid,
    locale: locale,
    comments: "Publishing update" // optional
);
Console.WriteLine($"Published ID: {publishedID}");
```

</div>

.NET signature: `Task<int?> PublishContent(int? contentID, string guid, string locale, string? comments = null)`

---

## Unpublishing

Unpublishes a single content item through the batch workflow system. Takes the same parameters as publishing.

In JavaScript, to unpublish more than one item use [`batchWorkflowContent()`](#bulk-workflow-operations) with `WorkflowOperationType.Unpublish`.

<div class="code-tabs" data-tabs="JavaScript,.NET">

```ts
const unpublishedIds = await apiClient.contentMethods.unPublishContent(123, "your-guid", "en-us", "Temporary unpublish")
console.log("Unpublished ID:", unpublishedIds[0])
```

```csharp
var unpublishedID = await client.contentMethods.UnPublishContent(
    contentID: 123,
    guid: guid,
    locale: locale,
    comments: "Temporarily unpublishing"
);
```

</div>

.NET signature: `Task<int?> UnPublishContent(int? contentID, string guid, string locale, string? comments = null)`

---

## Requesting Approval

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 only — if true, returns the batch ID immediately without waiting |

<div class="code-tabs" data-tabs="JavaScript,.NET">

```ts
const requestedIds = await apiClient.contentMethods.contentRequestApproval(123, "your-guid", "en-us", "Ready for review")
```

```csharp
var id = await client.contentMethods.ContentRequestApproval(123, guid, locale, "Ready for review");
```

</div>

.NET signature: `Task<int?> ContentRequestApproval(int? contentID, string guid, string locale, string? comments = null)`

---

## Approving

Approves a content item in the workflow system. Same parameters as requesting approval.

<div class="code-tabs" data-tabs="JavaScript,.NET">

```ts
const approvedIds = await apiClient.contentMethods.approveContent(123, "your-guid", "en-us", "Approved for publication")
```

```csharp
var id = await client.contentMethods.ApproveContent(123, guid, locale, "Approved for publication");
```

</div>

.NET signature: `Task<int?> ApproveContent(int? contentID, string guid, string locale, string? comments = null)`

---

## Declining

Declines a content item in the workflow system. Same parameters as approving.

<div class="code-tabs" data-tabs="JavaScript,.NET">

```ts
const declinedIds = await apiClient.contentMethods.declineContent(123, "your-guid", "en-us", "Needs revision")
```

```csharp
var id = await client.contentMethods.DeclineContent(123, guid, locale, "Needs revision");
```

</div>

.NET signature: `Task<int?> DeclineContent(int? contentID, string guid, string locale, string? comments = null)`

---

## Deleting

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 only — if true, returns the batch ID immediately without waiting |

<div class="code-tabs" data-tabs="JavaScript,.NET">

```ts
const deletedIds = await apiClient.contentMethods.deleteContent(123, "your-guid", "en-us", "Removing outdated content")
```

```csharp
var deletedID = await client.contentMethods.DeleteContent(
    contentID: 123,
    guid: guid,
    locale: locale,
    comments: "Removing outdated content"
);
```

</div>

.NET signature: `Task<int?> DeleteContent(int? contentID, string guid, string locale, string? comments = null)`

---

## Bulk Workflow Operations

`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 |

### WorkflowOperationType Values

| 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.

```ts
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:

```ts
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:

```ts
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:

```ts
const contentIDs = [301, 302]

const declinedIds = await apiClient.contentMethods.batchWorkflowContent(
	contentIDs,
	"your-guid",
	"en-us",
	WorkflowOperationType.Decline,
)
console.log("Declined content IDs:", declinedIds)
```

> Not available in the .NET SDK yet. In .NET, save in batches and then call `PublishContent` (or the relevant workflow method) per item.

---

## Content Item History

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.

```ts
const history = await apiClient.contentMethods.getContentHistory("en-us", "your-guid", 123, 25, 0)
console.log(history.items.length)
```

> Not available in the .NET SDK yet.

---

## Content Item Comments

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.

```ts
const comments = await apiClient.contentMethods.getContentComments("en-us", "your-guid", 123, 10, 0)
console.log(comments.items.length)
```

> Not available in the .NET SDK yet.

---

## Best Practices for Imports and Syncs

When importing content from an external system or syncing with a third-party data source, follow these guidelines.

1. **Do all lookups first** — check which items already exist before making any changes.
2. **Batch your saves** — use the bulk save method to write multiple items in a single call.
3. **Batch your publishes** — in JavaScript, use `batchWorkflowContent()` to publish all saved items at once. In .NET, publish per item.
4. **Use single-threaded processing** — process batches sequentially, not in parallel.

### Why Avoid Parallelism?

**Do not use parallel API calls to speed up imports.** While it may seem faster, parallel requests can:

- Overwhelm the API and cause rate limiting
- Lead to race conditions and inconsistent state
- Result in batch conflicts and failed operations

Instead, batch your updates in chunks and process the chunks sequentially in a single thread.

### Batch Save and Publish Pattern

A recommended batch size is 50–100 items.

<div class="code-tabs" data-tabs="JavaScript,.NET">

```ts
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`)
}
```

```csharp
const int BATCH_SIZE = 50;
var allIds = new List<object?>();

for (int i = 0; i < contentItems.Count; i += BATCH_SIZE)
{
    var batch = contentItems.GetRange(i, Math.Min(BATCH_SIZE, contentItems.Count - i));
    var savedIds = await client.contentMethods.SaveContentItems(batch, guid, locale);
    allIds.AddRange(savedIds);
    Console.WriteLine($"Saved batch {i / BATCH_SIZE + 1}: {savedIds.Count} items");
}

// Publish each item
foreach (var idObj in allIds)
{
    if (idObj is int id && id > 0)
    {
        await client.contentMethods.PublishContent(id, guid, locale);
    }
}
```

</div>

### Strategy for Large Ongoing Syncs

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.

```ts
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,
	}
}
```

> This example uses `getContentList()` and `batchWorkflowContent()`, which are JavaScript-only. In .NET, use `GetContentItems()` for the lookup and `PublishContent()` per item.

### Strategy for Small Lists or Initial Imports

For smaller datasets or one-time initial imports, page through the existing content list and do duplicate checking locally.

```ts
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`)
}
```

### Summary

| 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** |

---

## Complete Bulk Workflow Example

This example creates, saves, and publishes content items in bulk while tracking the correlation between the source records and the resulting content IDs.

```ts
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!
```

> Uses `batchWorkflowContent()`, which is JavaScript-only.

---

## Error Handling

All methods throw on failure — `Exception` objects in JavaScript, `ApplicationException` in .NET.

<div class="code-tabs" data-tabs="JavaScript,.NET">

```ts
try {
	const contentItem = await apiClient.contentMethods.getContentItem(123, "your-guid", "en-us")
} catch (error) {
	console.error("Failed to get content item:", error.message)
}
```

```csharp
try
{
    var item = await client.contentMethods.GetContentItem(123, guid, locale);
}
catch (ApplicationException ex)
{
    Console.Error.WriteLine($"Error: {ex.Message}");
}
```

</div>
