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
import {
buildClient,
inspectItem,
type ItemTypeDefinition,
} from "@datocms/cma-client-node";
type EnvironmentSettings = { locales: "en" };
/*
* BlogPost
* ā”œā”€ title: string
* ā”œā”€ description: string
* ā”œā”€ featured_image: file
* └─ content_blocks: modular_content
* └─ HeroBlock: headline
*/
// šŸ‘‡ Definitions can be generated automatically using CLI: https://www.datocms.com/cma-ts-schema
type BlogPost = ItemTypeDefinition<
EnvironmentSettings,
"UZyfjdBES8y2W2ruMEHSoA",
{
title: { type: "string" };
description: { type: "string" };
featured_image: { type: "file" };
content_blocks: { type: "rich_text"; blocks: Block };
}
>;
type Block = ItemTypeDefinition<
EnvironmentSettings,
"DB5xsyzCQ3iHTx0dZPb3sw",
{
headline: { type: "string" };
}
>;
async function run() {
// Make sure the API token has access to the CMA, and is stored securely
const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });
const record = await client.items.find<BlogPost>("T4m4tPymSACFzsqbZS65WA", {
nested: true,
});
console.log("-- BEFORE UPDATE --");
console.log(inspectItem(record));
const item = await client.items.update<BlogPost>("T4m4tPymSACFzsqbZS65WA", {
// Field we want to update with a new value
title: "[EDIT] My first blog post!",
// Fields we omit are left UNTOUCHED - their existing values remain unchanged
// description: (omitted - keeps existing value)
// Fields we explicitly set to null/empty are DELETED/CLEARED
featured_image: null, // This removes the featured image
content_blocks: [], // This clears all content blocks!
});
console.log("-- AFTER UPDATE --");
console.log(inspectItem(item));
}
run();
-- BEFORE UPDATE --
ā”” Item "T4m4tPymSACFzsqbZS65WA" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
ā”œ title: "My first blog post!"
ā”œ description: "An introduction to our new blog platform"
ā”œ featured_image: null
ā”” content_blocks
ā”” [0] Item "WtzyjA4sTLiLiOQ9TBNgtQ" (item_type: "DB5xsyzCQ3iHTx0dZPb3sw")
ā”” headline: "Hello!"
-- AFTER UPDATE --
ā”” Item "T4m4tPymSACFzsqbZS65WA" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
ā”œ title: "[EDIT] My first blog post!"
ā”œ description: "An introduction to our new blog platform"
ā”œ featured_image: null
ā”” content_blocks: []

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

Updating Block Fields

As with creation, you cannot edit blocks directly; you must update the parent record that contains them. The payload you send uses a mix of block IDs and block objects to define the desired final state.

The rules for adding, updating, keeping, deleting, and reordering blocks are covered in detail in the main records guide.

šŸ› ļø Block and Structured Text utilities

We provide many utility functions to help you work with blocks and structured text nodes effectively. Check out our helper libraries for common operations:

  • Block Utils - Functions like buildBlockRecord(), duplicateBlockRecord(), and localization helpers
  • Structured Text Utils - Tree manipulation utilities like mapNodes(), filterNodes(), and findFirstNode()

This comprehensive example demonstrates many modular content field operations in a single API call.

The script shows how to:

  • Fetch existing content using nested: true to get full block objects with their attributes
  • Add new blocks by creating them with buildBlockRecord() and specifying the item_type relationship
  • Duplicate existing blocks using duplicateBlockRecord() to clone blocks (removing all IDs, including nested block IDs) for template reuse
  • Update existing blocks by passing the block ID and modified attributes
  • Remove blocks by filtering them out of the sections array
  • Keep blocks unchanged by passing just their ID strings
  • Reorder blocks by arranging them in the desired sequence within the array

The example demonstrates proper handling of block references (IDs vs full objects) and shows how a single update operation can perform multiple block manipulations simultaneously. For more information about block management, see the Creating and updating blocks guide.

import {
buildBlockRecord,
buildClient,
duplicateBlockRecord,
inspectItem,
SchemaRepository,
type ApiTypes,
type ItemTypeDefinition,
} from "@datocms/cma-client-node";
type EnvironmentSettings = { locales: "en" };
/*
* LandingPage
* ā”œā”€ title: string
* └─ sections: modular_content
* ā”œā”€ HeroBlock: headline, subtitle
* ā”œā”€ CallToActionBlock: button_text, button_url
* └─ TestimonialBlock: quote, author
*/
// šŸ‘‡ Definitions can be generated automatically using CLI: https://www.datocms.com/cma-ts-schema
type LandingPage = ItemTypeDefinition<
EnvironmentSettings,
"ZV0o9497SsqWxQR8HEQddw",
{
title: { type: "string" };
sections: {
type: "rich_text";
blocks: HeroBlock | CallToActionBlock | TestimonialBlock;
};
}
>;
type HeroBlock = ItemTypeDefinition<
EnvironmentSettings,
"d-CHYg-rShOt3kiL6ZN1yA",
{
headline: { type: "string" };
subtitle: { type: "text" };
}
>;
type CallToActionBlock = ItemTypeDefinition<
EnvironmentSettings,
"I8Q6k-HqQmaZ498WKtvFbg",
{
button_text: { type: "string" };
button_url: { type: "string" };
}
>;
type TestimonialBlock = ItemTypeDefinition<
EnvironmentSettings,
"Dy9C52o4S6eF3mqSOmeUtg",
{
quote: { type: "text" };
author: { type: "string" };
}
>;
async function run() {
// Make sure the API token has access to the CMA, and is stored securely
const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });
// Schema repository (used for block duplication below)
const schemaRepository = new SchemaRepository(client);
// Get the current record with nested blocks to see existing content
const currentPage = await client.items.find<LandingPage>(
"W4wcrs_2REiM4fc6dlDZCQ",
{
nested: true,
},
);
console.log("-- BEFORE UPDATE --");
console.log(inspectItem(currentPage));
// 1. DUPLICATE EXISTING BLOCKS: All testimonial blocks
const duplicatedTestimonialBlocksOrNull = await Promise.all(
currentPage.sections.map((block) =>
block.__itemTypeId === "Dy9C52o4S6eF3mqSOmeUtg"
? // duplicateBlockRecord() clones the block object and removes block IDs (this applies to nested blocks too)
// This creates a new block template that can be used elsewhere or modified
duplicateBlockRecord<TestimonialBlock>(block, schemaRepository)
: null,
),
);
const updateOperation: ApiTypes.ItemUpdateSchema<LandingPage> = {
sections: [
// 2. ADD NEW BLOCK: New hero at top for better first impression
buildBlockRecord<HeroBlock>({
item_type: { type: "item_type", id: "d-CHYg-rShOt3kiL6ZN1yA" },
headline: "Transform Your Business Today",
subtitle:
"Join thousands of companies already using our platform to streamline their operations and boost productivity.",
}),
// Put the first duplicate testimonial as second block
...duplicatedTestimonialBlocksOrNull.filter(isNonNullable).slice(0, 1),
...currentPage.sections
// 3. REMOVE EXISTING BLOCKS: Old hero blocks
.filter((block) => block.__itemTypeId !== "d-CHYg-rShOt3kiL6ZN1yA")
.map((block) => {
// 4. UPDATE EXISTING BLOCKS: Add UTM parameters to all CTA URLs
if (block.__itemTypeId === "I8Q6k-HqQmaZ498WKtvFbg") {
const url = new URL(
block.attributes.button_url || "https://www.datocms.com",
);
url.searchParams.set("utm_source", "landing_page");
url.searchParams.set("utm_medium", "cta");
url.searchParams.set("utm_campaign", "q1_2024");
return buildBlockRecord<CallToActionBlock>({
item_type: { id: "I8Q6k-HqQmaZ498WKtvFbg", type: "item_type" },
id: block.id,
button_url: url.toString(),
// We don't need to pass button_text, as we're not changing it
});
}
// 5. KEEP OTHER BLOCKS UNCHANGED (ie. existing testimonials)
return block.id;
}),
],
};
console.log("-- UPDATE OPERATION --");
console.log(inspectItem(updateOperation));
await client.items.update<LandingPage>(currentPage, updateOperation);
const updatedPage = await client.items.find(currentPage, { nested: true });
console.log("-- AFTER UPDATE --");
console.log(inspectItem(updatedPage));
}
run();
function isNonNullable<T>(
value: T | null | undefined,
): value is NonNullable<T> {
return value !== null && value !== undefined && value !== false;
}
-- BEFORE UPDATE --
ā”” Item "W4wcrs_2REiM4fc6dlDZCQ" (item_type: "ZV0o9497SsqWxQR8HEQddw")
ā”œ title: "Product Launch Landing Page"
ā”” sections
ā”œ [0] Item "BxsiUvpRQV20x8iJYXSr-w" (item_type: "d-CHYg-rShOt3kiL6ZN1yA")
│ ā”œ headline: "Revolutionary New Solution"
│ ā”” subtitle: "Discover the future of productivity with our cutting-edge platform designed f..."
ā”œ [1] Item "WjyqFR6xQqmxDoQNodlx8g" (item_type: "I8Q6k-HqQmaZ498WKtvFbg")
│ ā”œ button_text: "Get Started Free"
│ ā”” button_url: "https://www.datocms.com/signup"
ā”œ [2] Item "M4O1B4uwTzW0Chv41suTFA" (item_type: "Dy9C52o4S6eF3mqSOmeUtg")
│ ā”œ quote: "This platform completely transformed how our team collaborates. We've seen a ..."
│ ā”” author: "Sarah Chen, Product Manager at TechCorp"
ā”œ [3] Item "Tlzw3TmDR6eRPOS2B3JS2A" (item_type: "Dy9C52o4S6eF3mqSOmeUtg")
│ ā”œ quote: "The best investment we've made this year. The ROI was evident within the firs..."
│ ā”” author: "Michael Rodriguez, CTO at InnovateLabs"
ā”” [4] Item "MNksTfS8R2O_BYL1iCQAaw" (item_type: "I8Q6k-HqQmaZ498WKtvFbg")
ā”œ button_text: "Start Your Free Trial"
ā”” button_url: "https://www.datocms.com/trial"
-- UPDATE OPERATION --
ā”” Item
ā”” sections
ā”œ [0] Item (item_type: "d-CHYg-rShOt3kiL6ZN1yA")
│ ā”œ headline: "Transform Your Business Today"
│ ā”” subtitle: "Join thousands of companies already using our platform to streamline their op..."
ā”œ [1] Item (item_type: "Dy9C52o4S6eF3mqSOmeUtg")
│ ā”œ quote: "This platform completely transformed how our team collaborates. We've seen a ..."
│ ā”” author: "Sarah Chen, Product Manager at TechCorp"
ā”œ [2] Item "WjyqFR6xQqmxDoQNodlx8g" (item_type: "I8Q6k-HqQmaZ498WKtvFbg")
│ ā”” button_url: "https://www.datocms.com/signup?utm_source=landing_page&utm_medium=cta&utm_cam..."
ā”œ [3] "M4O1B4uwTzW0Chv41suTFA"
ā”œ [4] "Tlzw3TmDR6eRPOS2B3JS2A"
ā”” [5] Item "MNksTfS8R2O_BYL1iCQAaw" (item_type: "I8Q6k-HqQmaZ498WKtvFbg")
ā”” button_url: "https://www.datocms.com/trial?utm_source=landing_page&utm_medium=cta&utm_camp..."
-- AFTER UPDATE --
ā”” Item "W4wcrs_2REiM4fc6dlDZCQ" (item_type: "ZV0o9497SsqWxQR8HEQddw")
ā”œ title: "Product Launch Landing Page"
ā”” sections
ā”œ [0] Item "V5VQsv0wTeGpDjx4gM2bDw" (item_type: "d-CHYg-rShOt3kiL6ZN1yA")
│ ā”œ headline: "Transform Your Business Today"
│ ā”” subtitle: "Join thousands of companies already using our platform to streamline their op..."
ā”œ [1] Item "BAHvU2p5T6W8GGg4lF0ZpA" (item_type: "Dy9C52o4S6eF3mqSOmeUtg")
│ ā”œ quote: "This platform completely transformed how our team collaborates. We've seen a ..."
│ ā”” author: "Sarah Chen, Product Manager at TechCorp"
ā”œ [2] Item "WjyqFR6xQqmxDoQNodlx8g" (item_type: "I8Q6k-HqQmaZ498WKtvFbg")
│ ā”œ button_text: "Get Started Free"
│ ā”” button_url: "https://www.datocms.com/signup?utm_source=landing_page&utm_medium=cta&utm_cam..."
ā”œ [3] Item "M4O1B4uwTzW0Chv41suTFA" (item_type: "Dy9C52o4S6eF3mqSOmeUtg")
│ ā”œ quote: "This platform completely transformed how our team collaborates. We've seen a ..."
│ ā”” author: "Sarah Chen, Product Manager at TechCorp"
ā”œ [4] Item "Tlzw3TmDR6eRPOS2B3JS2A" (item_type: "Dy9C52o4S6eF3mqSOmeUtg")
│ ā”œ quote: "The best investment we've made this year. The ROI was evident within the firs..."
│ ā”” author: "Michael Rodriguez, CTO at InnovateLabs"
ā”” [5] Item "MNksTfS8R2O_BYL1iCQAaw" (item_type: "I8Q6k-HqQmaZ498WKtvFbg")
ā”œ button_text: "Start Your Free Trial"
ā”” button_url: "https://www.datocms.com/trial?utm_source=landing_page&utm_medium=cta&utm_camp..."

This comprehensive example demonstrates all single block field operations in multiple API calls, showing how single block fields can hold exactly one block or be null.

The script shows how to:

  • Fetch existing content using nested: true to get the full block object with its attributes
  • Update existing block by passing the block ID and modified attributes
  • Duplicate existing block using duplicateBlockRecord() to clone a block (removing all IDs, including nested block IDs) for template reuse
  • Replace with different block type by creating a new block with buildBlockRecord() and item_type relationship
  • Remove block by setting the field value to null
  • Keep block unchanged by not including the field in the update payload

The example demonstrates proper handling of block references (IDs vs full objects) and shows how single block fields can switch between different block types or be completely removed/added as needed. For more information about block management, see the Creating and updating blocks guide.

import {
buildBlockRecord,
buildClient,
duplicateBlockRecord,
inspectItem,
SchemaRepository,
type ApiTypes,
type ItemTypeDefinition,
} from "@datocms/cma-client-node";
type EnvironmentSettings = { locales: "en" };
/*
* ProductPage
* ā”œā”€ title: string
* ā”œā”€ price: float
* └─ hero_section: single_block
* ā”œā”€ HeroBlock: headline, description, background_image
* ā”œā”€ CallToActionBlock: button_text, button_url, style
* └─ VideoBlock: video_url, thumbnail_image, autoplay
*/
// šŸ‘‡ Definitions can be generated automatically using CLI: https://www.datocms.com/cma-ts-schema
type ProductPage = ItemTypeDefinition<
EnvironmentSettings,
"ZV0o9497SsqWxQR8HEQddw",
{
title: { type: "string" };
price: { type: "float" };
hero_section: {
type: "single_block";
blocks: HeroBlock | CallToActionBlock | VideoBlock;
};
}
>;
type HeroBlock = ItemTypeDefinition<
EnvironmentSettings,
"d-CHYg-rShOt3kiL6ZN1yA",
{
headline: { type: "string" };
description: { type: "text" };
background_image: { type: "file" };
}
>;
type CallToActionBlock = ItemTypeDefinition<
EnvironmentSettings,
"I8Q6k-HqQmaZ498WKtvFbg",
{
button_text: { type: "string" };
button_url: { type: "string" };
style: { type: "string" };
}
>;
type VideoBlock = ItemTypeDefinition<
EnvironmentSettings,
"Dy9C52o4S6eF3mqSOmeUtg",
{
video_url: { type: "string" };
thumbnail_image: { type: "file" };
autoplay: { type: "boolean" };
}
>;
// Make sure the API token has access to the CMA, and is stored securely
const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });
async function run() {
// Schema repository for block duplication
const schemaRepository = new SchemaRepository(client);
// Get the current record with nested blocks to see existing content
const currentProduct = await client.items.find<ProductPage>(
"W4wcrs_2REiM4fc6dlDZCQ",
{
nested: true,
},
);
console.log("-- BEFORE UPDATE --");
console.log(inspectItem(currentProduct));
// 1. UPDATE EXISTING BLOCKS: Update CTAs for A/B testing
if (
currentProduct.hero_section &&
currentProduct.hero_section.__itemTypeId === "I8Q6k-HqQmaZ498WKtvFbg"
) {
await client.items.update<ProductPage>(currentProduct, {
hero_section: buildBlockRecord<CallToActionBlock>({
item_type: { type: "item_type", id: "I8Q6k-HqQmaZ498WKtvFbg" },
id: currentProduct.hero_section.id,
button_text:
currentProduct.hero_section.attributes.button_text?.toUpperCase() ||
"SHOP NOW",
style: "primary-large",
// button_url unchanged, so we don't need to include it
}),
});
console.log("-- EXISTING BLOCK UPDATED --");
await inspectItemWithNestedBlocks(currentProduct);
}
// 2. DUPLICATE EXISTING BLOCK
await client.items.update<ProductPage>(currentProduct, {
// duplicateBlockRecord() clones the block object and removes block IDs (this applies to nested blocks too)
// This creates a new block template that can be used elsewhere or modified
hero_section: await duplicateBlockRecord<
HeroBlock | CallToActionBlock | VideoBlock
>(currentProduct.hero_section!, schemaRepository),
});
console.log("-- BLOCK DUPLICATE --");
await inspectItemWithNestedBlocks(currentProduct);
// 3. REPLACE WITH DIFFERENT BLOCK TYPE
const upload = await client.uploads.createFromUrl({
url: "https://picsum.photos/800/600?random=1",
});
const productWithVideo = await client.items.update<ProductPage>(
currentProduct,
{
hero_section: buildBlockRecord<VideoBlock>({
item_type: { type: "item_type", id: "Dy9C52o4S6eF3mqSOmeUtg" },
video_url: "https://videos.datocms.com/product-demo.mp4",
thumbnail_image: {
upload_id: upload.id,
},
autoplay: false,
}),
},
);
console.log("-- BLOCK REPLACED --");
await inspectItemWithNestedBlocks(currentProduct);
// 4. REMOVE BLOCK
await client.items.update<ProductPage>(productWithVideo, {
hero_section: null,
});
console.log("-- BLOCK REMOVED --");
await inspectItemWithNestedBlocks(currentProduct);
}
run();
async function inspectItemWithNestedBlocks(item: ApiTypes.Item) {
const itemWithNestedBlocks = await client.items.find(item, { nested: true });
console.log(inspectItem(itemWithNestedBlocks));
}
-- BEFORE UPDATE --
ā”” Item "W4wcrs_2REiM4fc6dlDZCQ" (item_type: "ZV0o9497SsqWxQR8HEQddw")
ā”œ title: "Premium Wireless Headphones"
ā”œ price: 299.99
ā”” hero_section
ā”” Item "MolF0AwpSLeXrdE5kdcEtw" (item_type: "I8Q6k-HqQmaZ498WKtvFbg")
ā”œ button_text: "Buy Now"
ā”œ button_url: "https://example.com/buy"
ā”” style: "primary"
-- EXISTING BLOCK UPDATED --
ā”” Item "W4wcrs_2REiM4fc6dlDZCQ" (item_type: "ZV0o9497SsqWxQR8HEQddw")
ā”œ title: "Premium Wireless Headphones"
ā”œ price: 299.99
ā”” hero_section
ā”” Item "MolF0AwpSLeXrdE5kdcEtw" (item_type: "I8Q6k-HqQmaZ498WKtvFbg")
ā”œ button_text: "BUY NOW"
ā”œ button_url: "https://example.com/buy"
ā”” style: "primary-large"
-- BLOCK DUPLICATE --
ā”” Item "W4wcrs_2REiM4fc6dlDZCQ" (item_type: "ZV0o9497SsqWxQR8HEQddw")
ā”œ title: "Premium Wireless Headphones"
ā”œ price: 299.99
ā”” hero_section
ā”” Item "QXqFgPHVTfq8F1tOmQEwEg" (item_type: "I8Q6k-HqQmaZ498WKtvFbg")
ā”œ button_text: "Buy Now"
ā”œ button_url: "https://example.com/buy"
ā”” style: "primary"
-- BLOCK REPLACED --
ā”” Item "W4wcrs_2REiM4fc6dlDZCQ" (item_type: "ZV0o9497SsqWxQR8HEQddw")
ā”œ title: "Premium Wireless Headphones"
ā”œ price: 299.99
ā”” hero_section
ā”” Item "JIHRl3kyQiGXAJHcP-7v7Q" (item_type: "Dy9C52o4S6eF3mqSOmeUtg")
ā”œ video_url: "https://videos.datocms.com/product-demo.mp4"
ā”œ thumbnail_image
│ ā”” upload_id: "WwqHexgISQqQdMKJSYE8VA"
ā”” autoplay: false
-- BLOCK REMOVED --
ā”” Item "W4wcrs_2REiM4fc6dlDZCQ" (item_type: "ZV0o9497SsqWxQR8HEQddw")
ā”œ title: "Premium Wireless Headphones"
ā”œ price: 299.99
ā”” hero_section: null

This example demonstrates common structured text document transformations using DAST tree manipulation utilities for content modernization and consistency workflows.

The script shows how to:

  • Transform heading levels using mapNodes() to demote all h1 headings to h2 for better document structure
  • Update link attributes by adding target="_blank" to all external links and item links for improved user experience
  • Replace text content across all span nodes to update brand names or terminology throughout the document (e.g., rebranding from "ZEIT" to "Vercel")
  • Clean document structure using filterNodes() to remove empty paragraphs and normalize text

The example demonstrates practical DAST manipulation using datocms-structured-text-utils for batch content updates, ensuring consistency across large content repositories without manual editing. For more information about structured text manipulation, see the Structured Text Guide.

import {
buildClient,
inspectItem,
type ItemTypeDefinition,
} from "@datocms/cma-client-node";
import {
filterNodes,
isHeading,
isItemLink,
isLink,
isParagraph,
isSpan,
mapNodes,
} from "datocms-structured-text-utils";
type EnvironmentSettings = { locales: "en" };
/*
* BlogPost
* ā”œā”€ title: string
* ā”œā”€ slug: string
* └─ content: structured_text (no blocks for this example)
*/
// šŸ‘‡ Definitions can be generated automatically using CLI: https://www.datocms.com/cma-ts-schema
type BlogPost = ItemTypeDefinition<
EnvironmentSettings,
"UZyfjdBES8y2W2ruMEHSoA",
{
title: { type: "string" };
slug: { type: "string" };
content: { type: "structured_text" };
}
>;
async function run() {
// Make sure the API token has access to the CMA, and is stored securely
const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });
// Get the current blog post with its structured text content
const currentPost = await client.items.find<BlogPost>(
"T4m4tPymSACFzsqbZS65WA",
);
if (!currentPost.content) {
return;
}
console.log("-- BEFORE UPDATE --");
console.log(inspectItem(currentPost));
// Transform the structured text document with three key operations
let updatedContent = mapNodes(currentPost.content, (node) => {
// 1. TRANSFORM HEADING LEVELS: Convert all h1 to h2 for better hierarchy
if (isHeading(node)) {
if (node.level === 1) {
return {
...node,
level: 2, // Demote h1 to h2
};
}
}
// 2. UPDATE LINK ATTRIBUTES: Add target="_blank" to all links
if (isLink(node)) {
return {
...node,
meta: [
...(node.meta || []),
{
id: "target",
value: "_blank",
},
],
};
}
// 3. UPDATE ITEM LINK ATTRIBUTES: Add target="_blank" to item links too
if (isItemLink(node)) {
return {
...node,
meta: [
...(node.meta || []),
{
id: "target",
value: "_blank",
},
],
};
}
// 4. REPLACE TEXT CONTENT: Update brand name from "ZEIT" to "Vercel"
if (isSpan(node) && node.value.includes("ZEIT")) {
return {
...node,
value: node.value.replace(/ZEIT/g, "Vercel"),
};
}
// Keep all other nodes unchanged
return node;
});
// 5. REMOVING NODES: Remove empty paragraphs
updatedContent = filterNodes(updatedContent, (node) => {
if (isParagraph(node)) {
return node.children.some(
(child) => !isSpan(child) || child.value.trim().length > 0,
);
}
return true;
})!;
// Update the blog post with the transformed content
const updatedPost = await client.items.update<BlogPost>(currentPost.id, {
content: updatedContent,
// Keep title and slug unchanged
});
console.log("-- AFTER UPDATE --");
console.log(inspectItem(updatedPost));
}
run();
-- BEFORE UPDATE --
ā”” Item "T4m4tPymSACFzsqbZS65WA" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
ā”œ title: "What is ZEIT?"
ā”œ slug: "what-is-zeit"
ā”” content
ā”œ heading (level: 1)
│ ā”” span "Understanding ZEIT"
ā”œ paragraph
│ ā”” span "ZEIT is a cloud platform for static sites and serverless functions. I..."
ā”œ paragraph
│ ā”” span ""
ā”œ paragraph
│ ā”” span ""
ā”œ heading (level: 1)
│ ā”” span "Key Features"
ā”œ paragraph
│ ā”œ span "ZEIT offers automatic HTTPS, global CDN distribution, and instant dep..."
│ ā”œ link (url: "https://example.com/blog")
│ │ ā”” span "detailed comparison"
│ ā”” span " for more insights."
ā”” paragraph
ā”œ span "Visit our "
ā”œ itemLink (item: "DvYpzVRHT2mdarqJ3ct4ow")
│ ā”” span "migration guide"
ā”” span " for step-by-step instructions."
-- AFTER UPDATE --
ā”” Item "T4m4tPymSACFzsqbZS65WA" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
ā”œ title: "What is ZEIT?"
ā”œ slug: "what-is-zeit"
ā”” content
ā”œ heading (level: 2)
│ ā”” span "Understanding Vercel"
ā”œ paragraph
│ ā”” span "Vercel is a cloud platform for static sites and serverless functions...."
ā”œ heading (level: 2)
│ ā”” span "Key Features"
ā”œ paragraph
│ ā”œ span "Vercel offers automatic HTTPS, global CDN distribution, and instant d..."
│ ā”œ link (url: "https://example.com/blog", meta: {target="_blank"})
│ │ ā”” span "detailed comparison"
│ ā”” span " for more insights."
ā”” paragraph
ā”œ span "Visit our "
ā”œ itemLink (item: "DvYpzVRHT2mdarqJ3ct4ow", meta: {target="_blank"})
│ ā”” span "migration guide"
ā”” span " for step-by-step instructions."

This comprehensive example demonstrates structured text field operations using DAST tree manipulation utilities for real-world content auditing and modernization workflows.

The script shows how to:

  • Fetch existing content using nested: true to get full block and inline item objects
  • Duplicate existing blocks using duplicateBlockRecord() to clone blocks (removing all IDs, including nested block IDs) and add them as new blocks in the document
  • Transform blocks using mapNodes() to update embedded blocks while preserving DAST structure

The example demonstrates advanced DAST manipulation using datocms-structured-text-utils for content operations like URL normalization, affiliate link tracking, block duplication for templates, and text formatting cleanup - common tasks in content management workflows. For more information about block management, see the Creating and updating blocks guide.

import {
buildBlockRecord,
buildClient,
duplicateBlockRecord,
inspectItem,
SchemaRepository,
type ApiTypes,
type BlockNodeInNestedResponse,
type ItemTypeDefinition,
} from "@datocms/cma-client-node";
import {
findFirstNode,
isBlock,
isInlineBlock,
mapNodes,
} from "datocms-structured-text-utils";
type EnvironmentSettings = { locales: "en" };
/*
* Article
* ā”œā”€ title: string
* ā”œā”€ author: string
* └─ content: structured_text
* ā”œā”€ CtaBlock: title, description, button_text, button_url
* ā”œā”€ ProductMentionInline: product_name, price, affiliate_url
* └─ ImageGalleryBlock: title, images
*/
// šŸ‘‡ Definitions can be generated automatically using CLI: https://www.datocms.com/cma-ts-schema
type Article = ItemTypeDefinition<
EnvironmentSettings,
"ZV0o9497SsqWxQR8HEQddw",
{
title: { type: "string" };
author: { type: "string" };
content: {
type: "structured_text";
blocks: CtaBlock | ImageGalleryBlock;
inline_blocks: ProductMentionInline;
};
}
>;
type CtaBlock = ItemTypeDefinition<
EnvironmentSettings,
"d-CHYg-rShOt3kiL6ZN1yA",
{
title: { type: "string" };
description: { type: "text" };
button_text: { type: "string" };
button_url: { type: "string" };
}
>;
type ProductMentionInline = ItemTypeDefinition<
EnvironmentSettings,
"VGXgXav9SwG5P48frGrFxA",
{
product_name: { type: "string" };
price: { type: "float" };
affiliate_url: { type: "string" };
}
>;
type ImageGalleryBlock = ItemTypeDefinition<
EnvironmentSettings,
"I8Q6k-HqQmaZ498WKtvFbg",
{
title: { type: "string" };
images: { type: "gallery" };
}
>;
// Make sure the API token has access to the CMA, and is stored securely
const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });
async function run() {
// Schema repository for block duplication
const schemaRepository = new SchemaRepository(client);
// Get the current article with nested blocks to analyze existing content
const currentArticle = await client.items.find<Article>(
"RSfdsZbbR7ixGgMBSmcaVA",
{
nested: true,
},
);
if (!currentArticle.content) {
return;
}
console.log("-- BEFORE UPDATE --");
console.log(inspectItem(currentArticle));
let newContent = currentArticle.content as NonNullable<
ApiTypes.ItemUpdateSchema<Article>["content"]
>;
// 1. DUPLICATING BLOCKS: Find first block node containing a CTA block, and
// repeat it at the end of the document
const firstNodeContainingCtaBlock = findFirstNode(
newContent,
(node): node is BlockNodeInNestedResponse<CtaBlock> => {
return (
isBlock(node) &&
typeof node.item !== "string" &&
node.item.__itemTypeId === "d-CHYg-rShOt3kiL6ZN1yA"
);
},
);
if (firstNodeContainingCtaBlock) {
// duplicateBlockRecord() clones the block object and removes block IDs (this applies to nested blocks too)
// This creates a new block template that can be used elsewhere or modified
const duplicateCtaBlock = await duplicateBlockRecord<CtaBlock>(
firstNodeContainingCtaBlock.node.item,
schemaRepository,
);
newContent.document.children.push({
type: "block",
item: duplicateCtaBlock,
});
}
// 2. UPDATING EXISTING BLOCKS: Either inside 'block' or 'inlineBlock' nodes
newContent = mapNodes(currentArticle.content, (node) => {
if (isBlock(node)) {
// Normalize existing CTA blocks URLs
if (
node.item.__itemTypeId === "d-CHYg-rShOt3kiL6ZN1yA" &&
node.item.attributes.button_url?.includes("old-domain.com")
) {
return {
...node,
item: buildBlockRecord<CtaBlock>({
item_type: { type: "item_type", id: "d-CHYg-rShOt3kiL6ZN1yA" },
id: node.item.id,
button_url: node.item.attributes.button_url.replace(
"old-domain.com",
"new-domain.com",
),
// Skip other attributes to keep them unchanged
}),
};
}
// If we don't need to make a change to the block (ie. Image Galleries),
// just pass the block ID
return { ...node, item: node.item.id };
}
// Normalize update referrals in Product Mention blocks
if (isInlineBlock(node)) {
if (
node.item.__itemTypeId === "VGXgXav9SwG5P48frGrFxA" &&
node.item.attributes.affiliate_url?.includes("ref=blog")
) {
const updatedUrl = new URL(node.item.attributes.affiliate_url);
updatedUrl.searchParams.set("ref", "blog");
updatedUrl.searchParams.set("source", "article_mention");
return {
...node,
item: buildBlockRecord<ProductMentionInline>({
item_type: { type: "item_type", id: "VGXgXav9SwG5P48frGrFxA" },
id: node.item.id,
affiliate_url: updatedUrl.toString(),
// Keep product_name and price unchanged
}),
};
}
// If we don't need to make a change to the node, just pass the block ID
return { ...node, item: node.item.id };
}
// Keep any other dast node unchanged
return node;
});
await client.items.update<Article>(currentArticle.id, {
content: newContent,
// Keep title and author unchanged
});
const updatedArticleWithNestedBlocks = await client.items.find(
currentArticle,
{ nested: true },
);
console.log(inspectItem(updatedArticleWithNestedBlocks));
}
run();
-- BEFORE UPDATE --
ā”” Item "RSfdsZbbR7ixGgMBSmcaVA" (item_type: "ZV0o9497SsqWxQR8HEQddw")
ā”œ title: "The Future of E-commerce Technology"
ā”œ author: "Alex Thompson"
ā”” content
ā”œ paragraph
│ ā”œ span "E-commerce is evolving rapidly with new technologies like "
│ ā”œ inlineBlock
│ │ ā”” Item "fjmlyIjHTKqcAN3quV0rjg" (item_type: "VGXgXav9SwG5P48frGrFxA")
│ │ ā”œ product_name: "AI Shopping Assistant"
│ │ ā”œ price: 99.99
│ │ ā”” affiliate_url: "https://old-domain.com/product?ref=blog"
│ ā”” span " transforming how customers shop online."
ā”œ block
│ ā”” Item "Fg6G7h6sQ5KoUQGwyBKMrA" (item_type: "d-CHYg-rShOt3kiL6ZN1yA")
│ ā”œ title: "Join the Revolution"
│ ā”œ description: "Stay ahead of the curve with our e-commerce insights."
│ ā”œ button_text: "Subscribe Now"
│ ā”” button_url: "https://old-domain.com/subscribe"
ā”” block
ā”” Item "QXFFGKZjSVWKdph58Gl_Nw" (item_type: "I8Q6k-HqQmaZ498WKtvFbg")
ā”œ title: "E-commerce Innovation Gallery"
ā”” images
ā”œ [0]
│ ā”œ upload_id: "DIMKDxbVTxuJ_-SEIgEg8g"
│ ā”œ alt: "Modern e-commerce interface"
│ ā”” title: "Next-gen Shopping"
ā”” [1]
ā”œ upload_id: "SUpTsatBQrSpErzL1uwcTA"
ā”œ alt: "AI-powered recommendations"
ā”” title: "Smart Product Discovery"
ā”” Item "RSfdsZbbR7ixGgMBSmcaVA" (item_type: "ZV0o9497SsqWxQR8HEQddw")
ā”œ title: "The Future of E-commerce Technology"
ā”œ author: "Alex Thompson"
ā”” content
ā”œ paragraph
│ ā”œ span "E-commerce is evolving rapidly with new technologies like "
│ ā”œ inlineBlock
│ │ ā”” Item "fjmlyIjHTKqcAN3quV0rjg" (item_type: "VGXgXav9SwG5P48frGrFxA")
│ │ ā”œ product_name: "AI Shopping Assistant"
│ │ ā”œ price: 99.99
│ │ ā”” affiliate_url: "https://old-domain.com/product?ref=blog&source=article_mention"
│ ā”” span " transforming how customers shop online."
ā”œ block
│ ā”” Item "Fg6G7h6sQ5KoUQGwyBKMrA" (item_type: "d-CHYg-rShOt3kiL6ZN1yA")
│ ā”œ title: "Join the Revolution"
│ ā”œ description: "Stay ahead of the curve with our e-commerce insights."
│ ā”œ button_text: "Subscribe Now"
│ ā”” button_url: "https://new-domain.com/subscribe"
ā”œ block
│ ā”” Item "QXFFGKZjSVWKdph58Gl_Nw" (item_type: "I8Q6k-HqQmaZ498WKtvFbg")
│ ā”œ title: "E-commerce Innovation Gallery"
│ ā”” images
│ ā”œ [0]
│ │ ā”œ upload_id: "DIMKDxbVTxuJ_-SEIgEg8g"
│ │ ā”œ alt: "Modern e-commerce interface"
│ │ ā”” title: "Next-gen Shopping"
│ ā”” [1]
│ ā”œ upload_id: "SUpTsatBQrSpErzL1uwcTA"
│ ā”œ alt: "AI-powered recommendations"
│ ā”” title: "Smart Product Discovery"
ā”” block
ā”” Item "Notn7oYoSr6XWf1OITcrtA" (item_type: "d-CHYg-rShOt3kiL6ZN1yA")
ā”œ title: ""
ā”œ description: ""
ā”œ button_text: ""
ā”” button_url: "https://new-domain.com/subscribe"

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" } }
}
}
]
}
}
🌐 Unified field processing utilities

We provide utilities that offer a unified interface for working with DatoCMS field values that may or may not be localized. They eliminate the need for conditional logic when processing fields that could be either localized or non-localized. Check out our Unified Field Processing utilities for streamlined localization handling.

If the all_locales_required option in a model is turned off, then its records do not need all environment's locales to be defined for localized fields, so you're free to add/remove locales during an update operation.

Suppose your environment's locales are English, Italian and German (['en', 'it', 'de']) and the following record currently defines en and it locales on its localized fields.

To add the de locale to this record, you have to send an update request containing all localized fields. For each one of these, you must define the exiting locales plus the ones you want to add:

import {
buildClient,
inspectItem,
type ItemTypeDefinition,
} from "@datocms/cma-client-node";
type EnvironmentSettings = { locales: "en" | "it" | "de" };
// šŸ‘‡ Definitions can be generated automatically using CLI: https://www.datocms.com/cma-ts-schema
type Article = ItemTypeDefinition<
EnvironmentSettings,
"UZyfjdBES8y2W2ruMEHSoA",
{
title: { type: "string"; localized: true };
content: { type: "text"; localized: true };
author: { type: "text" };
}
>;
async function run() {
// Make sure the API token has access to the CMA, and is stored securely
const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });
const item = await client.items.update<Article>("T4m4tPymSACFzsqbZS65WA", {
title: {
en: "My title",
it: "Il mio titolo",
de: "Mein Titel",
},
content: {
en: "Article content",
it: "Contenuto articolo",
de: "Artikelinhalt",
},
// when adding a locale, non-localized fields (author) can be skipped
});
console.log(inspectItem(item));
}
run();
ā”” Item "T4m4tPymSACFzsqbZS65WA" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
ā”œ author: "Stefano Verna"
ā”œ title
│ ā”œ de: "Mein Titel"
│ ā”œ en: "My title"
│ ā”” it: "Il mio titolo"
ā”” content
ā”œ de: "Artikelinhalt"
ā”œ en: "Article content"
ā”” it: "Contenuto articolo"

If the all_locales_required option in a model is turned off, then its records do not need all environment's locales to be defined for localized fields, so you're free to add/remove locales during an update operation.

This example demonstrates two approaches for removing a locale from records:

  • When schema is known: When you know the exact structure of your models, you can use ItemTypeDefinitions to work with full type safety. This approach is ideal for specific, targeted operations.

  • When schema is unknown: When you need to work with models dynamically (without knowing their structure ahead of time), you can use client.fields.list() to discover field definitions at runtime. This approach is perfect for bulk operations across multiple models.

Both approaches remove the it locale by omitting the unwanted locale from all localized fields while preserving other locales and non-localized fields.

import type { ApiTypes } from "@datocms/cma-client-node";
import {
buildClient,
inspectItem,
isLocalized,
type ItemTypeDefinition,
type LocalizedFieldValue,
} from "@datocms/cma-client-node";
import lodash from "lodash";
// Make sure the API token has access to the CMA, and is stored securely
const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });
// When you know the exact model structure, you can define types
// and work with full type safety and IntelliSense!
async function removeLocaleWhenSchemaIsKnown() {
type EnvironmentSettings = { locales: "en" | "it" | "de" };
// šŸ‘‡ Definitions can be generated automatically using CLI: https://www.datocms.com/cma-ts-schema
type CtaBlock = ItemTypeDefinition<
EnvironmentSettings,
"d-CHYg-rShOt3kiL6ZN1yA",
{
title: { type: "string" };
button_text: { type: "string" };
button_url: { type: "string" };
}
>;
type Article = ItemTypeDefinition<
EnvironmentSettings,
"UZyfjdBES8y2W2ruMEHSoA",
{
title: { type: "string"; localized: true };
content: { type: "structured_text"; localized: true };
cta_block: { type: "single_block"; blocks: CtaBlock };
cover_image: { type: "file" };
}
>;
// First, fetch the existing record to get current values
const existingRecord = await client.items.find<Article>(
"T4m4tPymSACFzsqbZS65WA",
);
console.log("-- BEFORE UPDATE --");
console.log(inspectItem(existingRecord));
// Update the record to remove the "it" locale by omitting it from all localized fields
// Using lodash omit() for clean, readable locale removal
const updatedRecord = await client.items.update<Article>(
"T4m4tPymSACFzsqbZS65WA",
{
title: lodash.omit(existingRecord.title, "it"),
content: lodash.omit(existingRecord.content, "it"),
// Do not pass non-localized fields (ie. cover_image, cta_block), as we want to keep them unchanged
},
);
console.log("-- AFTER LOCALE REMOVAL --");
console.log(inspectItem(updatedRecord));
}
// When you don't know the model structure ahead of time,
// you can dynamically load the fields and perform the same operation
async function removeLocaleWhenSchemaIsUnknown() {
// Get the model fields
const fields = await client.fields.list("ZV0o9497SsqWxQR8HEQddw");
// Filter to only localized fields using the isLocalized helper
const localizedFields = fields.filter(isLocalized);
// Process all records of this model type
for await (const record of client.items.listPagedIterator({
filter: { type: "ZV0o9497SsqWxQR8HEQddw" },
})) {
const updatePayload: ApiTypes.ItemUpdateSchema = {};
// Build update payload by processing each localized field
for (const field of localizedFields) {
const fieldValue = record[field.api_key] as LocalizedFieldValue;
// Remove the "it" locale from each field's localized values
updatePayload[field.api_key] = lodash.omit(fieldValue, "it");
}
// Update the record with the modified locale data
await client.items.update(record.id, updatePayload);
}
console.log("Removed 'it' locale from all records of the model");
}
async function run() {
// Run both examples
await removeLocaleWhenSchemaIsKnown();
await removeLocaleWhenSchemaIsUnknown();
}
run();
-- BEFORE UPDATE --
ā”” Item "T4m4tPymSACFzsqbZS65WA" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
ā”œ title
│ ā”œ de: "Content-Management verstehen"
│ ā”œ en: "Understanding Content Management"
│ ā”” it: "Capire la gestione dei contenuti"
ā”œ content
│ ā”œ de
│ │ ā”” paragraph
│ │ ā”” span "Ein umfassender Leitfaden für moderne Content-Management-Systeme und ..."
│ ā”œ en
│ │ ā”” paragraph
│ │ ā”” span "A comprehensive guide to modern content management systems and best p..."
│ ā”” it
│ ā”” paragraph
│ ā”” span "Una guida completa ai sistemi di gestione dei contenuti moderni e all..."
ā”œ cta_block: "ZPfQFuaqTn2cdoQnPSsu_g"
ā”” cover_image
ā”” upload_id: "adCusKKeRPO5wtjrIIcGjw"
-- AFTER LOCALE REMOVAL --
ā”” Item "T4m4tPymSACFzsqbZS65WA" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
ā”œ title
│ ā”œ de: "Content-Management verstehen"
│ ā”” en: "Understanding Content Management"
ā”œ content
│ ā”œ de
│ │ ā”” paragraph
│ │ ā”” span "Ein umfassender Leitfaden für moderne Content-Management-Systeme und ..."
│ ā”” en
│ ā”” paragraph
│ ā”” span "A comprehensive guide to modern content management systems and best p..."
ā”œ cta_block: "ZPfQFuaqTn2cdoQnPSsu_g"
ā”” cover_image
ā”” upload_id: "adCusKKeRPO5wtjrIIcGjw"
Removed 'it' locale from all records of the model

When working with localized content, you may need to duplicate content from one locale to another. This is particularly useful when:

  • Adding a new locale and wanting to start with existing content as a baseline
  • Creating region-specific variations (e.g., copying English to Austrian English)
  • Providing fallback content for incomplete translations

This example demonstrates how to copy all content from one locale (en) to another (en-AT) across all models in your project, handling both simple fields and complex nested block structures.

The script iterates through all content models, identifies localized fields, and processes each record. For each localized field, it uses specialized utility functions to handle the complexity of nested blocks and maintain data integrity.

Key utilities used:

  • mapNormalizedFieldValuesAsync(): Transforms localized field values by applying an async function to each locale. This allows you to process each language version independently while maintaining the localized structure.

  • mapBlocksInNonLocalizedFieldValue(): Recursively processes blocks within the field value, allowing you to transform nested blocks. In this example, it's used to remove IDs from blocks when copying them to a new locale, ensuring new block instances are created rather than referencing existing ones.

The script is designed to:

  • Handle deeply nested blocks correctly by creating new instances for the target locale
  • Preserve existing content in other locales efficiently
  • Work with all field types (Modular Content, Single Block, Structured Text, etc.)
import type { ApiTypes } from "@datocms/cma-client-node";
import {
buildClient,
inspectItem,
isItemWithOptionalMeta,
isLocalized,
mapBlocksInNonLocalizedFieldValue,
mapNormalizedFieldValuesAsync,
SchemaRepository,
type LocalizedFieldValue,
} from "@datocms/cma-client-node";
import assert from "node:assert";
async function run() {
// Make sure the API token has access to the CMA, and is stored securely
const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });
const schemaRepository = new SchemaRepository(client);
// Iterate through all content models
for (const model of await schemaRepository.getAllModels()) {
const fields = await schemaRepository.getItemTypeFields(model);
const localizedFields = fields.filter(isLocalized);
// Process all records of this model type
for await (const record of client.items.listPagedIterator({
filter: { type: model.api_key },
version: "current",
nested: true, // Important: get full block objects, not just IDs!
})) {
const updatePayload: ApiTypes.ItemUpdateSchema = {};
let hasChanges = false;
for (const field of localizedFields) {
const fieldValueWithNestedBlocks = record[
field.api_key
] as LocalizedFieldValue;
// Skip if en content doesn't exist
if (!fieldValueWithNestedBlocks["en"]) {
continue;
}
// Process the locales by converting any full block object to just IDs
const newFieldValue = (await mapNormalizedFieldValuesAsync(
fieldValueWithNestedBlocks,
field,
async (_locale, fieldValueForLocale) => {
return mapBlocksInNonLocalizedFieldValue(
fieldValueForLocale,
field.field_type,
schemaRepository,
(block) => {
assert(isItemWithOptionalMeta(block));
// Passing just the ID => "Keep the existing block unchanged"
return block.id;
},
);
},
)) as LocalizedFieldValue;
// Duplicate en content and assign it as en-AT
// Use recursive mapping to remove IDs from any block
newFieldValue["en-AT"] = await mapBlocksInNonLocalizedFieldValue(
fieldValueWithNestedBlocks["en"],
field.field_type,
schemaRepository,
(block) => {
assert(isItemWithOptionalMeta(block));
// Block with no ID => "Create new block instance"
const { id, ...blockWithoutId } = block;
return blockWithoutId;
},
);
updatePayload[field.api_key] = newFieldValue;
hasChanges = true;
}
// Update the record if there are changes
if (hasChanges) {
console.log("-- EXISTING RECORD --");
console.log(inspectItem(record));
console.log("-- UPDATE PAYLOAD --");
console.log(inspectItem(updatePayload));
await client.items.update(record.id, updatePayload);
const nestedRecord = await client.items.find(record.id, {
nested: true,
});
console.log("-- RECORD AFTER UPDATE --");
console.log(inspectItem(nestedRecord));
}
}
}
}
run();
-- EXISTING RECORD --
ā”” Item "Bz0dHLjeRuCW10fJl1GF0w" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
ā”œ name
│ ā”œ de: "Premium Kabellose Kopfhƶrer"
│ ā”” en: "Premium Wireless Headphones"
ā”œ description
│ ā”œ de: "Erleben Sie kristallklaren Klang mit unseren hochwertigen kabellosen Kopfhƶre..."
│ ā”” en: "Experience crystal-clear audio with our top-of-the-line wireless headphones f..."
ā”œ features
│ ā”œ de
│ │ ā”œ [0] Item "YazSxGr2TlKLJZjJmyhg0Q" (item_type: "T4m4tPymSACFzsqbZS65WA")
│ │ │ ā”œ title: "Aktive GerƤuschunterdrückung"
│ │ │ ā”” description: "Blockieren Sie unerwünschte GerƤusche mit unserer fortschrittlichen ANC-Techn..."
│ │ ā”” [1] Item "D1Zh8Ff2SaC1CCG67Ic1sQ" (item_type: "T4m4tPymSACFzsqbZS65WA")
│ │ ā”œ title: "30 Stunden Akkulaufzeit"
│ │ ā”” description: "GanztƤgiges Hƶren mit Schnellladefunktion."
│ ā”” en
│ ā”œ [0] Item "HqZZmo8sRKuKMfaUZbkNig" (item_type: "T4m4tPymSACFzsqbZS65WA")
│ │ ā”œ title: "Active Noise Cancellation"
│ │ ā”” description: "Block out unwanted noise with our advanced ANC technology."
│ ā”” [1] Item "NKaGQ1AUQZSfHHhL0c3eLA" (item_type: "T4m4tPymSACFzsqbZS65WA")
│ ā”œ title: "30-Hour Battery Life"
│ ā”” description: "All-day listening with fast charging capabilities."
ā”” price: 299.99
-- UPDATE PAYLOAD --
ā”” Item
ā”œ description
│ ā”œ de: "Erleben Sie kristallklaren Klang mit unseren hochwertigen kabellosen Kopfhƶre..."
│ ā”œ en: "Experience crystal-clear audio with our top-of-the-line wireless headphones f..."
│ ā”” en-AT: "Experience crystal-clear audio with our top-of-the-line wireless headphones f..."
ā”œ features
│ ā”œ de
│ │ ā”œ [0] "YazSxGr2TlKLJZjJmyhg0Q"
│ │ ā”” [1] "D1Zh8Ff2SaC1CCG67Ic1sQ"
│ ā”œ en
│ │ ā”œ [0] "HqZZmo8sRKuKMfaUZbkNig"
│ │ ā”” [1] "NKaGQ1AUQZSfHHhL0c3eLA"
│ ā”” en-AT
│ ā”œ [0] Item (item_type: "T4m4tPymSACFzsqbZS65WA")
│ │ ā”œ title: "Active Noise Cancellation"
│ │ ā”” description: "Block out unwanted noise with our advanced ANC technology."
│ ā”” [1] Item (item_type: "T4m4tPymSACFzsqbZS65WA")
│ ā”œ title: "30-Hour Battery Life"
│ ā”” description: "All-day listening with fast charging capabilities."
ā”” name
ā”œ de: "Premium Kabellose Kopfhƶrer"
ā”œ en: "Premium Wireless Headphones"
ā”” en-AT: "Premium Wireless Headphones"
-- RECORD AFTER UPDATE --
ā”” Item "Bz0dHLjeRuCW10fJl1GF0w" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
ā”œ name
│ ā”œ de: "Premium Kabellose Kopfhƶrer"
│ ā”œ en: "Premium Wireless Headphones"
│ ā”” en-AT: "Premium Wireless Headphones"
ā”œ description
│ ā”œ de: "Erleben Sie kristallklaren Klang mit unseren hochwertigen kabellosen Kopfhƶre..."
│ ā”œ en: "Experience crystal-clear audio with our top-of-the-line wireless headphones f..."
│ ā”” en-AT: "Experience crystal-clear audio with our top-of-the-line wireless headphones f..."
ā”œ features
│ ā”œ de
│ │ ā”œ [0] Item "YazSxGr2TlKLJZjJmyhg0Q" (item_type: "T4m4tPymSACFzsqbZS65WA")
│ │ │ ā”œ title: "Aktive GerƤuschunterdrückung"
│ │ │ ā”” description: "Blockieren Sie unerwünschte GerƤusche mit unserer fortschrittlichen ANC-Techn..."
│ │ ā”” [1] Item "D1Zh8Ff2SaC1CCG67Ic1sQ" (item_type: "T4m4tPymSACFzsqbZS65WA")
│ │ ā”œ title: "30 Stunden Akkulaufzeit"
│ │ ā”” description: "GanztƤgiges Hƶren mit Schnellladefunktion."
│ ā”œ en
│ │ ā”œ [0] Item "HqZZmo8sRKuKMfaUZbkNig" (item_type: "T4m4tPymSACFzsqbZS65WA")
│ │ │ ā”œ title: "Active Noise Cancellation"
│ │ │ ā”” description: "Block out unwanted noise with our advanced ANC technology."
│ │ ā”” [1] Item "NKaGQ1AUQZSfHHhL0c3eLA" (item_type: "T4m4tPymSACFzsqbZS65WA")
│ │ ā”œ title: "30-Hour Battery Life"
│ │ ā”” description: "All-day listening with fast charging capabilities."
│ ā”” en-AT
│ ā”œ [0] Item "Txk_qqFJSL6VP3wFzmE_2w" (item_type: "T4m4tPymSACFzsqbZS65WA")
│ │ ā”œ title: "Active Noise Cancellation"
│ │ ā”” description: "Block out unwanted noise with our advanced ANC technology."
│ ā”” [1] Item "eValhxXxTZe-6FW-mwe80g" (item_type: "T4m4tPymSACFzsqbZS65WA")
│ ā”œ title: "30-Hour Battery Life"
│ ā”” description: "All-day listening with fast charging capabilities."
ā”” price: 299.99

Mass 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.

This example demonstrates how to edit specific blocks no matter where they're embedded — in Modular Content, Single Block, or Structured Text fields, including deeply nested structures and localized content.

The script shows how to:

This approach ensures that block properties are systematically updated across all content, regardless of how deeply nested they may be within your content structure. In this specific example, CTA blocks get their style property automatically set to "primary" for high-intent copy or "muted" for standard copy based on the button text content.

import {
buildBlockRecord,
buildClient,
inspectItem,
isItemWithOptionalMeta,
mapBlocksInNonLocalizedFieldValue,
mapNormalizedFieldValuesAsync,
SchemaRepository,
type ApiTypes,
type BlockInNestedResponse,
type ItemTypeDefinition,
} from "@datocms/cma-client-node";
import assert from "node:assert";
type EnvironmentSettings = { locales: "en" | "it" };
// šŸ‘‡ Definitions can be generated automatically using CLI: https://www.datocms.com/cma-ts-schema
type CtaBlock = ItemTypeDefinition<
EnvironmentSettings,
"DC2XVF6BTjGBgQaoaih6Og",
{
title: { type: "string" };
description: { type: "text" };
button_text: { type: "string" };
button_url: { type: "string" };
style: { type: "string" }; // "primary" | "muted"
}
>;
// Simplified style decision logic
function computeCtaStyle(text: string | null): "primary" | "muted" {
if (!text) return "muted";
return /buy|get started|start free|sign up|upgrade/i.test(text)
? "primary"
: "muted";
}
// Make sure the API token has access to the CMA, and is stored securely
const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });
async function run() {
const schemaRepository = new SchemaRepository(client);
const ctaBlockModel = await schemaRepository.getItemTypeById(
"DC2XVF6BTjGBgQaoaih6Og",
);
// 1. Find all models that can embed CTA blocks (directly or indirectly)
const modelsEmbeddingCtas = await schemaRepository.getModelsEmbeddingBlocks([
ctaBlockModel,
]);
// 2. Process each model and its records
for (const model of modelsEmbeddingCtas) {
console.log(
`\nšŸ“‹ Processing records of model: ${model.name} (${model.api_key})`,
);
const fields = await schemaRepository.getItemTypeFields(model);
for await (const record of client.items.rawListPagedIterator({
filter: { type: model.id },
version: "current",
nested: true, // Get full block objects
})) {
console.log(`\n--- Processing ${record.id} ---`);
console.log("BEFORE:");
console.log(inspectItem(record));
const updatedAttributes: ApiTypes.ItemUpdateSchema = {};
// 3. Use mapNormalizedFieldValuesAsync to handle localized/non-localized uniformly
for (const field of fields) {
const fieldValue = record.attributes[field.api_key];
let fieldHasChanges = false;
const updatedFieldValue = await mapNormalizedFieldValuesAsync(
fieldValue,
field,
async (_locale, normalizedFieldValue) =>
mapBlocksInNonLocalizedFieldValue(
normalizedFieldValue,
field.field_type,
schemaRepository,
(block) => {
assert(isItemWithOptionalMeta(block));
if (block.__itemTypeId !== "DC2XVF6BTjGBgQaoaih6Og") {
return block.id; // Keep non-CTA blocks as is
}
const ctaBlock = block as BlockInNestedResponse<CtaBlock>;
const currentStyle = ctaBlock.attributes.style;
const desiredStyle = computeCtaStyle(
ctaBlock.attributes.button_text,
);
if (currentStyle !== desiredStyle) {
fieldHasChanges = true;
// Return an updated block record with new style
return buildBlockRecord<CtaBlock>({
item_type: {
type: "item_type",
id: "DC2XVF6BTjGBgQaoaih6Og",
},
id: ctaBlock.id,
style: desiredStyle,
});
}
return block.id; // No change needed
},
),
);
if (fieldHasChanges) {
updatedAttributes[field.api_key] = updatedFieldValue;
}
}
// 4. Update the record if there were changes
if (Object.keys(updatedAttributes).length > 0) {
const updatedRecord = await client.items.update(
record.id,
updatedAttributes,
);
console.log("AFTER:");
await inspectItemWithNestedBlocks(updatedRecord);
} else {
console.log("✨ No changes needed for this record");
}
}
}
}
run();
async function inspectItemWithNestedBlocks(item: ApiTypes.Item) {
const itemWithNestedBlocks = await client.items.find(item, { nested: true });
console.log(inspectItem(itemWithNestedBlocks));
}
šŸ“‹ Processing records of model: Landing Page (landing_page)
--- Processing IwcHsSQ5SSa2LYzpk_Ddjw ---
BEFORE:
ā”” Item "IwcHsSQ5SSa2LYzpk_Ddjw" (item_type: "KUz2pYAvQvOWqv3dVwVw3w")
ā”” hero_cta
ā”” Item "fnclskI4RG25uLQC92XE5g" (item_type: "DC2XVF6BTjGBgQaoaih6Og")
ā”œ title: "Transform Your Business"
ā”œ description: "Take the next step forward"
ā”œ button_text: "Get started"
ā”œ button_url: "/start"
ā”” style: "muted"
AFTER:
ā”” Item "IwcHsSQ5SSa2LYzpk_Ddjw" (item_type: "KUz2pYAvQvOWqv3dVwVw3w")
ā”” hero_cta
ā”” Item "fnclskI4RG25uLQC92XE5g" (item_type: "DC2XVF6BTjGBgQaoaih6Og")
ā”œ title: "Transform Your Business"
ā”œ description: "Take the next step forward"
ā”œ button_text: "Get started"
ā”œ button_url: "/start"
ā”” style: "primary"
šŸ“‹ Processing records of model: Article (article)
--- Processing JPplplyPTMKpCbB-wipxeA ---
BEFORE:
ā”” Item "JPplplyPTMKpCbB-wipxeA" (item_type: "ONxSjA4WTWaoNJY2zokUoQ")
ā”œ title
│ ā”œ en: "Sample Article"
│ ā”” it: "Articolo di Esempio"
ā”œ content
│ ā”œ en
│ │ ā”œ paragraph
│ │ │ ā”” span "Introduction text"
│ │ ā”œ block
│ │ │ ā”” Item "BOps1UFgSU-CSm-fRjoqCA" (item_type: "DC2XVF6BTjGBgQaoaih6Og")
│ │ │ ā”œ title: "Join Our Platform"
│ │ │ ā”œ description: "Start your journey today"
│ │ │ ā”œ button_text: "Sign up"
│ │ │ ā”œ button_url: "/signup"
│ │ │ ā”” style: "muted"
│ │ ā”” paragraph
│ │ ā”” span "Conclusion text"
│ ā”” it
│ ā”œ paragraph
│ │ ā”” span "Testo introduttivo"
│ ā”” block
│ ā”” Item "SSVQgIbZS_uoPQWg-qYzWg" (item_type: "DC2XVF6BTjGBgQaoaih6Og")
│ ā”œ title: "Scopri di Più"
│ ā”œ description: "Leggi la nostra guida"
│ ā”œ button_text: "Learn more"
│ ā”œ button_url: "/guide"
│ ā”” style: "primary"
ā”” sidebar
ā”” [0] Item "f86JBAwxTsasu7J3XxXqJg" (item_type: "DC2XVF6BTjGBgQaoaih6Og")
ā”œ title: "Special Offer"
ā”œ description: "Limited time promotion"
ā”œ button_text: "Buy now"
ā”œ button_url: "/buy"
ā”” style: "muted"
AFTER:
ā”” Item "JPplplyPTMKpCbB-wipxeA" (item_type: "ONxSjA4WTWaoNJY2zokUoQ")
ā”œ title
│ ā”œ en: "Sample Article"
│ ā”” it: "Articolo di Esempio"
ā”œ content
│ ā”œ en
│ │ ā”œ paragraph
│ │ │ ā”” span "Introduction text"
│ │ ā”œ block
│ │ │ ā”” Item "BOps1UFgSU-CSm-fRjoqCA" (item_type: "DC2XVF6BTjGBgQaoaih6Og")
│ │ │ ā”œ title: "Join Our Platform"
│ │ │ ā”œ description: "Start your journey today"
│ │ │ ā”œ button_text: "Sign up"
│ │ │ ā”œ button_url: "/signup"
│ │ │ ā”” style: "primary"
│ │ ā”” paragraph
│ │ ā”” span "Conclusion text"
│ ā”” it
│ ā”œ paragraph
│ │ ā”” span "Testo introduttivo"
│ ā”” block
│ ā”” Item "SSVQgIbZS_uoPQWg-qYzWg" (item_type: "DC2XVF6BTjGBgQaoaih6Og")
│ ā”œ title: "Scopri di Più"
│ ā”œ description: "Leggi la nostra guida"
│ ā”œ button_text: "Learn more"
│ ā”œ button_url: "/guide"
│ ā”” style: "muted"
ā”” sidebar
ā”” [0] Item "f86JBAwxTsasu7J3XxXqJg" (item_type: "DC2XVF6BTjGBgQaoaih6Og")
ā”œ title: "Special Offer"
ā”œ description: "Limited time promotion"
ā”œ button_text: "Buy now"
ā”œ button_url: "/buy"
ā”” style: "primary"

This example demonstrates how to remove specific blocks no matter where they're embedded — in Modular Content, Single Block, or Structured Text fields, including deeply nested structures and localized content.

The script shows how to:

This approach ensures that deprecated or invalid blocks are systematically removed from all content, regardless of how deeply nested they may be within your content structure.

import {
buildClient,
filterBlocksInNonLocalizedFieldValue,
inspectItem,
isItemWithOptionalMeta,
mapNormalizedFieldValuesAsync,
SchemaRepository,
type ApiTypes,
type BlockInNestedResponse,
type ItemTypeDefinition,
} from "@datocms/cma-client-node";
import assert from "node:assert";
type EnvironmentSettings = { locales: "en" | "it" };
// šŸ‘‡ Definitions can be generated automatically using CLI: https://www.datocms.com/cma-ts-schema
type ProductBlock = ItemTypeDefinition<
EnvironmentSettings,
"DC2XVF6BTjGBgQaoaih6Og",
{
sku: { type: "string" };
}
>;
// Mock external ecommerce system check
async function isValidSKU(sku: string | null): Promise<boolean> {
// For demo purposes, consider SKUs starting with "INVALID" as discontinued
return Boolean(sku && !sku.startsWith("INVALID"));
}
async function run() {
// Make sure the API token has access to the CMA, and is stored securely
const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });
const schemaRepository = new SchemaRepository(client);
const productBlockModel = await schemaRepository.getItemTypeById(
"DC2XVF6BTjGBgQaoaih6Og",
);
// 1. Find all models that can embed Product blocks (directly or indirectly)
const modelsEmbeddingProductBlocks =
await schemaRepository.getModelsEmbeddingBlocks([productBlockModel]);
// 2. Process each model and its records
for (const model of modelsEmbeddingProductBlocks) {
console.log(
`\nšŸ“‹ Processing records of model: ${model.name} (${model.api_key})`,
);
const fields = await schemaRepository.getItemTypeFields(model);
for await (const record of client.items.rawListPagedIterator({
filter: { type: model.id },
version: "current",
nested: true, // Get full block objects
})) {
console.log(`\n--- Processing ${record.id} ---`);
console.log("BEFORE:");
console.log(inspectItem(record));
const updatedAttributes: ApiTypes.ItemUpdateSchema = {};
// 3. Use mapNormalizedFieldValuesAsync to handle localized/non-localized fields uniformly
for (const field of fields) {
const fieldValue = record.attributes[field.api_key];
let fieldHasChanges = false;
const updatedFieldValue = await mapNormalizedFieldValuesAsync(
fieldValue,
field,
async (_locale, normalizedFieldValue) => {
// 4. Use filterBlocksInNonLocalizedFieldValue to recursively filter blocks
const filteredValue = await filterBlocksInNonLocalizedFieldValue(
normalizedFieldValue,
field.field_type,
schemaRepository,
async (block) => {
assert(isItemWithOptionalMeta(block));
// Only check Product blocks
if (block.__itemTypeId !== "DC2XVF6BTjGBgQaoaih6Og") {
return true; // Keep other blocks
}
const productBlock =
block as BlockInNestedResponse<ProductBlock>;
// Check if the product SKU is still valid in external system
const isValid = await isValidSKU(productBlock.attributes.sku);
if (!isValid) {
fieldHasChanges = true;
}
return isValid;
},
);
return filteredValue;
},
);
if (fieldHasChanges) {
updatedAttributes[field.api_key] = updatedFieldValue;
}
}
// 5. Update the record if there were changes
if (Object.keys(updatedAttributes).length > 0) {
const updatedRecord = await client.items.update(
record.id,
updatedAttributes,
);
console.log("AFTER:");
console.log(inspectItem(updatedRecord));
} else {
console.log("✨ No changes needed for this record");
}
}
}
}
run();
šŸ“‹ Processing records of model: Product Page (product_page)
--- Processing RZfnYc3iSya7yYaTxoW0VA ---
BEFORE:
ā”” Item "RZfnYc3iSya7yYaTxoW0VA" (item_type: "KUz2pYAvQvOWqv3dVwVw3w")
ā”” featured_product
ā”” Item "XayqICFqQEyEqGFypcK07w" (item_type: "DC2XVF6BTjGBgQaoaih6Og")
ā”” sku: "INVALID-SKU-004"
AFTER:
ā”” Item "RZfnYc3iSya7yYaTxoW0VA" (item_type: "KUz2pYAvQvOWqv3dVwVw3w")
ā”” featured_product: null
šŸ“‹ Processing records of model: Article (article)
--- Processing Iaa0ZiZMSjCqeFWfs3JeuQ ---
BEFORE:
ā”” Item "Iaa0ZiZMSjCqeFWfs3JeuQ" (item_type: "ONxSjA4WTWaoNJY2zokUoQ")
ā”œ title
│ ā”œ en: "Sample Article"
│ ā”” it: "Articolo di Esempio"
ā”œ content
│ ā”œ en
│ │ ā”œ paragraph
│ │ │ ā”” span "Introduction text"
│ │ ā”œ block
│ │ │ ā”” Item "Z-xM-VpKTzytfo-5jzSlOw" (item_type: "DC2XVF6BTjGBgQaoaih6Og")
│ │ │ ā”” sku: "INVALID-SKU-001"
│ │ ā”” paragraph
│ │ ā”” span "Conclusion text"
│ ā”” it
│ ā”œ paragraph
│ │ ā”” span "Testo introduttivo"
│ ā”” block
│ ā”” Item "YoCq3DtzSa2NIbB6ARsiVw" (item_type: "DC2XVF6BTjGBgQaoaih6Og")
│ ā”” sku: "VALID-SKU-002"
ā”” sidebar
ā”” [0] Item "Ulk5GEEoRGSURyyPNSBcow" (item_type: "DC2XVF6BTjGBgQaoaih6Og")
ā”” sku: "INVALID-SKU-003"
AFTER:
ā”” Item "Iaa0ZiZMSjCqeFWfs3JeuQ" (item_type: "ONxSjA4WTWaoNJY2zokUoQ")
ā”œ title
│ ā”œ en: "Sample Article"
│ ā”” it: "Articolo di Esempio"
ā”œ content
│ ā”œ en
│ │ ā”œ paragraph
│ │ │ ā”” span "Introduction text"
│ │ ā”” paragraph
│ │ ā”” span "Conclusion text"
│ ā”” it
│ ā”œ paragraph
│ │ ā”” span "Testo introduttivo"
│ ā”” block "YoCq3DtzSa2NIbB6ARsiVw"
ā”” sidebar: []

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.

import {
ApiError,
buildClient,
type ApiTypes,
type ItemTypeDefinition,
} from "@datocms/cma-client-node";
type EnvironmentSettings = { locales: "en" };
// šŸ‘‡ Definitions can be generated automatically using CLI: https://www.datocms.com/cma-ts-schema
type Counter = ItemTypeDefinition<
EnvironmentSettings,
"UZyfjdBES8y2W2ruMEHSoA",
{
counter: { type: "integer" };
description: { type: "string" };
}
>;
// Make sure the API token has access to the CMA, and is stored securely
const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });
async function run() {
const itemId = "T4m4tPymSACFzsqbZS65WA";
console.log("šŸš€ Starting concurrent updates simulation...\n");
// Create two competing updates that will run concurrently
const updateA = updateRecordWithRetry(
itemId,
(record) => ({
counter: (record.counter || 0) + 10,
description: `Updated by Process A at ${new Date().toISOString()}`,
}),
"Process A",
);
const updateB = updateRecordWithRetry(
itemId,
(record) => ({
counter: (record.counter || 0) + 5,
description: `Updated by Process B at ${new Date().toISOString()}`,
}),
"Process B",
);
try {
// Run both updates concurrently - one will likely trigger STALE_ITEM_VERSION
const [resultA, resultB] = await Promise.all([updateA, updateB]);
console.log("\nāœ… Both updates completed successfully!");
console.log(
"Final counter value:",
Math.max(resultA.counter || 0, resultB.counter || 0),
);
// Get the final state to see which update won
const finalRecord = await client.items.find<Counter>(itemId);
console.log("\nFinal record state:");
console.log("- Counter:", finalRecord.counter);
console.log("- Description:", finalRecord.description);
console.log("- Version:", finalRecord.meta.current_version);
} catch (error) {
console.error("āŒ Unexpected error:", error);
}
}
async function updateRecordWithRetry(
itemId: string,
updateFunction: (record: ApiTypes.Item<Counter>) => {
counter?: number;
description?: string;
},
operationName: string,
) {
// Get the current record
const record = await client.items.find<Counter>(itemId);
console.log(
`${operationName}: Got record version ${record.meta.current_version}`,
);
try {
// Apply the update with optimistic locking
const updatedRecord = await client.items.update<Counter>(itemId, {
...updateFunction(record),
meta: { current_version: record.meta.current_version },
});
console.log(
`${operationName}: Update successful! New version: ${updatedRecord.meta.current_version}`,
);
return updatedRecord;
} catch (e) {
// Handle STALE_ITEM_VERSION error by retrying
if (e instanceof ApiError && e.findError("STALE_ITEM_VERSION")) {
console.log(
`${operationName}: āŒ STALE_ITEM_VERSION detected! Record was modified by another client.`,
);
console.log(`${operationName}: šŸ”„ Retrying with fresh data...`);
// Recursive retry with exponential backoff
await new Promise((resolve) =>
setTimeout(resolve, Math.random() * 100 + 50),
);
return updateRecordWithRetry(itemId, updateFunction, operationName);
}
throw e;
}
}
run();
šŸš€ Starting concurrent updates simulation...
Process A: Got record version L80WevK_R6Gh6ijMyW0AkQ
Process B: Got record version L80WevK_R6Gh6ijMyW0AkQ
Process A: Update successful! New version: QHXWBDr7S9ipQo6_JjQpAg
Process B: āŒ STALE_ITEM_VERSION detected! Record was modified by another client.
Process B: šŸ”„ Retrying with fresh data...
Process B: Got record version QHXWBDr7S9ipQo6_JjQpAg
Process B: Update successful! New version: A4VMeUdtRT26NWd07g6pzw
āœ… Both updates completed successfully!
Final counter value: 15
Final record state:
- Counter: 15
- Description: Updated by Process B at 2025-09-26T08:25:53.088Z
- Version: A4VMeUdtRT26NWd07g6pzw

Body parameters

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

item_type Optional

The record's model

creator Optional

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

Returns

Returns a resource object of type item