- name
- document-editing
- description
- How to create, read, update, and delete documents. Covers the document scripts, markdown content model, parent-child hierarchy, and position ordering.
# Document Editing
Documents are stored in the SQL database via Drizzle ORM. Each document has a
title, stable description, markdown content, optional parent (for nesting), and
a position for ordering. The description explains why the page exists and what
belongs there; it is not a changing synopsis of the current body.
When operating Content through a connected MCP server, call these actions
yourself; route work through `ask_app` only when delegating to Content's own
agent is the explicit point of the request. Reaching tools you can call
directly through `ask_app` adds a second interpretation of the request and
hides real errors.
## Scripts
Always use the dedicated scripts for document operations. Never use raw `db-exec` SQL.
In dev, call actions with `pnpm action <name>`; in production, call native
tools. Never use `curl`, raw HTTP requests, or `db-exec` with raw SQL for
document operations. `.env` is loaded automatically — never manually set
`DATABASE_URL` or other env vars.
### list-documents
List document metadata in a tree structure. This intentionally does not return full document bodies; call `get-document` for the one document you need to read.
```bash
pnpm action list-documents
pnpm action list-documents --format json
```
### search-documents
Search documents by title and content. Results include snippets, not full document bodies; call `get-document` before editing or summarizing a specific result.
```bash
pnpm action search-documents --query "meeting notes"
pnpm action search-documents --query "project plan" --format json
```
### get-document
Get a single document by ID with full content and description. MCP callers read
the complete values from `structuredContent`; the text preview may be shortened.
```bash
pnpm action get-document --id abc123
pnpm action get-document --id abc123 --format json
```
### create-document
Create a new document.
```bash
pnpm action create-document --title "Meeting Notes" --content "# Meeting Notes\n\nAttendees: ..."
pnpm action create-document --title "Sub Page" --parentId parent123
pnpm action create-document --title "My Page" --icon "📝"
pnpm action create-document --title "Research" --description "Evidence and source notes that support the current project"
pnpm action create-document --title "Placeholder 1" --spaceName "Foobar"
```
If the result has `creativeContextProjectionStatus: "pending"`, the Page is
already committed. Retry the same arguments with the returned `id` to repair
its Creative Context projection; do not create another Page.
When a user asks for a Page in an interactive Content conversation, creation is
not a complete handoff. After `create-document` succeeds, call `navigate` with
the returned document `id`, then call `view-screen` and compare its document ID
with the create result. Navigation is asynchronous: if `view-screen` still
reports the previous Page, repeat `navigate` and `view-screen` up to two more
times. Say the Page is open only after the IDs match. If they never match, say
the Page was created but navigation could not be verified, and provide the
stable Page link from the create result. Do not navigate for background, batch,
or API creation unless the caller explicitly asked to open the result.
When the user names a workspace ("in my Foobar workspace"), pass `spaceName`
(or a `spaceId` from `list-content-spaces`). A named workspace that does not
resolve is an error, never a silent fall back. With no `parentId`, `spaceId`,
or `spaceName` the page is created in the caller's Personal workspace, so read
the returned `spaceId` before telling the user where the page landed. The
Workspaces catalog is not a create target: its rows only list workspaces, and
`add-database-item` against it is rejected.
### update-document
Update a page's or database's metadata while preserving omitted fields. To
replace its guidance, pass only `id` and `description`; an empty string clears
the description. For a database, use its backing `documentId`, not its collection
`databaseId`. The saved description is returned in MCP `structuredContent`.
Agent responses contain metadata only; `get-document` returns the body.
This does not change the Markdown body; body edits use `edit-document`.
### import-content
Turn Markdown files the user hands over into new pages. Prefer it to
`create-document` whenever the source is a file: it takes the title from
frontmatter or the first heading, keeps the original file, uploads referenced
images, records "Imported from <file>" in History, and names everything that
did not come across.
```bash
pnpm action import-content --dryRun true --parentId abc123 --files '[{"name":"guide.md","text":"# Guide\n..."},{"name":"logo.png"}]'
pnpm action import-content --dryRun false --parentId abc123 --idempotencyKey guide-1 --files '[{"name":"guide.md","text":"..."},{"name":"logo.png","url":"https://..."}]'
```
Run the dry run first and tell the user each page's `status` and `notes`
before applying. Applying fails with `IMPORT_IMAGE_NOT_UPLOADED` until every
image named in `uploads` has a `url`; leave an image nobody can supply out of
`files`, and its references become visible placeholders on the page. Other
formats come back in `skipped` as not supported yet. Pass an
`idempotencyKey` on every apply, and reuse it only to retry the same files,
with the same image `url`s, into the same place. `IMPORT_INCOMPLETE` means
some pages landed before a failure: its `details` name the `importId` and
created page ids (with `documentIdsComplete: false`, more may exist), and the
same call again finishes the import. `IMPORT_PAGE_TRASHED` means the import
was undone or its pages trashed; restore them, or import with a new key.
`undo-content-import --importId <id>` moves the import's pages to Trash. It
needs only editor access, not the admin access `delete-document` needs,
because it moves only pages the caller imported that nobody has changed since.
It refuses with `IMPORT_PAGE_CHANGED` when a page was edited, moved, or given
child pages since; ask before using `delete-document` instead.
`IMPORT_IN_PROGRESS` means the import is still adding pages; undo it again once
it finishes. While the Import dialog is open,
`view-screen` returns `contentImport` with its status, destination, file
names, and counts, never file contents. `import-content-source` is unrelated:
it syncs a connected local folder.
### edit-document
Surgically edit document content using search-and-replace. **Preferred over `update-document --content` for modifications** — sends only the changed text instead of regenerating the entire document.
```bash
# Single edit
pnpm action edit-document --id abc123 --find "old text" --replace "new text"
# Delete text
pnpm action edit-document --id abc123 --find "delete me" --replace ""
# Batch edits
pnpm action edit-document --id abc123 --edits '[{"find":"old","replace":"new"},{"find":"also old","replace":"also new"}]'
```
External MCP, WebMCP, tool, and A2A callers first read the document, then pass
its `baseRevision` and one stable `idempotencyKey`. When the returned body is
literally empty, pass non-whitespace `initializeContent` instead of `find` or `edits`.
Initialization rejects whitespace-only and all other nonempty bodies, preserves
the Markdown bytes exactly, and safely replays an identical retry.
### update-document
Update an existing document's metadata or browser-owned content. External
callers use the revisioned `edit-document` protocol for every body change.
```bash
pnpm action update-document --id abc123 --title "New Title"
pnpm action update-document --id abc123 --content "# Updated Content\n\nNew text here"
pnpm action update-document --id abc123 --title "New Title" --content "New content"
pnpm action update-document --id abc123 --description "Stable guidance for what belongs on this page"
```
### Suggested edits
When the user asks to **suggest**, **propose**, or **leave changes for review**,
do not call `edit-document` or `update-document`. Read the current Page with
`get-document`, then call `suggest-document-edit` with the Page's `id`, its
`baseRevision`, a fresh `idempotencyKey`, and the exact `find` text plus the
`replace` Markdown (omit `replace` to propose deleting the text). `find` must
match the page's current text exactly once. Content builds the tracked change
and anchor server-side; the page stays unchanged until a reviewer accepts.
The action separates disjoint punctuation and word changes into independent
review edits. Its result gives the first `suggestionId`, all `suggestionIds`,
and a `proposalId`. To add another
find/replace call to that proposal, pass its `proposalId`, the same `summary`,
and a fresh `idempotencyKey`. Retry the same call with its original key.
An unchanged replacement creates no suggestion and reports an error.
Inside tables, callouts, toggles, and columns, suggest text: edit a cell, or
edit or add a paragraph in a callout, toggle, or column. Keep each `find`
within one cell or one frame, and make one call per cell. A `find`/`replace`
that changes text on both sides of a cell or frame edge, adds or removes
table rows or cells or a column, or changes a callout's icon, a toggle's
title, or an image, fails with `suggestion_structure_unsupported`.
Use `suggest-document-edit` for every suggested body edit. The generic
`create-resource-suggestion` action remains for advanced proposals that build
typed Page-body operations by hand: `resourceType: "document"`, adapter kind
`content.document-markdown`, the Page's `revision` (or exact `updatedAt`) as
`baseRevision`, a fresh `idempotencyKey`, and one operation carrying the
complete current and proposed Markdown in `before.markdown` and
`after.markdown`, the changed segment in `changedText`, and an `anchor` object.
Use `list-resource-suggestions` to inspect pending and historical proposals.
Only accept or reject when the user has asked for that decision and the caller
has editor authority. A suggestion's author may instead withdraw their own
pending suggestion with comment access (`decision: "withdrawn"`); withdrawn
suggestions leave the Page unchanged and drop out of review. Call
`decide-resource-suggestion` with a fresh idempotency key and the suggestion's
`baseRevision` as `observedBase`. An accept that fails with `suggestion_stale`
didn't land because the text around it changed; the Page is unchanged and the
suggestion stays pending, so tell the user which suggestion it was instead of
counting it as accepted. Suggested edits are
unavailable for local-file, source-owned, externally linked, or trashed Pages,
Collection Pages, Pages with inline databases, and collection-item Pages without
an accessible primary Blocks field.
```bash
pnpm action suggest-document-edit --id abc123 \
--baseRevision 'body:0:sha256:…' --idempotencyKey '<uuid>' \
--find 'Exact current sentence.' --replace 'Proposed replacement sentence.'
```
### delete-document
Move a page and all its sub-pages to Trash. IDs, bodies, hierarchy, and
collection membership remain intact so the subtree can be restored. Agents pass
the page's exact `updatedAt` from `<current-screen>` or a fresh read, plus an
idempotency key. A conflict means the page changed: read it again before
deciding to trash it. Collection pages use `delete-content-database` instead.
```bash
pnpm action delete-document --id abc123 \
--expectedUpdatedAt '<updatedAt>' --idempotencyKey '<uuid>'
```
Restore the receipt's `trashRootId` with its `trashedAt` (also listed by
`list-content-trash`), or permanently delete it only after it is in Trash:
```bash
pnpm action restore-document --id abc123 \
--expectedTrashedAt '<trashedAt>' --idempotencyKey '<uuid>'
pnpm action plan-content-trash-purge --mode selection --documentIds '["abc123"]'
pnpm action permanently-delete-document --id abc123 --planId '<reviewed plan ID>' --scopeToken '<opaque plan token>'
```
Permanent deletion requires the exact `planId` and `scopeToken` returned by
`plan-content-trash-purge`. Inspect the plan's affected and blocked items before
executing it; if Trash changes after review, create and inspect a new plan.
## Comments
Comments are Notion/Google-Docs-style **inline comments**. Selecting text and commenting leaves the passage **highlighted inline** via a ProseMirror decoration overlay — nothing is written into the markdown body, so the document round-trips unchanged. Each thread stores the quoted text plus surrounding context (`anchorPrefix`/`anchorSuffix`) and an approximate `anchorStartOffset`, so the highlight follows the text as the document is edited, disambiguates repeated text, and degrades gracefully (the thread stays in the sidebar) when its text is deleted.
Resolving a thread clears its highlight and moves it to a collapsible **"Resolved (n)"** sidebar section, from which it can be **reopened**. Comments support **@mentions** of org members, stored as a `mentions` array of `{email, name}`.
```bash
# List threads (returns anchor fields + parsed mentions)
pnpm action list-comments --documentId abc123
# Plain comment on the document
pnpm action add-comment --documentId abc123 --content "Looks good"
# Inline-anchored comment with a mention
pnpm action add-comment --documentId abc123 --content "@Sam check this" \
--quotedText "the second paragraph" --anchorPrefix "above " --anchorSuffix " here" \
--anchorStartOffset 120 --mentions '[{"email":"sam@x.com","name":"Sam"}]'
# Reply to a thread
pnpm action add-comment --documentId abc123 --threadId t123 --content "Agreed"
# Resolve / reopen the whole thread
pnpm action update-comment --id c123 --resolved true
pnpm action update-comment --id c123 --resolved false
```
`--authorName` sets the comment's display name; it defaults to a name derived from the author's email.
A new comment emails the document owner, earlier authors in the thread, and anyone mentioned, unless they turned those emails off. The current user's own switch is `get-content-notification-prefs` and `update-content-notification-prefs --emailNotifications=false`, the same one Settings shows on Notifications. Share invites always send.
### refresh-list
Trigger the UI to refresh the document list.
```bash
pnpm action refresh-list
```
Always run this after any document modification to update the sidebar.
`create-document`, `update-document`, and `delete-document` already signal a UI
refresh, so in practice only call `refresh-list` directly if you mutate
documents another way and the UI doesn't update.
## Document Schema
| Column | Type | Description |
| ------------- | ------- | -------------------------------------- |
| `id` | text | Primary key (12-char hex string) |
| `parent_id` | text | Parent document ID (null for root) |
| `title` | text | Document title (default: "Untitled") |
| `description` | text | Stable semantic guidance for the page |
| `content` | text | Markdown content |
| `icon` | text | Emoji icon (optional) |
| `position` | integer | Sort order within parent (0-based) |
| `is_favorite` | integer | Whether document is favorited (0 or 1) |
| `created_at` | text | ISO timestamp |
| `updated_at` | text | ISO timestamp |
### Related Tables
Documents live in the SQL `documents` table via Drizzle; the framework injects
the live column schema separately, so this covers only semantics the schema
can't convey:
- `document_shares` holds per-user/per-org grants with a `viewer`, `editor`,
or `admin` role.
- `document_versions`, `document_comments`, and `document_sync_links` all
carry `owner_email` so a workspace can upgrade from local mode to a real
account without losing history, comments, or Notion links.
- A collection is a normal document (`content_databases` +
`document_property_definitions`) whose rows are also documents, linked
through `content_database_items`. Row pages are omitted from the ordinary
sidebar tree — they're reached through the collection view.
Documents are **private by default**; use `share-resource` /
`set-resource-visibility` (`resourceType document`) to change access.
## Content Format
Documents use **markdown** for content. The editor renders markdown in real time.
### Math
Content renders LaTeX with KaTeX while preserving the source in NFM. Use the
canonical Content delimiters when an agent creates or edits math:
```md
Inline math: The relationship is $`E = mc^2`$ in this example.
Block math:
$$
\int_0^1 x^2 dx = \frac{1}{3}
$$
```
- Inline math uses ``$`...`$``. Do not substitute ordinary `$...$`; dollar
amounts in prose are intentionally not interpreted as equations.
- Block equations use `$$` on their own lines before and after the LaTeX.
- Keep the raw LaTeX intact when editing around an equation. Invalid or
unsupported expressions remain visible as source so they can be repaired.
- Markdown exports preserve these delimiters. HTML and PDF-ready exports render
the equation.
## Parent-Child Hierarchy
Documents form a tree via `parent_id`:
- Root documents have `parent_id = null`
- Child documents reference their parent's `id`
- Deleting a parent recursively deletes all children
- Position determines ordering within the same parent
To reorganize the tree, move each subtree with `move-document` using ids from
a prior action result. When the plan calls for a target page that does not
exist yet — a new section, a grouping page — create it first with
`create-document` and use the returned id as `parentId`.
Descriptions are owned; context is inherited. `get-document` and `view-screen`
return the focused page's own description plus a computed root-to-parent
`contextPath`. Use that path to understand where the page lives, but never copy
ancestor descriptions into the child. Collection, property, and option
descriptions narrow the guidance further when working with structured values.
## Screen Context And IDs
Screen context is auto-included as a `<current-screen>` block on every message —
check it before acting instead of calling `view-screen` by default. Call
`view-screen` explicitly only when that snapshot is truncated or doesn't yet
reflect something that changed earlier in the same turn (for example, right
View on GitHub