---
name: roam-syntax
description: >
Write and read content in a Roam Research graph correctly. Roam's markdown differs from standard
markdown, and its MCP tools render block metadata (headings, block references, view types) into a
wire format you must handle when writing content back. Use this whenever creating, editing,
searching, or organizing content in a Roam graph — including via the roam-mcp tools (create_page,
create_block, update_block, get_page, get_block, search) — or when you encounter Roam syntax like
[[page links]], ((block refs)), #tags, {{[[TODO]]}}, attribute:: values, {{[[query]]}}, or
tags, even if the user doesn't say "Roam". Covers writing syntax, reading tagged output
without losing block references, queries, components, and graph conventions. Not for
standard-markdown files or non-Roam note apps.
---
# Roam syntax
You're working in a Roam Research graph — an **outliner**, not a document editor. A page is a tree of
**blocks** (one bullet = one block); nesting (indentation) is the structure, and `[[links]]` /
`((block refs))` are the point. Use the graph; don't dump flat prose into it.
Two rules dominate everything else:
1. **Roam's markdown is not standard markdown** — the differences below silently corrupt content or
pollute the graph if you use standard-markdown habits.
2. **Reads return a rendered wire format; `update_block` writes literally.** Handle the ``
tags and `((uid))[...]` block-ref previews correctly or you'll destroy references on
write-back.
When working through the roam-mcp tools, call `get_graph_guidelines` before your first read or write in
a graph — it returns the user's own conventions plus a compact `roamSyntax` summary of the gotchas
below.
## Gotchas (the highest-signal part — read this)
### Writing: Roam markdown ≠ standard markdown
| You want | Use this (Roam) | NOT (standard markdown) |
| --------------- | --------------------------------------------------------- | ------------------------------------------------------- |
| Italics | `__text__` (double underscore) | `*text*` / `_text_` (see note) |
| Bold | `**text**` | `__text__` (that's italics) |
| Highlight | `^^text^^` | — |
| Strikethrough | `~~text~~` | — |
| Checkbox / task | `{{[[TODO]]}}` / `{{[[DONE]]}}` **at block start** | `- [ ]` / `- [x]` |
| Heading | `#` / `##` / `###` at block start (H1–H3 only; `####`→H3) | `####`+ |
| Numbered list | `childrenViewType: "numbered"` on the parent | `1.` / `2.` markers |
| Table | write a **pipe table** — create ops convert it | hand-building `{{[[table]]}}` |
| Nesting | **indent** child blocks | headings (in bullet markdown) do NOT slurp what follows |
- **Italics is `__text__`.** On `update_block` (literal), `*text*` / `_text_` are stored as-is and
render as plain characters; `create_page` / `create_block` normalize them to `__text__`. Always write
`__text__` so reads and writes agree.
- `{{[[TODO]]}}` renders a checkbox anywhere, but it must **lead the block** or clicking it won't
toggle to `{{[[DONE]]}}`.
- Headings are structural, but in **bullet-list markdown they stay flat** — a heading does not pull
the blocks after it in as children. Nest by indenting. (`####`+ silently clamps to H3.)
### create parses; `update_block` is literal
- `create_page` / `create_block` **parse** the markdown you pass (all the syntax above; a pipe table
becomes a `{{[[table]]}}` block; nested bullets become a block tree).
- `update_block` stores its `string` **literally** — no markdown parse, no new children. Use it for
text edits; use its params `heading` (0–3) and `childrenViewType` (`bullet`/`numbered`/`document`)
for structure, and `create_block` for new nested content.
- `childrenViewType` is a param on `create_page` / `update_page` / `update_block` — **not**
`create_block`. To make a new block's children numbered: create it, then `update_block` its uid.
### The escape rule (avoid polluting the graph)
Markup renders **live** when written. If your text is _documenting_ syntax (e.g. explaining how
`[[links]]` or `{{[[TODO]]}}` work), wrap the examples in `` `inline code` `` — otherwise you create
real pages, refs, and checkboxes.
## Reading Roam output
`get_page` / `get_block` / `search` / `get_backlinks` / `get_comments` / `roam_query` /
`semantic_search` return **rendered** markdown, one block per line:
```
-
```
- The trailing **`` tag** carries metadata: `uid` — the handle for every follow-up call,
optional `heading` (1–3), `childrenViewType`, `refs` (backlink count — high = edit with care),
`hiddenChildren="N"` (the subtree was cut off by `maxDepth`; read deeper with a higher `maxDepth`
before assuming you've seen everything), and `truncated="N"` (search/semantic results only: N
characters of a long block were cut — `get_block` the uid for the full text **before editing it**,
or a write-back would overwrite the block with just the visible prefix).
- **Strip the whole `` tag** before showing content to the user. When **quoting or showing**
the user their own notes, reproduce the text **verbatim** (don't silently reword or reformat). When
the user explicitly asks you to summarize or transform, do that.
- A **block reference** reads as `((uid))[preview]`, where `[` holds the referenced
block's text. When **displaying**, show the preview text and drop the `((uid))` + tag.
- **A `uid` should be there on every block.** If one is ever missing, that block's stored uid is
invalid (rare — legacy or imported content), and it **cannot be addressed**: `update_block`,
`delete_block` and `move_block` all require a uid. Leave that block alone. Show its content if
asked, but don't invent a uid and don't re-create its content elsewhere as a workaround — that
just duplicates it. This covers the `truncated="N"`-without-`uid` case above: with no uid you
can't fetch the full text, so don't edit that block from the visible prefix at all.
## Writing
- **New content** → `create_page` / `create_block` with markdown (parsed). Build hierarchy by
indenting; use headings as labels, not as containers.
- **Editing an existing block's text** → `update_block` (literal). Structure/display via its `heading`
/ `childrenViewType` params.
- **Write-back is the dangerous case.** When you edit a block you read, pass **only the content** to
`update_block`:
- drop the leading `- ` / indentation and the trailing `` tag (they are not part of the
block string), **but**
- **keep any `((uid))][...]` intact** — the server decodes it back to `((uid))`.
- Do **not** write the display-cleaned preview alone (the `[` text without its `((uid))`) — that
replaces the live reference with static text. A tags-only strip that leaves the preview behind
stores it as duplicate content.
- Editing text **inside** a `][…]` preview is a silent no-op — the server discards the
preview on write and keeps only `((uid))`. To change the previewed text itself: `get_block` the
**referenced** uid (it's the `((uid))` in the annotation), then edit its full returned content
with this same write-back procedure — don't reconstruct it from the preview, which may be
capped or have nested refs flattened.
See `references/reading-writing-via-mcp.md` for worked read→edit→write examples that don't lose refs,
and why the round-trip behaves this way.
## Behavioral doctrine
These are **defaults for when the user hasn't asked for a specific change** — explicit user intent
always wins, and the graph's own `[[roam/agent guidelines]]` (returned by `get_graph_guidelines`)
govern style and conventions. The **syntax and data-integrity rules** in this skill apply
regardless: no convention can make `update_block` parse markdown, make a truncated prefix
complete, or keep a reference live after its `((uid))` is dropped. If the user says "fix /
rewrite / delete this block," do exactly that.
- **One fact per block.** Nest details under a short lead block instead of writing long blocks —
blocks are the unit of reference, so long prose blocks defeat the outliner.
- **Don't duplicate content across blocks or pages** — link it with `((block refs))` or
`[[page refs]]` instead.
- **Append over edit.** Under ambiguity, add a child or sibling block rather than modifying existing
content — especially human-authored blocks.
- **Comment, don't rewrite.** To give feedback on someone's block, use `add_comment`, which preserves
the original.
- **Daily note is the safe fallback.** If unsure where content belongs, today's daily note is almost
always right.
- **Don't move or delete without explicit permission** — both can silently destroy meaning; a delete
also rewrites every block that referenced the target.
- **Respect load-bearing blocks.** A block with a high `refs` count is structurally important — edit
it with extra care.
- **Create page links intentionally, and don't fabricate block refs.** `[[Page]]` links AND mints
the page if it doesn't exist — link entities that genuinely recur, don't bracket every noun. Use
`((uid))` only for blocks already in your working context — don't search the graph just to
manufacture a block ref.
- **Search before create; read before write.** Check whether a page/section already exists, and read
existing content before changing it.
## Reference files — load the one you need
- `references/syntax.md` — full Roam-flavored markdown: inline formatting, links / refs / aliases /
embeds, headings, callouts, attributes, code, images, separators, class-tag styling, and the
complete WRONG-vs-RIGHT table. **Load when** writing anything beyond the gotchas above.
- `references/components.md` — the `{{...}}` component family (table, kanban, video, mermaid, embed
variants, mentions, calc, roam/render, roam/css, slider, …). **Load when** inserting a component.
- `references/queries.md` — `{{query}}` clauses/operators and page-ref inheritance, plus `:q` Datalog
(run via the `datalog_query` tool) and the datascript schema. **Load when** building a query.
- `references/reading-writing-via-mcp.md` — how the MCP render-on-read / literal-on-write model works,
the round-trip / decode behavior, and which tool to use for what. **Load when** editing content you
read back, or unsure which write tool to use.
## Compatibility
Assumes a Roam Research graph, typically via the roam-mcp MCP server / CLI (npm
`@roam-research/roam-mcp`). The syntax guidance also applies to hand-writing Roam markdown.