# cms-edit batch run — JSON Operation Scripts

`batch run` executes a sequence of `cms-edit` operations from a JSON file. It is best suited for **patching fields on existing entries** — for creating complete new pages, use `create from-json` instead.

```bash
cms-edit batch run --json '[...]'
cms-edit batch run --json '[...]' --dry-run   # validate without saving
```

---

## Batch File Format

A batch file is a JSON array of operation objects:

```json
[
  { "cmd": "open", "args": ["/solutions"] },
  { "cmd": "set",  "args": ["@c1", "heading", "New Heading"] },
  { "cmd": "rtf",  "args": ["@c2", "body", "Updated body.\n\nSecond paragraph."] },
  { "cmd": "save" }
]
```

Each operation has:

| Field | Type | Description |
|---|---|---|
| `cmd` | string | The operation to execute (see Supported Commands) |
| `args` | string[] | Positional arguments (same as CLI args) |
| `opts` | object | Named options (same as CLI flags, without `--`) |
| `onError` | `"stop"` \| `"continue"` | What to do if this step fails. Default: `"stop"` |
| `as` | string | Variable name to bind the ref assigned by an `add` op (e.g. `"$item"`) |

---

## Supported Commands

### `open`
Load a page or article by slug.
```json
{ "cmd": "open", "args": ["/solutions"] }
```

### `set`
Set a scalar field on an entry.
```json
{ "cmd": "set", "args": ["@c1", "heading", "New Heading"] }
{ "cmd": "set", "args": ["@c2", "backgroundColour", "Navy"] }
{ "cmd": "set", "args": ["@c3", "heading", null] }
```

### `rtf`
Set a rich text field from a markdown string.
```json
{ "cmd": "rtf", "args": ["@c2", "body", "## Heading\n\nParagraph text."] }
```

### `rtf-replace`
Find and replace text within an existing rich text field.
```json
{
  "cmd": "rtf-replace",
  "args": ["@c2", "body"],
  "opts": {
    "find": "old phrase",
    "replacePlain": "new phrase",
    "mode": "all"
  }
}
```
Options: `find` (required), `replacePlain`, `replaceMarkdown`, `mode` (`"all"` or `"exactlyOne"`), `ignoreMarks`, `tableCol`, `tableRow`, `section`.

### `add`
Create a new entry and link it into the tree.
```json
{ "cmd": "add", "args": ["HeroSimple", "component"] }
{ "cmd": "add", "args": ["Generic", "component"], "opts": { "parent": "@c3" } }
{ "cmd": "add", "args": ["Generic", "component"], "opts": { "after": "@c5" } }
{ "cmd": "add", "args": ["Generic", "component"], "opts": { "target": "topContent" } }
```
Options: `parent` (ref of a collection to add inside), `after` (ref to insert after), `target` (field name, default `"content"`), `existingId` (link an existing entry instead of creating).

### `links-add`
Create a new CTA link and attach it to an entry's `links` field.
```json
{ "cmd": "links-add", "args": ["@c1"], "opts": { "type": "external", "label": "Learn More", "href": "https://example.com", "variant": "link" } }
{ "cmd": "links-add", "args": ["@c1"], "opts": { "type": "internal", "label": "Contact", "slug": "/contact" } }
```
Options: `type`, `label`, `href`, `slug`, `id`, `assetId`, `variant`.

### `save`
Write all modified entries to Contentful as drafts.
```json
{ "cmd": "save" }
```

---

## Variable Binding — Safe `add` + `set` Patterns

**The problem:** After an `add parent @cN` operation, all entries that come after `@cN` in the depth-first tree are renumbered. Any `set` using a hardcoded `@cM` ref may then target the wrong entry, and the batch reports `✓` — a silent false positive.

**The solution:** Use `"as": "$varname"` on the `add` op to capture the assigned ref, then use `"$varname"` in subsequent `set`/`rtf` args:

```json
[
  { "cmd": "open", "args": ["/solutions/hepatology"] },
  {
    "cmd": "add",
    "args": ["Generic", "component"],
    "opts": { "parent": "@c3" },
    "as": "$item1"
  },
  { "cmd": "set", "args": ["$item1", "heading", "2017 — Cohort Launch"] },
  { "cmd": "rtf", "args": ["$item1", "body", "The cohort was established.\n\nInitial enrolment began."] },
  {
    "cmd": "add",
    "args": ["Generic", "component"],
    "opts": { "parent": "@c3" },
    "as": "$item2"
  },
  { "cmd": "set", "args": ["$item2", "heading", "2019 — First Publication"] },
  { "cmd": "save" }
]
```

`$item1` and `$item2` always refer to the entries that were just created, regardless of how subsequent adds shift ref numbers.

---

## Ref Renumbering — What Happens Under the Hood

After any `add parent @cN`, refs are reassigned in depth-first traversal order from the root. Items that came *after* `@cN` in the tree get bumped forward.

**Example:** Page with 10 entries (`@c0`–`@c9`). Timeline collection is `@c8`, Disease Areas is `@c9`.

| After add | New item ref | Disease Areas ref |
|---|---|---|
| Initial | — | @c9 |
| Add 1 to @c8 | @c9 | @c10 |
| Add 2 to @c8 | @c10 | @c11 |
| Add 3 to @c8 | @c11 | @c12 |

If you write `set @c10 heading "..."` after Add 1, you are setting Disease Areas — not the new item. The batch reports `✓` because the field was set successfully, just on the wrong entry.

**The tool warns when this happens:**
```
  ✓ [3] add  — Created component "Generic" as @c9 (ID: abc123)
  ⚠  Refs renumbered after add-to-parent. Use "as": "$varname" on the add op and "$varname" in subsequent args to avoid ref drift.
```

---

## Safe Patterns

### Pattern A — Variable binding (recommended)
Use `"as": "$name"` and `"$name"` in args. Refs always point to the right entry.

### Pattern B — All adds first, then sets
Do all `add` operations in one batch. Re-snapshot to see final refs. Write a second batch with `set`/`rtf` on the stable refs.

```bash
# Batch 1: add all items
cms-edit batch run --json '[...]'

# Inspect final refs
cms-edit snapshot

# Batch 2: set fields using the stable refs from snapshot
cms-edit batch run --json '[...]'
```

### Pattern C — Append to end only
If new items go at the end of the tree (after all existing entries), no existing refs are displaced. Use `after` to append at the tail.

### Pattern D — Use `create from-json` for new pages
For building complete page trees from scratch, `create from-json` is safer than a `run` batch — it manages internal refs automatically and validates slugs before writing.

---

## Complete Example — Patching Fields on an Existing Page

```json
[
  { "cmd": "open", "args": ["/solutions"] },
  { "cmd": "set",  "args": ["@c1", "heading", "Evidence-Driven Solutions"] },
  { "cmd": "set",  "args": ["@c1", "preHeading", "What We Do"] },
  {
    "cmd": "rtf",
    "args": [
      "@c2",
      "body",
      "We partner with life sciences companies.\n\nOur platform integrates real-world data across disease areas."
    ]
  },
  { "cmd": "save" }
]
```

Run:
```bash
cms-edit batch run --json '[...]' --dry-run   # validate first
cms-edit batch run --json '[...]'             # apply
cms-edit open --page-slug /solutions       # inspect result
```

---

## Error Handling

By default, execution stops on the first error. Use `"onError": "continue"` to keep running:

```json
[
  { "cmd": "open", "args": ["/solutions"] },
  { "cmd": "set",  "args": ["@c99", "heading", "Test"], "onError": "continue" },
  { "cmd": "save" }
]
```

The final exit code is non-zero if any step failed, even with `"continue"`.
