# Document Workspace

> Applies to: ChengOS v0.1.0+ ｜ Last verified: 2026-08-12 ｜ Source: `crates/cheng-core/src/domain/document`, `crates/cheng-nodes/src/nodes/builtin/document`, `chengflow-ui/src/features/document`, `chengflow-ui/src/features/shadow`

A ChengOS document is not a Markdown file. It is a **block tree** stored in the database, with
its own version history and a staging area — the *shadow session* — that lets an agent propose
edits without touching the document until a human accepts them.

That design is what makes documents safe to hand to a model.

## The domain model

```text
Document (aggregate root)
  ├── BlockTree
  │     └── Block
  │           ├── BlockType     (paragraph, heading1..3, code, ...)
  │           ├── BlockContent  (text, code, todo, image, callout, ...)
  │           └── BlockMeta     (priority, tags)
  ├── DocumentVersion[]         (history snapshots)
  └── ShadowSession?            (staged, not yet applied)
        └── ShadowOperation[]
```

Every block has a stable id, a parent, and an ordered position among its siblings. Editing a
document therefore means addressing a **block**, not a character offset — which is exactly what
makes an LLM edit reviewable: the diff is a list of blocks, not a text patch.

**Block types**: `paragraph`, `heading1`, `heading2`, `heading3`, `code`, `bullet_list`,
`numbered_list`, `todo_item`, `quote`, `callout`, `image`, `table_view`, `embed`, `divider`.

**Block metadata** carries two fields that matter far more than they look:

- **priority** — `low`, `normal`, `medium`, `high`, `critical`
- **tags** — a comma-separated list

These are not decoration. They are the filter axis used when feeding a document to a model
(see *Feeding a document to an LLM* below), so a 40-page document can be reduced to "only the
critical blocks tagged `spec`" without a retrieval pipeline.

## Documents in the editor

The document workbench lives beside the canvas: a document list, tabs for open documents, and a
block editor. Documents belong to a **workspace**, and the active workspace is injected into
node execution automatically — most document nodes leave `workspace_id` blank and inherit it.

## The document nodes

Seven nodes are visible on the canvas; the rest are internal machinery used by the hub.

| Node | What it does |
|---|---|
| `document/create` | Creates a document, optionally with a first block |
| `document/query` | Reads a document — as blocks, or as Markdown with `as_markdown` |
| `document/update_block` | Replaces one block's content or type |
| `document/format_for_llm` | Renders a document for a model, with filtering |
| `document/extract_items` | Turns block tags and lines into loop-ready items |
| `document/ops_hub` | The whole document toolkit, exposed to an agent |

`document/query` returns both the block list and derived counts (`block_count`,
`character_count`), plus `plain_text` — enough to branch on document size before deciding how to
process it.

The `document_id` field is a **dynamic select**: the dropdown is populated from
`GET /api/v1/documents?limit=200`, which the `X-Workspace-Id` middleware has already scoped to
the active workspace. You pick a document by title, and the node stores its UUID.

## Feeding a document to an LLM

`document/format_for_llm` exists because dumping a whole document into a prompt is usually the
wrong move. It takes four knobs:

| Option | Values | Default |
|---|---|---|
| `priority_filter` | `all`, `high_only`, `critical_only`, `normal_and_above` | `all` |
| `tag_filter` | comma-separated tags | none |
| `sort_order` | `original`, `priority_desc`, `priority_asc` | `original` |
| `output_format` | `markdown`, `plain_text`, `structured` | `markdown` |

Plus `max_chars` (0 = unlimited) and `include_metadata`. `structured` keeps block ids in the
output, which is what you want when the model is expected to *edit* the document — it needs the
ids to address its own changes back.

## Shadow sessions: safe AI editing

Every write an agent makes goes through a shadow session rather than straight into the tree.

```text
shadow_create ──► shadow_write ──► shadow_complete ──► shadow_await_review
    (fork)          (stage ops)      (generating→        (blocks until a
                                      pending_review)      human decides)
                                                                │
                                              accept ───────────┴─────── reject
                                                 │                          │
                                              merged                    discarded
```

**Shadow status**: `generating`, `pending_review`, `merged`, `discarded`, `partially_accepted`,
`expired`. `merged`, `discarded`, and `expired` are terminal.

`partially_accepted` is the interesting one — review is per-block, so a user can take three of
an agent's five edits and leave the rest pending.

In the UI this surfaces as the shadow review bar and a block-level diff viewer: staged
insertions, updates, and deletions are overlaid on the real document, and there is a conflict
resolution modal for when the underlying document moved while the agent was writing.

Two consequences worth internalising:

1. A write operation from the hub can return `requires_review: true` with a `shadow_id`. The
   execution enters `waiting_for_review` and stays there durably until a decision arrives —
   see [the execution model](/docs/exec-model).
2. Every write operation accepts `dry_run: true`, which stages and returns the diff without
   applying anything. This is the cheap way to let a model check its own work.

## The operations hub

`document/ops_hub` is one node connected to an agent's `tools` port that exposes the entire
document toolkit. The agent picks a capability through the `operation` field; the node's
toggle switches decide which operations exist at all.

**Read operations** (default **on**): `query`, `format_for_llm`, `document_info`,
`list_documents`, `search_documents`, `diff_document`, `export_document`.

**Write operations** (default **off**, each behind its own switch): `create`, `create_block`,
`update_block`, `replace_text`, `delete_block`, `move_block`, `batch_ops`, `rename_document`,
`duplicate_document`, `import_document`, `delete_document`, `restore_from_export`.

An operation whose switch is off fails immediately with no side effect — the agent cannot talk
its way past the toggle, because the check happens before the operation runs.

The call shape is `operation` and `document_id` at the top level, everything else inside
`config`. Flat top-level parameters are also accepted and folded into `config`, because models
produce both shapes.

Three operations deserve a callout:

- **`replace_text`** does exact text replacement while preserving the rest of the block. Scope
  it to one block with `block_id`, or to the whole document with `match_scope: document` and
  `max_replacements`. This beats `update_block` whenever the model only needs to change a phrase.
- **`batch_ops`** applies up to 100 operations atomically through a single shadow session, so a
  multi-part rewrite is reviewed as one change set instead of twelve.
- **`export_document`** with `output_format: snapshot` produces a self-contained restorable
  payload. Take a snapshot before a risky edit, and `restore_from_export` rolls back to it.
  Note that full version-history rollback is not implemented — snapshot and reparsed-content
  restore are the two available paths.

## Turning a document into loop items

`document/extract_items` converts a document's tags and lines into an array a loop or batch
subflow can iterate. Choose **what** to extract with `target` — `tags`, `lines`, `pairs`,
`objects`, `groups` — and **how to combine** it with `combine`: `list`, `sequential`, `random`,
`selected`, `zip`, or `cartesian` (capped at 10,000 items).

The practical pattern: tag blocks in a document by the task they represent, extract `pairs`,
and feed them to a [batch subflow](/docs/editor-subflows) that processes each one.

## Next steps

- [Table Workspace](/docs/data-table-workspace) — the structured-data counterpart.
- [Knowledge Bases & Workspace Context](/docs/data-knowledge-base) — where documents get indexed.
- [Approvals & Human Review](/docs/exec-approvals-reviews) — the review mechanism shadow sessions ride on.
