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
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.
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:
- A write operation from the hub can return
requires_review: truewith ashadow_id. The execution enterswaiting_for_reviewand stays there durably until a decision arrives — see the execution model. - 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_textdoes exact text replacement while preserving the rest of the block. Scope it to one block withblock_id, or to the whole document withmatch_scope: documentandmax_replacements. This beatsupdate_blockwhenever the model only needs to change a phrase.batch_opsapplies 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_documentwithoutput_format: snapshotproduces a self-contained restorable payload. Take a snapshot before a risky edit, andrestore_from_exportrolls 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 that processes each one.
Next steps
- Table Workspace — the structured-data counterpart.
- Knowledge Bases & Workspace Context — where documents get indexed.
- Approvals & Human Review — the review mechanism shadow sessions ride on.