v2.0 - npm install -g @markdownai/core

The AI Workflow Engine

Stop dumping entire files into your AI's context window. MarkdownAI phases your runbooks, injects live data, and delivers exactly what your agent needs -- one step at a time, never stale.

MarkdownAI workflow engine -- phase-based AI runbook architecture diagram showing @phase, @query, and MCP server integration
Built by -- Staff Content Engineer at MongoDB, creator of thedecipherist.com.

. .

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.

The difference between a prompt engineer's workflow and a production workflow is exactly this. Prompt people write the instructions. Production people eliminate every unnecessary thing Claude has to do before writing the instructions.

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 actual difference: Template engines make static output. MarkdownAI makes live context - context where the computation already happened, where the workflow is managed for you, and where the AI can work instead of gather.

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.

996
Tests passing
99
Features documented
11
Output formats
6
Packages

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

1

Install

npm install -g @markdownai/core
2

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
3

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.

@env

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.

@define

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.

@include

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.

@if

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.

| @render

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

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

@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

@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

@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

@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

@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

@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

@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

@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

@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

@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
@mkdir

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

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
listUnordered bullet list
numberedOrdered numbered list
linksClickable markdown links
tableGrid table with headers
codeFenced code block
inlineEmbedded scalar value
barHorizontal ASCII bar chart
flowASCII flow diagram with arrows
treeIndented ASCII tree
timelineLeft-to-right ASCII timeline
jsonPretty-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 / argsFull raw $ARGUMENTS string
argsListPositional args, shell-style parsed
arg0 arg1 arg2 arg3Shorthand positionals
CLAUDE_EFFORTlow, medium, high, xhigh, or max
CLAUDE_SESSION_IDCurrent Claude Code session ID
CLAUDE_SKILL_DIRDirectory 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
--verboseShow warnings in output
--strictTreat warnings as errors
--silentSuppress 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 serveStart the MCP server
mai initInstall 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 initCreate or import a security policy
mai security showDisplay the active security policy
mai security shell enableEnable shell command execution
mai security shell add <pattern>Add a command pattern to the allowlist
mai security http enableEnable 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.

MarkdownAIn8n / Make / Relay
Target userDeveloper building with AI agentsNon-technical operator
Workflow formatMarkdown file in your repoVisual node graph in a browser
Version controlGit-native, diff-friendlyExport JSON, not code-native
AI integrationMCP-native, phase-awareAPI calls as workflow steps
Live data injectionResolved at agent read timePulled at workflow trigger time
Context scopingOne phase per agent callFull payload per trigger
Installnpm install -g @markdownai/coreDocker stack or cloud subscription

Directive Reference

Every directive supported by MarkdownAI, grouped by category.

Header and Activation

DirectiveDescription
@markdownaiActivate MarkdownAI processing. Must be on line 1 (or first line after YAML frontmatter).
@markdownai v2.0Pin to a specific specification version.
@markdownai shell-inline="passthrough"Let Claude Code's !`cmd` syntax pass through unintercepted.

Environment and Variables

DirectiveDescription
@env VAR requiredDeclare a required variable. mai validate fails if unset.
@env VAR required maskedRequired and suppress from output.
@env VAR fallback=valueDeclare a default when variable is not set.
{{ env.VAR }}Inline interpolation of any environment variable.
{{ var ?? "default" }}Interpolation with explicit fallback.

Macros and Composition

DirectiveDescription
@define name ... @define-endDefine 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

DirectiveDescription
@if <expr>Include content if expression is true.
@elseif <expr>Alternate branch.
@elseFallback branch.
@if-endClose 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

DirectiveDescription
@foreach <var> in <source> ... @foreach-endRender 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

DirectiveDescription
@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-endQuery 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-endMake an HTTP request and use the response body.

Execution

DirectiveDescription
@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.

DirectiveDescription
@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-endSet a YAML frontmatter field. Supports field[append], field[N], nested field[N].sub.
@render-template ... @render-template-endRender a template with injected parameters and write the result. Idempotent by default; force overwrites.

Pipeline and Rendering

DirectiveDescription
| sort / sort -n / sort -rSort alphabetically, numerically, or in reverse.
| grep <pattern> / grep -v / grep -iInclude, exclude, or case-insensitive match.
| head -N / tail -NKeep first or last N lines.
| uniqDeduplicate consecutive lines.
| wc -lCount lines.
@render type="<format>"Render accumulated pipeline result in the specified format.

Phases and Graphs

DirectiveDescription
@phase <name> ... @phase-endOpen 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

DirectiveDescription
@markdownai-detect /Detect which loaded plugins match the current project.
@plugin-data name="..." /Return the descriptor for a named plugin.
@plugin-meta ... @plugin-meta-endPlugin identity block. Valid only inside *.plugin.md files.
@plugin-detect ... @plugin-detect-endDetection signals block. Declares required dirs, files, markers.
@plugin-layout ... @plugin-layout-endDirectory layout block. Describes the plugin's expected directory structure.
@plugin-conventions ... @plugin-conventions-endConventions block. Naming rules, file format expectations.

Caching

DirectiveDescription
@cache sessionCache in memory for the current run.
@cache session ttl=NSession cache, expires after N seconds.
@cache persistCache to disk across runs.
@cache persist ttl=NDisk cache with expiry.
@cache mock=./file.jsonAlways use fixture file, never call live source.

AI-Native

DirectiveDescription
@consumer=aiTag a block for AI consumers only.
@consumer=humanTag a block for human readers only.
@prompt <role> ... @prompt-endEmbed instructions for AI consumers (not in human output).
@constraint[severity] <text> /Declare a machine-readable rule.
@note ... @note-endHuman-readable source comment, always stripped. Add visible to render as a blockquote.

Sandbox builtins

Functions usable inside @if conditions and {{ }} interpolations.

FunctionDescription
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.

1

Install the CLI

npm install -g @markdownai/core
mai --version
2

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.

3

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
4

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.