Writing Content
Goal
Syntax mastery. Zero guesswork. Natural writing flow. Structure opt-in. Prose preserved.
3.1 Free-Form First
Write like Obsidian/Notion. Markdown flows naturally. Structure embeds only where needed.
- Text before TOON = preserved.
- Text after TOON = preserved.
- Text between TOON = preserved.
- Schema validation = TOON rows + properties only. Free-form ignored. Never touched by
onde lint. - Parser attaches free-form to nearest
## HeadingasBodyblocks. Round-trip safe. - Pre-heading text (before first
##) = discarded. Start files with heading or attach prose to previous node.
Rule: Document = prose first. Database second. TOON = embedded island.
3.2 Two Node Modes
Pick mode by content shape. One graph output. Mix freely in same file.
| Mode | Shape | Token Cost | Use Case |
|---|---|---|---|
| Standard (Mode 1) | Fat. Properties + body + checklists | ~100-500/node | Unique items, deep rationale, complex edges |
| Dense/TOON (Mode 2) | Skinny. Pipe rows. Positional cols | ~15/node | Uniform lists, dashboards, agent context |
Standard Example:
## Fix auth bug
type:: task
id:: task-882
status:: BLOCKED
priority:: A
assigned:: [[sarah-chen]]
depends:: [[task-881]] {critical: true}
Token refresh fails silently. Need retry logic.
- [x] Audit service
- [ ] Implement backoff
Dense Example (flat):
## Sprint Backlog:: task[3]{id title status priority}
- task-881 | Fix auth middleware | DONE | A
- task-882 | Fix token refresh | BLOCKED | A
- task-883 | Update docs | TODO | B
Dense Example (indented visual tree):
## Sprint Backlog:: task[6]{id title status priority}
- task-881 | Fix auth middleware | DONE | A
- task-882 | Fix token refresh | BLOCKED | A {depends: [[task-881]]}
- task-890 | QA auth flow | BLOCKED | A {depends: [[task-882]]}
- task-895 | Migrate OpenAPI 3.1 | TODO | B {depends: [[task-881]]}
- task-870 | Update API rate limiter | BLOCKED | A
- task-883 | Update API docs | TODO | B
- task-885 | Write integration tests | TODO | B
Indent = visual grouping only. Same data, same graph. Human-readable hierarchy, not a tree structure.
3.3 TOON Syntax (Typed Object Outline Notation)
Header encodes type, count, columns. Rows map positionally. Dual purpose: view + definition.
## Title:: type[N]{col1 col2 col3}
| Token | Meaning | Required? |
|---|---|---|
type | Node type from schema | Yes |
[N] | Row count hint | No. Auto-fixed by lint if wrong. |
{cols} | Positional column map | Yes (dense) |
- val | val | Pipe-delimited row | Yes |
| Shared props | status:: TODO above rows | No. Applies to all rows in block. |
| Indent (2-space) | Visual grouping on pipe rows | No. Parser strips it. |
Rules:
- Column mismatch = error. Count mismatch = auto-fixed.
- Same ID under multiple parents = same node in graph. Deduplicated.
- No auto-edge from indent. Agent declares edges via
{depends: [[task-1]]}.
3.3.1 Indented Pipe Rows (Visual Tree)
Dense pipe rows can be indented with 2-space increments. This creates a visual hierarchy for human scanning — but the parser treats every row as flat. Indent depth is stripped and discarded during parsing. Each - line becomes an identical DenseRow in the AST regardless of nesting level.
Core Rules
| Rule | Detail |
|---|---|
| Indent = visual only | Parser strips leading whitespace. Each - row = flat DenseRow. Indent depth discarded. |
### by prop:: val mixing | Group headers and indented rows coexist freely. Both parsed independently — indent on a row doesn’t affect group scope. |
| Edge props build the graph | {k: v} on last column or [[link]] cells declare actual edges. Multi-target: {depends: [[a]] [[b]]}. |
| Deduplication | Same ID under multiple visual parents = one node in graph. Visual tree is display only. |
| No auto-edge from indent | Indentation never creates parent/child edges. Agent decides all relationships explicitly via {}. |
Example: Dependency Chain
## Sprint Backlog:: task[6]{id title status priority}
- task-881 | Fix auth middleware | DONE | A
- task-882 | Fix token refresh | BLOCKED | A {depends: [[task-881]]}
- task-890 | QA auth flow | BLOCKED | A {depends: [[task-882]]}
- task-895 | Migrate OpenAPI 3.1 | TODO | B {depends: [[task-881]]}
- task-870 | Update API rate limiter | BLOCKED | A
- task-883 | Update API docs | TODO | B
- task-885 | Write integration tests | TODO | B
The indented structure communicates “task-882 depends on task-881, task-890 depends on task-882” to a human reader. But to the parser, these are 6 flat rows. The {depends: ...} edge props create the actual graph edges. Without those {} braces, no edges exist — indent alone does nothing.
Example: Mixing with Group Headers
## Backlog:: task[5]{id title status priority}
by status:: BLOCKED
- task-882 | Fix token refresh | BLOCKED | A
- task-890 | QA auth flow | BLOCKED | A {depends: [[task-882]]}
by status:: TODO
- task-883 | Update API docs | TODO | B
- task-885 | Write tests | TODO | B {depends: [[task-883]]}
Group headers (by status::) are structural — they influence onde organize output and grouping queries. Indented pipe rows are cosmetic. Both coexist without conflict: the indent on task-890 is purely visual, while by status:: TODO is a structural grouping boundary.
When to Use Indent
Use indent when you want human-readable hierarchy without changing the graph structure. Common patterns:
| Pattern | Example | When |
|---|---|---|
| Dependency chains | Parent → child → grandchild | Task backlogs, build orders |
| Feature clusters | Epic → sub-tasks | Sprint planning, roadmaps |
| Status nesting | Blocked → blocking items | Status dashboards |
When NOT to Rely on Indent
Indent does not create edges. If you need actual graph relationships, always add explicit {k: v} edge properties. A deeply indented row without {depends: [[...]]} is visually nested but graph-orphaned.
Rule: skinny rows, fat files. Dense rows show scan-friendly columns. Indent for visual hierarchy. {} for graph edges. Full detail lives in Mode 1 files linked by ID.
3.4 Properties & Links
key:: value syntax. Strict spacing. One space after ::.
Property Types
Inferred or schema-enforced: string, int, float, bool, date, datetime, enum.
status:: BLOCKED # enum
deadline:: 2026-04-15 # date
confidence:: 0.85 # float
active:: true # bool
Links & Edges
[[target]] creates directed edge. Braces attach edge data.
assigned:: [[sarah-chen]]
depends:: [[task-881]] {critical: true, reason: "auth first"}
- Multi-target:
depends:: [[task-1]] [[task-2]]. Space-separated. Requires@manyin schema. - No comma inside
[[ ]]. Comma splits{key: val}pairs. - Edge props typed.
{critical: "yes"}fails if schema expects bool. Use{critical: true}.
Four Link Modes
| Mode | Syntax | Lookup | Use |
|---|---|---|---|
| ID | [[task-882]] | O(1) exact | Agent default. Stable. |
| Title | [[sarah-chen]] | Fuzzy match | Human-friendly. |
| Path | [[/tasks/auth.md]] | File path | Unambiguous. |
| Relative | [[./tests.md]] | Relative path | Compact clusters. |
Resolution priority: path > relative > ID > title. Convert anytime: onde relink --to id.
3.5 Grouping & Sorting
Structure output without breaking graph. Pure markdown.
Group Headers
by property:: value. No - prefix. Separates rows.
## Active Tasks:: task[4]{id title priority}
by status:: BLOCKED
- task-882 | Fix token refresh | A
by status:: DOING
- task-881 | Fix auth middleware | A
Nested Groups
Indent 2 spaces per level. Inner inherits outer context.
by status:: TODO
by priority:: A
- task-881 | Fix auth | [[sarah]]
by priority:: B
- task-883 | Update docs | [[tom]]
Sort Directive
@sort: field asc, field desc. Instruction to parser/lint. Nodes listed in declared order.
## Backlog:: task[5]{id title priority}
@sort: priority asc, deadline asc
Schema defaults (@sort, @group) apply when no CLI flags given. Override anytime: onde find task --sort deadline.
3.6 Complex Nodes → Split to Mode 1
Dense row need body text? Checklists? Multi-line rationale? Split it.
- Keep dense row for scan. Link to full file by ID.
- Rule: skinny rows, fat files.
- Dense shows columns.
{}declares edges. Full detail lives intasks/fix-token-refresh.md. - Automate:
onde organize --split tasks/overview.md→ extracts rows to individual files.
3.7 Syntax Traps (Avoid These)
| Trap | Wrong | Right | Why |
|---|---|---|---|
Space before :: | status : TODO | status:: TODO | Parser expects exact :: |
| Empty value | priority:: | priority:: A | Missing value = syntax error |
| Comma in links | [[task-1, task-2]] | [[task-1]] [[task-2]] | Parser breaks at comma |
| Wrong edge type | {critical: "yes"} | {critical: true} | Schema enforces bool |
| Column mismatch | Header 3 cols, row 4 | Match exactly | Error. Count mismatch auto-fixed. |
| Pre-heading text | Prose before first ## | Start with ## | Discarded by parser |
3.8 How Free-Form Text Survives
- Parser classifies lines. Non-structural →
Bodyaccumulator. - Attached to most recent
## Heading. onde lintnever rewrites body/free-form text. Only touches properties, links, TOON rows, counts, IDs.- Schema validation skips
Block::Bodyentirely. - Round-trip guarantee: write prose → lint → prose identical. Zero mutation.
Next Step: → The Lint Loop: Your Sync, Heal & Validate Command