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.
Created by John Gruber & Aaron Swartz (2004). Works everywhere -- GitHub, Obsidian, Boxel, email, static sites.
# is required. Max 6 levels. Setext style (=== / --- underlines) also works for H1/H2.inline code-, *, or + for unordered. First number in ordered list sets the start value.```javascript
function greet(name) {
return `Hello, ${name}!`;
}
```
function greet(name) { return `Hello, ${name}!`; }
~~~ also work.Formal specification (2014). Everything in Level 1 still works. CommonMark added precise rules for edge cases.
| Rule | What it means |
|---|---|
| Lazy continuation | Lines after > can omit the > prefix |
| List item alignment | Content under list items must align with text after the marker |
| Tight vs loose lists | Blank line between items wraps in <p>, otherwise no wrapping |
| Block > inline | Block structures take precedence over inline when ambiguous |
| Entity references | © © © all render as © |
| Info strings | Text after fence opening: first word = language, rest = implementation-defined |
GFM (v0.29, 2019) -- strict superset of CommonMark. Adds tables, tasks, diagrams, math, alerts, and more.
| Feature | Status | Priority | |:----------|:-------:|---------:| | Auth | Done | High | | Dashboard | WIP | Medium |
| Feature | Status | Priority |
|---|---|---|
| Auth | Done | High |
| Dashboard | WIP | Medium |
| Delimiter | Alignment |
|---|---|
:--- | Left (default) |
:---: | Center |
---: | Right |
\|. Inline formatting works inside cells.> [!NOTE] > Useful background information. > [!WARNING] > Something could go wrong. > [!TIP] > A helpful shortcut.
| Type | Color | Use for |
|---|---|---|
NOTE | Blue | Supplementary info |
TIP | Green | Helpful shortcuts |
IMPORTANT | Purple | Required knowledge |
WARNING | Yellow | Potential risks |
CAUTION | Red | Negative consequences |
```mermaid graph LR A[Start] --> B{Decision} B -->|Yes| C[Ship it] B -->|No| D[Fix it] ```
| Language | Renders |
|---|---|
```geojson | Interactive map (points, polygons, lines) |
```topojson | Topology-compressed map |
```stl | 3D model viewer (WebGL) |
| Syntax | Result |
|---|---|
<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 |
<script> <style> <iframe> <textarea> <title> <xmp> <noembed> <noframes> <plaintext>Obsidian extends GFM for personal knowledge management. BFM supports all of these features.
%% content is preserved in source, hidden in display. Good for author notes. For collaborative review comments, see Level 6.-, _, / (for nesting). Indexed and queryable.> [!question] Can callouts fold? > Yes they can! > [!tip]- Collapsed by default > Click to reveal. > [!warning]+ Open but collapsible > Can be folded shut.
| Type | Aliases | Color |
|---|---|---|
note | Blue | |
tip | hint | Cyan |
important | Purple | |
warning | caution, attention | Orange |
danger | error | Red |
success | check, done | Green |
failure | fail, missing | Red |
question | help, faq | Yellow |
example | Purple | |
quote | cite | Gray |
abstract | summary, tldr | Teal |
bug | Red | |
todo | Blue | |
info | Blue |
+ = open, collapsible. - = collapsed. Callouts nest with extra > prefixes.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.
```toc style: nestedList maxLevel: 3 ``` ```table-of-contents style: nestedList maxLevel: 3 ```
```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.The Obsidian Tasks plugin (3.3M downloads) extends markdown tasks with emoji-based metadata. BFM preserves these.
| Emoji | Meaning | Example |
|---|---|---|
| 📅 | 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 |
```tasks computed block and the TasksField. When pasting from Obsidian, task metadata is preserved.--- title: My Document tags: - project - active aliases: - Alt Name cssclasses: - wide-layout date: 2026-04-03 ---
tags merge with inline #tags. aliases work for wiki link resolution.:field[id] with %%%field definitions -- they are typed, editable, and support JQXL formulas.
```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.
```dataview TABLE author, date, status FROM #project WHERE status != "done" SORT date DESC LIMIT 10 ```
| DQL Type | Renders via | FieldDef to build | Notes |
|---|---|---|---|
TABLE | DataviewTableField | Wraps %%%table + JQXL query. Columns from DQL select, FROM mapped to card type filter, WHERE/SORT/LIMIT translated. | Most DQL maps cleanly |
LIST | DataviewListField | Wraps :::list[query]. Each result rendered as ::card[... | strip]. FROM/WHERE translated to card query filters. | Falls back to title-only strips |
TASK | DataviewTaskField | Queries cards with a boolean completed field. Renders as checkbox list with card title. Needs a convention for which field = "done". | Requires task card convention |
CALENDAR | DataviewCalendarField | Queries cards with a date field, renders month grid heatmap. Each cell links to cards on that date. | Most complex, lowest priority |
%%%table, :::list, :::grid).
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.
BFM extends the standard Obsidian YAML frontmatter with Boxel card properties. Only include what you need -- most are optional.
--- 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 ---
| Property | Source | Purpose |
|---|---|---|
title | Obsidian | Document title (falls back to first heading) |
tags | Obsidian | Searchable labels, merged with inline #tags |
aliases | Obsidian | Alternative names for wiki link resolution |
cssclasses | Obsidian | CSS classes on the document container |
adoptsFrom | Boxel | Card type (module + name). Omit for default document card |
theme | Boxel | Theme card path (sets cardInfo.theme) |
thumbnailURL | Boxel | Card preview image (sets cardInfo.thumbnailURL) |
fields | Boxel | Field schema map (shorthand for %%%field blocks) |
renderers | Boxel | Map fenced languages to custom FieldDef/CardDef |
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.
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.
```mermaid → built-in MermaidField ```csv → built-in CsvTableField ```math → built-in MathField ```slides → built-in SlideField
--- 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 ```
```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.```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.
| Fence | Without override | With override |
|---|---|---|
```mermaid | Built-in MermaidField | Your custom DiagramField |
```csv | Built-in CsvTableField | Your custom SpreadsheetField |
```timeline | Syntax-highlighted code | Your TimelineField |
```kanban | Syntax-highlighted code | Your KanbanField |
```anything | Syntax-highlighted code | Whatever FieldDef you map it to |
Three card formats, matching Boxel's rendering system:
| Format | Syntax | Height | Use |
|---|---|---|---|
| Atom | :card (single colon) | Inline | Chip in running text |
| Embedded | ::card (double colon) | Auto | Standard card block, natural height |
| Fitted | ::card[... | size] | Fixed | Card constrained to specific 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.
| Category | Constant | W × H |
|---|---|---|
| BADGES (150px wide) | ||
small-badge | 150 × 40 | |
medium-badge | 150 × 65 | |
large-badge | 150 × 105 | |
| STRIPS (250px wide) | ||
strip | 250 × 40 | |
double-strip | 250 × 65 | |
triple-strip | 250 × 105 | |
| WIDE STRIPS (400px wide) | ||
double-wide-strip | 400 × 65 | |
triple-wide-strip | 400 × 105 | |
| TILES | ||
small-tile | 150 × 170 | |
tile | 250 × 170 | |
grid-tile | 170 × 250 | |
tall-tile | 150 × 275 | |
large-tile | 250 × 275 | |
| CARDS (400px wide) | ||
compact-card | 400 × 170 | |
full-card | 400 × 275 | |
expanded-card | 400 × 445 | |
embedded format -- auto-height, card shows everything it has.fitted format -- card adapts content to the constraint via fit-grid and line-budget modifiers.WxH, w:N, h:N, or w:N% for any size not covered by the constants.
| Syntax | Format | Sizing |
|---|---|---|
:card[path] | Atom | Inline (flows in text) |
::card[path] | Embedded | Auto-height |
::card[path | strip] | Fitted | 250 × 40 (or any constant) |
::card[path | 400x200] | Fitted | Custom exact |
::card[path | isolated] | Isolated | Full standalone page |
::card:::)Triple colon ::: wraps content blocks. Single = inline, double = block, triple = container. Two container types: :::list (vertical stack) and :::grid (columns).
:::list ::card[./Cards/aero-stock | strip] ::card[./Cards/nvda-stock | strip] ::card[./Cards/tsla-stock | strip] :::
:::grid[cols:3] ::card[./Cards/aero-stock] ::card[./Cards/nvda-stock] ::card[./Cards/tsla-stock] :::
:::grid[cols:2 gap:1rem] ::card[./Cards/team-alice] ::card[./Cards/team-bob] ::card[./Cards/team-carol] ::card[./Cards/team-dan] :::
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[query] type: Project filter: eq: { status: active } sort: by: due-date direction: asc limit: 10 :::
:::grid[cols:3 query] type: TeamMember filter: eq: { department: Engineering } sort: by: name :::
:::list[query] type: StockTicker expression: | . | select(.change > 0) | sort_by(-.change) size: strip :::
:::list and :::grid are layout-aware -- control columns, gaps, card sizes.| Approach | Syntax | Power | Priority |
|---|---|---|---|
| :::list / :::grid + query | Boxel native | Full query API, typed cards, any fitted size, JQXL transforms | Preferred |
| %%%table + JQXL | Boxel native | Tabular data from field context, formulas | Preferred |
```dataview DQL | Obsidian compat | Best-effort translation to Boxel queries | Compat |
[key:: value] | Obsidian compat | Best-effort inline metadata extraction | Compat |
::: opens a container. ::: on its own line closes it. Content inside follows normal BFM rules. Containers can nest.| Colons | Level | Purpose |
|---|---|---|
: | Inline | Atom -- flows in text (:card, :field) |
:: | Block | Embedded -- own line, auto-height (::card, ::field, ::table) |
::: | Container | Wrapper -- :::list, :::grid |
| Syntax | Level | Renders |
|---|---|---|
: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] | Block | Card by query (agent-friendly) |
::card[#local-id] | Block | Document-local card |
![[Card Name]] | Block | Wiki-style card embed |
:::list...::: | Container | Vertical stack |
:::grid[cols:N]...::: | Container | Column grid |
BFM field references are explicit. Every selector MUST begin with # or .. Bare selectors are invalid. No implicit fallback between namespaces.
| Prefix | Namespace | Resolves from |
|---|---|---|
#id | Markdown-local | %%%field declaration in this document |
.path | Containing card | Card root, jq-style path traversal |
| Syntax | Level | Mode |
|---|---|---|
:field[.path] | Inline | Display (containing card) |
:field[.path@edit] | Inline | Edit (containing card) |
:field[#id] | Inline | Display (markdown-local) |
:field[#id@edit] | Inline | Edit (markdown-local) |
::field[.path] | Block | Display (containing card) |
::field[#id] | Block | Display (markdown-local) |
.path always resolves from the containing card root. #id always resolves from the current document. No ambiguity, no fallback, no guessing.
%%%field)%%% fences declare markdown-local field instances. Hidden in display mode. Referenced by #id.
```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 %%%
## 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 %%%
%%%field <id> then YAML body then %%%. The word after field is the ID. Body uses adoptsFrom: (module + name) and value:.
| Delimiter | Display | What it's for |
|---|---|---|
```lang | Visible -- renders in-place | Code, mermaid, csv, slides, geojson, math |
%% | Hidden -- stripped | Author comments, private notes |
%%%type id | Hidden -- structural | Field, card, and table definitions |
A formula field uses expression: instead of value:. The expression is JQXL where . is the containing card root.
%%%field total adoptsFrom: module: ./fields name: NumberField expression: ROUND(.subtotal * (1 + .taxRate); 2) %%%
Total: :field[#total]
local("id")%%%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]
. = containing card root (always)local("id") = another %%%field in this document (explicit cross-reference)#id to .id or vice versa.
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.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.
client, field name)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.
Transforms use the same jq pipe syntax as formula fields and table queries. One expression language everywhere -- no Liquid, no Handlebars, just jq.
// is jq's alternative operator)| Transform | jq syntax | Example | Result |
|---|---|---|---|
| Uppercase | ascii_upcase | :field[.name | ascii_upcase] | ACME CORP |
| Lowercase | ascii_downcase | :field[.name | ascii_downcase] | acme corp |
| String length | length | :field[.name | length] | 9 |
| Array length | length | :field[.items | length] | 3 |
| Slice | .[:N] | :field[.desc | .[:50]] | First 50 chars |
| Join | join("sep") | :field[.tags | join(", ")] | Comma-separated |
| Default | // "fallback" | :field[.x // "N/A"] | Value or N/A |
| Round | ROUND(.; N) | :field[.rate | ROUND(.; 2)] | 3.14 |
| Currency | DOLLAR(.; N) | :field[.total | DOLLAR(.; 2)] | $1,234.56 |
| Percent | . * 100 | ROUND(.; N) | :field[.rate | . * 100 | ROUND(.; 1) | tostring + "%"] | 42.0% |
| Date format | strftime("fmt") | :field[.due | strftime("%b %d")] | Apr 18 |
| Conditional | if . > 0 then "up" else "down" end | :field[.change | if . > 0 then "+" else "" end] | + |
| Chain | | f1 | f2 | :field[.name | ascii_upcase | .[:10]] | ACME CORP |
expression: .revenue - .expenses in a %%%field block, you already know :field[.revenue | DOLLAR(.; 0)] in the body.ROUND, DOLLAR, VLOOKUP, SUMIF, TEXT, DATE, IF, CONCATENATE, and more. See the JQXL Syntax Reference for the complete index.
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.
%%%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)]**
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.
%%%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)]
:::list[query] type: TeamMember from: .team size: strip :::
%%%table with a JQXL expression: that maps rows. Best for structured tabular data.%%%field whose expression maps items to markdown strings and joins them. Best for bullet lists, paragraphs, or mixed content.:::list[query] or :::grid[query]. Best for rendering actual card components.
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 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 %%%
%%%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 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) }) %%%
# 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]
| # | Description | Qty | Unit Price | Line Total |
|---|---|---|---|---|
| 1 | Platform License (Enterprise) | 1 | $18,000.00 | $18,000.00 |
| 2 | Implementation & Integration | 60 | $200.00 | $12,000.00 |
| 3 | Training (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 |
subtotal feeds into discount-amount, tax, and totaldiscount-row, notes-section, approval-notice return content or null. Null fields render nothing.if/elif/else/end, as $var bindings, to_entries | map(...) pipelines%%%table with per-row transforms and formatted columns::field[#id] and ::table[id] placement. All logic lives in the expressions.
--- 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]
--- 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 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.
%%%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]
%%%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]
::field[#id] -- if the expression returns null, nothing renders. This keeps the markdown clean and readable.%%%table charges columns: Item, Qty, Amount from: .lineItems map: {Item: .item, Qty: .qty, Amount: DOLLAR(.amount; 2)} %%% ::table[charges]
| Item | Qty | Amount |
|---|---|---|
| License | 1 | $5,000.00 |
| Setup | 1 | $2,500.00 |
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).
# 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]
```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.::card[path].
| Category | Fence | Body | FieldDef | Source |
|---|---|---|---|---|
| CONTENT RENDERERS (data inside the fence) | ||||
| Diagram | ```mermaid | Mermaid DSL | MermaidField | GitHub |
| Map | ```geojson / ```topojson | GeoJSON | GeoJsonField | GitHub |
| 3D Model | ```stl | STL data | StlField | GitHub |
| Math | ```math | LaTeX | MathField | GitHub |
| Table | ```csv | CSV data | CsvTableField | BFM |
| Slides | ```slides | Markdown (--- separates) | SlideField | BFM / Obsidian |
| Whiteboard | ```excalidraw | Excalidraw JSON | ExcalidrawField | Obsidian #1 |
| Kanban | ```kanban | Markdown (## lanes, - cards) | KanbanField | Obsidian #9 |
| COMPUTED RENDERERS (data from the document, body = options) | ||||
| Table of contents | ```toc / ```table-of-contents | Options (depth, style) | TocField | Obsidian |
| Comment summary | ```comments | Options (filter) | CommentsField | BFM |
| Task rollup | ```tasks | Options or Tasks query | TasksField | Obsidian #4 |
| COMPUTED RENDERERS -- REALM (data from the realm, body = options) | ||||
| Backlinks | ```backlinks | Options (depth, format) | BacklinksField | Obsidian core |
| Outgoing links | ```outlinks | Options (format) | OutlinksField | Obsidian core |
| Graph | ```graph | Options (depth, filter) | GraphField | Obsidian core |
| Tags | ```tags | Options (scope, filter) | TagsField | Obsidian core |
| QUERY RENDERERS (body = query, data from realm) | ||||
| Dataview | ```dataview | DQL query | DataviewField | Obsidian #3 |
| BOXEL EXTENSIONS (not from Obsidian/GitHub) | ||||
| Canvas | ```canvas | Card layout JSON | CanvasField | Obsidian core |
| Timeline | ```timeline | Date + event entries | TimelineField | BFM |
| Gallery | ```gallery | Image URLs or card paths | GalleryField | BFM |
| Chart | ```chart | Chart config (type, data) | ChartField | BFM |
| Embed | ```embed | URL + options | EmbedField | BFM |
BFM supports two query backends. Both work in field expressions and table queries.
%%%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 %%%
|) 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.```dataview { "filter": { "type": "StockTicker", "every": [{ "eq": { "sector": "Technology" } }] }, "sort": [{ "by": "price", "direction": "desc" }], "page": { "size": 10 } } ```
Beyond Obsidian's %%hidden%% comments. BFM adds @mentions, collaborative annotations, human + agent comments, threaded discussions, review workflows, and approval marks.
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 [.
| Facet | Purpose | Values |
|---|---|---|
name | Display name override (fallback: kebab-to-title-case of agent ID) | Any string |
as | Whose authority is used | @user or self |
model | Model variant | opus, sonnet |
env | Runtime environment | claude-code, github-pr, matrix, browser |
role | Capability | co-author, moderator, reviewer |
mode | Behavior mode | suggest, comment, edit, approve |
requested-by | Who instructed the agent | @user |
@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]
@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].run: abc123). self is a reserved keyword for as:.
| Type | Created by | Syntax / Trigger | Purpose |
|---|---|---|---|
| Author comment | Document author | %%hidden%% | Private notes, hidden in display |
| Human comment | Any collaborator | {>> text --@chris} | Review feedback, questions |
| Agent comment | AI agent | {>> text --@agent:name[facets]} | Automated review, suggestions |
| Arrow shorthand | Any | -> text (converted to {>>}) | Quick annotation on current block |
| Approval | Human | {>> Approved. --@chris ✅} | Sign-off (same as Tasks ✅) |
| Rejection | Human | {>> Rejected. --@chris ❌} | Rejected (same as Tasks ❌) |
Author comments (Obsidian-compatible): Private, stored inline in markdown.
CriticMarkup annotations (new in BFM v3): Inline, attributed comments that agents and humans can emit directly in markdown.
{>> text --@user} = comment anchored to preceding block.--@name = human. --@agent:name[facets] = agent with identity and authority.!! suffix = resolved.Arrow trigger: Quick comment on the current block.
-> arrow is converted to a {>> ... --@user} comment inline at that location.Multiple comments on the same block form a thread. All stored inline, one after another:
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 !!}
The largest technology IPO since Arm Holdings.
BFM uses the same emoji set as Obsidian Tasks for consistent semantics across tasks and comments.
| Emoji | Meaning | In Tasks | In 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 |
📅 2026-04-15 means the same thing as a task with 📅 2026-04-15. The ```tasks and ```comments computed blocks both understand these signifiers.| Human | Agent | |
|---|---|---|
| Syntax | {>> text --@alice} | {>> text --@agent:name[facets]} |
| Visual | A Mint | AI Lavender |
| Resolve | !! suffix | No (humans resolve) |
| Approve | ✅ suffix | No |
Comments can have a checkbox, making them trackable like todos:
[ ] / [x] at the start of the comment text = task-comment. Use the same emoji signifiers as Obsidian Tasks: 📅 due date, ⏫ priority, 🔁 recurring, ➕ created.{>> ... } 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.
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.
A canvas has nodes (positioned rectangles) and edges (connections between them). Four built-in node types from the spec:
| Node Type | Required Fields | Description |
|---|---|---|
text | text (markdown string) | Markdown text block |
file | file (path), optional subpath (#heading) | Embedded file from the realm |
link | url | External URL preview |
group | optional label, background, backgroundStyle | Visual container for other nodes |
All nodes share: id, type, x, y, width, height, optional color (hex or preset 1-6).
```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" } ] } ```
| Field | Type | Description |
|---|---|---|
id | string | Unique edge identifier |
fromNode / toNode | string | Node IDs to connect |
fromSide / toSide | top | right | bottom | left | Which side the edge attaches to |
fromEnd / toEnd | none | arrow | Arrow direction (default: toEnd=arrow) |
color | hex or 1-6 | Edge color |
label | string | Text label on the edge |
| Preset | Color |
|---|---|
1 | Red |
2 | Orange |
3 | Yellow |
4 | Green |
5 | Cyan |
6 | Purple |
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).
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).
```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 } ] } ```
| Field | Type | Description |
|---|---|---|
| JSON CANVAS BASE (Obsidian compatible) | ||
id | string | Unique frame identifier |
type | string | text, file, link, group (JSON Canvas standard) |
x, y | number | World position in pixels |
width, height | number | Frame dimensions |
color | hex or 1-6 | Frame color |
| BFM SURFACE EXTENSIONS | ||
type: "card" | string | Boxel card frame (new node type) |
card | string | Card path (relative or absolute URL) |
format | string | fitted, embedded, or isolated |
isolatedMode | string | windowed (scroll, fixed height) or natural (auto-height, no scroll) |
isolatedZoom | number | 0.25 to 1.0 -- CSS zoom for isolated content. Renders card wider than frame, then scales down |
zOrder | number | Explicit layer position (lower = behind). JSON Canvas uses array order |
hidden | boolean | Soft-delete -- frame stays in data but doesn't render |
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.
| Format | Height | Overflow | Interactive | Resize | Use |
|---|---|---|---|---|---|
fitted | Fixed | Hidden | No (picture) | All 8 handles | Grid tiles, dashboard widgets |
embedded | Auto | Visible | No (picture) | Horizontal only | Detail panels, side-by-side comparison |
isolated windowed | Fixed | Scroll | Yes (live) | All 8 handles | Editable card in a window frame |
isolated natural | Auto | Visible | Yes (live) | Horizontal only | Full card at natural height |
isolatedZoom lets you see more of the card in a smaller frame by rendering at a wider width and CSS-scaling down.
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.
```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 } ] } ```
Frames can reference cards from any realm, not just the current one. Use absolute URLs:
```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" } ] } ```
| Feature | JSON Canvas 1.0 | BFM 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 |
.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.
| Use case | How |
|---|---|
| Dashboard | Fitted card frames for widgets, grouped by section |
| Card gallery | Grid of fitted frames, click to open isolated detail |
| Comparison | Same card in multiple frames at different formats/sizes |
| Design review | Cards at various fitted sizes to check responsive layout |
| Cross-realm moodboard | Cards from different realms on one canvas |
| Brainstorm | Text nodes for ideas, card nodes for evidence, edges for connections |
| Architecture | Text nodes for components, edges for data flow, groups for boundaries |
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.
: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.
![[my-drawing.excalidraw]]
```excalidraw { "type": "excalidraw", "elements": [ { "type": "rectangle", "x": 10, "y": 10, "width": 200, "height": 100 }, { "type": "text", "x": 50, "y": 40, "text": "Hello" } ] } ```
.excalidraw.md files. BFM accepts uncompressed Excalidraw JSON in a fenced block. Pasted Obsidian embeds (![[drawing.excalidraw]]) resolve as card embeds.```kanban ## Backlog - Design review - API documentation - [ ] Write migration guide ## In Progress - Parser implementation - [x] 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 not done due before next week priority is high path includes Projects ```
```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 # Welcome Introduction to the project --- # Key Findings <!-- .slide: data-background="#1a1a2e" --> ::card[./Cards/revenue-chart] --- # Next Steps - [ ] Ship v1 - [ ] Gather feedback ```
```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 case | Use | Why |
|---|---|---|
| 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 ::card | Obsidian compat / standalone card |
Like ```toc reads the document, these read the realm -- other cards that reference or relate to this one.
```backlinks ``` ```backlinks format: strip depth: 1 ```
```outlinks ```
```graph depth: 2 ```
```tags ``` ```tags scope: realm ```
Fenced renderers that go beyond Obsidian/GitHub. New content types native to BFM.
```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 ```
```gallery - ./assets/screenshot-1.png - ./assets/screenshot-2.png - ./assets/screenshot-3.png - ./assets/screenshot-4.png cols: 2 ```
```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] ```
```embed url: https://github.com/cardstack/boxel ```
```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 ```
```csv Name, Department, Salary Alice, Engineering, 145000 Bob, Design, 125000 ```
| Name | Department | Salary |
|---|---|---|
| Alice | Engineering | 145,000 |
| Bob | Design | 125,000 |
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.
```toc ``` ```toc depth: 2 style: numbered ```
```comments ``` ```comments filter: open ```
```tasks ```
- [ ] tasks + {>> [ ] } task-comments)| Fence | Data from | Options | Renders |
|---|---|---|---|
```toc | Document headings | depth, style | Nested linked heading list |
```table-of-contents | Document headings | Same (Obsidian alias) | Same |
```comments | Inline {>>} comments | filter: open, resolved, all | Counts + open task list |
```tasks | - [ ] + task-comments | filter: open, done, all | Progress bar + checklist |
```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.
--- renderers: toc: type: FancyTocField from: ./fancy-toc comments: type: ReviewPanelField from: ./review-panel ---
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.
%%%card stock-nvda type: StockTickerCard from: ./stock-ticker-card symbol: NVDA price: 892.50 change: 2.4 %%% ::card[#stock-nvda]
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.
## 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] :::
Report/
sprint-12.json
sprint-12/
TeamMember/
alice.json
bob.json
Milestone/
parser-done.json
renderer-wip.json
| Context | Syntax | Resolves 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 |
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.
%%%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] :::
adoptsFrom works. It writes YAML in a %%%card block. The card type (StockTickerCard) provides the schema -- the agent just fills in the fields.%%%field | %%%card | |
|---|---|---|
| Creates | Contained data (no file) | Real .json file |
| Has own URL | No | Yes |
| Linkable from other docs | No | Yes |
| Appears in realm search | No | Yes |
| Has fitted/embedded views | Embedded only | All formats |
| Survives if doc deleted | No (deleted with doc) | Orphaned (file remains) |
| Boxel equivalent | contains(FieldDef) | linksTo(CardDef) |
| Use for | Status, progress, metrics, config | People, projects, products, events, anything with identity |
Every BFM syntax element, organized by level. Each links back to where it's defined.
| Syntax | What | |
|---|---|---|
| LEVEL 1 -- ORIGINAL MARKDOWN | ||
# H1 ... ###### H6 | Headings | ↑ L1 |
**bold** *italic* ***both*** | Emphasis | ↑ L1 |
[text](url) | Link | ↑ L1 |
 | Image | ↑ L1 |
- item / 1. item | Lists | ↑ L1 |
> text | Block quote | ↑ L1 |
`code` / ```fenced``` | Code | ↑ L1 |
--- | Horizontal rule | ↑ L1 |
| LEVEL 2 -- COMMONMARK | ||
| Lazy continuation | Lines after > can omit prefix | ↑ L2 |
| List item alignment | Content aligns with text after marker | ↑ L2 |
| Tight vs loose lists | Blank line between items wraps in <p> | ↑ L2 |
| Block > inline precedence | Block structures win when ambiguous | ↑ L2 |
© © © | HTML entities (named, numeric, hex) | ↑ L2 |
| Info strings on fences | First word = language, rest = options | ↑ L2 |
| LEVEL 3 -- GITHUB FLAVORED | ||
| col | col | with :---|---: | Table | ↑ L3 |
- [ ] / - [x] | Task list | ↑ L3 |
~~text~~ | Strikethrough | ↑ L3 |
[^1] + [^1]: text | Footnote | ↑ L3 |
$inline$ / $$block$$ | Math (LaTeX) | ↑ L3 |
> [!NOTE] (5 types) | Alerts | ↑ L3 |
```mermaid ```geojson ```stl | Diagrams, 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/tag | Tags | ↑ L4 |
^block-id | Block reference | ↑ L4 |
> [!type]+/- (14+ types) | Callouts with fold | ↑ L4 |
--- YAML --- | Frontmatter | ↑ L4 |
```toc / ```dataview | TOC, 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 end | Conditional content (null = hidden) | ↑ L5 |
%%%field + map(...) | join | Repeated 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: JQXL | Formula (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 frontmatter | Card type | ↑ L5 |
renderers: in frontmatter | Fence overrides | ↑ L5 |
fields: in frontmatter | Field schema (shorthand) | ↑ L5 |
```lang id | Named fence (becomes referenceable field) | ↑ L5 |
| LEVEL 6 -- COMMENTS & ANNOTATIONS | ||
@chris | Human 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 |
-> text | Arrow 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 |
Need a source for this.