Show examples in:
Javascript HTTP
Content Management API > Record

Update a record

📚 New to DatoCMS records?

We strongly recommend reading the Introduction to Records guide first. The payload for updating a record follows the same structure as creating one, so that guide is also an essential prerequisite.

The fundamental rules for structuring field values (i.e., strings, numbers, objects, references) are the same for both creating and updating records. For a complete reference on how to format the value for every field type, please see the Field Types Overview in the main records guide.

When updating an existing record, you only need to provide the fields you want to change. Any fields you omit from your payload will remain untouched.

⚠️ Null vs. Omitted Fields

There's a crucial difference between omitting a field and explicitly setting it to null or an empty value:

  • Omitted fields keep their existing values unchanged
  • Fields set to null or empty values (like [] for arrays) are cleared/deleted

The following sections highlight the rules and strategies that are specific to the update process.

TypeScript typing

Writing an update payload without typed schemas means writing blind: every field is unknown, typos compile fine, and mistakes only surface as 422s from the API. The single biggest lever you have is passing a generated Schema.X marker as the generic on items.update. TypeScript then enforces the model's field names, types, and allowed block shapes at compile time:

// ❌ Untyped: every field is `unknown`, typos compile.
await client.items.update("record-id", { /* … */ });
// ✅ Typed: field names, types, and block shapes enforced.
await client.items.update<Schema.Article>("record-id", { /* … */ });

When you're rebuilding a field value piece-by-piece — typically an array of blocks or links — annotate the accumulator with FieldValueInRequest<T, 'field_key'>. The first argument accepts any item-shaped value the CMA produces (top-level record or nested block, narrowed or not) or a Schema.X marker directly when no value is in scope yet:

const article = await client.items.find<Schema.Article>("record-id", { nested: true });
if (article.content) {
const content: NonNullable<FieldValueInRequest<typeof article, "content">> =
/* ...transform article.content... */;
await client.items.update<Schema.Article>("record-id", { content });
}
// Same expression on a narrowed nested block:
for (const block of article.sections) {
if (isBlockOfType(Schema.HeroBlock.ID, block)) {
const ctas: NonNullable<FieldValueInRequest<typeof block, "ctas">> = [];
// …rebuild the nested field…
}
}
// Or, if you don't have a value yet, pass the marker:
type ArticleContent = NonNullable<FieldValueInRequest<Schema.Article, "content">>;
⚠️ Field values are always nullable

Even fields with a required validator are typed as Nullable, because the API may still accept/return null in some scenarios.

The example therefore checks if (article.content) and uses NonNullable<…> to narrow the type. Without this, accessing nested properties like content.document.children would require optional chaining (?.).

Updating Block Fields

The general workflow is the same for every block field type:

  1. Generate types with the TypeScript schema generator.
  2. Fetch records with nested: true so blocks come back as full objects you can edit. Without it you only get IDs — fine for keep/reorder/delete, not for editing.
  3. Build the payload following the per-field-type rules below. Lean on the provided helpers as much as possible instead of assembling structures by hand.
  4. Send a single client.items.update<Article> call, omitting fields you don't change.

Modular Content

Payload is an array of blocks. Each entry expresses one operation:

OperationEntry
Keepblock ID string
EditbuildBlockRecord<T>({ id, ...changedAttrs })
CreatebuildBlockRecord<T>({ ...allAttrs })
CloneduplicateBlockRecord<T>(block, schemaRepository)
Deleteomit from the array
Reorderrearrange the array

Single Block

A single-block field holds at most one block (or null), so the payload is the value itself — not an array. The Modular Content rules apply, minus anything position-related.

Structured Text

Structured Text stores rich content as a DAST tree. Embedded records show up as two extra node kinds: block and inlineBlock. Both wrap a full DatoCMS block, so the same buildBlockRecord/duplicateBlockRecord primitives from Modular Content and Single Block still apply, but they're composed inside a tree-mutation pipeline rather than dropped into an array slot.

The pipeline has two passes, each exposing the document at a different level of granularity. Pick the highest-level pass that can still see what you want to change. Going lower buys you more power but costs you a lot of tree gymnastics.

Pass 1: Rewrite the prose (dastdown)

datocms-structured-text-dastdown translates the DAST tree to and from a markdown-like text format:

A normal paragraph with ==highlighted==, ++underlined++ and ~~struck~~ text.
Links can carry trailers: [our docs](https:datocms.com){target="_blank"}.
References to other records read like [this article](dato:item/abc123),
or inline as <inlineItem id="abc123"/>.
> A pull quote from the interview.
> {attribution="Jane Doe"}
<block id="def456"/>
Inline blocks <inlineBlock id="ghi789"/> sit mid-sentence.

This allows you to make changes with ordinary string operations and let parse() rebuild the tree:

import { parse, serialize } from "datocms-structured-text-dastdown";
const text = serialize(currentRecord.content);
const edited = text
.replace(/Jane Doe/g, "Jane Smith")
.replace(/==([^=]+)==/g, "**$1**");
const content = parse(edited, currentRecord.content);

Blocks appear in dastdown only as ID placeholders: you can move or delete them, but you can't touch what's inside.

If you can describe the change as something you'd do in a text editor — find-and-replace, rewriting a paragraph, reshaping a list — dastdown is the right tool. It also wins on the opposite end of the spectrum from Pass 2: one-off spot edits, and agentic flows where an LLM reads the serialized text and rewrites it directly.

Pass 2: Mutate the tree (mapNodes + block helpers)

Reach for Pass 2 when dastdown can't see what you want to change — anything that depends on node kind rather than text, or on a block's internal fields rather than its position. Both flavors of edit live inside the same mapNodes walk:

  • Transforming prose nodes. Rewrite every link's URL, lowercase every heading, bump every level: 2 heading to level: 3, drop every empty paragraph, wrap every occurrence of "click here" in a link. dastdown is the right tool while edits stay mostly text-shaped; once you find yourself writing regex to fish node kinds back out of the serialized form, the AST is the cleaner layer.
  • Editing or building embedded blocks. Edit a block's attributes, swap it for another, or drop a brand-new block into the tree. Embedded blocks are opaque to Pass 1 (dastdown serializes them to <block id="…"/> placeholders that hide their fields), so anything touching block contents lands here. Use buildBlockRecord<T> to shape the payload — pass an id to edit an existing block, omit it to create a new one — and duplicateBlockRecord<T> to deep-clone one.

mapNodes from datocms-structured-text-utils walks the tree bottom-up (a node's descendants have already been transformed by the time the callback sees it, and what you return for that node is final). Return one node (1:1, the default), an array splatted into siblings (1:N — split, wrap, insert), or null/undefined to drop (1:0 — illegal at the root).

When a node doesn't need to change, just return node. The CMA accepts the nested-response shape it came in as.

Adding root-level nodes (a brand-new paragraph, a fresh top-level block) sits just outside the callback: mapNodes can't splat at the root, so push directly into content.document.children after the walk — using buildBlockRecord / duplicateBlockRecord to shape any new block entry.

⚠️ Combine passes in order: 1 → 2

Pass 1's parse() uses the original document as the lookup table for <block id="…"/> placeholders. A block created by Pass 2 first would either be missing from that lookup (and parse throws) or get silently overwritten when Pass 1 rehydrates. If you need both, always run Pass 1 before Pass 2.

Block & node helpers

Curated index of the helpers used above.

Block helpers

Used by all three block field types. Full reference: block-processing-utilities.

NeedHelper
Build a block (create / edit)buildBlockRecord<T>(...)
Clone a block (deep copy, IDs stripped)duplicateBlockRecord<T>(block, schemaRepository)
Inspect a record or block (debug)inspectItem(record)
Inline narrowing on a block unionblock.__itemTypeId === "..."
Type-guard predicate for Array#filter / Array#findisBlockOfType(id)
Recurse into every block (any depth, any field)visitBlocks* / mapBlocks* / filterBlocks* / findAllBlocks* / reduceBlocks* / someBlocks* / everyBlocks* InNonLocalizedFieldValue
Structured-text node helpers

Used only by structured_text. Each helper has an *Async mirror (mapNodesmapNodesAsync) for async callbacks. Full reference: tree-manipulation-utilities.

NeedHelper
Narrow a node to a kindisParagraph, isHeading, isSpan, isLink, isItemLink, isInlineItem, isBlock, isInlineBlock, isList, ...
Narrow a block / inline-block node to a specific model in one stepisBlockWithItemOfType(id), isInlineBlockWithItemOfType(id)
Walk every node (side effect)forEachNode
Transform every node (1:1, splat into siblings, or drop)mapNodes
Find first / collect every matchfindFirstNode, collectNodes
Fold to a single valuereduceNodes
Short-circuit checkssomeNode, everyNode
ASCII-tree debuginspect

Updating Localized Fields

➡️ Before proceeding, ensure you have read the general guide on Localization

When you send an update request, the API follows these strict rules.

Rule 1: To change a locale value, send the whole set

When you update a translated field, you must provide the entire object for that field, including all the languages you want to keep unchanged. You can't just send the one language you're changing.

  • Correct: To update the Italian title, you send both English and Italian:
    {
    "title": {
    "en": "Hello World",
    "it": "Ciao a tutti! (Updated)"
    }
    }
  • Incorrect: If you only send the Italian value, the API will assume you want to delete the English one!
Rule 2: To add/remove a language, send all translated fields

This is the only time you can't just send the one field you're changing. To add or remove a language from an entire record, you must include all translated fields in your request. This is to enforce the Locale Sync Rule and ensure all fields remain consistent.

  • Example: To add French to a blog post that already has a translated title and content, your request must include both fields with the new fr locale.
Rule 3: Limited permissions? Only send what you can manage

If your API key only has permission for certain languages (e.g., only English), you must only include those languages in your update. The system is smart and will automatically protect and preserve the content for the languages you can't access (like Italian or French).

Update scenarios at a glance

This table shows what happens in different situations. The key takeaway is that your update payload defines the new final state for the languages you are allowed to manage.

Your Role managesRecord currently HasYour payload sendsResult
EnglishEnglishEnglish✅ English is updated.
English, ItalianEnglishEnglish, Italian✅ English is updated.
➕ Italian is added.
English, ItalianEnglish, ItalianEnglish✅ English is updated.
➖ Italian is removed.
English, ItalianEnglish, ItalianEnglish, Italian✅ English is updated.
✅ Italian is updated.
Eng, Ita, FreEnglish, ItalianEnglish, French✅ English is updated.
➖ Italian is removed.
➕ French is added.
EnglishEnglish, ItalianEnglish✅ English is updated.
🛡️ Italian is preserved.
English, ItalianEnglish, FrenchEnglish, Italian✅ English is updated.
🛡️ French is preserved.
➕ Italian is added.
English, ItalianEnglish, FrenchItalian➖ English is removed.
🛡️ French is preserved.
➕ Italian is added.
Block fields

The rules about localization work in combination with the rules for updating blocks: you use full block objects to create/update and block IDs to leave unchanged, but you do so within the object for a specific locale.

Example: Updating a block in one locale

This payload updates the title of an existing block in the en locale, while leaving the second English block and all Italian blocks untouched. The it locale needs to be included in the payload, or the Italian locale will be deleted!

{
"content_blocks": {
"en": [
{
"id": "dhVR2HqgRVCTGFi0bWqLqA",
"type": "item",
"attributes": { "title": "Updated English Title" }
},
"kL9mN3pQrStUvWxYzAbCdE"
],
"it": [
"dhVR2HqgRVCTGFi_0bWqLqA",
"kL9mN3pQrStUvWxYzAbCdE"
]
}
}
Example: Adding a new block to one locale

This payload adds a new block to the it locale only. The en locale needs to be included in the payload, or the Italian locale will be deleted!

{
"content_blocks": {
"en": [
"dhVR2HqgRVCTGFi_0bWqLqA",
"kL9mN3pQrStUvWxYzAbCdE"
],
"it": [
"fG8hI1jKlMnOpQrStUvWxY",
{
"type": "item",
"attributes": { "title": "Nuovo Blocco" },
"relationships": {
"item_type": { "data": { "id": "BxZ9Y2aKQVeTnM4hP8wLpD", "type": "item_type" } }
}
},
"dhVR2HqgRVCTGFi0bWqLqA"
]
}
}
Example: Adding a new locale

To add a new locale to an existing record, you must provide values for all localized fields for that new locale, and include existing locales that you want to preserve.

{
"title": {
"en": "English Title",
"fr": "Titre Français",
},
"content_blocks": {
"en": [
"dhVR2HqgRVCTGFi_0bWqLqA",
"kL9mN3pQrStUvWxYzAbCdE"
],
"fr": [
{
"type": "item",
"attributes": { "title": "Nouveau Bloc Français" },
"relationships": {
"item_type": { "data": { "id": "BxZ9Y2aKQVeTnM4hP8wLpD", "type": "item_type" } }
}
}
]
}
}
One code path for localized and non-localized fields

Helpers shipped with our JS CMA clients let you skip the "does this field have a locale object or just a value" branching when reading or transforming field values — jump to Unified locale helpers.

Unified locale helpers

These utilities let you treat localized and non-localized field values with the same code path — no branching on "does this field have a locale object or just a value". Each helper has an *Async mirror (mapNormalizedFieldValuesmapNormalizedFieldValuesAsync) for async callbacks. See the full helper reference for signatures and examples.

  • mapNormalizedFieldValues(): apply a transformation to each locale (or to the single value on non-localized fields)
  • filterNormalizedFieldValues(): keep only locales / values matching a predicate
  • visitNormalizedFieldValues(): run a side effect for each locale / value
  • someNormalizedFieldValues(): true if at least one locale / value matches
  • everyNormalizedFieldValue(): true if all locales / values match
  • toNormalizedFieldValueEntries() / fromNormalizedFieldValueEntries(): convert to / from a unified [locale, value][] shape for iteration

Bulk block operations

Sometimes, you need to perform mass operations on any block of a specific kind, regardless of where they're embedded in your content structure — whether in Modular Content fields, Single Block fields, or deeply nested within Structured Text documents. In these cases, manually traversing each record and field would be extremely time-consuming and error-prone.

DatoCMS provides powerful utilities that can systematically discover, traverse, and manipulate blocks across your entire content hierarchy. These utilities handle the complexity of localized content, nested structures, and different field types automatically, making what would otherwise be a complex operation straightforward and reliable.

Optimistic Locking

To prevent clients from accidentally overwriting each other's changes, the update endpoint supports optimistic locking. You can include the record's current version number in the meta object of your payload.

If the version on the server is newer than the one you provide, the API will reject the update with a 422 STALE_ITEM_VERSION error, indicating that the record has been modified since you last fetched it.

Body parameters

type string Required

Must be exactly "item".

meta.created_at string Optional

Date of creation

meta.first_published_at null, string Optional

Date of first publication

meta.current_version string Optional

The ID of the current record version (for optimistic locking, see the example)

Example: "4234"
meta.stage string, null Optional

The new stage to move the record to

relationships.item_type.data Optional

The record's model

relationships.creator.data Optional

The entity (account/collaborator/access token/sso user) who created the record

Returns

Returns a resource object of type item.

Examples

PUT https://site-api.datocms.com/items/:item_id HTTP/1.1
Authorization: Bearer YOUR-API-TOKEN
Accept: application/json
X-Api-Version: 3
Content-Type: application/vnd.api+json
{
"data": {
"type": "item",
"id": "hWl-mnkWRYmMCSTq4z_piQ"
}
}
Terminal window
curl -g 'https://site-api.datocms.com/items/:item_id' \
-X PUT \
-H "Authorization: Bearer YOUR-API-TOKEN" \
-H "Accept: application/json" \
-H "X-Api-Version: 3" \
-H "Content-Type: application/vnd.api+json" \
--data-binary '{"data":{"type":"item","id":"hWl-mnkWRYmMCSTq4z_piQ"}}'
await fetch("https://site-api.datocms.com/items/:item_id", {
method: "PUT",
headers: {
Authorization: "Bearer YOUR-API-TOKEN",
Accept: "application/json",
"X-Api-Version": "3",
"Content-Type": "application/vnd.api+json",
},
body: JSON.stringify({
data: { type: "item", id: "hWl-mnkWRYmMCSTq4z_piQ" },
}),
});
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: cache-control: max-age=0, private, must-revalidate
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 28
{
"data": {
"type": "item",
"id": "hWl-mnkWRYmMCSTq4z_piQ",
"relationships": {
"item_type": {
"data": {
"type": "item_type",
"id": "DxMaW10UQiCmZcuuA-IkkA"
}
}
},
"attributes": {
"title": "My first blog post!",
"content": "Lorem ipsum dolor sit amet...",
"category": "24",
"image": {
"alt": "Alt text",
"title": "Image title",
"custom_data": {},
"focal_point": null,
"upload_id": "20042921"
}
},
"meta": {
"created_at": "2020-04-21T07:57:11.124Z",
"updated_at": "2020-04-21T07:57:11.124Z",
"published_at": "2020-04-21T07:57:11.124Z",
"first_published_at": "2020-04-21T07:57:11.124Z",
"publication_scheduled_at": "2020-04-21T07:57:11.124Z",
"unpublishing_scheduled_at": "2020-04-21T07:57:11.124Z",
"status": "published",
"is_current_version_valid": true,
"is_published_version_valid": true,
"current_version": "4234",
"stage": null,
"has_children": true
}
}
}