Skip to main content

runxmd — run your Markdown files

The simplest thing: write - @python, - @node, - @go (or any of 10 language plugins) directly in a .md file. Run it. Get a rendered output file with your prose intact and every code block replaced by its result. No new syntax. No @workflow required to get started.

PyPI version Downloads status deps python


The idea in one sentence

runxmd adds state and execution to .md files. You keep writing Markdown exactly as you do today — it still renders in GitHub, VS Code, and every viewer. The difference is that bare - @plugin steps execute directly, output renders back inline, and @memory persists state across runs. Static doc → stateful doc. Same file.

The bigger goal — save tokens, stop making models recompute. When a document already carries the results of its code (the render output), an LLM reading it doesn't have to mentally execute the snippets, guess at outputs, or burn tokens reasoning through logic it can't actually run. The answer is already in the file. runxmd turns "here's code, imagine what it prints" into "here's code and exactly what it printed" — so the model reads facts instead of computing them. Deterministic output, computed once by the real interpreter, reused by every reader (human or model) forever after.


What's new in v1.0.3 — the trust layer

"The answer is already in the file" only helps if you can tell the answer is still the answer. v1.0.3 makes that checkable, and makes a render safe to diff and safe to gate CI on.

Feature What it does
Provenance headers Every render / results / output file starts with an invisible <!-- runxmd-provenance --> comment: SHA-256 of the source, runxmd version, UTC time, platform, interpreter versions, and which steps were non-deterministic.
runxmd verify <render> Re-hashes the source. Exit 0 = current, 3 = STALE (source changed since the render), 2 = no header. Accepts multiple files.
run --strict Any failed step → non-zero exit. runxmd run is now a CI gate.
run --check Rebuild the render in memory, diff against the committed one, exit 1 on drift. A doctest for the whole document. Writes nothing.
Output normalization (default on, --raw off) Paths under the doc dir → relative, $HOME~, hostname → HOST, \/. Renders diff cleanly across machines. Plus a per-step redact: param.
run --pure Refuse non-deterministic steps (@http, @llm); exit 2 if any are present. The render is then a computed fact, not a sample. Such steps are also tagged inline and in provenance.
session: step param Opt in to shared scope — a variable from one step visible in the next. Default stays isolated.
run --cache Opt-in content-addressed cache: skip a language step whose plugin, params, interpreter version and script bytes are unchanged (--force to bust).
timeout: / stdin: / if: step params Kill a runaway step (exit 124); feed stdin; guard a step on memory.* / context.
run --json One JSON execution trace: every step's stdout, stderr, exit code, duration.
write projection Now records status: / exit_code: / stderr: per step — a re-run diff catches a step that started failing without printing anything.
GRAMMAR.md The step grammar is not YAML. Every divergence is now documented, and runxmd validate rejects (rather than silently misparses) tabs, folded scalars, flow collections, anchors, unknown plugins, and bad @on_done hooks.
CI artifacts A composite GitHub Action (uses: shubham10divakar/xmd@v1.0.3), a CI workflow, and pre-commit hooks.

Details: Commands · runxmd verify · Step params · Sharing state · Use in CI · SPEC-v0.0.3.md §6.


The simplest possible file

No @workflow. No @memory. No annotations at all — just prose and code steps.

# Analysis

This document runs three Python snippets and renders the results inline.

- @python
  run: |
    print("=== Arithmetic ===")
    x = 10
    print("x =", x)

This section explains what the next snippet does.
Feel free to write as much prose as you like between steps.

- @python
  run: |
    print("=== String ===")
    msg = "hello world"
    print(msg.upper())

Final note before the last snippet.

- @python
  run: |
    print("=== List ===")
    nums = [1, 2, 3, 4, 5]
    print("sum:", sum(nums))

Closing prose — steps are done.

Run it:

runxmd run analysis.md

You get analysis_render.md — your original document with every code block replaced by its output, prose preserved exactly as written:

# Analysis

This document runs three Python snippets and renders the results inline.

=== Arithmetic ===
x = 10

This section explains what the next snippet does.
Feel free to write as much prose as you like between steps.

=== String ===
HELLO WORLD

Final note before the last snippet.

=== List ===
sum: 15

Closing prose — steps are done.

The source file is never modified. The render file is your shareable, LLM-readable output. Re-run any time to refresh it.


Worked examples in this repo

Two complete, runnable showcases live in examples/showcase/. Each is a real .md you can run, paired with its committed render output so you can see exactly what runxmd produces — the render file is the token-saving artifact: a model reads the precomputed results instead of trying to execute the code in its head.

1. Polyglot capability showcase

examples/showcase/readme_showcase.mdreadme_showcase_render.md

Runs Python, Node.js, Perl, and PowerShell — inline snippets and external scripts — at basic / medium / advanced levels, with tables, blockquotes, and headings woven between the steps to prove all Markdown survives untouched.

cd examples/showcase
runxmd run readme_showcase.md

2. File-existence guardrails

examples/showcase/file_checks_test.mdfile_checks_test_render.md

The common "does this file/dir exist before I proceed?" guardrail at three levels: a single inline check, a multi-file inline check, and an advanced external script that audits a whole directory and prints a PASS/FAIL summary.

Before (source — code the reader would have to mentally execute):

## Level 1 — Simple: does a specific file exist?

- @python
  run: |
    import pathlib
    target = pathlib.Path("scripts/basic.py")
    if target.exists():
        print(f"  FOUND    : {target}")
        print("  GUARDRAIL PASS: required file is present")
    else:
        print("  GUARDRAIL FAIL: required file is missing")

After (file_checks_test_render.md — the answer is already in the file):

## Level 1 — Simple: does a specific file exist?

=== Simple File Check ===
  FOUND    : scripts\basic.py
  Size     : 648 bytes
  Is file  : True

  GUARDRAIL PASS: required file is present

The prose, heading, and tables are identical between the two — only the - @python block is replaced with its real, computed output.


Output modes

Every run produces an output file automatically. You control the format with an @on_done hook — or leave it out to get the default.

Hook in @on_done Output file Content
(none — default) {stem}_render.md Prose kept, steps replaced with results, @sections stripped
render / render(name.md) same, explicit Same as default, named
results / results(name.md) {stem}_results.md Step outputs only — no prose, no code, no @sections
write / write(name.md) {stem}_output.md Full replica with result: fields injected into each step

Default (render) is best for sharing and LLM consumption — it reads like a notebook. results is best when you want only the raw output. write is best when you want to re-run the file and see diffs over time.

Example — explicitly choose render:

@on_done
render(reports/analysis_rendered.md)

Example — output only:

@on_done
results

Install

pip install runxmd

Zero third-party dependencies — pure Python standard library (≥ 3.9).

Or from source:

git clone https://github.com/shubham10divakar/xmd.git
cd xmd
pip install -e .

Running the tests

pip install -e ".[test]"
pytest

Platform compatibility

runxmd runs on Windows, Linux, and macOS — the core runtime is pure Python stdlib with no platform-specific code.

Platform Status Notes
Windows Full support
Linux Full support
macOS Full support

PowerShell note: On Windows, @powershell uses powershell -ExecutionPolicy Bypass. On Linux and macOS, it uses pwsh (PowerShell Core) — install from https://aka.ms/install-powershell if needed.


Security & trust model

runxmd executes code embedded in a Markdown file. Treat a runnable .md exactly as you would treat a shell script: only run files you trust.

What this means in practice:

  • runxmd run file.md executes the code inside it. @python, @node, @shell, the *_script plugins, etc. all run real commands on your machine with your permissions. There is no sandbox.
  • Only run .md files you wrote or have reviewed. A document from an untrusted source can contain - @shell / - @python steps that do anything you could do from a terminal. This is the same trust decision as bash somescript.sh.
  • The source file is never modified by default. Output goes to a new _render.md / _results.md / _output.md file. Memory write-back into the source only happens with the explicit --write-back flag.
  • @llm and agent --autonomous run model-generated commands. With --autonomous, the LLM both writes and runs the steps — that is arbitrary code execution directed by a model. Use it only on goals and in environments where that is acceptable.
  • No network or filesystem access beyond what your steps request. runxmd itself makes no outbound calls (except @http/@llm when you use them) and installs nothing — detect-don't-install means a missing interpreter fails loudly rather than fetching anything.
  • Inspect before running. Use runxmd parse file.md to see the structured steps, or runxmd validate file.md to list sections, before executing an unfamiliar document.

In short: the power of runxmd is that the document is the program. The responsibility that comes with it is the same as any executable — run only what you trust.


Check what's available on your machine

Before writing a multi-language doc, run:

runxmd check

It scans every interpreter, shows what's installed, and gives install hints for anything missing:

runxmd check — interpreter availability

  Language     Plugin            Executable   Status      Version
  ---------------------------------------------------------------
  @python      @python_script    python       ✓  found    Python 3.14.5
  @node        @node_script      node         ✓  found    v24.15.0
  @typescript  @typescript_script ts-node     ✗  missing  install: npm install -g ts-node
  @ruby        @ruby_script      ruby         ✗  missing  install: https://...
  @bash        @bash_script      bash         ✓  found    GNU bash 5.3.9
  @go          @go_script        go           ✗  missing  install: https://go.dev/dl/
  @r           @r_script         Rscript      ✗  missing  install: https://cran.r-project.org/
  @php         @php_script       php          ✗  missing  install: https://...
  @perl        @perl_script      perl         ✓  found    v5.42.2
  @powershell  @powershell_script powershell  ✓  found    5.1.26100

  5 of 10 languages available.

Only install the languages you actually use — runxmd never installs anything itself.


.md and .xmd — same Markdown, one is a signal

runxmd runs both .md and .xmd files identically. The only difference is the name:

  • .md — a regular Markdown file that may or may not have runnable steps.
  • .xmd — the same format, byte-for-byte. The .xmd extension is a label that signals "this file is meant to be executed."
runxmd run notes.md       # runs — finds steps inside
runxmd run pipeline.xmd   # identical engine, identical grammar

Adding @workflow when you need it

For simple scripts the bare - @plugin syntax is enough. Add @workflow when you need named workflows — so you can run one selectively, or link it from a task.

@workflow deploy
- @shell
  run: docker pull myapp:latest
- @shell
  run: docker restart myapp
runxmd run deploy.md --workflow deploy   # run only this workflow

Stateful docs with @memory

@memory makes a Markdown file a system instead of a static doc.

@memory
name: "world"
runtime.status: "pending"

@workflow hello
- @python
  run: |
    print("hello {{ memory.name }}")

@on_done
set: memory.runtime.status = "done"
render
  1. Read-in — memory keys are loaded at run start.
  2. Substitution{{ memory.key }} anywhere in a step resolves live.
  3. Write-back — use --write-back to persist runtime.* keys back into the source file after the run.

Field ownership

  • runtime.* keys — only the runtime writes these (via @on_done set:).
  • Everything else — yours. The runtime reads them but never overwrites them.

The full section set

Section Purpose Grammar
@goal Describes intent for humans/agents free prose
@memory Key-value state, optionally persisted key: value lines
@tasks Checklist the agent can tick - [ ] / - [x]
@workflow <name> Named group of steps - @plugin + params
@on_done Hooks that run after workflows finish set: / render / results / write

Steps written outside any @workflow (top-level in the file) run as an implicit unnamed workflow — no annotation needed.


Plugins

A step is - @plugin plus indented params:

Inline code plugins

Write code directly in the document using run: |:

Plugin Language Key params
@print text
@shell Shell (/bin/sh on Linux, cmd on Windows) run
@python Python run
@node JavaScript run
@typescript TypeScript (needs ts-node) run
@ruby Ruby run
@bash Bash run
@go Go run
@r R (needs Rscript) run
@php PHP run
@perl Perl run
@powershell PowerShell (powershell on Windows, pwsh on Linux/macOS) run

External script plugins

Point at an existing file on disk using path::

Plugin Runs Key params
@python_script .py file path, args (optional)
@node_script .js file path, args
@typescript_script .ts file path, args
@ruby_script .rb file path, args
@bash_script .sh file path, args
@go_script .go file path, args
@r_script .R file path, args
@php_script .php file path, args
@perl_script .pl file path, args
@powershell_script .ps1 file path, args
- @python_script
  path: scripts/analyze.py
  args: --input data.csv

path: is resolved relative to the MD file's directory — so scripts/foo.py always means "next to the file being run", regardless of where you invoke runxmd from. Absolute paths also work. args: is split on spaces and passed to the interpreter.

Other plugins

Plugin Does Key params
@http Make an HTTP request url, method, body
@write Write a file (creates dirs) path, content
@read Read a file into output path
@llm Call an LLM (needs ANTHROPIC_API_KEY) prompt, model, max_tokens

Detect, don't install. Language plugins use whatever is already on your machine. If an interpreter is missing, the step fails with a clear message and the run continues — runxmd never installs anything for you.

Prerequisites: runxmd itself has zero dependencies (pure Python ≥ 3.9). Each language plugin requires its interpreter on PATH. Run runxmd check to see exactly what is available on your machine before writing a multi-language doc.

Multi-language example

Mix languages freely in the same file:

# Polyglot Report

- @python
  run: |
    data = [1, 2, 3, 4, 5]
    print("Python sum:", sum(data))

- @node
  run: |
    const nums = [1, 2, 3, 4, 5];
    console.log("JS sum:", nums.reduce((a, b) => a + b, 0));

- @perl
  run: |
    my @nums = (1..5);
    printf "Perl sum: %d\n", eval join("+", @nums);

Each step runs in isolation. If an interpreter is missing, that step is marked ✗ and execution continues with the next step.

Sharing state between steps — session:

Isolation is the default (and the reason step order doesn't matter). When you do want a variable from one step visible in the next, give both steps the same session: name:

- @python
  session: calc
  run: |
    total = sum(range(100))

- @python
  session: calc
  run: |
    print("total:", total)     # sees `total` from the step above

Same-session steps re-run the earlier code as a prelude, so side effects in earlier session steps (prints, file writes, requests) happen again each time a later step runs. Use it for building up a computation; keep the default isolation for everything else.


Watch mode

runxmd watch report.md                  # re-run and re-render on every save
runxmd watch report.md --interval 0.5   # poll faster
runxmd watch report.md --max-runs 3     # stop after 3 runs

Agent mode

runxmd agent turns the doc from a program you run into a goal that runs itself:

Read @goal → plan @tasks → execute each task → update @memory → write back
runxmd agent project.md              # plan (if no tasks), then run linked workflows
runxmd agent project.md --replan     # regenerate tasks from the goal
runxmd agent project.md --autonomous # LLM generates AND runs steps for unlinked tasks
runxmd agent project.md --dry-run    # show the plan without executing or writing

Planning requires ANTHROPIC_API_KEY. See examples/AGENT.xmd.


Commands

runxmd run <file> [--workflow NAME] [--write-back] [--strict] [--check] [--pure] [--raw] [--no-provenance]
runxmd watch <file> [--interval S] [--max-runs N] [--write-back] [--strict] [--pure] [--raw]
runxmd agent <file> [--replan] [--autonomous] [--model M] [--max-tokens N] [--dry-run]
runxmd verify <render> [--source FILE]
runxmd check
runxmd parse <file>
runxmd validate <file>
runxmd --version
Flag Applies to Effect
--workflow NAME run, watch Run only the named workflow
--write-back run, watch Persist runtime.* memory back into the source file (default: off, source untouched)
--strict run, watch Exit non-zero if any step fails — use as a CI gate
--check run Don't write; compare a fresh render against the committed one and exit non-zero on drift (a doctest for the whole document)
--pure run, watch Refuse non-deterministic steps (@http, @llm); exit 2 if any are present
--cache run, watch Reuse a cached result for a language step whose plugin, params, interpreter version and script bytes are unchanged (--force to ignore existing entries)
--timeout SECONDS run, watch Default per-step timeout; a step's own timeout: param wins
--json run Print a JSON execution trace to stdout (suppresses normal output)
--raw run, watch Don't normalize step output (keep absolute paths, \, $HOME, hostname verbatim)
--no-provenance run, watch Omit the runxmd-provenance header from output files

Step params: timeout:, stdin:, if:

Any code step also takes:

- @python
  timeout: 10               # kill after 10s (exit 124)
  stdin: "input fed to the script"
  if: memory.enabled        # skip the step unless the guard holds
  run: |
    import sys; print(sys.stdin.read())

if: guards: memory.<key> (truthy), not memory.<key>, memory.<key> == "x" (also != > < >= <=), context (rolling summary set). A skipped step counts as ok and renders nothing.

runxmd verify — is this render still current?

Every render / results / output file carries an invisible provenance header:

<!-- runxmd-provenance
source: report.md
source_sha256: 3f9a…c21
runxmd_version: 1.0.3
generated_utc: 2026-08-27T09:14:22Z
platform: linux-x86_64
interpreters: {python: Python 3.14.5}
non_deterministic_steps: []
-->

runxmd verify report_render.md re-hashes report.md and exits 0 if it still matches, 3 if the source has changed since the render was generated (STALE), 2 if there's no header. Wire it into a pre-commit hook or CI so a stale render can't be trusted by accident.

Use in CI

Raw commands — in any workflow step:

pip install runxmd
runxmd verify docs/report_render.md         # 0 fresh · 3 stale · 2 no header
runxmd run docs/report.md --check           # rebuild in memory, diff, exit 1 on drift
runxmd run docs/report.md --strict          # exit non-zero if any step fails

GitHub Action — this repo ships a composite action:

# .github/workflows/docs.yml
jobs:
  renders-are-current:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shubham10divakar/xmd@v1          # runxmd verify
        with:
          # verify: "docs/*_render.md"         # default: all tracked *_render.* / *_results.* / *_output.*
          check: "docs/report.md README.md"    # also `runxmd run --check` these sources

A stale render fails the job with a ::error:: annotation.


Design principles

  • Compute once, read forever. The render output carries real results so models and humans read facts instead of recomputing them — fewer tokens, no guessing.
  • Run only what you trust. The document is the program; treat a runnable .md like a shell script.
  • No annotation required to get started. A file with bare - @plugin steps runs.
  • Source files are read-only by default. Output always goes to a new file.
  • Render is the default output. Prose is preserved; only code blocks are replaced.
  • Cross-platform. Runs on Windows, Linux, and macOS without changes.
  • Your Markdown, not a new format. Everything outside @sections is untouched prose.
  • Inline-first. Code lives in the document; the runtime is a thin dispatcher.
  • Detect, don't install. Use what's on the machine; fail clearly when it's not.
  • Stdlib only. Zero dependencies, including @http.

Status & roadmap

Current: v1.0.3 — see SPEC-v0.0.3.md for the full contract. (The spec document keeps its own v0.0.x numbering, independent of the package version; SPEC-v0.0.3.md is the current one.)

  • ✅ Parser, executor, CLI (run / watch / agent / verify / parse / validate / check)
  • ✅ Provenance headers + runxmd verify — a render carries the SHA-256 of its source
  • run --strict / run --check — use a document's outputs as a CI doctest
  • ✅ Output normalization (paths, $HOME, hostname, separators) + redact: — diffable renders
  • run --pure — refuse non-deterministic steps so the render is a computed fact
  • ✅ Plugins: shell, http, filesystem, llm
  • ✅ Inline language plugins: Python, Node.js, TypeScript, Ruby, Bash, Go, R, PHP, Perl, PowerShell
  • ✅ External script plugins: @python_script, @node_script, and one for every supported language
  • ✅ Bare steps without @workflow — implicit unnamed workflow
  • ✅ Render output (default) — prose preserved, code replaced with results
  • ✅ Results output — step outputs only, for LLM consumption
  • ✅ Write output — replica with result: fields for re-running
  • ✅ Memory: read / substitute / write-back, with field-ownership safety
  • ✅ Reactive runxmd watch
  • ✅ Agent engine (@goal → auto-generate @tasks → execute → update memory)
  • runxmd check — interpreter availability report with versions and install hints
  • ✅ Cross-platform: Windows, Linux, macOS (PowerShell uses pwsh on Linux/macOS)
  • ⏳ Declarative events (@on_file_change, @daily, @on_commit)
  • ⏳ Portable @task abstraction
  • ⏳ Multi-agent / distributed

Contributions and ideas welcome.


License

MIT — see LICENSE.

Release files for runxmd 1.0.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for runxmd 1.0.3
File Size Uploaded
runxmd-1.0.3.tar.gz 111.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for runxmd 1.0.3
File Interpreter ABI Platform
runxmd-1.0.3-py3-none-any.whl Python 3 none any Details

Total release size: 161.2 kB

Release files / runxmd-1.0.3.tar.gz

Download URL runxmd-1.0.3.tar.gz
Size 111.1 kB
Tags Source
SHA-256 checksum
How to use checksums
6e6e8770db4c396642248a6807a6486ecca1e5f14e572d0d662bfb0083a06700
BLAKE2b-256 checksum
How to use checksums
8e678dfe070e8b45cb81c3bf357d1b64c3e88b9d43f6b4fa3dfbaf95556742b5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.6

Release files / runxmd-1.0.3-py3-none-any.whl

Download URL runxmd-1.0.3-py3-none-any.whl
Size 50.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
10e979380e17d113ed08d3817f9710b8a9059c8f3e5949ec713a1a297de76926
BLAKE2b-256 checksum
How to use checksums
daf088a0c538014c3729fb0cbd4331f96aa410a72c4155c3b6b0bd1a7706b12c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.6

Release history Release notifications | RSS feed

1.0.4

2 release files

This release

1.0.3 This release

2 release files

1.0.2

2 release files

1.0.1

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page