# How agents edit

The model under an agent's edits in Entz, from XML reads, block ids and fingerprints to the six ops and their targets, guards, placeholders, chips and the answer to a write.

An agent never sends a document back whole. It reads it as XML, then sends a list of small edits, called ops, each naming what it changes by a block's id or by quoted words. This page describes that model for the curious, and for anyone writing their own prompts or agents on top of the connector. The agent reads the same rules in the connector's guide, the `index` topic of the `guide` tool, and [The eight tools](/docs/mcp/tools/) lists each tool's parameters.

## Reads

`read` gives a document, or one of its tabs, as one line of XML, in one of six kinds:

| Kind | Takes | Shows |
|---|---|---|
| `outline` | | Every block, its text cut at 80 characters, tables folded. The default, and where the agent starts. |
| `view` | `id` | One block in full: for a table, its rows and cells. |
| `search` | `text` | The blocks holding the words, case ignored, each match wrapped in `<hit n>`, the rest folded. A table shows only the rows and cells with a match. |
| `since` | `rev` | The blocks changed since that revision, in full and where they now stand; the rest folded; removed blocks as `<gone id/>`. |
| `at` | `rev` | The whole document as it was at that revision. |
| `full` | | Every block in full. For short documents. |

Folded blocks show as `<gap blocks='n'/>`. The agent is told to start a later turn with a `since` read of its last revision, which shows at once what people changed in between. To bring back words someone deleted, it reads them with `at` and inserts them again, as a new block.

An outline of a new document, broken into lines here:

```xml
<doc rev='2' outline='true'>
  <heading id='2kx9q1.15' h='5c1e0a9b' level='1'>Q3 launch plan</heading>
  <paragraph id='.16' h='0f3d27e4'>Q3 ships the connector to every workspace.</paragraph>
  <pending id='.17' h='a90b1c44' intent='Goals: the three outcomes'/>
  <pending id='.18' h='61d2f0a3' intent='Staffing: who owns each workstream'/>
</doc>
```

The block tags are `paragraph`, `heading` (with its `level`), `list_item` (with `list`, which is `bullet`, `ordered` or `task`, and `indent` and `checked` when set), `quote`, `code` (with its `language`), `table` (with `rows` and `cols`, holding `row` and `cell` elements, a cell with its `background`), `image`, `equation`, `divider`, `drawing` and `pending` (with its `intent`). Inside text, marks read as `<b>`, `<i>`, `<u>`, `<s>`, `<code>`, `<a href>`, `<mark>` (a highlight), `<color>` (a text colour) and `<math>`. A date chip reads as its label inside `<date value='2026-10-06'>`, and a dropdown as its pick inside `<dropdown>`, with its `set`, `name`, `index` and `options`.

## Ids, fingerprints and revisions

- **Ids.** Every block has an id of the form `<session>.<counter>`, such as `2kx9q1.15`: the session label of whoever made the block, then a counter. The id stays with the block through edits, moves and changes of kind; splitting a block makes a new one. A read shortens a run of ids from one session to `.N` after the first full one, so `2kx9q1.15` followed by `.16` means `2kx9q1.16`. Writes always take the full id.
- **Fingerprints.** Every block carries `h`, eight hexadecimal digits computed from its kind, its attributes, its text with its marks, and its rows. Any change to the block's words, marks, attributes or cells changes it; a move does not.
- **Revisions.** A revision, `rev`, counts the versions of a tab the connector has seen. It starts at 1 and rises whenever the tab changed since the connector last looked. Every read and every write's answer names the current one. The connector keeps the newest 256 for each tab, and starts again at 1 when it restarts, or when it reopens a document it closed after 10 minutes unused. A revision it no longer holds is refused as `rev_expired`, and the agent reads the outline again.

## Writes

`update` takes 1 to 64 ops for one tab. They apply in order and land as one change: when a call holds several ops, the connector tries them on a copy of the document first, so a refusal anywhere means nothing lands. The change is signed as the agent's, with its name and the time, which is how editors know to fade its words in.

### Ops

| Op | Fields | What it does |
|---|---|---|
| `insert` | `target`, `side`, `source` | Adds blocks at the start or end of the tab, before or after a block, at the end of a heading's section, or into a table cell. With quoted words as the target and a source `as: "text"`, adds words before or after them. |
| `replace` | `target`, `with`, a guard | Swaps whole blocks for the source's blocks, re-words one block in place, changes quoted words, or replaces a cell's contents. |
| `delete` | `target`, a guard | Removes whole blocks, or quoted words. A tab keeps at least one block. |
| `move` | `target`, `to` | Moves top-level blocks to a place given as `{"target": …, "side": …}`. |
| `set` | `target`, `attrs` | Sets attributes on blocks, such as `{"kind": "heading", "level": 2}`, `{"list": "task"}` or `{"checked": true}`; `null` removes one. On a cell, sets its `background`. |
| `format` | `target`, `add`, `remove` | Adds or removes marks on blocks or quoted words: `bold`, `italic`, `underline`, `strike`, `code`, `link`, `highlight`, `color`, `math`. A mark with a value is written `{"link": "https://…"}` or `{"highlight": "yellow"}`, and any mark may be written `{"type": "bold"}`. |

A few rules keep the ops predictable:

- **Sides.** On the tab (`root`), `side` is `start` or `end`. Next to a block it is `before` or `after`, and `end` on a heading means the end of its section: up to the next heading of the same or a higher level. Into a cell it is `start` or `end`, and next to quoted words `before` or `after`.
- **Re-wording in place.** Replacing one block `as: "text"` keeps the block: its id, its unchanged words and the comments on them stay. One paragraph replaced by one paragraph as Markdown is re-worded the same way, then takes the new paragraph's marks and chips. A heading, a list, or several blocks are replaced as new blocks.
- **Quoted words.** Replaced words keep the marks of the first word replaced. Deleted words are quoted exactly, a leading space included.
- **Tables.** A table never goes inside a cell. Blocks inside cells do not move: the table moves. A move's place cannot be one of the blocks moved, a block inside a table or a cell.
- **Not marks.** Dates and dropdowns are [chips](#chips), and comments are made with the `comments` tool.

### Targets

| Target | Names |
|---|---|
| `{"kind": "blocks", "ids": ["2kx9q1.16"]}` | Blocks, by full id. |
| `{"kind": "find", "text": "every workspace"}` | Words within one block, quoted exactly as a read shows them, without Markdown and with the same capitals. `nth`, counted from 1, picks one of several matches; `within` limits the search to one block, a table or one cell; `parentId` limits it to the section under a heading, or to one block. |
| `{"kind": "root"}` | The tab itself, for inserts at its start or end. |
| `{"kind": "cell", "table": "2kx9q1.30", "row": 1, "col": 2}` | A table cell by position, counted from 0 with row 0 the header row, or from the end with -1 for the last. A cell takes `replace` (its contents), `insert` and `set` (its `background`: `gray`, `red`, `orange`, `yellow`, `green`, `teal`, `blue`, `purple`, `pink` or `#rrggbb`, `null` for none). |

### Sources

The content an op writes is a source: `{"from": {"kind": "inline", "content": …}, "as": …}`, where `as` is one of:

- `markdown`: headings, lists, task lists, quotes, tables, code blocks and links. A `mermaid` code block is drawn as its diagram and a `latex` code block as its formula. Markdown also carries chips.
- `text`: plain text.
- `blocks`: blocks in Entz's own JSON form.

### Chips

A chip is a `<?entz block key?>` token in Markdown, with its entry under the source's `blocks` (for `create` and a new tab, the call's own `blocks`), one key per chip. Every token needs an entry, and every entry a token.

- **Block chips** stand alone on their line: `{"type": "pending", "intent": "…"}`, a placeholder; `{"type": "paragraph"}`, an empty paragraph; and `{"type": "divider"}`.
- **Date chips** go anywhere in text, table cells included: `{"type": "date", "value": "2026-10-06"}`, shown as its label, such as Oct 6, 2026.
- **Dropdown chips** go anywhere in text too: `{"type": "dropdown", "enum": "status", "name": "Status", "options": ["Not started", "In progress", "Done"], "index": 1}`.
  - `index` is the pick, counted from 0; it is left out when people will pick.
  - An option is a name or `{"name": …, "color": …}`, the colour one of those a cell takes. An option without one takes the colour of its place in the list.
  - Chips naming the same `enum` form one set: its `options` and `name` are given once, and later chips name the `enum` alone. A chip nobody picked from reads as the set's name, or Select when the set has none.
  - The agent changes a pick by replacing the chip.

### Guards

A `replace` or `delete` of whole blocks, and a `replace` of a whole cell by position, needs a guard, or it is refused as `missing_guard`:

| Guard | On | Refused when |
|---|---|---|
| `"ifHash": "<h>"` | one block | The block's fingerprint is no longer that one. |
| `"ifRev": <rev>` | blocks | Any of them changed since that revision. A move does not count. |
| `"ifRev": <rev>` | a cell by position | The table's rows or columns changed since that revision (the position may name another cell now), or the cell's words changed. |
| `"force": true` | blocks or a cell | Never. The agent is told to use it only when you asked it to overwrite whatever is there. |

Placeholders and quoted words need no guard: a quote touches only its own words, and is refused if they are gone. Beside the ops, a write-level `ifRev` refuses the whole write unless the tab is still at that revision.

### Placeholders

A placeholder is a block of kind `pending` with an `intent`, which the editor draws as the intent in grey brackets. The agent plants one per section, usually through `create`, then replaces each with its section, one `update` per section. Replacing one needs no guard, and the answer lists it under `pending_ended`. The guide asks the agent to fill them in reading order, the prose first and the diagrams after: a diagram takes longer to write, and its placeholder tells readers it is coming. An intent can plan the picture, as in "Rollout: a gantt of the milestones by team". Placeholders are left out of Markdown, HTML, Typst and plain text exports.

## What a write answers

```json
{
  "rev": 3,
  "session": "2kx9q1",
  "xml": "<doc rev='3'><op n='0'><heading id='2kx9q1.42' h='8d02c6a1' level='2'>Goals</heading><list_item id='.43' h='31e7f0b2' list='bullet'>Ship to every workspace</list_item><list_item id='.44' h='c90a5d17' list='bullet'>500 weekly active teams</list_item></op></doc>",
  "notices": [{ "notice": "pending_ended", "ids": ["2kx9q1.17"] }],
  "last": "2kx9q1.44",
  "synced": true,
  "link": "https://entz.app/doc/…"
}
```

| Field | Holds |
|---|---|
| `rev` | The tab's revision after the write. |
| `session` | The writer's session label, which starts every id it made. |
| `xml` | The blocks each op wrote, as a read shows them, op by op. |
| `keys` | Each block chip's key and the full ids it became, such as `{"s1": ["2kx9q1.40"]}`. Dates and dropdowns are marks on their label, so they have none. |
| `notices` | `pending_ended`, with the placeholders the write replaced, and `detached`, with the comment threads left without their words. |
| `last` | The last block written, where the agent's caret rests. |
| `synced` | Whether Entz confirmed the write within 15 seconds. |
| `link` | The document's link. |

The agent aims its next op at the ids in the answer, without reading again between its own writes.

## Limits

| What | Limit |
|---|---|
| Ops in one `update` | 64 |
| Text an outline shows per block | 80 characters |
| Revisions kept for each tab | the newest 256 |
| Time a write waits for Entz to confirm it | 15 seconds |
| Words that fade in per write | 300; the rest appear at once |
| A document the connector leaves unused | closed after 10 minutes |
| The agent's presence after its last work on a document | 90 seconds |
| One write's changes on the server | 1 MiB |
| A comment's text | 4,096 bytes |
| Comment threads on one document or tab | 1,000, resolved ones included |
| Comments in one thread | 100 |
| A tab's name | 200 characters |
