# Management SDK - Content

> Source: https://agilitycms.com/docs/dotNet/management-sdk-dotnet-content

The content client reads, lists, saves, deletes and runs workflow on content items. You reach it through `client.Content` on an `AgilityManagementClient`.

> **Note:** This page covers version 2.0 of the .NET Management SDK. Upgrading from 1.x? See [Migrating to 2.0](https://agilitycms.com/docs/dotNet/management-sdk-dotnet-migrating-to-2).

## Before you start

The examples assume a client, an instance GUID and a locale. To set up the client, see the [Intro](https://agilitycms.com/docs/dotNet/management-sdk-dotnet-intro).

```csharp
using System.Net;
using System.Text.Json.Nodes;
using Agility.Management.Sdk;
using Agility.Management.Sdk.Clients;
using Agility.Management.Sdk.Models;

using var client = new AgilityManagementClient(new AgilityManagementOptions
{
    AccessToken = Environment.GetEnvironmentVariable("AGILITY_TOKEN"),
});

var guid = "<<your-instance-guid>>";
var locale = "en-us";
```

Every method takes the instance GUID first, then the locale, then IDs, then optional settings. Every method also takes an optional `CancellationToken cancellationToken` as its last parameter. The signatures below leave it out.

### How saves and workflow work

- **Saves, deletes and workflow actions run as batches.** By default the SDK waits for each batch and returns a `BatchResult`. See [Batches](https://agilitycms.com/docs/dotNet/management-sdk-dotnet-intro#batches).
- **A save replaces the whole item.** Read the item, change it, and send all of it back. If you send only the field you changed, the other fields are lost.
- **A save always lands in Staging,** even for a published item. The live site keeps the previous version until you publish again.

## Method list

| Method | Description |
|---|---|
| `GetContentItemAsync` | Get a content item by ID |
| `GetContentItemsByIdAsync` | Get several content items in one request |
| `GetContentListAsync` | List the items in a container, with paging, sorting and filters |
| `SaveContentItemAsync` | Create or update one content item |
| `SaveContentItemsAsync` | Create or update several content items in one batch |
| `PublishContentItemAsync` | Publish a content item |
| `UnpublishContentItemAsync` | Unpublish a content item |
| `RequestApprovalContentItemAsync` | Request approval for a content item |
| `ApproveContentItemAsync` | Approve a content item |
| `DeclineContentItemAsync` | Decline a content item |
| `DeleteContentItemAsync` | Delete a content item |
| `BatchWorkflowContentItemsAsync` | Run one workflow action on many items in one batch |
| `GetCascadeItemsAsync` | List what a cascade publish would include |
| `PublishContentItemCascadeAsync` | Publish an item and its one-level nested content |
| `GetContentItemHistoryAsync` | Get an item's version history |
| `GetContentItemCommentsAsync` | Get an item's comments |

---

## Reading content

### GetContentItemAsync

Get a content item by its content ID.

```csharp
ContentItem item = await client.Content.GetContentItemAsync(guid, locale, 123);

string? title = item.Fields?["title"]?.GetValue<string>();
int? state = item.Properties?.State;   // compare with the ItemState enum
```

`Fields` is a `JsonObject` keyed by field name, in camelCase. It holds whatever the model defines: text, numbers, linked content and image objects. Read a value with `GetValue<T>()`, or turn a field into your own type with `Deserialize<T>()`.

**Signature:** `Task<ContentItem> GetContentItemAsync(string instanceGuid, string locale, int contentId)`

---

### GetContentItemsByIdAsync

Get several content items in one request.

```csharp
List<ContentItem> items = await client.Content.GetContentItemsByIdAsync(guid, locale, [123, 124, 125]);
```

Pass at least one ID. An empty list throws an `ArgumentException`.

**Signature:** `Task<List<ContentItem>> GetContentItemsByIdAsync(string instanceGuid, string locale, IEnumerable<int> contentIds)`

---

### GetContentListAsync

List the items in a container, one page at a time. Name the container by its reference name.

```csharp
ContentList page = await client.Content.GetContentListAsync(guid, locale, "blogposts",
    new ContentListOptions { Take = 50, Skip = 0, SortField = "title", SortDirection = "asc" });

Console.WriteLine($"Total: {page.TotalCount}");
foreach (JsonNode? row in page.Items ?? [])
{
    Console.WriteLine(row?["contentID"]);
}
```

`ContentListOptions` controls paging, sorting and filtering. Leave out the options to get the first page of everything.

| `ContentListOptions` | |
|---|---|
| `Take` | Items per page. The API's default is 50. |
| `Skip` | How many items to skip |
| `SortField` | The field to sort by |
| `SortDirection` | `asc` or `desc` |
| `Fields` | Comma-separated field names to return, such as `"title,category"`. This keeps large lists fast. |
| `ShowDeleted` | Include deleted items |
| `Filter` | A `ContentListFilterModel` (see below) |

To filter, set `Filter` to a `ContentListFilterModel`:

```csharp
var filter = new ContentListFilterModel
{
    GenericSearch = "launch",                       // free-text search
    StateIds = [(int)ItemState.Published],
    DateRange = new DateRangeFilter { StartDate = DateTime.UtcNow.AddDays(-30) },
    FieldFilters =
    [
        new FieldFilter { Field = "category", Value = new FieldFilterValue { StringValue = "news" } },
    ],
};

ContentList recent = await client.Content.GetContentListAsync(guid, locale, "blogposts",
    new ContentListOptions { Filter = filter, Take = 20 });
```

A `FieldFilterValue` holds one kind of value: `StringValue`, `BoolValue`, `DateRangeValue` (a `DateRangeFilter`) or `NumRangeValue` (a `NumRangeFilter` with `FromNum` and `ToNum`).

> **Note:** In 1.x, `GetContentItems` took a `filter` string that the API ignored. 2.0 uses the documented list route, so filters now apply.

**Signature:** `Task<ContentList> GetContentListAsync(string instanceGuid, string locale, string referenceName, ContentListOptions? options = null)`

---

## Saving content

### SaveContentItemAsync

Create or update one content item.

To update an item, read it first, change it, and send the whole item back:

```csharp
var existing = await client.Content.GetContentItemAsync(guid, locale, 123);
existing.Fields!["title"] = "Updated Title";

BatchResult saved = await client.Content.SaveContentItemAsync(guid, locale, existing);
```

To create an item, use a `ContentID` of `-1` and name the container (`ReferenceName`) and the model (`DefinitionName`):

```csharp
var created = await client.Content.SaveContentItemAsync(guid, locale, new ContentItem
{
    ContentID = -1,
    Properties = new ContentItemProperties
    {
        ReferenceName = "blogposts",
        DefinitionName = "BlogPost",
    },
    Fields = new JsonObject
    {
        ["title"] = "New Article",
        ["slug"] = "new-article",
        ["content"] = "<p>Article content here...</p>",
    },
});

int newContentId = created.ItemId!.Value;
```

Any `ContentID` of 0 or less creates a new item.

**Signature:** `Task<BatchResult> SaveContentItemAsync(string instanceGuid, string locale, ContentItem item, bool waitForBatch = true)`

**Returns:** A `BatchResult`. `ItemId` is the item's content ID.

---

### SaveContentItemsAsync

Save several content items in one batch. You can mix new items and updates in the same call.

```csharp
var items = new List<ContentItem>
{
    new()
    {
        ContentID = -1,
        Properties = new ContentItemProperties { ReferenceName = "blogposts", DefinitionName = "BlogPost" },
        Fields = new JsonObject { ["title"] = "Article 1" },
    },
    new()
    {
        ContentID = -1,
        Properties = new ContentItemProperties { ReferenceName = "blogposts", DefinitionName = "BlogPost" },
        Fields = new JsonObject { ["title"] = "Article 2" },
    },
};

BatchResult result = await client.Content.SaveContentItemsAsync(guid, locale, items);
Console.WriteLine($"Saved IDs: {string.Join(", ", result.ItemIds)}");
```

If any item fails, the call throws an `AgilityBatchException`. Its `Batch.Items` shows which items succeeded and which failed. (In 1.x, a failed item appeared as `-1` in the returned list.)

**Signature:** `Task<BatchResult> SaveContentItemsAsync(string instanceGuid, string locale, IReadOnlyList<ContentItem> items, bool waitForBatch = true)`

**Returns:** A `BatchResult`. `ItemIds` lists the content IDs.

---

## Workflow

Each workflow method takes an optional `comments` string for the item's history, and returns a `BatchResult`.

### PublishContentItemAsync

Publish a content item.

```csharp
await client.Content.PublishContentItemAsync(guid, locale, 123, comments: "Publishing update");
```

**Signature:** `Task<BatchResult> PublishContentItemAsync(string instanceGuid, string locale, int contentId, string? comments = null, bool waitForBatch = true)`

---

### UnpublishContentItemAsync

Unpublish a content item.

```csharp
await client.Content.UnpublishContentItemAsync(guid, locale, 123, comments: "Temporarily unpublishing");
```

**Signature:** `Task<BatchResult> UnpublishContentItemAsync(string instanceGuid, string locale, int contentId, string? comments = null, bool waitForBatch = true)`

---

### RequestApprovalContentItemAsync

Submit a content item for approval.

```csharp
await client.Content.RequestApprovalContentItemAsync(guid, locale, 123, comments: "Ready for review");
```

**Signature:** `Task<BatchResult> RequestApprovalContentItemAsync(string instanceGuid, string locale, int contentId, string? comments = null, bool waitForBatch = true)`

---

### ApproveContentItemAsync

Approve a content item.

```csharp
await client.Content.ApproveContentItemAsync(guid, locale, 123, comments: "Approved for publication");
```

**Signature:** `Task<BatchResult> ApproveContentItemAsync(string instanceGuid, string locale, int contentId, string? comments = null, bool waitForBatch = true)`

---

### DeclineContentItemAsync

Decline a content item.

```csharp
await client.Content.DeclineContentItemAsync(guid, locale, 123, comments: "Needs revision");
```

**Signature:** `Task<BatchResult> DeclineContentItemAsync(string instanceGuid, string locale, int contentId, string? comments = null, bool waitForBatch = true)`

---

### DeleteContentItemAsync

Delete a content item.

```csharp
await client.Content.DeleteContentItemAsync(guid, locale, 123, comments: "Removing outdated content");
```

**Signature:** `Task<BatchResult> DeleteContentItemAsync(string instanceGuid, string locale, int contentId, string? comments = null, bool waitForBatch = true)`

---

### BatchWorkflowContentItemsAsync

Run one workflow action on many content items in a single batch. The action is a `WorkflowOperationType`: `Publish`, `Unpublish`, `Approve`, `Decline` or `RequestApproval`.

```csharp
await client.Content.BatchWorkflowContentItemsAsync(guid, locale, [123, 124, 125], WorkflowOperationType.Publish);
```

Pass at least one ID.

**Signature:** `Task<BatchResult> BatchWorkflowContentItemsAsync(string instanceGuid, string locale, IEnumerable<int> contentIds, WorkflowOperationType operation, bool waitForBatch = true)`

---

## Publishing what an item depends on

An item can link to other content and nested lists. A cascade publish publishes the item together with its one-level nested content: the non-shared containers its fields point at (dynamic page lists are not included).

### GetCascadeItemsAsync

List the items a cascade publish would include, as a tree.

```csharp
CascadeItem tree = await client.Content.GetCascadeItemsAsync(guid, locale, 123);

foreach (var child in tree.Children ?? [])
{
    Console.WriteLine($"{child.ItemType} {child.ItemID}: {child.Title}");
}
```

**Signature:** `Task<CascadeItem> GetCascadeItemsAsync(string instanceGuid, string locale, int contentId)`

---

### PublishContentItemCascadeAsync

Publish an item and its one-level nested content. The API can create more than one batch, and this method doesn't wait for them. Wait for each one with `client.Batches.WaitForBatchAsync`:

```csharp
BatchCreateResult created = await client.Content.PublishContentItemCascadeAsync(guid, locale, 123);

foreach (var batchId in created.BatchIDs ?? [])
{
    await client.Batches.WaitForBatchAsync(guid, batchId);
}
```

**Signature:** `Task<BatchCreateResult> PublishContentItemCascadeAsync(string instanceGuid, string locale, int contentId, string? comments = null)`

---

## History and comments

### GetContentItemHistoryAsync

Get an item's version history, a page at a time.

```csharp
ContentItemHistoryResponse history = await client.Content.GetContentItemHistoryAsync(guid, locale, 123, take: 20);

foreach (var version in history.Items ?? [])
{
    Console.WriteLine($"v{version.VersionNumber} {version.State} by {version.CreatedBy} on {version.CreatedDate}");
}
```

**Signature:** `Task<ContentItemHistoryResponse> GetContentItemHistoryAsync(string instanceGuid, string locale, int contentId, int? take = null, int? skip = null)`

---

### GetContentItemCommentsAsync

Get an item's comments, a page at a time.

```csharp
ItemCommentsResponse comments = await client.Content.GetContentItemCommentsAsync(guid, locale, 123);

foreach (var comment in comments.Items ?? [])
{
    Console.WriteLine($"{comment.CreatedBy}: {comment.Comment}");
}
```

**Signature:** `Task<ItemCommentsResponse> GetContentItemCommentsAsync(string instanceGuid, string locale, int contentId, int? take = null, int? skip = null)`

---

## Batches

The batch client, `client.Batches`, checks on batches and lets you build your own.

### Waiting for a batch later

Pass `waitForBatch: false` to return as soon as the batch is queued. Wait for it later with `WaitForBatchAsync`:

```csharp
BatchResult queued = await client.Content.PublishContentItemAsync(guid, locale, 123, waitForBatch: false);

// ... later
Batch batch = await client.Batches.WaitForBatchAsync(guid, queued.BatchId);
```

`GetBatchAsync(guid, batchId)` gets a batch's current state without waiting.

**Signature:** `Task<Batch> WaitForBatchAsync(string instanceGuid, int batchId)`

---

### CreateBatchAsync

Group workflow actions on content items and pages into one batch of your own.

```csharp
BatchCreateResult batch = await client.Batches.CreateBatchAsync(guid, new CreateBatchWithItemsRequest
{
    BatchName = "Spring launch",
    Operation = WorkflowOperationType.Publish,
    Items =
    [
        new AddBatchItemRequest { ItemType = BatchItemType.ContentItem, ItemID = 123, LanguageCode = "en-us" },
        new AddBatchItemRequest { ItemType = BatchItemType.Page, ItemID = 7, LanguageCode = "en-us" },
    ],
}, processNow: true);
```

Set `Operation`, and give every item an `ItemType`. The SDK throws an `ArgumentException` if either is missing. With `processNow: true` the batch is queued for processing straight away. Otherwise it's left as a draft.

`PublishBatchAsync`, `UnpublishBatchAsync`, `ApproveBatchAsync`, `DeclineBatchAsync` and `RequestApprovalBatchAsync` run an action on every item in an existing batch. Each takes `(guid, batchId)` and returns a `BatchResult`.

**Signature:** `Task<BatchCreateResult> CreateBatchAsync(string instanceGuid, CreateBatchWithItemsRequest request, bool? processNow = null)`

---

## Best practices

### Bulk save and publish

To import or sync content, save items in chunks with `SaveContentItemsAsync`, then publish each chunk with one `BatchWorkflowContentItemsAsync` call:

```csharp
const int ChunkSize = 50;

foreach (var chunk in contentItems.Chunk(ChunkSize))
{
    BatchResult saved = await client.Content.SaveContentItemsAsync(guid, locale, chunk);
    await client.Content.BatchWorkflowContentItemsAsync(guid, locale, saved.ItemIds, WorkflowOperationType.Publish);
    Console.WriteLine($"Saved and published {saved.ItemIds.Count} items");
}
```

### Waiting for the website to see a change

A processed publish batch means the Management API has published the item. The Fetch API, which your website reads from, syncs shortly after. To wait for that too:

```csharp
await client.SyncStatus.WaitForFetchApiSyncAsync(guid, SyncMode.Fetch);
```

### Error handling

```csharp
try
{
    await client.Content.SaveContentItemAsync(guid, locale, item);
}
catch (AgilityBatchException ex)
{
    // The batch ran, but some items failed.
    foreach (var failed in ex.Batch?.Items?.Where(i => i.ErrorMessage is not null) ?? [])
    {
        Console.Error.WriteLine($"{failed.ItemID}: {failed.ErrorMessage}");
    }
}
catch (AgilityManagementException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
    Console.Error.WriteLine(ex.ApiMessage);
}
```

API and network errors throw an `AgilityManagementException`, and failed or timed-out batches throw an `AgilityBatchException` or `AgilityBatchTimeoutException`. See [Errors](https://agilitycms.com/docs/dotNet/management-sdk-dotnet-intro#errors).

The SDK retries reads that fail with a temporary error. It never retries saves, deletes or workflow actions, because repeating them would repeat the change.
