BFM v3 Syntax Guide

Boxel Flavored Markdown -- complete reference, building from Markdown through every extension layer

See also JQXL Syntax Reference -- the expression language for computed fields, formulas, and inline transforms used throughout BFM.

Level 1 Original Markdown

Created by John Gruber & Aaron Swartz (2004). Works everywhere -- GitHub, Obsidian, Boxel, email, static sites.

Headings

You writeYou get
# Heading 1
Heading 1
## Heading 2
Heading 2
### Heading 3
Heading 3
#### Heading 4   ##### H5   ###### H6
Heading 4   H5   H6
A space after # is required. Max 6 levels. Setext style (=== / --- underlines) also works for H1/H2.

Inline Formatting

You writeYou get
*italic* or _italic_
italic
**bold** or __bold__
bold
***bold italic***
bold italic
`inline code`
inline code

Links & Images

You writeYou get
[Click here](https://example.com)
[Link](url "Tooltip text")
Link (with hover tooltip)
![Alt text](photo.png)
Embedded image
<https://example.com>

Block Quotes

You writeYou get
> Quote text > > Second paragraph
Quote text

Second paragraph
> Outer quote > > Nested quote
Outer
Nested

Lists

You writeYou get
- Item one - Item two - Nested item - Item three
• Item one
• Item two
   • Nested item
• Item three
1. First 2. Second 3. Third
1. First
2. Second
3. Third
-, *, or + for unordered. First number in ordered list sets the start value.

Code Blocks

You write
```javascript
function greet(name) {
  return `Hello, ${name}!`;
}
```
You get
function greet(name) {
  return `Hello, ${name}!`;
}
Language after opening fence enables syntax highlighting. Triple tildes ~~~ also work.

Horizontal Rules, Escapes, HTML

You writeYou get
---

\*not italic\*
*not italic*
<kbd>Ctrl</kbd> + <kbd>C</kbd>
Ctrl + C

Level 2 CommonMark

Formal specification (2014). Everything in Level 1 still works. CommonMark added precise rules for edge cases.

Key Clarifications

RuleWhat it means
Lazy continuationLines after > can omit the > prefix
List item alignmentContent under list items must align with text after the marker
Tight vs loose listsBlank line between items wraps in <p>, otherwise no wrapping
Block > inlineBlock structures take precedence over inline when ambiguous
Entity references&copy; &#169; &#xA9; all render as ©
Info stringsText after fence opening: first word = language, rest = implementation-defined

Level 3 GitHub Flavored Markdown

GFM (v0.29, 2019) -- strict superset of CommonMark. Adds tables, tasks, diagrams, math, alerts, and more.

Tables

You write
| Feature   | Status  | Priority |
|:----------|:-------:|---------:|
| Auth      | Done    |     High |
| Dashboard | WIP     |   Medium |
You get
FeatureStatusPriority
AuthDoneHigh
DashboardWIPMedium
DelimiterAlignment
:---Left (default)
:---:Center
---:Right
Escape pipes in cells with \|. Inline formatting works inside cells.

Task Lists

You writeYou get
- [ ] Write docs - [x] Ship feature - [ ] Deploy
Write docs
Ship feature
Deploy

Strikethrough

You writeYou get
~~deleted text~~
deleted text
Double tildes only. Single tilde is NOT strikethrough in GFM.

Extended Autolinks

You writeYou get
www.example.com https://example.com user@example.com

Footnotes

You writeYou get
A claim[^1]. Inline^[note here]. [^1]: Source for the claim.
A claim1. Inline2.

Math (LaTeX)

You writeYou get
$E = mc^2$
E = mc² (inline)
$$\sum_{i=1}^{n} x_i$$
Σi=1n xi  (block, centered)

Alerts / Admonitions

You write
> [!NOTE]
> Useful background information.

> [!WARNING]
> Something could go wrong.

> [!TIP]
> A helpful shortcut.
You get
Note
Useful background information.
Warning
Something could go wrong.
Tip
A helpful shortcut.
TypeColorUse for
NOTEBlueSupplementary info
TIPGreenHelpful shortcuts
IMPORTANTPurpleRequired knowledge
WARNINGYellowPotential risks
CAUTIONRedNegative consequences

Mermaid Diagrams

You write
```mermaid
graph LR
    A[Start] --> B{Decision}
    B -->|Yes| C[Ship it]
    B -->|No| D[Fix it]
```
Renders as
Interactive diagram (flowchart, sequence, class, gantt, pie, mindmap, timeline, ER, git graph, sankey, quadrant, XY chart...)

Other Fenced Renderers

LanguageRenders
```geojsonInteractive map (points, polygons, lines)
```topojsonTopology-compressed map
```stl3D model viewer (WebGL)

HTML Pass-throughs

SyntaxResult
<sup>2</sup>Superscript: x2
<sub>i</sub>Subscript: xi
<details><summary>...</details>Collapsible section
<kbd>Ctrl</kbd>Keyboard key styling
<div align="center">...</div>Centered content
GFM blocks these tags for security: <script> <style> <iframe> <textarea> <title> <xmp> <noembed> <noframes> <plaintext>

Level 4 Obsidian Flavored Markdown

Obsidian extends GFM for personal knowledge management. BFM supports all of these features.

Wiki Links

You writeYou get
[[Page Name]]
Page Name (resolved by name in vault/realm)
[[Page Name|Display Text]]
[[Page#Heading]]
Page > Heading (deep link)
[[Page#^block-id]]
Page > block (block reference)
[[#Heading in same file]]

Wiki Embeds

You writeYou get
![[Other Note]]
Full note content embedded inline
![[Note#Section]]
Just that section embedded
![[image.png]] ![[image.png|300]] ![[image.png|300x200]]
Image (natural size)
Image (300px wide)
Image (exact dimensions)
![[doc.pdf]] ![[doc.pdf#page=3]]
PDF viewer
PDF at page 3
![[song.mp3]] ![[video.mp4]]
Audio player
Video player

Highlights

You writeYou get
==highlighted text==
highlighted text

Comments

You writeYou get
Visible %%hidden in display%% text
Visible text  (comment stripped)
%% Multi-line block comment. Hidden entirely. %%
(nothing rendered)
%% content is preserved in source, hidden in display. Good for author notes. For collaborative review comments, see Level 6.

Tags

You writeYou get
#project-alpha #status/active
#project-alpha #status/active
Valid chars: letters, numbers (not first), -, _, / (for nesting). Indexed and queryable.

Block References

You writeYou get
Key finding here. ^finding-1 See [[#^finding-1]]
Key finding here.

See finding-1 (links to the block)

Callouts (Extended)

You write
> [!question] Can callouts fold?
> Yes they can!

> [!tip]- Collapsed by default
> Click to reveal.

> [!warning]+ Open but collapsible
> Can be folded shut.
You get
Question -- Can callouts fold?
Yes they can!
Tip [collapsed -- click to expand]
Warning [v collapsible]
Can be folded shut.
TypeAliasesColor
noteBlue
tiphintCyan
importantPurple
warningcaution, attentionOrange
dangererrorRed
successcheck, doneGreen
failurefail, missingRed
questionhelp, faqYellow
examplePurple
quoteciteGray
abstractsummary, tldrTeal
bugRed
todoBlue
infoBlue
Fold: no suffix = always open. + = open, collapsible. - = collapsed. Callouts nest with extra > prefixes.

Fenced Computed Blocks

Obsidian plugins use fenced code blocks where the body is options, not content. The data comes from the document itself. BFM supports these for compatibility.

Table of contents (Obsidian plugin compatible)
```toc
style: nestedList
maxLevel: 3
```

```table-of-contents
style: nestedList
maxLevel: 3
```
Renders
Both ```toc and ```table-of-contents are accepted (Obsidian uses both). Body is YAML options, data comes from the document's headings. Pasted Obsidian vault docs work unmodified.

Tasks Emoji Format

The Obsidian Tasks plugin (3.3M downloads) extends markdown tasks with emoji-based metadata. BFM preserves these.

You writeYou get
- [ ] Ship v1 📅 2026-04-15 ⏫
Ship v1 Apr 15 · High
- [x] Write spec ✅ 2026-04-01
Write spec Done Apr 1
EmojiMeaningExample
📅Due date📅 2026-04-15
⏫High priority⏫
🔺Highest priority🔺
🔽Low priority🔽
🔁Recurring🔁 every week
⏳Scheduled date⏳ 2026-04-10
🛫Start date🛫 2026-04-05
➕Created date➕ 2026-04-01
✅Done date✅ 2026-04-03
❌Cancelled date❌ 2026-04-03
BFM passes emoji metadata through to the ```tasks computed block and the TasksField. When pasting from Obsidian, task metadata is preserved.

YAML Frontmatter

You write (top of file)
---
title: My Document
tags:
  - project
  - active
aliases:
  - Alt Name
cssclasses:
  - wide-layout
date: 2026-04-03
---
Result
Metadata indexed, searchable. tags merge with inline #tags. aliases work for wiki link resolution.

Inline Fields (Dataview) COMPAT

Obsidian compatibility, best-effort. BFM accepts Dataview inline field syntax for documents migrated from Obsidian. For new documents, prefer :field[id] with %%%field definitions -- they are typed, editable, and support JQXL formulas.
You writeYou get
Assigned to [assignee:: Alice] Due:: 2026-04-15
Assigned to Alice
Due: 2026-04-15
(queryable metadata)

Dataview Queries COMPAT

Obsidian compatibility, best-effort. BFM accepts ```dataview DQL syntax and translates it to Boxel card queries where possible. The Boxel data model differs from Obsidian's file-based vault, so not all DQL features map cleanly. For new documents, prefer %%%table with JQXL or :::list / :::grid with Boxel's native query API -- they are more powerful and tap into typed card fields, relationships, and realm-wide search.
You write (Obsidian DQL -- compatibility)
```dataview
TABLE author, date, status
FROM #project
WHERE status != "done"
SORT date DESC
LIMIT 10
```
Renders as
Best-effort query table. DQL translated to Boxel card queries at runtime.
DQL TypeRenders viaFieldDef to buildNotes
TABLEDataviewTableFieldWraps %%%table + JQXL query. Columns from DQL select, FROM mapped to card type filter, WHERE/SORT/LIMIT translated.Most DQL maps cleanly
LISTDataviewListFieldWraps :::list[query]. Each result rendered as ::card[... | strip]. FROM/WHERE translated to card query filters.Falls back to title-only strips
TASKDataviewTaskFieldQueries cards with a boolean completed field. Renders as checkbox list with card title. Needs a convention for which field = "done".Requires task card convention
CALENDARDataviewCalendarFieldQueries cards with a date field, renders month grid heatmap. Each cell links to cards on that date.Most complex, lowest priority
Implementation priority: TABLE > LIST > TASK > CALENDAR. Each is a standalone FieldDef that accepts DQL text as input, translates to a Boxel query, and renders results using the card/field system. All four are thin wrappers around the preferred Boxel-native query features (%%%table, :::list, :::grid).

Level 5 Boxel Extensions

Cards, fields, formulas, layouts, queries. Everything renders through the unified card/field system. v2 syntax: field/card/table fence languages, YAML body, type:/from:, kebab-case JQXL.

Extended Frontmatter

BFM extends the standard Obsidian YAML frontmatter with Boxel card properties. Only include what you need -- most are optional.

Boxel properties in YAML frontmatter
---
title: Q2 Market Report

adoptsFrom:                         # Card type (optional -- defaults to base document card)
  module: ./story
  name: Story

theme: ./Theme/literary-review      # Theme card reference

thumbnailURL: ./assets/hero.png     # Card preview image (overrides cardInfo.thumbnailURL)

fields:                             # Field schema (compact shorthand for %%%field blocks)
  status:
    type: StatusField
    from: ./story-fields
    default: draft
  total:
    type: NumberField
    from: ./fields
    expression: ROUND(.subtotal * 1.0825; 2)

renderers:                          # Map fenced languages to custom FieldDefs
  timeline:
    type: TimelineField
    from: ./timeline-widget
---
PropertySourcePurpose
titleObsidianDocument title (falls back to first heading)
tagsObsidianSearchable labels, merged with inline #tags
aliasesObsidianAlternative names for wiki link resolution
cssclassesObsidianCSS classes on the document container
adoptsFromBoxelCard type (module + name). Omit for default document card
themeBoxelTheme card path (sets cardInfo.theme)
thumbnailURLBoxelCard preview image (sets cardInfo.thumbnailURL)
fieldsBoxelField schema map (shorthand for %%%field blocks)
renderersBoxelMap fenced languages to custom FieldDef/CardDef
What's NOT in frontmatter:
icon -- comes from the card type (adoptsFrom), not the instance.
linkedCards -- computed from ::card[...] references in the body, not declared.
visibility -- managed at the realm level, not per-document.

Renderer Overrides

Frontmatter can redefine any fenced code block language to render as a custom FieldDef. This is how the compatibility-driven defaults (```mermaid, ```csv, etc.) become extensible -- any fence language can map to any card or field type.

Default behavior (no override needed)
```mermaid           → built-in MermaidField
```csv               → built-in CsvTableField
```math              → built-in MathField
```slides            → built-in SlideField
Custom overrides in frontmatter
---
renderers:
  mermaid:
    type: CustomDiagramField
    from: ./custom-diagram
  csv:
    type: SpreadsheetField
    from: ./spreadsheet
  timeline:
    type: TimelineField
    from: ./timeline-widget
  kanban:
    type: KanbanField
    from: ./kanban-board
  chart:
    type: ChartField
    from: ./chart-widget
  form:
    type: FormField
    from: ./form-builder
---
```timeline
- 2026-01: Project kickoff
- 2026-03: Alpha release
- 2026-06: Public launch
```

```kanban
## Backlog
- Design review
- API docs
## In Progress
- Parser implementation
## Done
- Spec draft
```

```chart
type: bar
data:
  labels: [Q1, Q2, Q3, Q4]
  values: [12, 19, 8, 15]
```

```form
fields:
  - name: email
    type: email
    required: true
  - name: message
    type: textarea
```
How it works: When the parser encounters a fenced block like ```timeline, it checks renderers.timeline in frontmatter. If found, it instantiates that FieldDef with the fence body as input. If not found, it falls back to built-in renderers, then to plain syntax-highlighted code.

Any fence language is valid. Invent ```kanban, ```chart, ```form, ```quiz -- as long as the frontmatter maps it to a FieldDef, it renders. This is the plugin system: no plugin API, just a FieldDef class and a frontmatter mapping.
FenceWithout overrideWith override
```mermaidBuilt-in MermaidFieldYour custom DiagramField
```csvBuilt-in CsvTableFieldYour custom SpreadsheetField
```timelineSyntax-highlighted codeYour TimelineField
```kanbanSyntax-highlighted codeYour KanbanField
```anythingSyntax-highlighted codeWhatever FieldDef you map it to

Card Embedding

Three card formats, matching Boxel's rendering system:

FormatSyntaxHeightUse
Atom:card (single colon)InlineChip in running text
Embedded::card (double colon)AutoStandard card block, natural height
Fitted::card[... | size]FixedCard constrained to specific dimensions
You writeYou get
:card[./Team/jane-doe]
J Jane Doe  atom -- inline chip, flows in text
::card[./Cards/stock-ticker]
NVDA  $892.50  +2.4%
NVIDIA Corp • NASDAQ • Updated 2m ago
Vol 48.2M • Market Cap $2.2T • P/E 65.2 • Day range $878-$895
embedded -- auto-height, shows as much as the card has

Fitted Sizes (Fixed Dimensions)

Fitted format constrains a card to a fixed size. The card adapts its content to fit using the fit-grid and line-budget modifiers. Use | after the path with a size constant.

Badges (150px wide)

You writeDimensions
::card[./room | small-badge]
Oceanview Suite
150 × 40
::card[./room | medium-badge]
Oceanview Suite
The Alabaster
150 × 65
::card[./room | large-badge]
Oceanview Suite
The Alabaster
Santorini, Greece
150 × 105

Strips (250px wide)

You writeDimensions
::card[./room | strip]
Oceanview Suite
250 × 40
::card[./room | double-strip]
Oceanview Suite
The Alabaster • Santorini
250 × 65
::card[./room | triple-strip]
Oceanview Suite
The Alabaster
Santorini, Greece
250 × 105

Wide Strips (400px wide)

You writeDimensions
::card[./room | double-wide-strip]
Oceanview Suite
The Alabaster • Santorini, Greece
400 × 65
::card[./room | triple-wide-strip]
Oceanview Suite
The Alabaster
Santorini, Greece
400 × 105

Tiles (square-ish)

You writeDimensions
::card[./room | small-tile]
Oceanview Suite
Santorini, Greece
★★★★★
$1,250/night
150 × 170
::card[./room | tile]
Oceanview Suite
The Alabaster • Santorini, Greece
★★★★★
$1,250/night
250 × 170 (default tile)
::card[./room | grid-tile]
Oceanview Suite
Santorini, Greece
Mar 15 → Mar 20
★★★★★
$1,250/night
170 × 250 (portrait)
::card[./room | tall-tile]
Oceanview Suite
Santorini
Mar 15 → Mar 20
★★★★★
$1,250/night
150 × 275
::card[./room | large-tile]
Oceanview Suite
The Alabaster • Santorini, Greece
Mar 15 → Mar 20 • 2 guests • 850 sqft • King
★★★★★
$1,250/night
250 × 275

Cards (400px wide)

You writeDimensions
::card[./room | compact-card]
Oceanview Suite
The Alabaster • Santorini, Greece
★★★★★
$1,250/night
400 × 170
::card[./room | full-card]
Oceanview Suite
The Alabaster • Santorini, Greece
Mar 15 → Mar 20 • 2 guests • 850 sqft • King
★★★★★
$1,250/night
400 × 275
::card[./room | expanded-card]
Oceanview Suite
The Alabaster • Santorini, Greece
Mar 15 → Mar 20 • 2 guests • 850 sqft • King
★★★★★
$1,250/night
Perched on the caldera edge with panoramic views of the Aegean Sea. Private plunge pool, outdoor dining terrace, and Bose sound system throughout.
400 × 445

Complete Size Reference

CategoryConstantW × H
BADGES (150px wide)
small-badge150 × 40
medium-badge150 × 65
large-badge150 × 105
STRIPS (250px wide)
strip250 × 40
double-strip250 × 65
triple-strip250 × 105
WIDE STRIPS (400px wide)
double-wide-strip400 × 65
triple-wide-strip400 × 105
TILES
small-tile150 × 170
tile250 × 170
grid-tile170 × 250
tall-tile150 × 275
large-tile250 × 275
CARDS (400px wide)
compact-card400 × 170
full-card400 × 275
expanded-card400 × 445

Custom Dimensions

You writeYou get
::card[./Charts/revenue | 400x200]
Card fitted to exactly 400 × 200px
::card[./Charts/revenue | w:400 h:200]
Same, explicit key syntax
::card[./Charts/revenue | h:300]
Height only -- width fills container
::card[./Charts/revenue | w:50%]
Percentage width, auto height
Default (no pipe): embedded format -- auto-height, card shows everything it has.
With size constant: fitted format -- card adapts content to the constraint via fit-grid and line-budget modifiers.
Custom dimensions: WxH, w:N, h:N, or w:N% for any size not covered by the constants.

Format Summary

SyntaxFormatSizing
:card[path]AtomInline (flows in text)
::card[path]EmbeddedAuto-height
::card[path | strip]Fitted250 × 40 (or any constant)
::card[path | 400x200]FittedCustom exact
::card[path | isolated]IsolatedFull standalone page

Card Query (Agent-Friendly)

You writeYou get
::card[StockTicker where symbol="NVDA"]
Card found by type + query -- agents don't need file paths
::card[TeamMember where name="Alice Chen"]
Alice Chen
Lead Engineer • Active
![[Stock Ticker]]
Wiki-style embed = same as ::card
::card[#local-id]
Card defined in this document's fences

Containers (Triple Colon :::)

Triple colon ::: wraps content blocks. Single = inline, double = block, triple = container. Two container types: :::list (vertical stack) and :::grid (columns).

:::list -- vertical stack of cards
:::list
::card[./Cards/aero-stock | strip]
::card[./Cards/nvda-stock | strip]
::card[./Cards/tsla-stock | strip]
:::
You get
AERO  $342.10  +5.1%
NVDA  $892.50  +2.4%
TSLA  $178.20  -1.8%
:::grid -- columns of cards
:::grid[cols:3]
::card[./Cards/aero-stock]
::card[./Cards/nvda-stock]
::card[./Cards/tsla-stock]
:::
You get
AERO +5.1%
AeroDef
NVDA +2.4%
NVIDIA
TSLA -1.8%
Tesla
:::grid with options
:::grid[cols:2 gap:1rem]
::card[./Cards/team-alice]
::card[./Cards/team-bob]
::card[./Cards/team-carol]
::card[./Cards/team-dan]
:::

Query-Powered Containers

Containers can be query-backed -- they fetch cards from the realm dynamically instead of listing paths manually. This is the preferred way to build dashboards, directories, and live reports. Much more powerful than Obsidian Dataview.

:::list with query -- all active projects, sorted by due date
:::list[query]
type: Project
filter:
  eq: { status: active }
sort:
  by: due-date
  direction: asc
limit: 10
:::
:::grid with query -- team members by department
:::grid[cols:3 query]
type: TeamMember
filter:
  eq: { department: Engineering }
sort:
  by: name
:::
:::list with JQXL transform -- filter and reshape
:::list[query]
type: StockTicker
expression: |
  . | select(.change > 0)
    | sort_by(-.change)
size: strip
:::
Why this is better than Dataview:
• Queries run against typed Boxel cards, not flat files -- fields have types, relationships, and schemas.
• Results render as real card components at any fitted size, not plain text tables.
• JQXL transforms can filter, reshape, and compute on query results (jq pipes, Excel functions).
• :::list and :::grid are layout-aware -- control columns, gaps, card sizes.
• Queries support the full Boxel query API -- type filters, field equality, ranges, sorting, pagination.
ApproachSyntaxPowerPriority
:::list / :::grid + queryBoxel nativeFull query API, typed cards, any fitted size, JQXL transformsPreferred
%%%table + JQXLBoxel nativeTabular data from field context, formulasPreferred
```dataview DQLObsidian compatBest-effort translation to Boxel queriesCompat
[key:: value]Obsidian compatBest-effort inline metadata extractionCompat
::: opens a container. ::: on its own line closes it. Content inside follows normal BFM rules. Containers can nest.
ColonsLevelPurpose
:InlineAtom -- flows in text (:card, :field)
::BlockEmbedded -- own line, auto-height (::card, ::field, ::table)
:::ContainerWrapper -- :::list, :::grid
SyntaxLevelRenders
:card[path]Inline (atom)Compact card chip in text
::card[path]Block (embedded)Full card, auto-height
::card[path | strip]Block (fitted)Card at named size
::card[Type where x=y]BlockCard by query (agent-friendly)
::card[#local-id]BlockDocument-local card
![[Card Name]]BlockWiki-style card embed
:::list...:::ContainerVertical stack
:::grid[cols:N]...:::ContainerColumn grid

Field Reference Resolution

BFM field references are explicit. Every selector MUST begin with # or .. Bare selectors are invalid. No implicit fallback between namespaces.

PrefixNamespaceResolves from
#idMarkdown-local%%%field declaration in this document
.pathContaining cardCard root, jq-style path traversal
You writeYou get
:field[.status]
On Track  (containing card's status field)
:field[.status@edit]
On TrackAt RiskOff Track
:field[.client.name]
Acme Corp  (card → client → name)
:field[.client.address.city]
San Francisco  (deep traversal)
:field[.items[0].price]
$5,000  (array access)
::field[#progress]
42%  (markdown-local field)
::field[#progress@edit]
42%
SyntaxLevelMode
:field[.path]InlineDisplay (containing card)
:field[.path@edit]InlineEdit (containing card)
:field[#id]InlineDisplay (markdown-local)
:field[#id@edit]InlineEdit (markdown-local)
::field[.path]BlockDisplay (containing card)
::field[#id]BlockDisplay (markdown-local)
Why explicit prefixes? The markdown source may live deep inside the card's object graph, but .path always resolves from the containing card root. #id always resolves from the current document. No ambiguity, no fallback, no guessing.

Markdown-Local Fields (%%%field)

%%% fences declare markdown-local field instances. Hidden in display mode. Referenced by #id.

Visibility hierarchy
```mermaid              <-- visible, renders as diagram
graph TD; A --> B
```

%%author note%%         <-- hidden, comment (Obsidian)

%%%field approval-note  <-- hidden, structural metadata
adoptsFrom:
  module: ./fields
  name: StringField
value: Pending review
%%%
Local fields -- can go anywhere in the document
## Project Status

Status: :field[.status@edit]          <-- from containing card

Progress: ::field[#progress@edit]     <-- from local field

%%%field progress
adoptsFrom:
  module: ./story-fields
  name: ProgressField
value: 42
%%%

Note: :field[#approval-note@edit]     <-- from local field

%%%field approval-note
adoptsFrom:
  module: ./fields
  name: StringField
value: Pending review
%%%
Syntax: %%%field <id> then YAML body then %%%. The word after field is the ID. Body uses adoptsFrom: (module + name) and value:.
DelimiterDisplayWhat it's for
```langVisible -- renders in-placeCode, mermaid, csv, slides, geojson, math
%%Hidden -- strippedAuthor comments, private notes
%%%type idHidden -- structuralField, card, and table definitions

Formula Fields

A formula field uses expression: instead of value:. The expression is JQXL where . is the containing card root.

You write
%%%field total
adoptsFrom:
  module: ./fields
  name: NumberField
expression: ROUND(.subtotal * (1 + .taxRate); 2)
%%%
Then in the body
Total: :field[#total]
Renders
Total: $1,353.13  (auto-computed from containing card's .subtotal and .taxRate)

Cross-referencing local fields with local("id")

Local field referencing another local field
%%%field subtotal-with-tax
adoptsFrom:
  module: ./fields
  name: NumberField
expression: ROUND(.subtotal * (1 + .taxRate); 2)
%%%

%%%field grand-total
adoptsFrom:
  module: ./fields
  name: NumberField
expression: local("subtotal-with-tax") + .shipping
%%%

Grand total: :field[#grand-total]
Expression context:
. = containing card root (always)
local("id") = another %%%field in this document (explicit cross-reference)
No implicit scope fallback. If a selector doesn't resolve, it's unresolved -- BFM never silently falls back from #id to .id or vice versa.
JQXL includes 100+ Excel-style formula functions as extensions to jq -- ROUND, SUM, IF, VLOOKUP, TEXT, DOLLAR, CONCATENATE, SUMIF, DATE, and more. Math, statistics, conditional aggregation, lookups, text, financial, and engineering functions are all supported. See the JQXL Syntax Reference for the complete list.

Mail Merge & Template Variables

BFM documents double as templates. Variables pull values from linked cards, external data, or the document's own fields -- enabling contracts, invoices, email campaigns, and personalized content.

BFM uses :field[id] for all variable interpolation -- local fields, linked card data, and computed values. Dot-path access reaches into linked cards. JQXL pipes transform values inline.

You writeYou get
:field[.status]
On Track  Local field (same as before)
:field[.client.name]
Acme Corp  (from linked card client, field name)
:field[.client.address.city]
San Francisco  (deep path into linked card)
:field[.sender.name]
Jane Doe  (from another linked card)
Rule: bare id = local field in this document. ref.path = linked card's field via the card's own linksTo relationships. No extra frontmatter needed -- the containing card IS the merge source.

Inline Transforms (JQXL Pipes)

Transforms use the same jq pipe syntax as formula fields and table queries. One expression language everywhere -- no Liquid, no Handlebars, just jq.

You writeYou get
:field[.client.name | ascii_upcase]
ACME CORP
:field[.total | DOLLAR(.; 2)]
$1,353.13
:field[.effective-date | strftime("%B %d, %Y")]
April 18, 2026
:field[.client.name | .[:20]]
Acme Corporation In...
:field[.items | length]
3
:field[.client.name // "TBD"]
TBD  (when client not yet linked -- // is jq's alternative operator)
:field[.tags | join(", ")]
AI, markets, climate
Transformjq syntaxExampleResult
Uppercaseascii_upcase:field[.name | ascii_upcase]ACME CORP
Lowercaseascii_downcase:field[.name | ascii_downcase]acme corp
String lengthlength:field[.name | length]9
Array lengthlength:field[.items | length]3
Slice.[:N]:field[.desc | .[:50]]First 50 chars
Joinjoin("sep"):field[.tags | join(", ")]Comma-separated
Default// "fallback":field[.x // "N/A"]Value or N/A
RoundROUND(.; N):field[.rate | ROUND(.; 2)]3.14
CurrencyDOLLAR(.; N):field[.total | DOLLAR(.; 2)]$1,234.56
Percent. * 100 | ROUND(.; N):field[.rate | . * 100 | ROUND(.; 1) | tostring + "%"]42.0%
Date formatstrftime("fmt"):field[.due | strftime("%b %d")]Apr 18
Conditionalif . > 0 then "up" else "down" end:field[.change | if . > 0 then "+" else "" end]+
Chain| f1 | f2:field[.name | ascii_upcase | .[:10]]ACME CORP
One expression language everywhere: formula fields, table queries, conditionals, mail merge transforms -- all JQXL (jq syntax). If you know expression: .revenue - .expenses in a %%%field block, you already know :field[.revenue | DOLLAR(.; 0)] in the body.

Excel functions: 100+ Excel-style formula functions are available as JQXL extensions -- ROUND, DOLLAR, VLOOKUP, SUMIF, TEXT, DATE, IF, CONCATENATE, and more. See the JQXL Syntax Reference for the complete index.

Repeating Content (Expression-Driven)

Instead of loop syntax, JQXL expressions produce the repeated content directly. A %%%field whose expression returns a formatted string (or markdown) handles iteration inside the expression -- the document just places the result.

Line items as a computed field
%%%field line-item-list
adoptsFrom: { module: ./fields, name: StringField }
expression: |
  .line-items
  | map("- " + .description + " -- " + DOLLAR(.amount; 2))
  | join("\n")
%%%

## Line Items

::field[#line-item-list]

**Total: :field[.total | DOLLAR(.; 2)]**
Renders
Line Items

• Story Engine License -- $5,000.00
• Field Registry Setup -- $2,500.00
• Custom Card Development -- $7,500.00

Total: $15,000.00
The expression does the iteration with map(...) and joins the results into a markdown string. The document just places ::field[#line-item-list]. No loop syntax, no rebinding of . -- it's all jq.
Invoice summary table as a computed field
%%%field invoice-rows
adoptsFrom: { module: ./fields, name: StringField }
expression: |
  .invoices
  | map("| " + .id + " | " + .client.name + " | " + DOLLAR(.amount; 2)
        + " | " + IF(.amount > 1000; "Large"; "Small") + " |")
  | join("\n")
%%%

| ID | Client | Amount | Size |
|----|--------|-------:|------|
::field[#invoice-rows]

Total invoices: :field[.invoices | length]
Sum: :field[.invoices | map(.amount) | add | DOLLAR(.; 2)]
Team cards via :::list with query
:::list[query]
type: TeamMember
from: .team
size: strip
:::
Renders
One strip-height card per team member. No loop -- the query container handles iteration.
Three patterns for repeated content:
• Tables -- %%%table with a JQXL expression: that maps rows. Best for structured tabular data.
• Prose lists -- %%%field whose expression maps items to markdown strings and joins them. Best for bullet lists, paragraphs, or mixed content.
• Card lists -- :::list[query] or :::grid[query]. Best for rendering actual card components.

Full Template Example: Quote Card

A complete quote with computed subtotal, tax, discount logic, and a line-item table -- showing how JQXL expressions handle all computation, conditionals, and repetition. No flow control in the document body.

Field definitions -- chained computed fields
%%%field subtotal
adoptsFrom: { module: ./fields, name: NumberField }
expression: |
  .line-items | map(.qty * .unit-price) | add
%%%

%%%field discount-amount
adoptsFrom: { module: ./fields, name: NumberField }
expression: |
  (.line-items | map(.qty * .unit-price) | add) as $sub
  | if $sub > 10000 then ROUND($sub * 0.10; 2)
    elif $sub > 5000 then ROUND($sub * 0.05; 2)
    else 0 end
%%%

%%%field tax
adoptsFrom: { module: ./fields, name: NumberField }
expression: ROUND((.subtotal - .discount-amount) * .tax-rate; 2)
%%%

%%%field total
adoptsFrom: { module: ./fields, name: NumberField }
expression: .subtotal - .discount-amount + .tax
%%%
Conditional fields -- expressions return content or null
%%%field discount-row
adoptsFrom: { module: ./fields, name: StringField }
expression: |
  if .discount-amount > 0 then
    "| Discount " + IF(.subtotal > 10000; "(10%)"; "(5%)")
    + " | -" + DOLLAR(.discount-amount; 2) + " |"
  else null end
%%%

%%%field notes-section
adoptsFrom: { module: ./fields, name: StringField }
expression: |
  if (.notes | length) > 0 then
    "### Notes\n\n"
    + (.notes | map("- " + .) | join("\n"))
  else null end
%%%

%%%field approval-notice
adoptsFrom: { module: ./fields, name: StringField }
expression: |
  if .total > 25000 then
    "> [!warning] This quote exceeds $25,000 and requires director sign-off."
  else null end
%%%
Table query -- line items with per-row computations
%%%table line-items
columns: "#, Description, Qty, Unit Price, Line Total"
from: .line-items
expression: |
  . | to_entries
  | map({
      "#": (.key + 1),
      Description: .value.description,
      Qty: .value.qty,
      "Unit Price": DOLLAR(.value.unit-price; 2),
      "Line Total": DOLLAR(.value.qty * .value.unit-price; 2)
    })
%%%
Document body -- no flow control, just field placement
# Quote :field[.quote-number]

**Prepared for:** :field[.client.name]
**Date:** :field[.created | strftime("%B %d, %Y")]
**Valid until:** :field[.expires | strftime("%B %d, %Y")]

---
::table[line-items]

---
| | |
|:--|--:|
| Subtotal | :field[.subtotal | DOLLAR(.; 2)] |
::field[#discount-row]
| Tax (:field[.tax-rate * 100 | ROUND(.; 1)]%) | :field[.tax | DOLLAR(.; 2)] |
| **Total** | **:field[.total | DOLLAR(.; 2)]** |

::field[#notes-section]

::field[#approval-notice]
Renders (with sample data)
Quote Q-2026-0042 DRAFT
Prepared for  Acme Corp
Date  April 04, 2026
Contact  Dana Reeves
Valid until  May 04, 2026
#DescriptionQtyUnit PriceLine Total
1Platform License (Enterprise)1$18,000.00$18,000.00
2Implementation & Integration60$200.00$12,000.00
3Training (full-day, on-site)3$2,500.00$7,500.00
Subtotal$37,500.00
Discount (10%)-$3,750.00
Tax (8.25%)$2,784.38
Total$36,534.38
⚠ Approval Required  This quote exceeds $25,000 and requires director sign-off before sending.
What this demonstrates:
• Chained computed fields -- subtotal feeds into discount-amount, tax, and total
• Conditional fields -- discount-row, notes-section, approval-notice return content or null. Null fields render nothing.
• Multi-line JQXL -- if/elif/else/end, as $var bindings, to_entries | map(...) pipelines
• Table queries -- %%%table with per-row transforms and formatted columns
• Clean document body -- no flow control, just ::field[#id] and ::table[id] placement. All logic lives in the expressions.

Full Template Example: Email Campaign

Email template (replaces Mailchimp)
---
title: Welcome Email
adoptsFrom: { module: ./email-template, name: EmailTemplate }
---
%%%field plan-section
adoptsFrom: { module: ./fields, name: StringField }
expression: |
  if .recipient.plan == "pro" then
    "As a **Pro** member, you get access to:\n\n"
    + (.pro-features | map("- " + .name + " -- " + .description) | join("\n"))
  else
    "Check out our Pro plan for advanced features."
  end
%%%

Hi :field[.recipient.first-name],

Welcome to :field[.sender.company]! We're excited to have you.

::field[#plan-section]

Best,
:field[.sender.name]
:field[.sender.title] at :field[.sender.company]

Full Template Example: Contract

Legal contract template
---
title: Service Agreement
adoptsFrom: { module: ./contract, name: ServiceContract }
fields:
  effective-date: { type: DueDateField, from: ./fields, value: 2026-05-01 }
  term-months: { type: NumberField, from: ./fields, value: 12 }
  monthly-rate: { type: NumberField, from: ./fields, value: 15000 }
  total-value:
    type: NumberField
    from: ./fields
    expression: .term-months * .monthly-rate
---
# Service Agreement

This Agreement is entered into as of :field[.effective-date | strftime("%B %d, %Y")]
by and between **:field[.provider.legal-name]** ("Provider") and
**:field[.client.legal-name]** ("Client").

## 1. Services

Provider agrees to deliver the services described in Exhibit A
for a period of :field[.term-months] months at a rate of
:field[.monthly-rate | DOLLAR(.; 0)] per month, totaling
:field[.total-value | DOLLAR(.; 0)].

## 2. Authorized Contacts

| Role | Name | Email |
|------|------|-------|
| Client Contact | :field[.client.contact-name] | :field[.client.contact-email] |
| Provider Lead | :field[.provider.lead-name] | :field[.provider.lead-email] |

Signed: :field[.client.signatory@edit]    Date: :field[.sign-date@edit]

Conditional Content (Expression-Driven)

Conditional content uses %%%field definitions whose expressions return content or null. When a field evaluates to null, it renders nothing -- the block simply disappears. No wrapping syntax needed.

Conditional callouts
%%%field risk-warning
adoptsFrom: { module: ./fields, name: StringField }
expression: |
  if .status == "at-risk" then
    "> [!warning] This project is at risk.\n> Review the timeline and escalate."
  else null end
%%%

%%%field completion-notice
adoptsFrom: { module: ./fields, name: StringField }
expression: |
  if .progress >= 100 then
    "> [!success] Project complete!"
  else null end
%%%

::field[#risk-warning]
::field[#completion-notice]
Renders
Fields that evaluate to null render nothing. Only the matching callout appears.
If/else as a single expression
%%%field margin-status
adoptsFrom: { module: ./fields, name: StringField }
expression: |
  if .margin > 30 then
    "Healthy margins at " + (.margin | tostring) + "%."
  else
    "> [!warning] Margins below 30% -- review expenses."
  end
%%%

::field[#margin-status]
Design principle: the document body is a layout, not a program. All conditional logic lives in JQXL expressions. The body just places fields with ::field[#id] -- if the expression returns null, nothing renders. This keeps the markdown clean and readable.

Table Queries

You write
%%%table charges
columns: Item, Qty, Amount
from: .lineItems
map: {Item: .item, Qty: .qty, Amount: DOLLAR(.amount; 2)}
%%%

::table[charges]
You get
ItemQtyAmount
License1$5,000.00
Setup1$2,500.00

Fenced Renderers: Fields or Cards

Every fenced renderer maps to the same card/field system. The key distinction: when a fenced block lives inside a document's containedFields, it's a field (contained, identified by ID). When it lives at its own URL or is embedded from elsewhere, it's a card (linked, with its own card URL).

Same syntax, two modes
# Mode 1: Field (contained in this document)
# No ID = anonymous field, rendered in-place

```mermaid
graph TD
    A --> B --> C
```

# Mode 2: Field with ID (referenceable, in containedFields)

```mermaid diagram-flow
graph TD
    A --> B --> C
```
::field[.diagram-flow]          <!-- render it elsewhere in the doc -->

# Mode 3: Card (standalone, has its own URL)

::card[./Diagrams/architecture-flow]
Rule: If the fence has an ID after the language (e.g., ```mermaid diagram-flow), it becomes a named field in containedFields and can be referenced with ::field[.diagram-flow]. Without an ID, it renders in-place as an anonymous field.

If you need the content at its own URL (e.g., a slide deck that other documents embed), make it a card file and use ::card[path].
CategoryFenceBodyFieldDefSource
CONTENT RENDERERS (data inside the fence)
Diagram```mermaidMermaid DSLMermaidFieldGitHub
Map```geojson / ```topojsonGeoJSONGeoJsonFieldGitHub
3D Model```stlSTL dataStlFieldGitHub
Math```mathLaTeXMathFieldGitHub
Table```csvCSV dataCsvTableFieldBFM
Slides```slidesMarkdown (--- separates)SlideFieldBFM / Obsidian
Whiteboard```excalidrawExcalidraw JSONExcalidrawFieldObsidian #1
Kanban```kanbanMarkdown (## lanes, - cards)KanbanFieldObsidian #9
COMPUTED RENDERERS (data from the document, body = options)
Table of contents```toc / ```table-of-contentsOptions (depth, style)TocFieldObsidian
Comment summary```commentsOptions (filter)CommentsFieldBFM
Task rollup```tasksOptions or Tasks queryTasksFieldObsidian #4
COMPUTED RENDERERS -- REALM (data from the realm, body = options)
Backlinks```backlinksOptions (depth, format)BacklinksFieldObsidian core
Outgoing links```outlinksOptions (format)OutlinksFieldObsidian core
Graph```graphOptions (depth, filter)GraphFieldObsidian core
Tags```tagsOptions (scope, filter)TagsFieldObsidian core
QUERY RENDERERS (body = query, data from realm)
Dataview```dataviewDQL queryDataviewFieldObsidian #3
BOXEL EXTENSIONS (not from Obsidian/GitHub)
Canvas```canvasCard layout JSONCanvasFieldObsidian core
Timeline```timelineDate + event entriesTimelineFieldBFM
Gallery```galleryImage URLs or card pathsGalleryFieldBFM
Chart```chartChart config (type, data)ChartFieldBFM
Embed```embedURL + optionsEmbedFieldBFM

Query Languages: JQXL and JSON

BFM supports two query backends. Both work in field expressions and table queries.

JQXL -- jq syntax (multiline OK)
%%%field profit
type: NumberField
from: ./fields
expression: |
  .revenue - .expenses
%%%

%%%table breakdown
expression: |
  .line-items
  | map({
      Item: .label,
      Amount: DOLLAR(.amount; 2),
      Pct: (ROUND(.amount / .subtotal * 100; 1) | tostring) + "%"
    })
  | sort_by(.Amount)
columns: Item, Amount, Pct
%%%
JQXL is syntactically identical to jq. Pipes, filters, map, select, sort_by -- all work. YAML multiline (|) keeps complex expressions readable. JQXL adds Excel-style formula helpers: ROUND, DOLLAR, SUM, IF, VLOOKUP, etc. See the JQXL Syntax Reference for the complete function index.
JSON query -- pass-through to Boxel's query engine
```dataview
{
  "filter": {
    "type": "StockTicker",
    "every": [{ "eq": { "sector": "Technology" } }]
  },
  "sort": [{ "by": "price", "direction": "desc" }],
  "page": { "size": 10 }
}
```
When the fence body is valid JSON, it's passed directly to Boxel's card query API. This is the native realm query language -- more powerful for complex filters, pagination, and type-aware queries. JQXL is better for transforming data you already have; JSON queries are better for fetching data from the realm.

Level 6 Comments, Mentions & Annotations

Beyond Obsidian's %%hidden%% comments. BFM adds @mentions, collaborative annotations, human + agent comments, threaded discussions, review workflows, and approval marks.

Actor Identity & Facets

Actors are humans or agents identified by @ tokens. Agents can carry facets -- structured metadata for authority, runtime, and provenance -- attached directly to the token with no space before [.

Humans

You writeYou get
@chris
C @chris:boxel.ai  (domain inferred from realm)
@alice:stack.cards
A @alice:stack.cards  (explicit domain)

Agents

You writeYou get
@agent:claude
AI @agent:claude  (bare agent mention)
@agent:claude-code[model: opus, as: @chris]
AIClaude CodeOpusas @chris
@agent:reviewer[as: self, requested-by: @chris]
AIReviewerselfreq @chris
@agent:github[env: github-pr, role: co-author]
AIGitHubPRco-author

Standard Facets

FacetPurposeValues
nameDisplay name override (fallback: kebab-to-title-case of agent ID)Any string
asWhose authority is used@user or self
modelModel variantopus, sonnet
envRuntime environmentclaude-code, github-pr, matrix, browser
roleCapabilityco-author, moderator, reviewer
modeBehavior modesuggest, comment, edit, approve
requested-byWho instructed the agent@user
Canonical form (no space before [, lowercase keys, preferred order)
@agent:claude-code[model: opus, env: claude-code, as: @chris]
@agent:reviewer[env: matrix, as: self, requested-by: @chris]
@agent:moderator[env: matrix, as: self, role: moderator]
@agent:browser[env: browser, mode: suggest]
Display names: Human actors (@chris) resolve to their Matrix user card display name. Agent actors use kebab-to-title-case by default (claude-code → Claude Code). Override with the name: facet when the ID isn't readable: @agent:cc-v4[name: Claude Code].

Facets are optional and additive. Unknown keys are preserved as extension metadata (e.g., run: abc123). self is a reserved keyword for as:.

Comment Types

TypeCreated bySyntax / TriggerPurpose
Author commentDocument author%%hidden%%Private notes, hidden in display
Human commentAny collaborator{>> text --@chris}Review feedback, questions
Agent commentAI agent{>> text --@agent:name[facets]}Automated review, suggestions
Arrow shorthandAny-> text (converted to {>>})Quick annotation on current block
ApprovalHuman{>> Approved. --@chris ✅}Sign-off (same as Tasks ✅)
RejectionHuman{>> Rejected. --@chris ❌}Rejected (same as Tasks ❌)

Comment Syntax

Author comments (Obsidian-compatible): Private, stored inline in markdown.

You writeYou get
%%TODO: verify these numbers%%
(hidden in display mode)
%% Block comment. Hidden entirely. %%
(nothing rendered)

CriticMarkup annotations (new in BFM v3): Inline, attributed comments that agents and humans can emit directly in markdown.

You writeYou get
{>> Need a source for this. --@bob}
B
Bob
Need a source for this.
{>> IEA says 4.1%, not 4.3%. --@agent:claude-code[model: opus, as: @chris]}
AI
Claude CodeOpusas @chrisIEA says 4.1%, not 4.3%.
{>> Fixed, thanks. --bob !!}
B
Bob Resolved
Fixed, thanks.
{>> text --@user} = comment anchored to preceding block.
--@name = human. --@agent:name[facets] = agent with identity and authority.
!! suffix = resolved.
Visible in source, rendered as bubbles in display.

Arrow trigger: Quick comment on the current block.

You writeYou get
The revenue grew 47% YoY. -> Need to verify this against Q1 filing
The revenue grew 47% YoY.
A
Alice just now
Need to verify this against Q1 filing
The -> arrow is converted to a {>> ... --@user} comment inline at that location.

Threaded Discussions

Multiple comments on the same block form a thread. All stored inline, one after another:

In the markdown source
The largest technology IPO since Arm Holdings.
{>> Is Arm still the right benchmark? --@bob}
{>> Reddit IPO was $6.5B. Arm was $54B. Arm is correct. --@agent:claude-code[model: opus, as: @chris]}
{>> Good point, keeping it. --@bob !!}
Renders as

The largest technology IPO since Arm Holdings.

B
Bob
Is Arm still the right benchmark?
AI
Claude Code
Reddit IPO was $6.5B. Arm was $54B. Arm is correct.
B
Bob Resolved
Good point, keeping it.

Comment Emoji Signifiers

BFM uses the same emoji set as Obsidian Tasks for consistent semantics across tasks and comments.

EmojiMeaningIn TasksIn Comments
✅Done / Approved- [x] task ✅ 2026-04-03{>> Approved. --@chris ✅}
❌Cancelled / Rejected- [-] task ❌ 2026-04-03{>> Rejected. --@chris ❌}
📅Due date- [ ] task 📅 2026-04-15{>> [ ] Fix this 📅 2026-04-15 --@bob}
⏫High priority- [ ] task ⏫{>> [ ] Urgent fix ⏫ --@bob}
🔺Highest priority- [ ] task 🔺{>> [ ] Blocker 🔺 --@bob}
🔽Low priority- [ ] task 🔽{>> [ ] Nice to have 🔽 --@bob}
🔁Recurring- [ ] task 🔁 every week{>> [ ] Weekly check 🔁 every Friday --@bot}
➕Created date- [ ] task ➕ 2026-04-01{>> text ➕ 2026-04-01 --@bob}
⏳Scheduled- [ ] task ⏳ 2026-04-10{>> [ ] Review ⏳ 2026-04-10 --@bob}
🛫Start date- [ ] task 🛫 2026-04-05
🏁On completion- [ ] task 🏁 delete
⛔Blocked by- [ ] task ⛔ abc123
Same emojis, same semantics. A comment with 📅 2026-04-15 means the same thing as a task with 📅 2026-04-15. The ```tasks and ```comments computed blocks both understand these signifiers.

Approvals, Flags & Resolution

You writeYou get
{>> Approved. --@chris ✅}
✅ Approved by @chris
{>> Rejected, needs rework. --@chris ❌}
❌ Rejected by @chris
{>> Fixed, thanks. --@bob !!}
B
Bob Resolved
Fixed, thanks.

Agent vs Human

HumanAgent
Syntax{>> text --@alice}{>> text --@agent:name[facets]}
VisualA MintAI Lavender
Resolve!! suffixNo (humans resolve)
Approve✅ suffixNo

Task-Comments

Comments can have a checkbox, making them trackable like todos:

You writeYou get
{>> [ ] Fix the API mismatch --@bob}
B
Bob
Fix the API mismatch
{>> [x] Update chart data --@alice !!}
A
Alice Resolved
Update chart data
{>> [ ] Get legal review 📅 2026-04-10 ⏫ --@chris}
C
Chris due Apr 10 · High
Get legal review
[ ] / [x] at the start of the comment text = task-comment. Use the same emoji signifiers as Obsidian Tasks: 📅 due date, ⏫ priority, 🔁 recurring, ➕ created.

Storage

Everything is inline. Comments live in the markdown source as {>> ... } blocks. No sidecar, no separate database, no metadata layer. Comments travel with the file, agents read and write them directly, and they survive copy/paste, sync, and export.

Canvas (JSON Canvas + Boxel Cards)

BFM's ```canvas block is compatible with JSON Canvas 1.0 (the open spec behind Obsidian Canvas) and extends it with Boxel card and field node types.

JSON Canvas Basics

A canvas has nodes (positioned rectangles) and edges (connections between them). Four built-in node types from the spec:

Node TypeRequired FieldsDescription
texttext (markdown string)Markdown text block
filefile (path), optional subpath (#heading)Embedded file from the realm
linkurlExternal URL preview
groupoptional label, background, backgroundStyleVisual container for other nodes

All nodes share: id, type, x, y, width, height, optional color (hex or preset 1-6).

Standard JSON Canvas (Obsidian compatible)
```canvas
{
  "nodes": [
    {
      "id": "a",
      "type": "text",
      "x": 0, "y": 0, "width": 250, "height": 120,
      "text": "## Project Overview\nThis is the main objective."
    },
    {
      "id": "b",
      "type": "file",
      "x": 300, "y": 0, "width": 250, "height": 150,
      "file": "./Report/q2-analysis.md"
    },
    {
      "id": "c",
      "type": "link",
      "x": 0, "y": 180, "width": 250, "height": 100,
      "url": "https://github.com/cardstack/boxel"
    },
    {
      "id": "g1",
      "type": "group",
      "x": -20, "y": -20, "width": 580, "height": 320,
      "label": "Research"
    }
  ],
  "edges": [
    {
      "id": "e1",
      "fromNode": "a",
      "toNode": "b",
      "fromSide": "right",
      "toSide": "left",
      "label": "informs"
    }
  ]
}
```
Renders
Interactive canvas. Pan, zoom, drag. Nodes positioned at exact coordinates. Edges drawn between nodes with optional labels and arrow directions.

Edges

FieldTypeDescription
idstringUnique edge identifier
fromNode / toNodestringNode IDs to connect
fromSide / toSidetop | right | bottom | leftWhich side the edge attaches to
fromEnd / toEndnone | arrowArrow direction (default: toEnd=arrow)
colorhex or 1-6Edge color
labelstringText label on the edge

Color Presets

PresetColor
1Red
2Orange
3Yellow
4Green
5Cyan
6Purple

BFM Surface Extensions

BFM extends JSON Canvas with Boxel's frame model -- where every frame is a card renderer with format, zoom, and interactivity controls. This is the persistence format for Boxel's infinite canvas (the Surface system).

Frames (Card Nodes)

In Boxel, canvas "nodes" are frames -- each frame renders a linked card at a specific format and size. A card can appear in multiple frames (duplicate views).

Frame with format, zoom, and z-order
```canvas
{
  "nodes": [
    {
      "id": "0",
      "type": "card",
      "card": "./InvoiceCard/inv-2026-002",
      "x": 120, "y": 120, "width": 280, "height": 364,
      "format": "fitted",
      "zOrder": 0
    },
    {
      "id": "1",
      "type": "card",
      "card": "./HotelRoomCard/garden-villa",
      "x": 432, "y": 120, "width": 280, "height": 364,
      "format": "fitted",
      "zOrder": 1
    },
    {
      "id": "dup-1",
      "type": "card",
      "card": "./HotelRoomCard/garden-villa",
      "x": 757, "y": 1040, "width": 599, "height": 717,
      "format": "isolated",
      "isolatedMode": "natural",
      "isolatedZoom": 1,
      "zOrder": 6
    }
  ]
}
```
FieldTypeDescription
JSON CANVAS BASE (Obsidian compatible)
idstringUnique frame identifier
typestringtext, file, link, group (JSON Canvas standard)
x, ynumberWorld position in pixels
width, heightnumberFrame dimensions
colorhex or 1-6Frame color
BFM SURFACE EXTENSIONS
type: "card"stringBoxel card frame (new node type)
cardstringCard path (relative or absolute URL)
formatstringfitted, embedded, or isolated
isolatedModestringwindowed (scroll, fixed height) or natural (auto-height, no scroll)
isolatedZoomnumber0.25 to 1.0 -- CSS zoom for isolated content. Renders card wider than frame, then scales down
zOrdernumberExplicit layer position (lower = behind). JSON Canvas uses array order
hiddenbooleanSoft-delete -- frame stays in data but doesn't render

Card Formats on Canvas

Each frame can render its card in a different format. This is the key Boxel extension -- the same card appears as a tile, a detail panel, and a full page, all on one canvas.

FormatHeightOverflowInteractiveResizeUse
fittedFixedHiddenNo (picture)All 8 handlesGrid tiles, dashboard widgets
embeddedAutoVisibleNo (picture)Horizontal onlyDetail panels, side-by-side comparison
isolated windowedFixedScrollYes (live)All 8 handlesEditable card in a window frame
isolated naturalAutoVisibleYes (live)Horizontal onlyFull card at natural height
Fitted and embedded are non-interactive -- they render the card as a static picture (pointer-events: none). Great for visual layout and comparison.

Isolated is interactive -- the card accepts mouse events, can scroll, and can be edited. isolatedZoom lets you see more of the card in a smaller frame by rendering at a wider width and CSS-scaling down.

Duplicate Frames

The same card can appear in multiple frames at different formats and sizes. This is how you build a dashboard with a grid of fitted tiles and a detail panel showing the selected card in isolated format.

Same card, three views on one canvas
```canvas
{
  "nodes": [
    {
      "id": "tile",
      "type": "card",
      "card": "./Theme/athletic-energy",
      "x": 120, "y": 120, "width": 280, "height": 364,
      "format": "fitted",
      "zOrder": 0
    },
    {
      "id": "detail",
      "type": "card",
      "card": "./Theme/athletic-energy",
      "x": 432, "y": 120, "width": 350, "height": 364,
      "format": "embedded",
      "zOrder": 1
    },
    {
      "id": "full",
      "type": "card",
      "card": "./Theme/athletic-energy",
      "x": 814, "y": 120, "width": 450, "height": 500,
      "format": "isolated",
      "isolatedMode": "windowed",
      "isolatedZoom": 0.5,
      "zOrder": 2
    }
  ]
}
```
Renders
Three frames showing the same Theme card: a 280x364 fitted tile (static), a 350px-wide embedded panel (static, auto-height), and a 450x500 isolated window at 50% zoom (interactive, scrollable).

Cross-Realm Cards

Frames can reference cards from any realm, not just the current one. Use absolute URLs:

Canvas mixing local and cross-realm cards
```canvas
{
  "nodes": [
    {
      "id": "local",
      "type": "card",
      "card": "./InvoiceCard/inv-2026-002",
      "x": 0, "y": 0, "width": 280, "height": 364,
      "format": "fitted"
    },
    {
      "id": "remote",
      "type": "card",
      "card": "https://app.boxel.ai/chris/hover-craft/NbaTradingCard/4d8b37ea",
      "x": 312, "y": 0, "width": 280, "height": 364,
      "format": "fitted"
    },
    {
      "id": "note",
      "type": "text",
      "x": 0, "y": 400, "width": 560, "height": 100,
      "text": "Comparing invoice layout with trading card layout"
    }
  ],
  "edges": [
    { "id": "e1", "fromNode": "local", "toNode": "note" },
    { "id": "e2", "fromNode": "remote", "toNode": "note" }
  ]
}
```

JSON Canvas Compatibility

FeatureJSON Canvas 1.0BFM Canvas
Text nodes✅✅ (with full BFM markdown inside)
File nodes✅✅ (resolved against realm)
Link nodes✅✅
Group nodes✅✅
Edges✅✅
Color presets 1-6✅✅
Card nodes--✅ Boxel card at any format
Format per node--✅ fitted / embedded / isolated
Isolated zoom--✅ 0.25-1.0 CSS zoom
Isolated mode--✅ windowed (scroll) / natural (auto-height)
Explicit z-order-- (array order)✅ per-frame zOrder field
Duplicate frames--✅ same card, multiple views
Hidden/soft-delete--✅ frame stays in data
Cross-realm cards--✅ absolute URL references
Interactive content--✅ isolated format only
Obsidian .canvas files import directly -- text, file, link, and group nodes render as-is. BFM's card frames, format, and zoom fields are ignored by Obsidian (graceful degradation). Going the other direction, Obsidian nodes are preserved when round-tripping through BFM.

Canvas Use Cases

Use caseHow
DashboardFitted card frames for widgets, grouped by section
Card galleryGrid of fitted frames, click to open isolated detail
ComparisonSame card in multiple frames at different formats/sizes
Design reviewCards at various fitted sizes to check responsive layout
Cross-realm moodboardCards from different realms on one canvas
BrainstormText nodes for ideas, card nodes for evidence, edges for connections
ArchitectureText nodes for components, edges for data flow, groups for boundaries

Built With BFM: Card & Field Catalog

Everything above is BFM syntax -- the primitives a parser implements. Everything below is built on top of that syntax using standard CardDef and FieldDef classes. Each renderer is just a card or field that knows how to render a fenced block's content. To add a new "plugin," write a FieldDef and map it via renderers: in frontmatter.

For implementers: Get the syntax primitives right first (:field, ::card, :::containers, %%%definitions, fenced blocks, {>>} comments). Then the catalog below comes for free -- each entry is just a FieldDef that handles a specific fence language.

Excalidraw (Whiteboard)

Obsidian compat -- .excalidraw.md files embed via wiki link
![[my-drawing.excalidraw]]
BFM -- fenced block with Excalidraw JSON
```excalidraw
{
  "type": "excalidraw",
  "elements": [
    { "type": "rectangle", "x": 10, "y": 10, "width": 200, "height": 100 },
    { "type": "text", "x": 50, "y": 40, "text": "Hello" }
  ]
}
```
Obsidian stores Excalidraw as LZ-compressed JSON inside .excalidraw.md files. BFM accepts uncompressed Excalidraw JSON in a fenced block. Pasted Obsidian embeds (![[drawing.excalidraw]]) resolve as card embeds.

Kanban (Board)

You write
```kanban
## Backlog
- Design review
- API documentation
- [ ] Write migration guide

## In Progress
- Parser implementation
- [x] Field registry

## Done
- Spec draft
- Syntax guide
```
Renders
Backlog
Design review
API documentation
Write migration guide
In Progress
Parser implementation
Field registry
Done
Spec draft
Syntax guide
## headings = lanes. - list items = cards. - [ ] / - [x] = cards with checkbox. Compatible with Obsidian Kanban plugin's markdown format.

Tasks Query (Obsidian Tasks Plugin Compat)

Obsidian Tasks query syntax (3.3M downloads)
```tasks
not done
due before next week
priority is high
path includes Projects
```
Renders
Fix the API mismatch 📅 2026-04-08 ⏫
Get legal review 📅 2026-04-10 ⏫
BFM's ```tasks block accepts both simple options (filter: open) and Obsidian Tasks plugin query syntax (not done, due before, priority is). Queries run against the document's own tasks and task-comments. For realm-wide task queries, use :::list[query] with card type filters.

Slides (Obsidian Core + Advanced Slides Compat)

Obsidian core slides + Advanced Slides / Slides Extended annotations
```slides
# Welcome
Introduction to the project

---
# Key Findings
<!-- .slide: data-background="#1a1a2e" -->

::card[./Cards/revenue-chart]

---
# Next Steps
- [ ] Ship v1
- [ ] Gather feedback
```
BFM's ```slides is compatible with Obsidian's core Slides plugin (--- separator) and the Advanced Slides / Slides Extended plugin (reveal.js <!-- .slide: --> annotations). Full BFM syntax works inside each slide -- cards, fields, mermaid, etc.

When to use which:

Use caseUseWhy
Diagram in a report```mermaid (field)Contained in the document
Reusable diagram::card[./Diagrams/flow]Has its own URL, shared across docs
Kanban in a project doc```kanban (field)Inline board, not reused
Standalone Kanban board::card[./Boards/sprint]Its own card, linked from multiple docs
Whiteboard sketch```excalidraw (field)Inline drawing
Reusable drawing![[drawing.excalidraw]] or ::cardObsidian compat / standalone card

Realm Computed Blocks (Backlinks, Graph, Tags)

Like ```toc reads the document, these read the realm -- other cards that reference or relate to this one.

Backlinks -- what links to this document
```backlinks
```

```backlinks
format: strip
depth: 1
```
Renders
Q1 Summary · links from "See also" section
Sprint 12 Retro · links from "References"
Team Dashboard · embedded as ::card
Outgoing links -- what this document links to
```outlinks
```
Graph -- visual connection map
```graph
depth: 2
```
Renders
Interactive node graph showing this document's connections to other cards in the realm (like Obsidian's local graph view)
Tags -- all tags in this document or realm
```tags
```

```tags
scope: realm
```
Renders
#markets 12 #ai 8 #climate 5

Boxel Extension Blocks

Fenced renderers that go beyond Obsidian/GitHub. New content types native to BFM.

Timeline -- chronological events
```timeline
- 2026-01: Project kickoff
- 2026-03: Alpha release
  Parser and serializer complete
- 2026-04: Beta release
  Display renderer, field hydration
- 2026-06: Public launch
```
Renders
Jan 2026 Project kickoff
Mar 2026 Alpha release
Parser and serializer complete
Apr 2026 Beta release
Display renderer, field hydration
Jun 2026 Public launch
Gallery -- image or card grid
```gallery
- ./assets/screenshot-1.png
- ./assets/screenshot-2.png
- ./assets/screenshot-3.png
- ./assets/screenshot-4.png
cols: 2
```
Renders
Responsive image grid (2 columns). Images lazy-load, lightbox on click.
Chart -- data visualization beyond mermaid
```chart
type: bar
title: Revenue by Quarter
data:
  labels: [Q1, Q2, Q3, Q4]
  datasets:
    - label: 2025
      values: [12, 19, 8, 15]
    - label: 2026
      values: [18, 24, 22, 28]
```
Renders
Interactive bar chart. Supports: bar, line, area, scatter, pie, donut, radar. YAML config, data inline or from JQXL expression.
Embed -- external URL preview
```embed
url: https://github.com/cardstack/boxel
```
Renders
cardstack/boxel
The Boxel platform for composable card-based applications. GitHub.
Canvas -- infinite card layout
```canvas
nodes:
  - id: a
    card: ./Cards/aero-stock
    x: 0
    y: 0
  - id: b
    card: ./Cards/nvda-stock
    x: 300
    y: 0
  - id: c
    text: "These two are correlated"
    x: 150
    y: 150
edges:
  - from: a
    to: c
  - from: b
    to: c
```
Renders
Interactive canvas with positioned cards, text nodes, and connecting edges. Pan, zoom, drag. Compatible with Obsidian .canvas JSON format.

CSV Auto-Table

CSV auto-table
```csv
Name, Department, Salary
Alice, Engineering, 145000
Bob, Design, 125000
```
Renders
NameDepartmentSalary
AliceEngineering145,000
BobDesign125,000

Fenced Computed Blocks (Boxel Built-ins)

Same pattern as Obsidian's ```toc -- the fence body is options, the data comes from the document itself. BFM provides built-in computed blocks for common views. All are overridable via renderers: in frontmatter.

Table of contents
```toc
```

```toc
depth: 2
style: numbered
```
Renders (reactive -- updates as headings change)
Comment summary
```comments
```

```comments
filter: open
```
Renders (reactive -- updates as comments are added/resolved)
7 comments 3 open 4 resolved
Fix the API mismatch --@bob
Get legal review --@chris
Verify Q1 figures --@alice
Task summary
```tasks
```
Renders (aggregates - [ ] tasks + {>> [ ] } task-comments)
6 / 10 complete
EU CBAM takes effect Q3 2026
California AIEIA in committee
Initial data collection
FenceData fromOptionsRenders
```tocDocument headingsdepth, styleNested linked heading list
```table-of-contentsDocument headingsSame (Obsidian alias)Same
```commentsInline {>>} commentsfilter: open, resolved, allCounts + open task list
```tasks- [ ] + task-commentsfilter: open, done, allProgress bar + checklist
Same pattern everywhere: ```mermaid renders data from inside the fence. ```toc renders data from the document. Both are fenced blocks. Both are overridable via renderers: in frontmatter. Both are implemented as FieldDefs under the hood.
Override with custom renderer
---
renderers:
  toc:
    type: FancyTocField
    from: ./fancy-toc
  comments:
    type: ReviewPanelField
    from: ./review-panel
---

Inline Card Creation

A %%%card block lets you create real card instances directly from a markdown document. The card type provides the schema/template -- you just fill in the data. The result is an actual .json file in the realm, not inline data.

This is how agents and humans create structured data without leaving the document. Write the card definition inline, and the system creates the real file as if you'd done it manually.

Basic Example

You write (in Report/q2-analysis.md)
%%%card stock-nvda
type: StockTickerCard
from: ./stock-ticker-card
symbol: NVDA
price: 892.50
change: 2.4
%%%

::card[#stock-nvda]
What happens
Report/
  q2-analysis.json              <-- your document
  q2-analysis/                  <-- auto-created subdirectory
    StockTickerCard/            <-- type subdirectory
      stock-nvda.json           <-- real card instance
%%%card stock-nvda = "create a StockTickerCard with ID stock-nvda." The system writes a real .json file at a deterministic path. #stock-nvda is shorthand for the relative path within the containing document.

Multiple Cards, Different Types

One document creating several card types
## Team

%%%card alice
type: TeamMember
from: ./team-member
name: Alice Chen
role: Lead Engineer
status: active
%%%

%%%card bob
type: TeamMember
from: ./team-member
name: Bob Park
role: Design Lead
status: active
%%%

## Milestones

%%%card parser-done
type: Milestone
from: ./milestone
name: BFM Parser
completion: 100
status: completed
dueDate: 2026-03-15
%%%

%%%card renderer-wip
type: Milestone
from: ./milestone
name: Display Renderer
completion: 60
status: in-progress
dueDate: 2026-04-15
%%%

The team: :card[#alice] and :card[#bob]

:::grid[cols:2]
::card[#parser-done]
::card[#renderer-wip]
:::
File tree created
Report/
  sprint-12.json
  sprint-12/
    TeamMember/
      alice.json
      bob.json
    Milestone/
      parser-done.json
      renderer-wip.json

How IDs Resolve

ContextSyntaxResolves to
Same document#stock-nvda./q2-analysis/StockTickerCard/stock-nvda
Another document::card[./Report/q2-analysis/StockTickerCard/stock-nvda]Full relative path
Wiki link[[stock-nvda]]Resolved by name in realm

The Agent Use Case

This is why inline card creation matters: an agent can create structured data without knowing the realm's file system. It writes a %%%card block in the markdown, and the system handles file creation, path resolution, and type validation.

Agent prompt: "Add three stock tickers to this report"
%%%card aero
type: StockTickerCard
from: ./stock-ticker-card
symbol: AERO
price: 342.10
change: 5.1
%%%

%%%card nvda
type: StockTickerCard
from: ./stock-ticker-card
symbol: NVDA
price: 892.50
change: 2.4
%%%

%%%card tsla
type: StockTickerCard
from: ./stock-ticker-card
symbol: TSLA
price: 178.20
change: -1.8
%%%

:::grid[cols:3]
::card[#aero]
::card[#nvda]
::card[#tsla]
:::
The agent doesn't need to know where card files go, what JSON format to use, or how adoptsFrom works. It writes YAML in a %%%card block. The card type (StockTickerCard) provides the schema -- the agent just fills in the fields.

Card vs Field -- When to Use Which

%%%field%%%card
CreatesContained data (no file)Real .json file
Has own URLNoYes
Linkable from other docsNoYes
Appears in realm searchNoYes
Has fitted/embedded viewsEmbedded onlyAll formats
Survives if doc deletedNo (deleted with doc)Orphaned (file remains)
Boxel equivalentcontains(FieldDef)linksTo(CardDef)
Use forStatus, progress, metrics, configPeople, projects, products, events, anything with identity

Syntax Index

Every BFM syntax element, organized by level. Each links back to where it's defined.

SyntaxWhat
LEVEL 1 -- ORIGINAL MARKDOWN
# H1 ... ###### H6Headings↑ L1
**bold** *italic* ***both***Emphasis↑ L1
[text](url)Link↑ L1
![alt](url)Image↑ L1
- item / 1. itemLists↑ L1
> textBlock quote↑ L1
`code` / ```fenced```Code↑ L1
---Horizontal rule↑ L1
LEVEL 2 -- COMMONMARK
Lazy continuationLines after > can omit prefix↑ L2
List item alignmentContent aligns with text after marker↑ L2
Tight vs loose listsBlank line between items wraps in <p>↑ L2
Block > inline precedenceBlock structures win when ambiguous↑ L2
&copy; &#169; &#xA9;HTML entities (named, numeric, hex)↑ L2
Info strings on fencesFirst word = language, rest = options↑ L2
LEVEL 3 -- GITHUB FLAVORED
| col | col | with :---|---:Table↑ L3
- [ ] / - [x]Task list↑ L3
~~text~~Strikethrough↑ L3
[^1] + [^1]: textFootnote↑ L3
$inline$ / $$block$$Math (LaTeX)↑ L3
> [!NOTE] (5 types)Alerts↑ L3
```mermaid ```geojson ```stlDiagrams, maps, 3D↑ L3
LEVEL 4 -- OBSIDIAN
[[Page]] [[Page|Text]]Wiki link↑ L4
![[Page]] ![[img|300]]Wiki embed↑ L4
==text==Highlight↑ L4
%%hidden%%Author comment↑ L4
#tag #nested/tagTags↑ L4
^block-idBlock reference↑ L4
> [!type]+/- (14+ types)Callouts with fold↑ L4
--- YAML ---Frontmatter↑ L4
```toc / ```dataviewTOC, Dataview (compat)↑ L4
📅 ⏫ 🔁 ⏳ ➕ ✅ ❌Tasks emoji metadata↑ L4
LEVEL 5 -- BOXEL SYNTAX
:card[path]Card atom (inline chip)↑ L5
::card[path]Card embedded (block, auto-height)↑ L5
::card[path | strip]Card fitted (16 named sizes)↑ L5
::card[path | 400x200]Card fitted (custom WxH)↑ L5
::card[Type where x=y]Card by query (agent-friendly)↑ L5
::card[#local-id]Document-local card ref↑ L5
:field[.path] / :field[.path@edit]Containing card field (inline)↑ L5
:field[#id] / :field[#id@edit]Markdown-local field (inline)↑ L5
::field[.path] / ::field[#id]Block field (card or local)↑ L5
::table[id]Table from query↑ L5
:::list...:::Vertical stack container↑ L5
:::grid[cols:N]...:::Column grid container↑ L5
%%%field + if ... else null endConditional content (null = hidden)↑ L5
%%%field + map(...) | joinRepeated content (expression-driven)↑ L5
%%%field id ... %%%Field definition (hidden)↑ L5
%%%card id ... %%%Card creation (real .json file)↑ L5
%%%table id ... %%%Table query definition (hidden)↑ L5
expression: JQXLFormula (jq syntax + Excel functions)↑ L5
:field[.ref.path]Linked card traversal↑ L5
:field[.x | jqxl]Inline JQXL transform↑ L5
:field[.x // "fallback"]Default value (jq alternative operator)↑ L5
local("id")Cross-reference local field in expression↑ L5
adoptsFrom: in frontmatterCard type↑ L5
renderers: in frontmatterFence overrides↑ L5
fields: in frontmatterField schema (shorthand)↑ L5
```lang idNamed fence (becomes referenceable field)↑ L5
LEVEL 6 -- COMMENTS & ANNOTATIONS
@chrisHuman mention↑ L6
@agent:name[facets]Agent with facets (model, as, env, role)↑ L6
{>> text --@actor}Inline comment↑ L6
{>> text --@actor !!}Resolved↑ L6
{>> text --@actor ✅} / ❌Approval / Rejection↑ L6
{>> [ ] text --@actor}Task-comment (trackable)↑ L6
-> textArrow shorthand (becomes {>>})↑ L6
DELIMITER HIERARCHY
:Inline (atom)↑ L5
::Block (embedded)↑ L5
:::Container (wrapper)↑ L5
```Fenced block (visible)↑ L1
%%Comment (hidden)↑ L4
%%%Definition (hidden, structural)↑ L5