# Update a record

> [!PROTIP] 📚 New to DatoCMS records?
> We strongly recommend reading the [Introduction to Records](https://www.datocms.com/docs/content-management-api/resources/item.md) guide first. The payload for updating a record follows the same structure as [creating one](https://www.datocms.com/docs/content-management-api/resources/item/create.md), 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](https://www.datocms.com/docs/content-management-api/resources/item.md#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.**

> [!WARNING] ⚠️ 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

###### Example Simple update operation

Code

```javascript
import { buildClient, inspectItem } from "@datocms/cma-client-node";
import type * as Schema from "./schema.js";

/*
 * BlogPost
 * ├─ title: string
 * ├─ description: string
 * ├─ featured_image: file
 * └─ content_blocks: modular_content
 *    └─ HeroBlock: headline
 */

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<Schema.BlogPost>(
    "T4m4tPymSACFzsqbZS65WA",
    {
      nested: true,
    },
  );

  console.log("-- BEFORE UPDATE --");
  console.log(inspectItem(record));

  const item = await client.items.update<Schema.BlogPost>(
    "T4m4tPymSACFzsqbZS65WA",
    {
      title: "[EDIT] My first blog post!",
      featured_image: null,
      content_blocks: [],
    },
  );

  console.log("-- AFTER UPDATE --");
  console.log(inspectItem(item));
}

run();
```

Returned output

```javascript
-- 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.

### TypeScript typing

Writing an update payload without typed schemas means writing blind: every field is `unknown`, typos compile fine, and mistakes only surface as `422`s from the API. The single biggest lever you have is passing a [generated `Schema.X` marker](https://www.datocms.com/cma-ts-schema.md) as the generic on `items.update`. TypeScript then enforces the model's field names, types, and allowed block shapes at compile time:

```ts
// ❌ 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:

```ts
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">>;
```

> [!WARNING] ⚠️ 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](https://www.datocms.com/cma-ts-schema.md).
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:

| Operation | Entry |
| --- | --- |
| Keep | block ID string |
| Edit | `buildBlockRecord<T>({ id, ...changedAttrs })` |
| Create | `buildBlockRecord<T>({ ...allAttrs })` |
| Clone | `duplicateBlockRecord<T>(block, schemaRepository)` |
| Delete | omit from the array |
| Reorder | rearrange the array |

###### Example Managing blocks in Modular Content fields

Demonstrates every Modular Content operation in a single `items.update` call — keep, edit, create, clone, delete, and reorder all happen at once — and shows that the same accumulator pattern composes recursively when a block holds its own modular field:

-   **Create** — top of the array: `buildBlockRecord<CallToActionBlock>({ item_type, ...attrs })` (no `id`, since it's brand new).
-   **Clone** — duplicate the first existing testimonial with `duplicateBlockRecord<TestimonialBlock>(block, schemaRepository)`. The helper deep-copies and strips block IDs, so the result becomes a separate block rather than the same one moved.
-   **Delete** — `.filter(...)` drops the trailing "Start Your Free Trial" CTA. Any block missing from the new array is removed; missing IDs are how the API encodes deletion.
-   **Edit (flat)** — `buildBlockRecord<CallToActionBlock>({ id, button_url })` with only the changed attributes; omitted attributes (`button_text`) stay as-is on the server.
-   **Edit (nested)** — when the iterated block is a `HeroBlock`, rebuild *its* `ctas` modular array with the **same** accumulator pattern: `NonNullable<FieldValueInRequest<typeof block, 'ctas'>>` types the inner array, and the inner loop reuses `buildBlockRecord` / bare-ID-string the same way the outer loop does. The pattern composes uniformly to whatever depth the schema reaches.
-   **Keep** — return the bare ID string. Cheapest possible payload entry, used both at the top level and inside the nested `ctas`.
-   **Reorder** — implicit; the new array's order *is* the final order.

`isBlockOfType(ID)` is the curried predicate for `Array#filter` / `Array#find` and the inline guard inside `.map` callbacks — narrows `block.attributes` and `block.__itemTypeId` to the matching block model.

Code

```javascript
import {
  buildBlockRecord,
  buildClient,
  duplicateBlockRecord,
  type FieldValueInRequest,
  inspectItem,
  isBlockOfType,
  SchemaRepository,
} from "@datocms/cma-client-node";
import * as Schema from "./schema.js";

/*
 * LandingPage
 * ├─ title: string
 * └─ sections: modular_content
 *    ├─ HeroBlock: headline, subtitle
 *    │  └─ ctas: modular_content
 *    │     └─ ButtonBlock: label, url
 *    ├─ CallToActionBlock: button_text, button_url
 *    └─ TestimonialBlock: quote, author
 */

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 currentPage = await client.items.find<Schema.LandingPage>(
    "W4wcrs_2REiM4fc6dlDZCQ",
    { nested: true },
  );

  console.log("-- BEFORE UPDATE --");
  console.log(inspectItem(currentPage));

  const firstTestimonial = currentPage.sections.find(
    isBlockOfType(Schema.TestimonialBlock.ID),
  );
  const duplicatedTestimonial = firstTestimonial
    ? await duplicateBlockRecord<Schema.TestimonialBlock>(
        firstTestimonial,
        schemaRepository,
      )
    : null;

  const sections: NonNullable<
    FieldValueInRequest<typeof currentPage, "sections">
  > = [
    // Create — fresh CallToActionBlock at the top
    buildBlockRecord<Schema.CallToActionBlock>({
      item_type: Schema.CallToActionBlock.REF,
      button_text: "Watch a 2-minute demo",
      button_url: "https://www.datocms.com/demo",
    }),

    // Clone — duplicated testimonial
    ...(duplicatedTestimonial ? [duplicatedTestimonial] : []),

    ...currentPage.sections
      // Delete — drop the trailing CTA by filtering it out
      .filter(
        (block) =>
          !(
            isBlockOfType(Schema.CallToActionBlock.ID, block) &&
            block.attributes.button_text === "Start Your Free Trial"
          ),
      )
      .map((block) => {
        if (isBlockOfType(Schema.HeroBlock.ID, block)) {
          // Edit (nested) — rebuild the HeroBlock's `ctas` using the SAME
          // accumulator pattern, applied recursively to a nested block's
          // modular field.
          const ctas: NonNullable<FieldValueInRequest<typeof block, "ctas">> =
            block.attributes.ctas.map((cta) => {
              if (
                isBlockOfType(Schema.ButtonBlock.ID, cta) &&
                cta.attributes.label === "Get started"
              ) {
                const url = new URL(
                  cta.attributes.url || "https://www.datocms.com",
                );
                url.searchParams.set("utm_source", "hero");
                return buildBlockRecord<Schema.ButtonBlock>({
                  id: cta.id,
                  url: url.toString(),
                });
              }
              return cta.id;
            });

          return buildBlockRecord<Schema.HeroBlock>({ id: block.id, ctas });
        }

        if (isBlockOfType(Schema.CallToActionBlock.ID, block)) {
          // Edit (flat)
          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<Schema.CallToActionBlock>({
            id: block.id,
            button_url: url.toString(),
          });
        }

        // Keep — return the bare ID for unchanged blocks
        return block.id;
      }),
  ];

  console.log("-- UPDATE OPERATION --");
  console.log(inspectItem({ sections }));

  await client.items.update<Schema.LandingPage>(currentPage, { sections });

  const updatedPage = await client.items.find<Schema.LandingPage>(currentPage, {
    nested: true,
  });

  console.log("-- AFTER UPDATE --");
  console.log(inspectItem(updatedPage));
}

run();
```

Returned output

```javascript
-- BEFORE UPDATE --
└ Item "W4wcrs_2REiM4fc6dlDZCQ" (item_type: "ZV0o9497SsqWxQR8HEQddw")
  ├ title: "Product Launch Landing Page"
  └ sections
    ├ [0] Item "Ngzm6x8JS2y9UuQnTf9wBw" (item_type: "d-CHYg-rShOt3kiL6ZN1yA")
    │ ├ headline: "Revolutionary New Solution"
    │ ├ subtitle: "Discover the future of productivity with our cutting-edge platform designed f..."
    │ └ ctas
    │   ├ [0] Item "IfiDssuLSEa5HH2-Ce2nCw" (item_type: "TNTfjVnfQieREzX98q3xnw")
    │   │ ├ label: "Get started"
    │   │ └ url: "https://www.datocms.com/signup"
    │   └ [1] Item "IOggXsdxRAmFWCOPhbFXYw" (item_type: "TNTfjVnfQieREzX98q3xnw")
    │     ├ label: "Read the docs"
    │     └ url: "https://www.datocms.com/docs"
    ├ [1] Item "Iu1vi__ASDeDrrpPpKbknQ" (item_type: "I8Q6k-HqQmaZ498WKtvFbg")
    │ ├ button_text: "Get Started Free"
    │ └ button_url: "https://www.datocms.com/signup"
    ├ [2] Item "NxszxFtnSpmftjnxguWPOg" (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 "MIJbkpwMQiqzwOYQkuAVIw" (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 "CVcgWRiaRYKF53DvqZHpFA" (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: "I8Q6k-HqQmaZ498WKtvFbg")
    │ ├ button_text: "Watch a 2-minute demo"
    │ └ button_url: "https://www.datocms.com/demo"
    ├ [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 "Ngzm6x8JS2y9UuQnTf9wBw"
    │ └ ctas
    │   ├ [0] Item "IfiDssuLSEa5HH2-Ce2nCw"
    │   │ └ url: "https://www.datocms.com/signup?utm_source=hero"
    │   └ [1] "IOggXsdxRAmFWCOPhbFXYw"
    ├ [3] Item "Iu1vi__ASDeDrrpPpKbknQ"
    │ └ button_url: "https://www.datocms.com/signup?utm_source=landing_page&utm_medium=cta&utm_cam..."
    ├ [4] "NxszxFtnSpmftjnxguWPOg"
    └ [5] "MIJbkpwMQiqzwOYQkuAVIw"

-- AFTER UPDATE --
└ Item "W4wcrs_2REiM4fc6dlDZCQ" (item_type: "ZV0o9497SsqWxQR8HEQddw")
  ├ title: "Product Launch Landing Page"
  └ sections
    ├ [0] Item "O43QkMpUQFy4mTck48Ayeg" (item_type: "I8Q6k-HqQmaZ498WKtvFbg")
    │ ├ button_text: "Watch a 2-minute demo"
    │ └ button_url: "https://www.datocms.com/demo"
    ├ [1] Item "JQOSGafZTgK1cDWaFgtK9Q" (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 "Ngzm6x8JS2y9UuQnTf9wBw" (item_type: "d-CHYg-rShOt3kiL6ZN1yA")
    │ ├ headline: "Revolutionary New Solution"
    │ ├ subtitle: "Discover the future of productivity with our cutting-edge platform designed f..."
    │ └ ctas
    │   ├ [0] Item "IfiDssuLSEa5HH2-Ce2nCw" (item_type: "TNTfjVnfQieREzX98q3xnw")
    │   │ ├ label: "Get started"
    │   │ └ url: "https://www.datocms.com/signup?utm_source=hero"
    │   └ [1] Item "IOggXsdxRAmFWCOPhbFXYw" (item_type: "TNTfjVnfQieREzX98q3xnw")
    │     ├ label: "Read the docs"
    │     └ url: "https://www.datocms.com/docs"
    ├ [3] Item "Iu1vi__ASDeDrrpPpKbknQ" (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..."
    ├ [4] Item "NxszxFtnSpmftjnxguWPOg" (item_type: "Dy9C52o4S6eF3mqSOmeUtg")
    │ ├ quote: "This platform completely transformed how our team collaborates. We've seen a ..."
    │ └ author: "Sarah Chen, Product Manager at TechCorp"
    └ [5] Item "MIJbkpwMQiqzwOYQkuAVIw" (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"
```

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

###### Example Managing Single Block fields

Demonstrates every Single Block operation across multiple `items.update` calls — the field holds at most one block (or `null`):

-   **Edit** — `buildBlockRecord<CallToActionBlock>({ id, button_text, style })` keeps the same block ID, changes the listed attributes, leaves the rest (`button_url`) untouched.
-   **Replace via duplicate** — `duplicateBlockRecord<HeroBlock | CallToActionBlock | VideoBlock>(currentProduct.hero_section, schemaRepository)` deep-copies and strips IDs; the slot ends up with a brand-new block, not the same one re-used.
-   **Replace with a different block type** — `buildBlockRecord<VideoBlock>({ item_type, ...attrs })` swaps the slot's model entirely (no `id`, since the block doesn't exist yet).
-   **Delete** — set the field to `null`. Omitting the field would leave the existing block in place (see the parent guide's null-vs-omit warning).

The response uses `__itemTypeId` as a discriminant: narrowing on it (`if (currentProduct.hero_section.__itemTypeId === CTA_ID)`) lets TypeScript see the matching block-model attributes before any string ops.

Code

```javascript
import {
  type ApiTypes,
  buildBlockRecord,
  buildClient,
  duplicateBlockRecord,
  inspectItem,
  SchemaRepository,
} from "@datocms/cma-client-node";
import * as Schema from "./schema.js";

/*
 * 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
 */

// 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 currentProduct = await client.items.find<Schema.ProductPage>(
    "W4wcrs_2REiM4fc6dlDZCQ",
    { nested: true },
  );

  console.log("-- BEFORE UPDATE --");
  console.log(inspectItem(currentProduct));

  if (
    currentProduct.hero_section &&
    currentProduct.hero_section.__itemTypeId === Schema.CallToActionBlock.ID
  ) {
    await client.items.update<Schema.ProductPage>(currentProduct, {
      hero_section: buildBlockRecord<Schema.CallToActionBlock>({
        id: currentProduct.hero_section.id,
        button_text:
          currentProduct.hero_section.attributes.button_text?.toUpperCase() ||
          "SHOP NOW",
        style: "primary-large",
      }),
    });
    console.log("-- EXISTING BLOCK UPDATED --");
    await inspectItemWithNestedBlocks(currentProduct);
  }

  await client.items.update<Schema.ProductPage>(currentProduct, {
    hero_section: await duplicateBlockRecord<
      Schema.HeroBlock | Schema.CallToActionBlock | Schema.VideoBlock
    >(currentProduct.hero_section!, schemaRepository),
  });
  console.log("-- BLOCK DUPLICATE --");
  await inspectItemWithNestedBlocks(currentProduct);

  const upload = await client.uploads.createFromUrl({
    url: "https://picsum.photos/800/600?random=1",
  });
  const productWithVideo = await client.items.update<Schema.ProductPage>(
    currentProduct,
    {
      hero_section: buildBlockRecord<Schema.VideoBlock>({
        item_type: Schema.VideoBlock.REF,
        video_url: "https://videos.datocms.com/product-demo.mp4",
        thumbnail_image: { upload_id: upload.id },
        autoplay: false,
      }),
    },
  );
  console.log("-- BLOCK REPLACED --");
  await inspectItemWithNestedBlocks(currentProduct);

  await client.items.update<Schema.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));
}
```

Returned output

```javascript
-- 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
```

#### Structured Text

Structured Text stores rich content as a [DAST](https://www.datocms.com/docs/structured-text/dast.md) 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`](https://github.com/datocms/structured-text/tree/main/packages/dastdown) translates the DAST tree to and from a markdown-like text format:

```markdown
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:

```ts
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.

> [!WARNING] ⚠️ 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.

###### Example Pass 1: Prose edits via dastdown

Demonstrates Pass 1 (dastdown round-trip) on a structured-text field with no embedded blocks. Three text-level transformations expressed as plain string operations:

-   **Brand swap** — `text.replace(/ZEIT/g, "Vercel")` rewrites every occurrence across spans, headings, and link text in a single regex.
-   **Autolink emails** — turn bare addresses into markdown links: `support@example.com` → `[support@example.com](mailto:support@example.com)`.
-   **Set `target="_blank"` on a specific link** — append the dastdown link-meta trailer `{target="_blank"}` to the matching `(url)`. The negative lookahead `(?!\{)` makes the regex idempotent (a second run is a no-op).

Block placeholders aren't relevant here, but `parse(text, currentGuide.body)` always uses the second argument as the `<block id="…"/>` lookup table; missing IDs throw, which is the signal to fall back to Pass 2.

Code

```javascript
import { buildClient, inspectItem } from "@datocms/cma-client-node";
import { parse, serialize } from "datocms-structured-text-dastdown";
import type * as Schema from "./schema.js";

/*
 * Guide
 * ├─ title: string
 * └─ body: structured_text (no embedded blocks for this example)
 */

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 currentGuide = await client.items.find<Schema.Guide>(
    "Q9zHRrIESkGYBV3hVVe2Hg",
  );

  // `currentGuide.body` is typed nullable — guard before touching the document.
  // Wrapping in `if (currentGuide.body) { ... }` also avoids a top-level
  // `return`, letting peek + mutate live in a single script.
  if (currentGuide.body) {
    console.log("-- BEFORE UPDATE --");
    console.log(inspectItem(currentGuide));

    const text = serialize(currentGuide.body);

    const edited = text
      .replace(/ZEIT/g, "Vercel")
      .replace(/(\b[\w.+-]+@[\w-]+\.[\w.-]+\b)/g, "[$1](mailto:$1)")
      .replace(
        /\(https:\/\/example\.com\/migration\)(?!\{)/g,
        '(https://example.com/migration){target="_blank"}',
      );

    // 2nd arg is the lookup table for `<block id="…"/>` placeholders; missing IDs throw.
    const body = parse(edited, currentGuide.body);

    await client.items.update<Schema.Guide>(currentGuide.id, { body });

    console.log("-- AFTER UPDATE --");
    console.log(
      inspectItem(await client.items.find<Schema.Guide>(currentGuide.id)),
    );
  }
}

run();
```

Returned output

```javascript
-- BEFORE UPDATE --
└ Item "Q9zHRrIESkGYBV3hVVe2Hg" (item_type: "f6sjkkPgQGiSDi6lwG3UjA")
  ├ title: "Deploying with ZEIT"
  └ body
    ├ heading (level: 1)
    │ └ span "Deploying with ZEIT"
    ├ paragraph
    │ └ span "ZEIT lets you ship static sites and serverless functions in seconds. ..."
    └ paragraph
      ├ span "Read our "
      ├ link (url: "https://example.com/migration")
      │ └ span "migration guide"
      └ span " before upgrading from ZEIT v1."

-- AFTER UPDATE --
└ Item "Q9zHRrIESkGYBV3hVVe2Hg" (item_type: "f6sjkkPgQGiSDi6lwG3UjA")
  ├ title: "Deploying with ZEIT"
  └ body
    ├ heading (level: 1)
    │ └ span "Deploying with Vercel"
    ├ paragraph
    │ ├ span "Vercel lets you ship static sites and serverless functions in seconds..."
    │ ├ link (url: "mailto:support@zeit.co")
    │ │ └ span "support@zeit.co"
    │ └ span " for help."
    └ paragraph
      ├ span "Read our "
      ├ link (url: "https://example.com/migration", meta: {target="_blank"})
      │ └ span "migration guide"
      └ span " before upgrading from Vercel v1."
```


###### Example Pass 2: Node transformations

Demonstrates Pass 2 on prose nodes — `mapNodes` with one of each return mode (1:1 transforms, 1:0 drop) plus a post-walk root-level append. The callback applies five edits across the tree:

-   **Demote `h1` → `h2`** for hierarchy hygiene (`as const` keeps the literal `level` type).
-   **Bold every span that mentions the brand**, deduping the `marks` array with a `Set` (canonical marks: `'strong' | 'emphasis' | 'code' | 'underline' | 'strikethrough' | 'highlight'`).
-   **Brand swap** — `value.replace(/ZEIT/g, "Vercel")` on every matching span.
-   **Add `target="_blank"`** to `link` and `itemLink` nodes, deduping any prior `target` entry in `meta`.
-   **Drop empty paragraphs** by returning `null` from the callback.

After `mapNodes` returns, a fresh paragraph is pushed into `content.document.children` — root-level inserts can't go through the callback (splat-at-root throws).

Code

```javascript
import { buildClient, inspectItem } from "@datocms/cma-client-node";
import {
  isHeading,
  isItemLink,
  isLink,
  isParagraph,
  isSpan,
  mapNodes,
  reduceNodes,
} from "datocms-structured-text-utils";
import type * as Schema from "./schema.js";

/*
 * BlogPost
 * ├─ title: string
 * ├─ slug: string
 * └─ content: structured_text (no embedded blocks for this example)
 */

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 currentPost = await client.items.find<Schema.BlogPost>(
    "T4m4tPymSACFzsqbZS65WA",
  );

  // Guard the nullable field — also keeps peek + mutate in a single script
  // without a top-level `return`.
  if (currentPost.content) {
    console.log("-- BEFORE UPDATE --");
    console.log(inspectItem(currentPost));

    const content = mapNodes(currentPost.content, (node) => {
      if (isHeading(node) && node.level === 1) {
        // `as const` preserves the literal `2` instead of widening to `number`.
        return { ...node, level: 2 as const };
      }

      if (isSpan(node) && node.value.includes("ZEIT")) {
        const marks = new Set(node.marks ?? []);
        marks.add("strong");
        return {
          ...node,
          value: node.value.replace(/ZEIT/g, "Vercel"),
          marks: [...marks],
        };
      }

      if (isLink(node) || isItemLink(node)) {
        const meta = [
          ...(node.meta ?? []).filter((m) => m.id !== "target"),
          { id: "target", value: "_blank" },
        ];
        return { ...node, meta };
      }

      if (
        isParagraph(node) &&
        reduceNodes(
          node,
          (acc, n) => (isSpan(n) ? acc + n.value.trim() : acc),
          "",
        ).length === 0
      ) {
        return null;
      }

      return node;
    });

    // Root-level inserts can't go through `mapNodes` (splat-at-root throws); push directly.
    content.document.children.push({
      type: "paragraph",
      children: [{ type: "span", value: "Last updated by the content team." }],
    });

    await client.items.update<Schema.BlogPost>(currentPost.id, { content });

    console.log("-- AFTER UPDATE --");
    console.log(
      inspectItem(await client.items.find<Schema.BlogPost>(currentPost.id)),
    );
  }
}

run();
```

Returned output

```javascript
-- 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: "fpgJWZadRI66eqXB-ucSSQ")
      │ └ 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 (marks: strong) "Understanding Vercel"
    ├ paragraph
    │ └ span (marks: strong) "Vercel is a cloud platform for static sites and serverless functions...."
    ├ heading (level: 2)
    │ └ span "Key Features"
    ├ paragraph
    │ ├ span (marks: strong) "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: "fpgJWZadRI66eqXB-ucSSQ", meta: {target="_blank"})
    │ │ └ span "migration guide"
    │ └ span " for step-by-step instructions."
    └ paragraph
      └ span "Last updated by the content team."
```


###### Example Pass 2 (cont.): Editing & creating embedded blocks

Demonstrates Pass 2 on embedded blocks — editing block attributes from inside a `mapNodes` callback, then duplicating and appending a block after the walk. What the script does:

-   **Edit CTA blocks**: rewrite `button_url` from `old-domain.com` to `new-domain.com`, returning `{ ...node, item: buildBlockRecord<CtaBlock>({ id, button_url }) }` from the `mapNodes` callback.
-   **Tag inline product mentions**: append `?source=article_mention` to `affiliate_url` with the same pattern, narrowing via `isInlineBlockWithItemOfType`.
-   **Pass through unmatched blocks** (the `ImageGalleryBlock` here) with a bare `return node` — the CMA accepts the nested-response shape it came in as.
-   **Duplicate the first CTA** with `duplicateBlockRecord<CtaBlock>` and push it as a new root-level child, *after* `mapNodes` runs.

A few things to notice:

-   **`isBlockWithItemOfType` has two call styles.** `isBlockWithItemOfType(ID, node)` is the inline form for `if`; the curried form `isBlockWithItemOfType(ID)` is the predicate for `findFirstNode` / `Array#find` / `Array#filter`. Same guard, different ergonomics. With `ID` declared `as const`, both auto-narrow `node.item` to the matching block-model shape.
-   **Source the duplicate from the original tree.** Look up the source block on `currentArticle.content` (the original response), not on the mapped result — `mapNodes` is allowed to rewrite `node.item`, so the post-map tree is not a reliable source for cloning.

Code

```javascript
import {
  buildBlockRecord,
  buildClient,
  duplicateBlockRecord,
  type FieldValueInRequest,
  inspectItem,
  SchemaRepository,
} from "@datocms/cma-client-node";
import {
  findFirstNode,
  isBlockWithItemOfType,
  isInlineBlockWithItemOfType,
  mapNodes,
} from "datocms-structured-text-utils";
import * as Schema from "./schema.js";

/*
 * Article
 * ├─ title: string
 * ├─ author: string
 * └─ content: structured_text
 *    ├─ CtaBlock: title, description, button_text, button_url
 *    ├─ ProductMentionInline: product_name, price, affiliate_url
 *    └─ ImageGalleryBlock: title, images
 */

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 });

  // `duplicateBlockRecord` looks up nested-block field definitions through this.
  const repo = new SchemaRepository(client);

  const currentArticle = await client.items.find<Schema.Article>(
    "RSfdsZbbR7ixGgMBSmcaVA",
    {
      nested: true,
    },
  );

  // Guard the nullable field — also keeps peek + mutate in a single script
  // without a top-level `return`.
  if (currentArticle.content) {
    console.log("-- BEFORE UPDATE --");
    console.log(inspectItem(currentArticle));

    let content: NonNullable<
      FieldValueInRequest<typeof currentArticle, "content">
    > = currentArticle.content;

    content = mapNodes(content, (node) => {
      if (isBlockWithItemOfType(Schema.CtaBlock.ID, node)) {
        const url = node.item.attributes.button_url;
        if (url?.includes("old-domain.com")) {
          return {
            ...node,
            item: buildBlockRecord<Schema.CtaBlock>({
              id: node.item.id,
              button_url: url.replace("old-domain.com", "new-domain.com"),
            }),
          };
        }
      }

      if (isInlineBlockWithItemOfType(Schema.ProductMentionInline.ID, node)) {
        const raw = node.item.attributes.affiliate_url;
        if (raw) {
          const url = new URL(raw);
          url.searchParams.set("source", "article_mention");
          return {
            ...node,
            item: buildBlockRecord<Schema.ProductMentionInline>({
              id: node.item.id,
              affiliate_url: url.toString(),
            }),
          };
        }
      }

      return node;
    });

    // Source the duplicate from the original tree: `mapNodes` may have
    // rewritten `node.item`, so post-map `content` is not safe to clone from.
    const firstCta = findFirstNode(
      currentArticle.content,
      isBlockWithItemOfType(Schema.CtaBlock.ID),
    );

    if (firstCta) {
      const dup = await duplicateBlockRecord<Schema.CtaBlock>(
        firstCta.node.item,
        repo,
      );
      content.document.children.push({ type: "block", item: dup });
    }

    await client.items.update<Schema.Article>(currentArticle.id, { content });

    console.log("-- AFTER UPDATE --");
    console.log(
      inspectItem(
        await client.items.find<Schema.Article>(currentArticle.id, {
          nested: true,
        }),
      ),
    );
  }
}

run();
```

Returned output

```javascript
-- 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 "LQlcO4LCTYaOrfj2A705DQ" (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 "FqTxaDO8TJmtkoTgDjbK8Q" (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 "JwT0hvgiR4WB0-SO9QG7Ig" (item_type: "I8Q6k-HqQmaZ498WKtvFbg")
        ├ title: "E-commerce Innovation Gallery"
        └ images
          ├ [0]
          │ ├ upload_id: "eLVHtrefRUq7qkVMpzG6mQ"
          │ ├ alt: "Modern e-commerce interface"
          │ └ title: "Next-gen Shopping"
          └ [1]
            ├ upload_id: "UFIyIRQFT1GQ7YAG34ub5w"
            ├ alt: "AI-powered recommendations"
            └ title: "Smart Product Discovery"

-- AFTER 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 "LQlcO4LCTYaOrfj2A705DQ" (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 "FqTxaDO8TJmtkoTgDjbK8Q" (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 "JwT0hvgiR4WB0-SO9QG7Ig" (item_type: "I8Q6k-HqQmaZ498WKtvFbg")
    │   ├ title: "E-commerce Innovation Gallery"
    │   └ images
    │     ├ [0]
    │     │ ├ upload_id: "eLVHtrefRUq7qkVMpzG6mQ"
    │     │ ├ alt: "Modern e-commerce interface"
    │     │ └ title: "Next-gen Shopping"
    │     └ [1]
    │       ├ upload_id: "UFIyIRQFT1GQ7YAG34ub5w"
    │       ├ alt: "AI-powered recommendations"
    │       └ title: "Smart Product Discovery"
    └ block
      └ Item "czplSGgiSnizmFZ7gMT-_g" (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 & node helpers

Curated index of the helpers used above.

###### Block helpers

Used by all three block field types. Full reference: [block-processing-utilities](https://github.com/datocms/js-rest-api-clients/tree/main/packages/cma-client#block-processing-utilities).

| Need | Helper |
| --- | --- |
| 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 union | `block.__itemTypeId === "..."` |
| Type-guard predicate for `Array#filter` / `Array#find` | `isBlockOfType(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 (`mapNodes` → `mapNodesAsync`) for async callbacks. Full reference: [tree-manipulation-utilities](https://github.com/datocms/structured-text/tree/main/packages/utils#tree-manipulation-utilities).

| Need | Helper |
| --- | --- |
| Narrow a node to a kind | `isParagraph`, `isHeading`, `isSpan`, `isLink`, `isItemLink`, `isInlineItem`, `isBlock`, `isInlineBlock`, `isList`, ... |
| Narrow a block / inline-block node to a specific model in one step | `isBlockWithItemOfType(id)`, `isInlineBlockWithItemOfType(id)` |
| Walk every node (side effect) | `forEachNode` |
| Transform every node (1:1, splat into siblings, or drop) | `mapNodes` |
| Find first / collect every match | `findFirstNode`, `collectNodes` |
| Fold to a single value | `reduceNodes` |
| Short-circuit checks | `someNode`, `everyNode` |
| ASCII-tree debug | `inspect` |

### Updating Localized Fields

**➡️ [Before proceeding, ensure you have read the general guide on Localization](https://www.datocms.com/docs/content-management-api/resources/item.md#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:
    
    ```json
    {
      "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 manages | Record currently Has | Your payload sends | Result |
| --- | --- | --- | --- |
| English | English | English | ✅ English is updated. |
| English, Italian | English | English, Italian | ✅ English is updated. ➕ Italian is **added**. |
| English, Italian | English, Italian | English | ✅ English is updated. ➖ Italian is **removed**. |
| English, Italian | English, Italian | English, Italian | ✅ English is updated. ✅ Italian is updated. |
| Eng, Ita, Fre | English, Italian | English, French | ✅ English is updated. ➖ Italian is **removed**. ➕ French is **added**. |
| English | English, Italian | English | ✅ English is updated. 🛡️ Italian is **preserved**. |
| English, Italian | English, French | English, Italian | ✅ English is updated. 🛡️ French is **preserved**. ➕ Italian is **added**. |
| English, Italian | English, French | Italian | ➖ 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.

<details>
<summary>Example: Updating a block in one locale</summary>

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!

```json
{
  "content_blocks": {
    "en": [
      {
        "id": "dhVR2HqgRVCTGFi0bWqLqA",
        "type": "item",
        "attributes": { "title": "Updated English Title" }
      },
      "kL9mN3pQrStUvWxYzAbCdE"
    ],
    "it": [
      "dhVR2HqgRVCTGFi_0bWqLqA",
      "kL9mN3pQrStUvWxYzAbCdE"
    ]
  }
}
```

</details>

<details>
<summary>Example: Adding a new block to one locale</summary>

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!

```json
{
  "content_blocks": {
    "en": [
      "dhVR2HqgRVCTGFi_0bWqLqA",
      "kL9mN3pQrStUvWxYzAbCdE"
    ],
    "it": [
      "fG8hI1jKlMnOpQrStUvWxY",
      {
        "type": "item",
        "attributes": { "title": "Nuovo Blocco" },
        "relationships": {
          "item_type": { "data": { "id": "BxZ9Y2aKQVeTnM4hP8wLpD", "type": "item_type" } }
        }
      },
      "dhVR2HqgRVCTGFi0bWqLqA"
    ]
  }
}
```

</details>

<details>
<summary>Example: Adding a new locale</summary>

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.

```json
{
  "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" } }
        }
      }
    ]
  }
}
```

</details>

> [!POSITIVE] 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](https://www.datocms.com/docs/content-management-api/resources/item/update.md#unified-locale-helpers).

###### Example Adding a new locale

Adds the `de` locale to a record that currently has `en` and `it`, in an environment whose locales are `['en', 'it', 'de']`. Per Rule 2 above, the payload must include **every** localized field, with the existing locales preserved alongside the new one. Requires [`all_locales_required`](https://www.datocms.com/docs/content-management-api/resources/item-type.md#object-payload) to be `false` on the model.

Code

```javascript
import { buildClient, inspectItem } from "@datocms/cma-client-node";
import type * as Schema from "./schema.js";

/*
 * Article
 * ├─ author: string
 * ├─ title: string (localized)
 * └─ content: text (localized)
 */

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<Schema.Article>(
    "T4m4tPymSACFzsqbZS65WA",
    {
      title: {
        en: "My title",
        it: "Il mio titolo",
        de: "Mein Titel",
      },
      content: {
        en: "Schema.Article content",
        it: "Contenuto articolo",
        de: "Artikelinhalt",
      },
    },
  );

  console.log(inspectItem(item));
}

run();
```

Returned output

```javascript
└ 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"
```


###### Example Removing an existing locale

If the [`all_locales_required`](https://www.datocms.com/docs/content-management-api/resources/item-type.md#object-payload) 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 `ItemTypeDefinition`s 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()`](https://www.datocms.com/docs/content-management-api/resources/field/instances.md) 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.

Code

```javascript
import type { ApiTypes } from "@datocms/cma-client-node";
import {
  buildClient,
  inspectItem,
  isLocalized,
  type LocalizedFieldValue,
} from "@datocms/cma-client-node";
import lodash from "lodash";
import type * as Schema from "./schema.js";

/*
 * Article
 * ├─ title: string (localized)
 * ├─ content: structured_text (localized)
 * ├─ cta_block: single_block
 * │  └─ CtaBlock: title, button_text, button_url
 * └─ cover_image: file
 *
 * ArticleCopy
 * ├─ title: string (localized)
 * ├─ content: structured_text (localized)
 * ├─ cta_block: single_block
 * │  └─ CtaBlock
 * └─ cover_image: file
 */

// 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 removeLocaleWhenSchemaIsKnown() {
  // First, fetch the existing record to get current values
  const existingRecord = await client.items.find<Schema.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<Schema.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();
```

Returned output

```javascript
-- 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
```


###### Example Copying content from one locale to another

Copies all localized content from `en` to `en-AT` across every model in the project — useful for seeding a new locale, region-specific variations (e.g. UK → Austrian English), or fallback content for incomplete translations.

The script iterates through every model, walks each localized field with [`mapNormalizedFieldValuesAsync`](https://github.com/datocms/js-rest-api-clients/tree/main/packages/cma-client#mapnormalizedfieldvalues--mapnormalizedfieldvaluesasync), and recurses into nested blocks with [`mapBlocksInNonLocalizedFieldValue`](https://github.com/datocms/js-rest-api-clients/tree/main/packages/cma-client#mapblocksinnonlocalizedfieldvalue) to strip their IDs — that way the target locale gets fresh block instances instead of references to the source ones.

Code

```javascript
import assert from "node:assert";
import type { ApiTypes } from "@datocms/cma-client-node";
import {
  buildClient,
  inspectItem,
  isItemWithOptionalMeta,
  isLocalized,
  type LocalizedFieldValue,
  mapBlocksInNonLocalizedFieldValue,
  mapNormalizedFieldValuesAsync,
  SchemaRepository,
} from "@datocms/cma-client-node";

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);

  for (const model of await schemaRepository.getAllModels()) {
    const fields = await schemaRepository.getItemTypeFields(model);
    const localizedFields = fields.filter(isLocalized);

    // Iterating across every model discovered at runtime, so no single
    // `<Schema.X>` generic fits — keep `listPagedIterator` untyped and narrow
    // field values dynamically below.
    for await (const record of client.items.listPagedIterator({
      filter: { type: model.api_key },
      version: "current",
      nested: true,
    })) {
      const updatePayload: ApiTypes.ItemUpdateSchema = {};
      let hasChanges = false;

      for (const field of localizedFields) {
        const fieldValueWithNestedBlocks = record[
          field.api_key
        ] as LocalizedFieldValue;

        if (!fieldValueWithNestedBlocks["en"]) {
          continue;
        }

        const newFieldValue = (await mapNormalizedFieldValuesAsync(
          fieldValueWithNestedBlocks,
          field,
          async (_locale, fieldValueForLocale) => {
            return mapBlocksInNonLocalizedFieldValue(
              fieldValueForLocale,
              field.field_type,
              schemaRepository,
              (block) => {
                assert(isItemWithOptionalMeta(block));
                return block.id;
              },
            );
          },
        )) as LocalizedFieldValue;

        // Strip IDs from cloned blocks so en-AT gets fresh instances rather
        // than references to the en blocks.
        newFieldValue["en-AT"] = await mapBlocksInNonLocalizedFieldValue(
          fieldValueWithNestedBlocks["en"],
          field.field_type,
          schemaRepository,
          (block) => {
            assert(isItemWithOptionalMeta(block));
            const { id, ...blockWithoutId } = block;
            return blockWithoutId;
          },
        );

        updatePayload[field.api_key] = newFieldValue;

        hasChanges = true;
      }

      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();
```

Returned output

```javascript
-- 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
```

#### 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 (`mapNormalizedFieldValues` → `mapNormalizedFieldValuesAsync`) for async callbacks. See the **[full helper reference](https://github.com/datocms/js-rest-api-clients/tree/main/packages/cma-client#unified-field-processing-localized--non-localized)** 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.

###### Example Edit blocks across all content

Edits specific blocks wherever they're embedded — Modular Content, Single Block, or Structured Text fields, including deeply nested structures and localized content.

To avoid scanning the whole project, the script discovers only the models that can (directly or transitively) embed the target block via [`SchemaRepository.getRawModelsEmbeddingBlocks()`](https://github.com/datocms/js-rest-api-clients/tree/main/packages/cma-client#schemarepository), then walks each record with [`mapNormalizedFieldValuesAsync`](https://github.com/datocms/js-rest-api-clients/tree/main/packages/cma-client#mapnormalizedfieldvalues--mapnormalizedfieldvaluesasync) and [`mapBlocksInNonLocalizedFieldValue`](https://github.com/datocms/js-rest-api-clients/tree/main/packages/cma-client#mapblocksinnonlocalizedfieldvalue). In this case, every CTA block gets its `style` set to `"primary"` for high-intent copy and `"muted"` otherwise, based on the button text.

Code

```javascript
import assert from "node:assert";
import {
  type ApiTypes,
  buildBlockRecord,
  buildClient,
  inspectItem,
  isItemWithOptionalMeta,
  mapBlocksInNonLocalizedFieldValue,
  mapNormalizedFieldValuesAsync,
  type RawApiTypes,
  SchemaRepository,
} from "@datocms/cma-client-node";
import * as Schema from "./schema.js";

/*
 * Article
 * ├─ title: string (localized)
 * ├─ content: structured_text (localized)
 * │  └─ CtaBlock: title, description, button_text, button_url, style
 * └─ sidebar: rich_text
 *    └─ CtaBlock
 *
 * LandingPage
 * └─ hero_cta: single_block
 *    └─ CtaBlock
 */

// 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(
    Schema.CtaBlock.ID,
  );

  // 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);

    // This script iterates records across every model discovered at runtime,
    // so there is no single `<Schema.X>` generic that fits — we keep
    // `rawListPagedIterator` untyped here and narrow block values dynamically
    // below. Per-model scripts should always pass the generated marker.
    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 !== Schema.CtaBlock.ID) {
                  return block.id; // Keep non-CTA blocks as is
                }

                // The raw iterator yields records (and their embedded blocks)
                // in the raw API shape; narrow to this specific block model
                // via the ItemInNestedResponse schema indexed by Schema.CtaBlock.
                const ctaBlock =
                  block as RawApiTypes.ItemInNestedResponse<Schema.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<Schema.CtaBlock>({
                    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));
}
```

Returned output

```javascript
📋 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"
```


###### Example Delete blocks across all content

Removes specific blocks wherever they're embedded — Modular Content, Single Block, or Structured Text fields, including deeply nested structures and localized content.

To avoid scanning the whole project, the script discovers only the models that can (directly or transitively) embed the target block via [`SchemaRepository.getRawModelsEmbeddingBlocks()`](https://github.com/datocms/js-rest-api-clients/tree/main/packages/cma-client#schemarepository), then walks each record with [`mapNormalizedFieldValuesAsync`](https://github.com/datocms/js-rest-api-clients/tree/main/packages/cma-client#mapnormalizedfieldvalues--mapnormalizedfieldvaluesasync) and [`filterBlocksInNonLocalizedFieldValue`](https://github.com/datocms/js-rest-api-clients/tree/main/packages/cma-client#filterblocksinnonlocalizedfieldvalue). The removal predicate is an async check (mocked here as an ecommerce SKU lookup), so the same shape works for any external decision.

Code

```javascript
import assert from "node:assert";
import {
  type ApiTypes,
  buildClient,
  filterBlocksInNonLocalizedFieldValue,
  inspectItem,
  isItemWithOptionalMeta,
  mapNormalizedFieldValuesAsync,
  type RawApiTypes,
  SchemaRepository,
} from "@datocms/cma-client-node";
import * as Schema from "./schema.js";

/*
 * Article
 * ├─ title: string (localized)
 * ├─ content: structured_text (localized)
 * │  └─ ProductBlock: sku
 * └─ sidebar: rich_text
 *    └─ ProductBlock
 *
 * ProductPage
 * └─ featured_product: single_block
 *    └─ ProductBlock
 */

// 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(
    Schema.ProductBlock.ID,
  );

  // 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);

    // This script iterates records across every model discovered at runtime,
    // so there is no single `<Schema.X>` generic that fits — we keep
    // `rawListPagedIterator` untyped here and narrow block values dynamically
    // below. Per-model scripts should always pass the generated marker.
    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 !== Schema.ProductBlock.ID) {
                  return true; // Keep other blocks
                }

                // The raw iterator yields records (and their embedded blocks)
                // in the raw API shape; narrow to this specific block model
                // via the ItemInNestedResponse schema indexed by Schema.ProductBlock.
                const productBlock =
                  block as RawApiTypes.ItemInNestedResponse<Schema.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();
```

Returned output

```javascript
📋 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.

###### Example Optimistic-locking update operation

Code

```javascript
import { ApiError, type ApiTypes, buildClient } from "@datocms/cma-client-node";
import type * as Schema from "./schema.js";

/*
 * Counter
 * ├─ counter: integer
 * └─ description: 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<Schema.Counter>(itemId);
    console.log("\nFinal record state:");
    console.log("- Schema.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<Schema.Counter>) => {
    counter?: number;
    description?: string;
  },
  operationName: string,
) {
  // Get the current record
  const record = await client.items.find<Schema.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<Schema.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();
```

Returned output

```javascript
🚀 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`**

- Optional
- Type: string

Date of creation

**`meta.first_published_at`**

- Optional
- Type: null, string

Date of first publication

**`meta.current_version`**

- Optional
- Type: string
- Example: `"4234"`

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

**`meta.stage`**

- Optional
- Type: string, null

The new stage to move the record to

**`item_type`**

- Optional
- Type: [ResourceLinkage\<"item_type"\>](https://www-draft.datocms.com/docs/content-management-api/resources/item_type.md)

The record's model

**`creator`**

- Optional
- Type: [ResourceLinkage\<"account"\>](https://www-draft.datocms.com/docs/content-management-api/resources/account.md), [ResourceLinkage\<"access_token"\>](https://www-draft.datocms.com/docs/content-management-api/resources/access_token.md), [ResourceLinkage\<"user"\>](https://www-draft.datocms.com/docs/content-management-api/resources/user.md), [ResourceLinkage\<"sso_user"\>](https://www-draft.datocms.com/docs/content-management-api/resources/sso_user.md), [ResourceLinkage\<"organization"\>](https://www-draft.datocms.com/docs/content-management-api/resources/organization.md)

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

## Returns

Returns a resource object of type [item](https://www.datocms.com/docs/content-management-api/resources/item.md)

## Related content in "Content Management API"

- [Content Management API Overview](https://www.datocms.com/docs/content-management-api.md)
- [Using the JavaScript CMA client](https://www.datocms.com/docs/content-management-api/using-the-nodejs-clients.md)
- [API versioning](https://www.datocms.com/docs/content-management-api/api-versioning.md)
- [Authentication](https://www.datocms.com/docs/content-management-api/authentication.md)
- [Environments](https://www.datocms.com/docs/content-management-api/setting-the-environment.md)
- [Error codes & handling failures (CMA)](https://www.datocms.com/docs/content-management-api/errors.md)
- [Pagination](https://www.datocms.com/docs/content-management-api/pagination.md)
- [Asynchronous jobs](https://www.datocms.com/docs/content-management-api/async-jobs.md)
- [CMA Technical Limits & Rate Limits](https://www.datocms.com/docs/content-management-api/technical-limits.md)
- [Record](https://www.datocms.com/docs/content-management-api/resources/item.md)
- [List all records](https://www.datocms.com/docs/content-management-api/resources/item/instances.md)
- [Create a new record](https://www.datocms.com/docs/content-management-api/resources/item/create.md)
- [Duplicate a record](https://www.datocms.com/docs/content-management-api/resources/item/duplicate.md)
- [Update a record](https://www.datocms.com/docs/content-management-api/resources/item/update.md)
- [Referenced records](https://www.datocms.com/docs/content-management-api/resources/item/references.md)
- [Retrieve a record](https://www.datocms.com/docs/content-management-api/resources/item/self.md)
- [Delete a record](https://www.datocms.com/docs/content-management-api/resources/item/destroy.md)
- [Publish a record](https://www.datocms.com/docs/content-management-api/resources/item/publish.md)
- [Unpublish a record](https://www.datocms.com/docs/content-management-api/resources/item/unpublish.md)
- [Publish items in bulk](https://www.datocms.com/docs/content-management-api/resources/item/bulk_publish.md)
- [Unpublish items in bulk](https://www.datocms.com/docs/content-management-api/resources/item/bulk_unpublish.md)
- [Destroy items in bulk](https://www.datocms.com/docs/content-management-api/resources/item/bulk_destroy.md)
- [Move items to stage in bulk](https://www.datocms.com/docs/content-management-api/resources/item/bulk_move_to_stage.md)
- [Scheduled publication](https://www.datocms.com/docs/content-management-api/resources/scheduled-publication.md)
- [Scheduled unpublishing](https://www.datocms.com/docs/content-management-api/resources/scheduled-unpublishing.md)
- [Upload](https://www.datocms.com/docs/content-management-api/resources/upload.md)
- [Site](https://www.datocms.com/docs/content-management-api/resources/site.md)
- [Model/Block model](https://www.datocms.com/docs/content-management-api/resources/item-type.md)
- [Field](https://www.datocms.com/docs/content-management-api/resources/field.md)
- [Fieldset](https://www.datocms.com/docs/content-management-api/resources/fieldset.md)
- [Record version](https://www.datocms.com/docs/content-management-api/resources/item-version.md)
- [Upload permission](https://www.datocms.com/docs/content-management-api/resources/upload-request.md)
- [Upload track](https://www.datocms.com/docs/content-management-api/resources/upload-track.md)
- [Manual tags](https://www.datocms.com/docs/content-management-api/resources/upload-tag.md)
- [Smart tags](https://www.datocms.com/docs/content-management-api/resources/upload-smart-tag.md)
- [Upload Collection](https://www.datocms.com/docs/content-management-api/resources/upload-collection.md)
- [Search Index](https://www.datocms.com/docs/content-management-api/resources/search-index.md)
- [Search result](https://www.datocms.com/docs/content-management-api/resources/search-result.md)
- [Search indexing activity](https://www.datocms.com/docs/content-management-api/resources/search-index-event.md)
- [Environment](https://www.datocms.com/docs/content-management-api/resources/environment.md)
- [Maintenance mode](https://www.datocms.com/docs/content-management-api/resources/maintenance-mode.md)
- [Menu Item](https://www.datocms.com/docs/content-management-api/resources/menu-item.md)
- [Schema Menu Item](https://www.datocms.com/docs/content-management-api/resources/schema-menu-item.md)
- [Uploads filter](https://www.datocms.com/docs/content-management-api/resources/upload-filter.md)
- [Model filter](https://www.datocms.com/docs/content-management-api/resources/item-type-filter.md)
- [Plugin](https://www.datocms.com/docs/content-management-api/resources/plugin.md)
- [Workflow](https://www.datocms.com/docs/content-management-api/resources/workflow.md)
- [Asynchronous job](https://www.datocms.com/docs/content-management-api/resources/job.md)
- [Job result](https://www.datocms.com/docs/content-management-api/resources/job-result.md)
- [Account](https://www.datocms.com/docs/content-management-api/resources/account.md)
- [Organization](https://www.datocms.com/docs/content-management-api/resources/organization.md)
- [Invitation](https://www.datocms.com/docs/content-management-api/resources/site-invitation.md)
- [Collaborator](https://www.datocms.com/docs/content-management-api/resources/user.md)
- [Role](https://www.datocms.com/docs/content-management-api/resources/role.md)
- [API token](https://www.datocms.com/docs/content-management-api/resources/access-token.md)
- [Webhook](https://www.datocms.com/docs/content-management-api/resources/webhook.md)
- [Webhook call](https://www.datocms.com/docs/content-management-api/resources/webhook-call.md)
- [Build trigger](https://www.datocms.com/docs/content-management-api/resources/build-trigger.md)
- [Deploy activity](https://www.datocms.com/docs/content-management-api/resources/build-event.md)
- [Subscription limit](https://www.datocms.com/docs/content-management-api/resources/subscription-limit.md)
- [Subscription feature](https://www.datocms.com/docs/content-management-api/resources/subscription-feature.md)
- [SSO Settings](https://www.datocms.com/docs/content-management-api/resources/sso-settings.md)
- [SSO User](https://www.datocms.com/docs/content-management-api/resources/sso-user.md)
- [SSO Group](https://www.datocms.com/docs/content-management-api/resources/sso-group.md)
- [White-label settings](https://www.datocms.com/docs/content-management-api/resources/white-label-settings.md)
- [Audit log event](https://www.datocms.com/docs/content-management-api/resources/audit-log-event.md)