How-to
Part of the meshfox documentation.
A few common recipes, each pointing at a real, running example elsewhere in this repo rather than repeating it.
Structured docs instead of a Makefile
This very README is the worked example: every ## section is a node (see
"Concept" in README.md), and its runnable fences — CLI.canvas.md runs real CLI
invocations, DEVELOPMENT.canvas.md runs the real build/test/lint commands — replace
what would otherwise be a Makefile's targets. meshfox validate/meshfox check double as the pre-commit/CI gate ("Full check" in DEVELOPMENT.canvas.md),
and meshfox list prints every runnable block as a tree instead of grepping
the file for what's runnable.
A canvas's own Python environment
examples/python-venv.canvas.md — a
canvas-local .meshfox/<canvas filename>.venv/, created once and reported as a computed
meshfox:var (PYTHON) every other Python fence references via
interpreter="$PYTHON -u", so nothing hardcodes a path or depends on
whatever Python happens to be on $PATH. The venv/install step itself is
meshfox's own built-in interpreter="@python_venv": a text fence supplies
requirements.txt, while a toml fence supplies pyproject.toml, with no
shell script of your own to keep in sync. PYTHON points at a small launcher,
<venv>/bin/python3-mf, which is the venv's Python with the block's working
directory added to PYTHONPATH — so a block can import a module that sits
next to its canvas without a sys.path.insert(0, os.getcwd()) of its own.
Which Python. By default the venv is built with the python3 first on
$PATH — which may be too new for some packages (a wheel that doesn't exist yet
for the latest release). Two ways to pick another one. In a toml fence,
state it in the pyproject: requires-python = ">=3.10,<3.14" makes the
builtin use python3 if that fits and otherwise the newest python3.N that
does (the block fails, listing what it found, if none does). Or, for any fence,
name the interpreter per machine or project in .meshfox/config.toml:
[interpreters.python_venv]
python = "python3.12" # a command name or a path; wins over requires-python
An existing venv that contradicts either is emptied and rebuilt with the chosen Python (the requirements are installed again; a venv meshfox didn't create is never touched). With neither, a venv keeps whatever Python made it.
For independent environments in the same canvas, set name= and var=
inside the interpreter specification. Both are optional: name= selects a
subdirectory of the canvas's usual venv, and var= changes the exported
variable name (default PYTHON). The same name shares an environment;
different names isolate installed packages. For example:
<!-- meshfox:var name="XLS_PYTHON" from="xls-setup" -->
```text name="xls-setup" interpreter="@python_venv name=xls var=XLS_PYTHON" cache
python-calamine
```
```python name="read-xls" interpreter="$XLS_PYTHON -u"
from python_calamine import CalamineWorkbook
```
Names must start with an ASCII letter or underscore, followed by ASCII
letters, digits, underscores or hyphens. Output variable names follow the
same rules without hyphens. @node_env accepts the same options, with
NODE as its default output variable.
Node has the same shape: examples/node-env.canvas.md
uses interpreter="@node_env" — a text fence of npm packages (or a json
fence holding a package.json) is installed into a canvas-local
.meshfox/<canvas filename>.node/ (only when the list changed), and the
computed NODE variable is a launcher that makes those packages visible to
both require and import. Blocks then say interpreter="$NODE".
Rendering rich output (a pandas DataFrame preview)
examples/pandas-dataframe.canvas.md
— the output="markdown" fence attribute (SPEC.md's "Runnable code
fences"/"Cached output") splices a cached block's captured stdout into the
canvas as real Markdown instead of the default passive text fence, so a
command that already prints Markdown worth rendering — here, a pandas
DataFrame via df.to_markdown() — shows up as an actual table rather than
a wall of pipe characters. Builds on the same venv pattern as "A canvas's own
Python environment" above.
Running a block under a different language/tool
examples/interpreters.canvas.md — every
shape of interpreter= side by side: a bare command (python3), one with
its own flags (python3 -u), any other tool on $PATH (node), an
interactive tty block handing over a real REPL, and the same mechanism on
a file-type node instead of a fence.
A file node with an interpreter= is a full block, not just a shortcut:
examples/runnable-files.canvas.md shows
one with dependencies, cached output, meshfox:arg arguments passed both as
environment variables and as --name=value flags (argv="false" turns the
flags off), confirm, and the same run/chain/history controls a fence gets —
see SPEC.md's "Runnable file nodes".
A block's code is written to a temp file whose extension follows the fence
language, because runtimes pick a mode from it (Node reads a .tmp file as
CommonJS only, so an ESM import needs .js): python → .py,
javascript/js/node/mjs → .js, typescript/ts → .ts,
ruby/rb → .rb, perl/pl → .pl, php, lua, sql, go,
bash/sh/zsh → .sh. A language with no entry keeps .tmp. A block
that also needs to find files next to the canvas wants the tangle flag —
see "Importing a file that sits beside the canvas" below.
examples/services.canvas.md covers long-lived
services: graceful Stop/Restart, the YAML interpreter="@service" lifecycle
(start, stop, optional monitor, readiness and health checks), run-scoped
lease and defer cleanup, and oneshot/reuse for borrowing external resources
without stopping resources started outside meshfox. It also includes a runnable
Docker Compose stack. Ordinary service blocks now receive SIGTERM with a
30-second grace period before SIGKILL.
Importing a file that sits beside the canvas
examples/tangle.canvas.md — a runtime looks up
relative imports (Ruby's require_relative, Node's import "./helper.mjs"),
package.json and tsconfig next to the script file, and by default that
file is a throwaway in the system temp directory. Put the tangle flag next
to the block's interpreter= and the code instead runs from a hidden file
with a stable name in the block's working directory,
.meshfox-tangle-<canvas file>__<block>.<ext>, written only when the code
changes and left in place after the run. Add .meshfox-tangle-* to your
.gitignore. If the directory can't be written the run fails with a clear
error rather than quietly importing from somewhere else. Leftovers of blocks
that no longer exist are removed when the canvas's worker starts, and
meshfox prune clears those of renamed or deleted canvases.
To rebuild an environment of a canvas that still exists (say after the system
Python was upgraded), meshfox env list shows them and meshfox env rm <canvas> [--name NAME] [--python | --node] deletes them — then run the setup
block again with meshfox run --fresh, since a cached block does not notice
that its directory is gone. env rm refuses while a worker is running for the
canvas.
Two side effects of running from the project directory: a file watcher there
(nodemon, vite, ...) can see the file appear and change (it does not see a
re-run of unchanged code), and the runtime now finds the project's own
configuration — Node reads the nearest package.json, so in a
"type": "module" project a tangled block is an ES module (no require),
where the same block without tangle runs as CommonJS from the temp dir.
Bare package imports are a different matter: those are the job of
@python_venv / @node_env (see "A canvas's own Python environment").
Files as a pipeline's inputs and outputs
examples/artifacts.canvas.md — outputs=
and inputs= on a fence make files part of the dependency graph: a block
that reads artifact-demo/source.csv finds the block that produces it
without any deps= between them, and re-runs only when that file's content
changed (delete an output and it is restored). Handy for a data-processing
project where parser code, patches and downloaded CSV/JSON/ZIP files feed
one another.
Checking that documentation stays consistent
LICENSE.canvas.md below is the real, load-bearing
example: its every-direct-dep-is-documented constraint fails meshfox check whenever a Cargo.toml/package.json dependency has crept in with no
matching row in that file's license tables — the same mechanism
"Constraints" in REFERENCE.canvas.md walks through in isolation
(examples/constraints.canvas.md's
"Dependency audit" node).
Publishing a canvas as a static site
See "Static export" in EXPORT.canvas.md for the real meshfox static
invocation against examples/hello.canvas.md
and site-template/ as a standalone example. This README's own repo builds and
publishes itself with site-template-archive/ (scripts/build-site.sh, see .gitignore's
/site-dist entry) — the live result is
meshfox.orofarne.net.
A second brain for an LLM agent
examples/second-brain.canvas.md — one
memory per node, tagged by type (user/feedback/project/reference),
with a constraint fence enforcing that a feedback/project memory always
carries a **Why:** line so a later session can judge an edge case instead
of blindly following the rule. The same schema this repo's own coding-agent
sessions use for their persistent memory, outside the chat window itself.
Summarizing a chat as a canvas (Claude skill)
A skill for regular Claude (claude.ai / desktop — not Claude Code): ask it
to "summarize this conversation as a meshfox canvas" and it answers with a
ready .canvas.md file — decisions, open questions, risks and action items
as small nodes, with edges for cross-links. Download
summary-to-canvas.zip
from the latest release and upload it under Settings → Capabilities →
Skills, then check what it wrote with meshfox validate <file> and open it
with meshfox view <file>. The zip is built from
skills/summary-to-canvas by the release
workflow; that folder's own README has the build block and install steps.
Calling an AI agent from a block
examples/agent-prompt.canvas.md —
interpreter="@agent" is one of a small, fixed set of built-in macro
interpreters meshfox ships in its own binary (crates/core/src/builtins/,
alongside @python_venv above) — the fence body becomes a one-shot prompt
to claude -p/codex exec, or, on a tty block, an interactive CLI
session instead (agent.sh's own [ -t 1 ] check tells the two apart,
no separate meshfox mechanism needed).
env= locals and bound arguments interpolate right into the prompt as
$NAME/${NAME} — whole-token only ($TOPIC matches, $TOPICS doesn't),
and only names belonging to this block are substituted; $$NAME escapes
to a literal $NAME for anything that shouldn't be touched. No
shell-quoting of your own to write for any of it. Substitution is one pass:
inserted variable values are never interpreted as more references.
Like a bash block, @agent runs as the current user. Meshfox adds no agent
access restrictions; the agent's own tool, sandbox, approval, and directory
trust settings apply. tty changes interactivity, not this access contract.
Dependency results become stale when the builtin script or effective
provider setting changes.
Which provider a @name macro actually calls (claude, codex or opencode, for
@agent) comes from a small settings file meshfox itself reads —
interpreters.agent.provider in .meshfox/config.toml next to the canvas,
or ~/.meshfox/config.toml globally (local wins, key by key) — a fact
about the machine/what's installed, not something a meshfox:var should
have to prompt for on every run. Every dotted key in there is exported to
a macro's own process as MESHFOX_CONFIG_<PATH> (e.g.
MESHFOX_CONFIG_INTERPRETERS_AGENT_PROVIDER), so a hand-written
interpreter= script can read the same settings too.
provider = "opencode" runs opencode run -- "$prompt" (and
opencode --prompt "$prompt" on a tty block). provider = "custom" runs
any command of your own:
[interpreters.agent]
provider = "custom"
command = 'aider --yes --message "$1"' # plain block (output captured)
tty_command = 'aider "$1"' # tty block; defaults to command
The command is a shell snippet run as bash -c "$command" agent "$prompt":
the prompt (after $VAR interpolation) is $1 and also
$MESHFOX_PROMPT, never spliced into the command text, so quotes in it
can't break or inject anything. command is required for custom; a
changed command makes the block's earlier runs stale.
Making a dependency optional
examples/optional-deps.canvas.md — a
? after a deps= entry makes that edge conditional on a variable:
deps="fix?USE_AGENT" runs fix first only while the bool variable
USE_AGENT is true; fix?!USE_AGENT negates it, and
fix?PROVIDER=claude compares any variable with a value (?! negates the
comparison too). The condition belongs to the dependency, not to the block it
points at, so another block can still depend on fix unconditionally. A
variable named in a condition is resolved like one named in env= — it shows
up in configure and in the run form only for chains that reach the edge —
and a from=-computed one runs its source first. A false condition drops the
dependency from the chain and from the block's freshness, so flipping the value
makes the block stale. This is the way to keep an optional agent step
(interpreter="@agent", with its provider choice, $VAR interpolation and
tty mode) in the chain instead of wrapping it in a shell script. Conditions
are valid in deps= only; see SPEC.md's deps entry. The condition covers
the whole dependency: the producer of an inputs= file or the from= source
of an env= variable is not pulled in while it is off, and the matching input
is ignored.
Sharing code between blocks, and testing it (`extends=`)
examples/extends.canvas.md — extends="defs"
puts the code of another block in front of this one's and runs the joined text.
Keep the functions in one block (the definitions), and let every block that
needs them extend it: the step that does the work adds a line that calls them,
the tests add the assertions. There is no module system to set up, so it works
the same in python, bash, node or anything else with an interpreter=,
and the tests see everything the definitions define, private names included.
The extended block is only borrowed from — it is not run and is not a
dependency — so the extending block still declares its own deps=, inputs=
and outputs=; the definitions hold nothing but definitions. The extending
block inherits the interpreter= (and env=) of what it extends, writes its
own deps/inputs/outputs, and goes stale when a definition changes. Entries
may be chained and are included once, in order; a cycle, a language mismatch
or a parent with deps=/outputs=/tty is a meshfox validate error. Line
numbers an interpreter reports count the joined text. See SPEC.md's extends
entry.
Showing a diagram, formula or chart (SVG)
examples/svg.canvas.md — an SVG is an ordinary
Markdown image (), shown the same way in the
web UI, the TUI, meshfox static and meshfox pdf, with no renderer built
into meshfox: any command that prints an SVG will do (Graphviz, Typst,
Mermaid, PlantUML, Vega, matplotlib, ...). output="image" (SPEC.md's "Image
output") takes the block's stdout as the SVG and stores it as that image;
output="markdown" does the same when a script prints the image line itself.
An SVG follows the light/dark theme through currentColor and
var(--mf-fg, …), and one drawn for a white page can be backed per block with
output-attrs="bg=#fff" (MARKDOWN.md's "SVG images").
Browsing a huge table (CSV, Parquet, ...)
examples/tables.canvas.md — a file node with
display="table" (SPEC.md's "Table previews") shows its target as an
interactive, read-only grid in the web UI and, full screen with Enter, in the
terminal viewer: sortable by column, filterable per column (>10, =x,
^start, ~part, null, ...), searchable across every column, with the whole
value of any cell a click away. Only the rows on screen are ever fetched, so a
CSV with a million rows opens as fast as one with twenty — the canvas generates
one to try, and turns it into Parquet (read in place, no import) with duckdb
itself. Any format the duckdb command-line tool reads works, .csv.gz
included.
CSV/TSV previews use strict dialect validation by default. For an export with
mixed CRLF/LF line endings, set csv-strict="false" on that particular
meshfox:node type="file" display="table" comment. This setting applies
only to that node's preview, does not skip error rows, and does not change
how runnable import blocks read the file.
It needs that duckdb executable on PATH (or duckdb_path under [tables]
in the config above): meshfox never installs it, and a table node without it
says so rather than showing anything. The table is read-only — edit the file
with any editor and the node reloads — and meshfox static/meshfox pdf print
its first 100 rows as a plain grid, failing loudly if a table can't be read.
Typed variables, forms and file pickers
examples/ffmpeg.canvas.md — a cut-down ffmpeg
how-to that uses every variable type at once and one form fence to set them:
type="file" (an input with mode="open", an output with mode="save",
ext="mp4,mkv" as a picker hint), type="dir", select, int, bool and
plain strings. A dir or file field has a Browse… button in the web UI
(Ctrl-O in the terminal) that opens a file browser over the worker's file
system — where the blocks actually run, which may not be where the UI is — and
answers with an absolute path. SPEC.md's "File variables" and "Path pickers"
have the details.
type="date" and type="datetime" have their own recipe, "Dates, times and
calendars", just below.
A chosen path is also a permission: a dir value becomes a trusted location
a file node may read below, and a file value one trusted file — exactly that
one, never its folder — so [report]($OUTPUT_DIR/probe.json) or [clip]($INPUT)
previews what you picked, while a canvas's own text still cannot reach outside
its directory by itself.
Needs ffmpeg and ffprobe on PATH; the first block generates a test clip,
so the example works before you pick a video of your own.
Dates, times and calendars
examples/dates.canvas.md — two small calculators built on type="date" and type="datetime" variables: calendar days between two dates, and hours between two moments that each carry their own UTC offset. A form sets the values and the autorun blocks recalculate on Apply.
A date is YYYY-MM-DD; a datetime is RFC3339 with an offset (2026-10-25T14:30:00+03:00). Both can be typed by hand and picked from a calendar: the browser's native date and date-time controls (the latter beside an editable UTC-offset box) in the web UI, a calendar modal on Ctrl-O in the terminal ([Pick… ^o] after the field). Because a block always receives the canonical form, plain shell parameter expansion takes it apart — the example does its arithmetic in bash without date, which differs between macOS and Linux.
Every value is checked against its type, wherever it comes from: --set, prompts and forms are rejected on entry, and a bad value in the environment, the variable cache, shared config or a from= block's output fails the run with a message naming the variable and where the value came from (a stale cache entry is fixed with --set). SPEC.md's "Date and datetime variables" and "Date pickers" have the details.
Lists, map and reduce
examples/lists.canvas.md — a type="list" variable holds an ordered list of strings, and a dependency deps='square[n=*$NUMBERS]' runs once per element: a producer lists the work, a parameterized block handles one element, and the consumer reads the whole list to reduce. Several * bindings give the cartesian product; an empty list simply removes the dependency, and a list a from= block has not produced yet is expanded as soon as that block finishes.
The list itself has no separator. A producer writes it as one NAME[]=item line per element (or a JSON array), and every block that reads it chooses the shape in its own env=: env="NUMBERS|space" for shell words, env="IDS=NUMBERS|pg" for a PostgreSQL array, no suffix for JSON. A list with choices is a set of checkboxes in a form and a numbered menu at the terminal. SPEC.md's "List variables" has the formats and the rules.
Calling an application in another canvas
A native meshfox fence describes a child invocation as YAML: canvas, run, and optional set, fresh, no-deps, or skip-fresh fields. The caller prepares child input and confirmation before its own chain starts, streams the child's steps and output, and forwards cancellation to that specific child run. The child keeps its own worker, working directory, locks and history.
examples/canvas-call.canvas.md contains a runnable parent, an embedded execution diagram, input, cancellation and failure examples. Its separate child canvas shows the other side of the call.
Computing a Markdown table from document tags
Use a starlark render fence to turn the document tree into a live Markdown view, without launching an external interpreter. The Computed tag summary node in examples/constraints.canvas.md counts nodes by tag and prints a table with md.table().
Open the example in WebUI or TUI: the table appears below the collapsed source and updates when tags or nodes change. md builders return typed MarkdownFragment values, so links and future formatting helpers can compose inside tables without double escaping.
Static-site and PDF exports include the computed table as a snapshot of the document at export time. The exported site does not run Starlark in the browser; regenerate the export to update its results. The live document's source, cached-output regions and history never store the generated output.
Reading a single-node canvas as a document
A canvas containing only its root node opens as a full-page document in the web UI's view mode: title, scrollable content and the usual runnable blocks, with the toolbar still available. Edit mode keeps the regular canvas. Adding a child restores the canvas in view mode too; folding a larger tree does not activate the document layout. No option is needed, and the root's saved position and size are preserved.