What's new in v2
v2 is a breaking syntax change. Bare @end, @endif, @endswitch, and @on complete -> X are no longer accepted. Pin to ^2.0.0 (v1 stays on ^1.x and keeps working) and run the migration tool once over each v1 file:
node ~/projects/markdownai/packages/parser/scripts/migrate-v1-to-v2.mjs <file> --in-place
See the Migrating from v1 section of the user guide for the five transformation classes and a worked example.
Unified directive grammar
Every directive uses the same three forms - self-closing, block with attributes, or block with attributes and body. The v1 split between "block" directives (closed with bare @end) and "inline" directives (single-line only, silently dropped continuations) is gone.
# 1. Self-closing (atomic, no body, no continuation)
@import ~/path/to/macros.md /
@touch path="src/foo.ts" /
@on-complete next-phase /
# 2. Block with attributes (no body)
@db
using="mdd"
find="features"
where='id == "X"'
label=feature
@db-end
# 3. Block with attributes + body
@phase 0_branch_check
required=true
>
@call branch-guard /
@on-complete 0_5_repo_version_check /
@phase-end
@<name>-end close tags everywhere
The close tag carries the directive name, so nested blocks read clearly. No more guessing which block a bare @end closes.
@phase X
@if {{ ready }}
@foreach f in {{ files }}
- {{ f }}
@foreach-end
@if-end
@phase-end
@on-complete X / replaces arrow transitions
The v1 arrow syntax (@on complete -> target) is now @on-complete target /. Same engine behavior, JSX-style self-close. Future extensions like @on-error and @on-timeout slot in naturally.
Synchronous MongoDB worker
@db using="..." actually hits Atlas or self-hosted Mongo now. In v1 the directive was stubbed and emitted an "async execution required" warning. v2 runs read-only queries through a sync worker so the result is available in the same render pass.
Struct labels for dot-access
@db ... as=row label=feature captures the row into ctx.data[label], so {{ feature.source_files }} dot-access works on real arrays and nested objects. Same shape works for @read and any directive that materializes data.
New sandbox builtins
The expression sandbox grew a set of helpers usable inside @if conditions and {{ }} interpolations: parse_brief, read_section, extract_paths, read_markdown_section, now_iso, now_ms, parse_iso_ms, uuid_v4, truncate, to_json, and allowed.
Cross-call session state for skill flows
A skill_session_id keys per-(session x document) state inside the MCP server. @set values persist across resolve_phase calls in multi-phase flows, so a skill can collect values in phase 1 and read them back in phase 5 without round-tripping through the host.
Plugin loader and @markdownai-detect
Framework descriptors (mdd.plugin.md, others) declare project layout at render time. Documents pull layout facts from the descriptor instead of guessing - no more layout-inference hallucinations when the AI hasn't seen the project before.
@touch directive
Idempotent empty-file creation for scaffolding. Safe to re-run.
@touch path="src/rules/parser.ts" /
Migration tool
A one-shot script rewrites v1 files to v2 syntax mechanically. Idempotent - re-running on a v2 file is a no-op.
node ~/projects/markdownai/packages/parser/scripts/migrate-v1-to-v2.mjs <file> --in-place
Why Developers Switch to MarkdownAI
Context rot is killing your Claude Code sessions
Every serious Claude Code session eventually degrades. Not because the model fails -- because the context window fills. Past tool results, failed attempts, and full-file reads accumulate until the AI loses the thread. The community calls this context rot. The structural fix is not better prompting. It is scoping.
MarkdownAI's @phase directive implements scoping at the file level. A 20-phase deployment runbook only ever loads one phase at a time. Claude executes phase 3, calls next_phase, and phase 4 loads. Phases 1, 2, and 5 through 20 have never touched the context window. There is nothing to rot.
The pattern is well-documented: see Context Rot in AI Coding Agents (MindStudio) and the Two Context Bloat Problems (Agenteer) for independent analysis of the same failure mode.
Your CLAUDE.md should not be 600 lines
Anthropic's own best practice: keep CLAUDE.md under 200 lines. Every serious project exceeds that within a week. The result is a bloated file the AI has to read entirely on every session start, burning context before any work begins.
The MarkdownAI architecture replaces the monolithic CLAUDE.md with a thin entry point that calls @read on phase-specific files and injects live state on demand. Claude sees a focused, accurate slice -- not a 600-line document it has to search.
This pattern is also discussed in the official Claude Code Best Practices documentation (Anthropic Engineering).
Runbooks that never go stale
DevOps and engineering teams typically have dozens of runbooks nobody has time to keep current. The port changes, the endpoint moves, the flag flips -- but the runbook does not know. The AI follows the instructions. The wrong thing happens.
MarkdownAI runbooks do not store values. They declare data sources. The @query, @db, @http, and @env directives resolve against live system state at the moment the agent reads the file. A runbook that calls @query docker service ls hands Claude a current container list -- not what you typed last month.
What is MarkdownAI
Most developers who land here expect another template engine or a documentation generator. MarkdownAI is neither. It is a workflow execution engine -- the difference is not cosmetic.
A template engine (Jinja, Handlebars, EEx) renders a file once at build time and hands the output to a browser. The template is consumed, gone. MarkdownAI renders at read time, for an AI agent, and the output is scoped to exactly the phase that agent is currently executing. A browser has no context window. Claude does.
One Reddit commenter called it "basically jinja with a lil flava." Here is the precise reason that is wrong:
Jinja, Handlebars, and EEx are template engines. You write a template with
{{ user.name }}, run a build step, and get an HTML file. The template is gone.
The output goes to a browser. That's the whole job.
MarkdownAI is different in every dimension that matters.
The consumer is an AI, not a browser
Jinja renders for browsers that display HTML. MarkdownAI renders for Claude, which reads Markdown. A browser needs bytes fast. Claude needs accurate facts and focused context - it doesn't benefit from stale data delivered quickly.
Read time, not build time
Template engines run when you deploy. MarkdownAI runs when Claude opens the file. That
means every @query, @env, @http, and
@db directive resolves against the real current state of your system - not
the state from your last build. The document doesn't store values. It fetches them.
The MCP server does the computation so Claude doesn't have to
This is the part that actually matters. When Claude reads a MarkdownAI document through
the MCP integration, every directive resolves in the server layer before Claude sees any
content. The @if conditions have already been evaluated. The database queries
have already run. The environment variables have already been substituted. Claude receives
resolved facts - not a list of conditions it needs to think through.
Without MarkdownAI, Claude hits a doc and has to figure out what's true. It stops to run a shell command. It stops again to check an env var. It stops again to verify a condition. Each stop costs context and interrupts the actual work. With MarkdownAI, those interruptions don't happen. The document arrives pre-resolved.
Something nobody talks about: every time Claude stops to check an environment variable, verify a file exists, confirm a port is open -- that's roughly 2 seconds gone. Tool call out, wait, result back, Claude re-orients, continues.
In a real workflow that's not 1 check. It's 15. That's 30 seconds of dead time and 15 context interruptions where Claude has to re-establish where it was.
MarkdownAI's MCP layer does all of that before Claude touches the session. Environment is pre-validated, state is pre-loaded, constraints are already in context. Claude reads phase 1 and immediately works.
No stops. No re-orientation. No context bloat from housekeeping that a script could handle in milliseconds.
Phases are not template partials
The @phase directive is where the comparison to template engines fully breaks
down. A MarkdownAI phase is a named, lazy-loaded chunk of a workflow document. A 20-phase
runbook doesn't load all 20 phases into Claude's context at once - the MCP server serves
one phase at a time. Claude reads phase 1, works through it, then calls
next_phase to advance. The server returns phase 2. Claude never holds the
full workflow in context. The document manages state. Claude follows steps.
This means a complex deployment runbook, a multi-step debugging workflow, or a long onboarding sequence can be arbitrarily large without ever flooding the AI's context window. Each phase is self-contained, each transition is explicit, and Claude never has to juggle what's relevant now versus what comes later.
Template engines have no concept of this because browsers don't have context windows.
The Three Reasons AI Workflows Break
Every developer who builds seriously with Claude Code or AI agents hits the same wall. It is not the model. It is the architecture.
Context rot. Your AI's output quality degrades as the context window fills with history, tool results, and noise from earlier in the session. By step 12, it is hallucinating variable names that do not exist.
Stale data. Your runbook tells the AI your API is at
localhost:3000. It moved to 8080 last Tuesday. The AI does not know. It acts
on what the doc says, not what is true right now. Static text cannot track a moving system.
Cold-start amnesia. Every new Claude Code session starts from zero. The AI re-derives the same conclusions from scratch. There is no memory of phase 3 state from the session that ended an hour ago.
MarkdownAI solves all three. The @phase directive exposes only the current
workflow step -- the rest of the document does not exist to the AI yet. The
@query, @db, @env, and @http directives
inject live system state at the moment the AI reads the file. The MCP server preserves
session state across next_phase calls so a 20-step runbook survives context
resets.
How the AI Workflow Engine Works
One line changes everything. Add @markdownai to the top of any
.md file and the mai tool treats it as a live document.
Everything after that line is standard Markdown, extended with directives. Directives
become their results. Conditions are evaluated. Macros expand. Data sources are queried.
The Six Packages
MarkdownAI is a six-package monorepo. Each layer has a single, well-defined job:
| Package | Name | Role |
|---|---|---|
parser/ |
@markdownai/parser |
Converts .md files to an AST - inert, no execution |
engine/ |
@markdownai/engine |
Walks the AST, runs directives, assembles output |
renderer/ |
@markdownai/renderer |
Formats data into 11 output styles (table, bar chart, flow, etc.) |
mcp/ |
@markdownai/mcp |
MCP server - serves live document execution to Claude and other AI tools |
core/ |
@markdownai/core |
The mai binary and all CLI commands |
vscode/ |
markdownai (VS Code) |
Language detection, syntax highlighting, snippets, completions, hover, diagnostics |
The parser is intentionally inert - it never executes anything. Security enforcement lives in the engine, not in individual directives. This means security gates apply regardless of which directive triggers the operation.
How to Install and Run Your First MarkdownAI Workflow
Install
npm install -g @markdownai/core
Create a live document
Save this as status.md:
@markdownai v2.0
# Project Status
**Branch:** {{ @query git branch --show-current }}
**Tests:** {{ @query pnpm test 2>&1 | tail -1 }}
**Last commit:** {{ @query git log --oneline -1 }}
## Source Files
@list ./src/ match="**/*.ts" | sort | @render type="table" columns="name,size"
## Environment
@if {{ env.NODE_ENV == "production" }}
Running in **production** mode.
@else
Running in **{{ env.NODE_ENV }}** mode.
@if-end
Render it
mai render status.md
Every directive runs. The output is clean Markdown with real data from your system - not values someone typed in last month.
What Directives Does MarkdownAI Support?
MarkdownAI extends standard Markdown with a small set of directives. They compose naturally - you can pipe data through transforms, embed expressions in prose, define reusable blocks, and conditionally show sections based on any runtime value.
Inline Interpolation
Embed live values directly in prose. Works in paragraphs, headings, and table cells.
Use {{ env.VAR }}, {{ @query git branch --show-current }},
or {{ @date format="YYYY-MM-DD" }} anywhere in your document.
Unset variables evaluate to empty string rather than an error.
Environment Variables
Declare the variables your document depends on. required causes
mai validate to fail if the variable is unset. masked
prevents the value from appearing in rendered output. fallback=value
provides a default when the variable is absent.
Macros
Define reusable blocks once with @define and call them anywhere with
@call. Macros can accept named parameters, contain any directives,
and be shared across documents by importing them from a shared file.
Includes and Imports
Pull in file content with @include - supports a line range for embedding
source code in docs. Paths accept {{ expression }} segments, so
@include ./{{arg0}}-mode.md routes to a different file per invocation
without any conditional chain. Use @import for definitions-only imports.
Conditionals
Full conditional system. Check env vars, file existence, string comparisons, and
compound logic with && and ||. The same expression
operators work in @if conditions, data query where filters,
and {{ }} interpolations.
Pipe and Render
Chain transforms before rendering: sort, grep,
head, tail, uniq, wc -l. All
built-in transforms are cross-platform pure Node.js. Feed the result into any of 11
output formats via @render.
What Data Sources Can MarkdownAI Inject at Runtime?
MarkdownAI can pull from the filesystem, structured files, databases, HTTP APIs, and shell commands. Every data source is jailed by default - see the Security section for how the gates work.
@list
List filesystem entries, or pull structured data from JSON, YAML, and CSV files.
Filter with glob patterns, where clauses, and depth limits. Specify
columns, sort order, and output format inline.
@list ./src/ match="**/*.ts"
@list ./config.yaml path=$.services
@list ./data.csv where="status == active"
@read
Extract a specific value from a structured file using dot-notation paths. Supports JSON, YAML, TOML, and CSV. Useful for pulling a single config value, a version number, or a record field into prose.
@read ./package.json path=$.version
@read ./config.yaml path=$.server.port
@read ./data.csv column=email where="active == true"
@query
Run a shell command and use its output. Supports a label= option to
store the result for reuse in conditions and interpolations. Disabled by default -
requires an explicit allowlist in your security config.
@query git branch --show-current
@query bash -c "find src -name '*.ts' | wc -l"
@query bash -c "git status --porcelain" label=dirty
@http
Make HTTP requests and embed the response. Assert status codes with
expected=, pass headers and bodies, and cache responses to avoid
hammering external APIs. All @http calls are blocked by default
until you add domains to the allowlist.
@http GET https://api.example.com/status
@http GET {{ env.API_ENDPOINT }}/health expected=200
@http GET https://api.example.com/data @cache persist ttl=3600
@db and @connect
Register a database connection with @connect, then query it using
MarkdownAI's database-agnostic query language. A document querying Postgres looks
identical to one querying MongoDB. Supports SQLite, PostgreSQL, MySQL, MSSQL, and
MongoDB.
@connect primary type="postgres" uri=env.POSTGRES_URI
@db using="primary" find="users" where="active==true"
@db using="primary" aggregate="sales" group="region" sum="revenue" | @render type="bar"
@phase and @graph
Structure a document as a sequential workflow with named phases and
@on-complete transitions. The MCP server exposes
list_phases, resolve_phase, and next_phase
tools so AI tools can navigate the workflow programmatically. Use
@graph to build a dependency graph from document relationships.
@event
Fire a named signal with a structured payload to one or more transports while a
document renders. Use it for progress indicators, live status updates, and debugging.
The mcp transport delivers events synchronously to the AI tool reading
the document. All other transports (log, file,
http, db) are fire-and-forget and never slow down rendering.
@event name='build-done' data='{"status":"ok"}' transport='mcp'
@event name='progress' data='step 2 of 4' transport='log,file'
@foreach and @set
Walk a list source with @foreach and render the body once per item. Bind
values to names with @set for downstream reuse. The two together turn a
document into a small program that can produce per-feature status, per-file rollups,
and templated sections without manual repetition.
@foreach doc in {{ @list ./.mdd/docs/ match="*.md" }}
@read-frontmatter path="{{ doc }}" field="status" label=s /
- {{ doc }} ({{ s }})
@foreach-end
@set today = {{ now_iso() }} /
@template and @data
@template inlines another .md file at the call site and binds it to
a data context, like a partial in Angular or Vue. @data composes a single object
from any in-scope values (db results, set variables, env fallbacks) using
<key> = <expression> assignments, dot-notation for nested keys, and
... spreads. Inside the partial, the bound value is accessible as
{{ data.* }}. Reads inherit from the caller's scope; writes stay local, so the
same partial can be rendered repeatedly inside @foreach without name collisions.
@data myReport
...baseConfig
site.name = "Acme"
site.theme = "dark"
@data-end
@foreach row in {{ users }}
@template ./user-card.md data=row /
@foreach-end
@read-frontmatter and @hash
Pull a single YAML field with @read-frontmatter without parsing the
whole document. Compute a content hash with @hash for change detection
or doc-integrity checks. @hash supports any Node crypto algorithm and
a regex-based line-exclude for self-referential fields.
@read-frontmatter path="doc.md" field="status" label=s
@hash path="doc.md" algo=sha256 length=8 label=h
@test and @check
@test runs the project test suite. @check runs typecheck,
lint, or build. Both inline the full combined output and expose label
(full text), label_exit (exit code), and label_summary
(one-line recognized summary from vitest, jest, playwright, tsc, eslint, prettier).
@test command="pnpm test" label=results
@check command="tsc --noEmit" label=tc
Filesystem Writes
Five directives that mutate the filesystem: @mkdir, @copy,
@append-if-missing, @update-frontmatter, and
@render-template. All gated behind filesystem.write_enabled
and confined to write_root. Built for bootstrap scaffolding, idempotent
config updates, and template-driven file generation.
@mkdir .mdd/docs
@copy from="tpl.md" to="doc.md" if-missing
@update-frontmatter path="doc.md" field="status" value="done"
Plugin System
Frameworks built on MarkdownAI can register themselves as plugins using a
*.plugin.md file. The plugin declares its name, detection signals
(required dirs, files, version fields), directory layout, and conventions in a
structured format. @markdownai-detect finds matching plugins in a project.
@plugin-data returns a named plugin's full descriptor. The
available_directives MCP tool returns the complete directive catalog so AI
tools never need to guess what's supported.
@markdownai-detect as=info include="layout" /
@plugin-data name="mdd" /
Security
Security is a first-class concern in MarkdownAI, not an afterthought. Every external operation is jailed by default and must be explicitly enabled. The security layer lives in the engine - it applies regardless of which directive triggers the operation.
📁 Filesystem Confinement
All file access (@include, @read, @list,
@tree) is confined to the document root - by default, the directory
containing the document being rendered. No directive can read files above this root
using path traversal. Content masking runs before any output is written, so sensitive
values matching your configured patterns are replaced with [MASKED].
🔒 Shell Execution Jail
@query shell execution is disabled by default (allowShell: false).
Enable it with an explicit allowlist:
mai security shell add "git *". All executions are subject to deny
patterns, jailRoot confinement, and optional audit logging. Commands
not matching the allowlist are blocked even with the master switch on.
🗃 Database Query Jail
Database access is read-only by default. Write operations, DDL statements, and
full-table scans on large collections are blocked unless explicitly permitted.
Configure allowed operations, blocked SQL keywords, and maximum result sizes via
mai security db commands.
🌐 HTTP Request Jail
All @http calls are blocked by default. Enable HTTP and add specific
domains to the allowlist:
mai security http enable, then
mai security http add-domain api.example.com. Wildcard subdomains
are supported. Requests to non-allowlisted domains fail regardless of configuration.
Immutable built-in rules. Some rules are hardcoded and cannot be disabled by any configuration - not by security policy, not by environment variables, not by document directives. Cloud metadata endpoints are always blocked (169.254.169.254, metadata.google.internal, and all similar credential-theft endpoints). Path traversal sequences are always blocked in jailed contexts. Content masking always runs before caching - credentials can never be stored in plain text. These rules are implemented as frozen, readonly arrays in the engine code.
Caching
Add @cache to any directive to avoid redundant fetches on repeated renders.
Cache modes range from in-memory session caching to persistent disk storage with expiry.
The mock mode is particularly useful for testing - your document runs with real directives
but against predictable fixture data.
| Mode | Behavior |
|---|---|
@cache session |
Store in memory for the current mai run. Gone when the process exits. |
@cache session ttl=N |
Session cache that expires after N seconds within the run. |
@cache persist |
Write to disk. Survives process restarts and re-renders. |
@cache persist ttl=N |
Disk cache with expiry. Stale entries are refetched automatically. |
@cache mock=./fixture.json |
Always return data from a local file - never call the live source. For testing. |
Cache Management
mai cache clear # clear all cached entries
mai cache clear my-doc.md # clear for one document
mai cache show # inspect what is currently cached
mai cache seed my-doc.md # pre-populate by running all fetches ahead of time
Content masking always runs before caching. Sensitive values that match your configured
secret patterns are replaced with [MASKED] before anything is written to
the cache. Credentials can never be stored in plain text, even in session memory.
Output Formats
The renderer supports 11 output formats. All are plain ASCII that renders correctly everywhere - in terminals, GitHub, VS Code preview, and AI context windows.
| Format | Output |
|---|---|
list | Unordered bullet list |
numbered | Ordered numbered list |
links | Clickable markdown links |
table | Grid table with headers |
code | Fenced code block |
inline | Embedded scalar value |
bar | Horizontal ASCII bar chart |
flow | ASCII flow diagram with arrows |
tree | Indented ASCII tree |
timeline | Left-to-right ASCII timeline |
json | Pretty-printed JSON |
Pipe Chains
Formats become most useful when combined with the pipe operator. A data fetch, a few transforms, and a render sink - all on one line:
@list ./src/ | sort | @render type="tree"
@query bash -c "df -h" | @render type="table"
@db using="primary" aggregate="sales" group="name" sum="revenue" | @render type="bar"
@query bash -c "git log --oneline -20" | grep "feat" | @render type="numbered"
How to Integrate MarkdownAI with Claude and AI Agents
MarkdownAI was designed with AI-native workflows in mind. Every feature is built to serve both humans reading rendered output and AI tools consuming live document context. There are three distinct integration points.
💻 MCP Server (11 tools)
Run mai serve to start an MCP server. Claude, Cursor, and any
MCP-compatible tool can query documents, execute directives, navigate phases, and get
live data - without reading raw source files. The exposed tools cover the full
document lifecycle: read_file, list_phases,
resolve_phase, next_phase, call_macro,
get_env, execute_directive, get_constraints,
invalidate_cache, available_directives, and the v2
get_session_state for cross-phase state lookup.
🔌 PreToolUse Hook
Run mai init to install a PreToolUse hook into your AI client. When the
AI calls Read on a MarkdownAI document (bare @markdownai or
YAML frontmatter followed by it), the hook blocks the raw read and returns a redirect
message with the full MCP tool catalogue and a five-step workflow. The AI fetches the
file through the MCP server instead - live data, every read, with zero ambiguity
about how to proceed.
⚡ SessionStart Hook
mai init also installs a SessionStart hook. If your project
has a CLAUDE-MarkdownAI.md file at the root, the hook renders it on every
session start (or resume, clear, compact) and injects the rendered output into the
AI's session context. Your CLAUDE.md is never touched. The rendered text
lives only in conversation context - no file mutation, no commit risk, no stale state
after an unclean session end.
🤖 AI-Native Features
@prompt embeds instructions for AI consumers that never appear in human
output. @constraint expresses rules as structured, machine-readable blocks.
@consumer=ai targets sections to AI readers only - token-efficient format
mode strips narrative prose, reducing consumption by up to 35% compared to the same
document in human mode.
Skill Context Variables
When a MarkdownAI document is used as a Claude Code skill file, the full slash command invocation context is available inside the document. This enables genuine engine-evaluated dispatch - the engine routes to the correct section before Claude even sees the file.
| Variable | Description |
|---|---|
ARGUMENTS / args | Full raw $ARGUMENTS string |
argsList | Positional args, shell-style parsed |
arg0 arg1 arg2 arg3 | Shorthand positionals |
CLAUDE_EFFORT | low, medium, high, xhigh, or max |
CLAUDE_SESSION_ID | Current Claude Code session ID |
CLAUDE_SKILL_DIR | Directory containing the skill file |
@markdownai v2.0
@include ./{{arg0}}-mode.md /
That single line replaces a five-branch @if/@elseif chain. Add a JS || default for when the skill is called with no argument:
@include ./{{arg0 || 'audit'}}-mode.md /
Use allowed() to restrict which values are accepted. It returns the value itself when it matches the list, or false when it doesn't - so the || default kicks in automatically:
@switch {{ allowed(argsList[0], ["audit","build","op"]) || "build" }}
@case "build"
Running build mode...
@case "audit"
Running audit mode...
@case "op"
Running op mode...
@switch-end
Works inside @foreach too - the loop variable is available in the path expression on every iteration:
@foreach section in intro,features,pricing
@include ./{{section}}.md /
@foreach-end
CLI Reference
All commands share these universal flags:
| Flag | Description |
|---|---|
--env <file> | Load a .env file for environment variables |
--cwd <path> | Run as if in a different directory |
--verbose | Show warnings in output |
--strict | Treat warnings as errors |
--silent | Suppress all output except fatal errors and security alerts |
Commands
| Command | Description |
|---|---|
mai render <file> | Execute and print rendered markdown to stdout |
mai render <file> -o <path> | Execute and write result to a file |
mai render <file> --skill-args "<args>" | Render with full Claude Code skill context. Sets ARGUMENTS, parses argsList, defaults data_root to cwd. |
mai render <file> --skill-dir <path> | Set CLAUDE_SKILL_DIR for the render. |
mai render <file> --skill-effort <low|medium|high> | Set EFFORT for the render. |
mai render <file> --skill-session-id <uuid> | Set CLAUDE_SESSION_ID for the render. |
mai validate <file> | Check for errors and warnings; exits 1 on error |
mai parse <file> | Output the document AST as JSON |
mai eval "<expression>" | Evaluate a single expression against the environment |
mai strip <file> | Remove all directives; output plain Markdown |
mai serve | Start the MCP server |
mai init | Install the PreToolUse and SessionStart hooks into your AI client. Idempotent. |
mai build <file> -o <output> | Render and write to disk (alias for render -o) |
mai watch <file> -o <output> | Watch for changes and re-render automatically |
mai cache clear [file] | Clear cache for one document or all |
mai cache show [file] | Inspect cache entries |
mai cache seed <file> | Pre-populate cache by running all fetches |
mai security init | Create or import a security policy |
mai security show | Display the active security policy |
mai security shell enable | Enable shell command execution |
mai security shell add <pattern> | Add a command pattern to the allowlist |
mai security http enable | Enable HTTP requests |
mai security http add-domain <domain> | Add a domain to the allowlist |
mai security db add <connection> | Register a database connection |
mai security db test "<query>" | Test whether a query would be permitted |
How does MarkdownAI prevent context rot?
Context rot is the gradual degradation of AI agent output quality as the context window fills with accumulated noise -- conversation history, intermediate tool results, failed attempts, and full-file reads. By session hour two, even capable models hallucinate variable names that do not exist and repeat approaches they already tried. The problem is not the model. It is the architecture.
MarkdownAI's @phase directive eliminates context rot at the file level. Each phase is a named, isolated block. The MCP server's resolve_phase tool returns exactly one phase per call. The agent never sees phases it is not currently executing. Previous phases never entered the current context window. There is no accumulated noise because there is no accumulation.
Related directives: @phase, @on-complete. Related MCP tools: next_phase, get_session_state.
How does MarkdownAI automate AI runbooks with live data?
A runbook is only useful if it reflects the current state of the system. Static runbooks go stale the moment the system changes. The standard response -- "update the runbook" -- fails at scale because teams have hundreds of runbooks and nobody has time.
MarkdownAI runbooks do not store state. They declare data sources. The @db directive queries your database when the agent reads the file -- not when you wrote it. The @http directive hits your health endpoint live. The @query directive runs a shell command and returns the actual current output. Combined with @phase, a MarkdownAI runbook is a self-contained execution system: phase-gated, live-data-backed, and safe to run against a production system because every fact is fetched fresh on every execution.
How do you keep CLAUDE.md small at scale?
Anthropic recommends keeping CLAUDE.md under 200 lines. In practice, every project that uses Claude Code seriously exceeds this limit within a week -- conventions, schemas, connection strings, workflow steps, and environment context all accumulate. The result is a bloated file that burns context before any actual work begins.
The MarkdownAI architecture solves this with a thin entry point. The CLAUDE.md or CLAUDE-MarkdownAI.md file stays under 20 lines. It calls @read on phase-specific instruction files that live in .mdd/. It fetches live state from the database or environment at session start. The agent sees a focused, accurate, pre-resolved context -- not a file it has to parse to find what is relevant to the current step.
How does MarkdownAI solve AI agent cold-start amnesia?
Every new Claude Code session starts from zero. The model has no memory of what phase 3 decided, what state was built up in the last session, or what variable values were set two hours ago. Complex multi-session workflows break at every context reset because the agent re-derives the same conclusions from scratch.
MarkdownAI's MCP server solves this at the protocol level. A skill_session_id keys per-session, per-document state inside the server. @set values written in phase 1 are readable in phase 5 even across full session resets. The document manages state. The agent follows steps. A 20-phase deployment runbook survives overnight without losing position.
How is MarkdownAI different from n8n, Make, and visual workflow builders?
Visual workflow builders (n8n, Make, Relay, Zapier) and MarkdownAI solve different problems for different audiences. Visual builders are designed for non-technical operators connecting SaaS tools via a drag-and-drop interface. MarkdownAI is designed for developers who want their workflow definition to live in the repository as a text file, reviewed in pull requests, diffed in git, and executed by an AI agent via MCP.
| MarkdownAI | n8n / Make / Relay | |
|---|---|---|
| Target user | Developer building with AI agents | Non-technical operator |
| Workflow format | Markdown file in your repo | Visual node graph in a browser |
| Version control | Git-native, diff-friendly | Export JSON, not code-native |
| AI integration | MCP-native, phase-aware | API calls as workflow steps |
| Live data injection | Resolved at agent read time | Pulled at workflow trigger time |
| Context scoping | One phase per agent call | Full payload per trigger |
| Install | npm install -g @markdownai/core | Docker stack or cloud subscription |
Directive Reference
Every directive supported by MarkdownAI, grouped by category.
Header and Activation
| Directive | Description |
|---|---|
@markdownai | Activate MarkdownAI processing. Must be on line 1 (or first line after YAML frontmatter). |
@markdownai v2.0 | Pin to a specific specification version. |
@markdownai shell-inline="passthrough" | Let Claude Code's !`cmd` syntax pass through unintercepted. |
Environment and Variables
| Directive | Description |
|---|---|
@env VAR required | Declare a required variable. mai validate fails if unset. |
@env VAR required masked | Required and suppress from output. |
@env VAR fallback=value | Declare a default when variable is not set. |
{{ env.VAR }} | Inline interpolation of any environment variable. |
{{ var ?? "default" }} | Interpolation with explicit fallback. |
Macros and Composition
| Directive | Description |
|---|---|
@define name ... @define-end | Define a reusable macro block. |
@call name / | Expand a macro at this location. |
@call name param=value / | Expand a macro with named parameters. |
@include ./path.md / | Include another file's content verbatim. |
@include ./file.ts lines=N-M / | Include specific line range. |
@import ./path.md / | Import definitions (macros, connections, env defaults) from another file. |
@import ./path.md @cache session / | Import with session caching. |
Conditionals
| Directive | Description |
|---|---|
@if <expr> | Include content if expression is true. |
@elseif <expr> | Alternate branch. |
@else | Fallback branch. |
@if-end | Close conditional block. |
Operators: ==, !=, <, >,
<=, >=, &&, ||,
!, startsWith, endsWith, includes,
file.exists, file.isFile, file.isDir,
file.containsLine(path, regex), file.containsSection(path, heading),
file.frontmatterField(path, field)
Iteration and Variables
| Directive | Description |
|---|---|
@foreach <var> in <source> ... @foreach-end | Render the body once per item. Source can be a directive expression, a frontmatter list field, a label, or a comma-separated literal. |
@set <var> = "literal" / | Bind a literal value. |
@set <var> = {{ expr }} / | Bind an interpolated expression. |
Data Sources
| Directive | Description |
|---|---|
@list <path> / | List filesystem entries or structured data from JSON/YAML/CSV. |
@read <path> / | Read and extract a value from a structured file. |
@read-frontmatter path="..." field="..." / | Read a single YAML field from a document's frontmatter. |
@hash path="..." algo=sha256 length=N / | Compute a content hash with optional line-exclude regex. |
@tree <path> / | Render a directory tree. |
@date format="..." / | Current date/time in any format. |
@count <path> "<pattern>" / | Count files matching a pattern. |
@connect <name> type="<db>" uri=env.VAR / | Register a named database connection. |
@db ... @db-end | Query a collection or table. Synchronous Mongo worker in v2. |
@query <command> / | Execute a shell command and use its stdout. |
@query <command> label=name / | Execute and store result in named label. |
@http ... @http-end | Make an HTTP request and use the response body. |
Execution
| Directive | Description |
|---|---|
@test command="..." label=name / | Run the test suite. Inlines full output. Exposes name (full text), name_exit (exit code), name_summary (recognized one-liner). |
@check command="..." label=name / | Run typecheck / lint / build. Auto-detects from package.json scripts when command= is omitted. |
Filesystem Writes
All write directives require filesystem.write_enabled = true. Confined to write_root and allowed_write_paths. Immutable always-block rules still apply.
| Directive | Description |
|---|---|
@touch path="..." / | Idempotent empty-file creation for scaffolding. Safe to re-run. |
@mkdir <path> / | Create a directory. Recursive by default. |
@copy from="..." to="..." [if-missing] / | Copy a file. if-missing makes it idempotent. |
@append-if-missing path="..." text="..." / | Append a line only if not already present. |
@update-frontmatter ... @update-frontmatter-end | Set a YAML frontmatter field. Supports field[append], field[N], nested field[N].sub. |
@render-template ... @render-template-end | Render a template with injected parameters and write the result. Idempotent by default; force overwrites. |
Pipeline and Rendering
| Directive | Description |
|---|---|
| sort / sort -n / sort -r | Sort alphabetically, numerically, or in reverse. |
| grep <pattern> / grep -v / grep -i | Include, exclude, or case-insensitive match. |
| head -N / tail -N | Keep first or last N lines. |
| uniq | Deduplicate consecutive lines. |
| wc -l | Count lines. |
@render type="<format>" | Render accumulated pipeline result in the specified format. |
Phases and Graphs
| Directive | Description |
|---|---|
@phase <name> ... @phase-end | Open a named phase block. |
@on-complete <target> / | Transition to next phase or macro on completion. Replaces v1 arrow syntax. |
@graph / | Build a dependency graph from document relationships. |
Plugin System
| Directive | Description |
|---|---|
@markdownai-detect / | Detect which loaded plugins match the current project. |
@plugin-data name="..." / | Return the descriptor for a named plugin. |
@plugin-meta ... @plugin-meta-end | Plugin identity block. Valid only inside *.plugin.md files. |
@plugin-detect ... @plugin-detect-end | Detection signals block. Declares required dirs, files, markers. |
@plugin-layout ... @plugin-layout-end | Directory layout block. Describes the plugin's expected directory structure. |
@plugin-conventions ... @plugin-conventions-end | Conventions block. Naming rules, file format expectations. |
Caching
| Directive | Description |
|---|---|
@cache session | Cache in memory for the current run. |
@cache session ttl=N | Session cache, expires after N seconds. |
@cache persist | Cache to disk across runs. |
@cache persist ttl=N | Disk cache with expiry. |
@cache mock=./file.json | Always use fixture file, never call live source. |
AI-Native
| Directive | Description |
|---|---|
@consumer=ai | Tag a block for AI consumers only. |
@consumer=human | Tag a block for human readers only. |
@prompt <role> ... @prompt-end | Embed instructions for AI consumers (not in human output). |
@constraint[severity] <text> / | Declare a machine-readable rule. |
@note ... @note-end | Human-readable source comment, always stripped. Add visible to render as a blockquote. |
Sandbox builtins
Functions usable inside @if conditions and {{ }} interpolations.
| Function | Description |
|---|---|
allowed(value, list, opts?) | Returns value when it is in list, otherwise false. Combine with || for a safe default. |
parse_brief(text) | Parse a structured brief block into a field map. |
read_section(path, heading) | Return the body of a section in a markdown file. |
read_markdown_section(path, heading) | Same as read_section with explicit markdown handling. |
extract_paths(text) | Extract file paths from a block of text. |
now_iso() | Current time as an ISO 8601 string. |
now_ms() | Current time as a Unix millisecond timestamp. |
parse_iso_ms(iso) | Parse an ISO 8601 string into a Unix millisecond timestamp. |
uuid_v4() | Generate a UUID v4. |
truncate(text, n) | Truncate a string to n characters. |
to_json(value) | JSON-stringify a value for use in attribute strings. |
VS Code Extension
The MarkdownAI VS Code extension provides full IDE support for .md files
that begin with @markdownai. Open the Extensions panel
(Ctrl+Shift+X / Cmd+Shift+X), search for
MarkdownAI, and click Install. Or install from the
VS Code Marketplace.
Live Preview
Open any MarkdownAI file and click the preview icon in the editor title bar, or run MarkdownAI: Open MarkdownAI Preview from the Command Palette. The panel opens to the side, runs the engine on the saved file, and shows rendered output. It refreshes automatically every time you save.
Language Detection
Any .md file whose first line is @markdownai is
automatically re-tagged as the markdownai language type, activating all
extension features. Files with YAML frontmatter before @markdownai are
also detected.
Syntax Highlighting
TextMate grammar covers all MarkdownAI directives: @markdownai,
@define, @phase, @if, @include,
@import, @env, @call, @list,
@read, @db, @http, @query,
@render, @prompt, @constraint,
@cache, and {{ }} interpolations.
Snippets and Completion
15+ tab-triggered snippets for all directives. Type @def and expand
to a complete @define/@define-end block, @if to
a full conditional, @phase to a phase skeleton. As you type
@, completions show all valid directives with descriptions.
Navigation
Hover over any @define name or @call to see the macro
definition inline. Press F12 on any @call name to jump to
the @define that declares it, even across files linked by
@import. Right-click any @define name for Find All References.
Diagnostics
The extension checks documents as you type and reports:
@call to undefined macros (error),
@include/@import pointing to missing files (error),
@env variables without a fallback (warning, configurable),
and undefined macros in @call positions (warning, configurable).
Architecture
MarkdownAI is a six-package npm workspaces monorepo, all TypeScript with strict mode throughout. Each package has a single, well-bounded responsibility.
| Package | Role |
|---|---|
@markdownai/parser |
AST production only. Parses .md files to a typed node tree. Intentionally inert - no execution, no I/O. |
@markdownai/engine |
Walks the AST, runs directives, resolves env variables, manages the pipe chain, applies caching, and enforces security gates. |
@markdownai/renderer |
Formats data into 11 output styles. Accepts structured data from the engine and produces ASCII-safe Markdown. |
@markdownai/mcp |
MCP server with 11 tools. Serves live document execution, phase navigation, constraint access, directive introspection, and cross-phase session state to AI tools. |
@markdownai/core |
The mai binary. Wires the other packages together and exposes all CLI commands. |
markdownai (VS Code) |
Language detection, TextMate grammar, snippets, completions, hover, go-to-definition, find references, diagnostics, live preview. |
The separation between parser and engine is intentional. The parser never executes anything - you can safely parse any document in any environment. Security enforcement sits in the engine, between the directive runner and every external system. 996 tests across all packages, including unit tests for every directive, E2E CLI pipeline tests, MCP protocol conformance tests, and a dedicated AI-native feature test suite.
Installation
MarkdownAI installs globally via npm. One command puts mai on your path.
Node.js 18 or higher required.
Install the CLI
npm install -g @markdownai/core
mai --version
Install the VS Code extension
Open Extensions (Ctrl+Shift+X), search MarkdownAI, click Install. Gives you syntax highlighting, snippets, completions, hover, diagnostics, and live preview.
Install the PreToolUse hook
Lets your AI client (Claude Code, Cursor) automatically render MarkdownAI documents before reading them. Claude always sees live data, not stale source files.
mai init
Initialize a security policy
Before using data sources that touch external systems, set up a security policy for
your project. This creates .markdownai/security.json and walks you
through enabling the gates you actually need.
cd your-project
mai security init
Platform note: Works on macOS, Linux, and Windows. WSL is recommended
on Windows for the shell command features (@query with shell execution
enabled). Built-in pipe transforms (sort, grep, head, tail, uniq, wc -l) are
cross-platform pure Node.js - no shell required.