DEV Community

Cover image for Compose your agent prompts once, compile them to every harness
Dean Sharon
Dean Sharon

Posted on

Compose your agent prompts once, compile them to every harness

If you build LLM agents, you probably write the same prompt more than once. A Markdown config for one tool, a rules file for another, and then the actual [{role, content}] messages your SDK sends. The instructions are the same. The formats aren't, so you copy, tweak, and watch them drift.

I know I'm not the only one, because people build whole codegen pipelines to deal with it. One popular repo, wshobson/agents, generates its agent configs for six platforms (Claude Code, Codex, Cursor, OpenCode, Gemini, Copilot) from a single Markdown source of truth, using a Makefile. That is a lot of hand-rolled machinery for "write it once, emit it everywhere." I wanted that without owning a build script, so I wrote a small compiler.

What MDS is

MDS (Markdown Script) is a template language for composable prompt engineering. You write Markdown with imports, variables, functions and conditionals, and it compiles ahead of time to clean Markdown, or to a JSON chat-message array. There is no runtime. The compiled output is exactly what your tool or SDK reads, so nothing new runs inside your agent loop.

Composing a prompt from parts

Variables come from frontmatter and interpolate with single braces:

---
lang: TypeScript
---
You are a senior {lang} code reviewer.
Enter fullscreen mode Exit fullscreen mode

Define a reusable piece once with a function:

@define reviewer(lang):
You are a senior {lang} code reviewer. Flag correctness bugs first, then style. Keep it concise.
@end
Enter fullscreen mode Exit fullscreen mode

Then import it wherever you need it, under an alias:

@import "./_persona.mds" as p

{p.reviewer("TypeScript")}
Enter fullscreen mode Exit fullscreen mode

Files prefixed with _ are partials: dependencies that never get emitted on their own. Change the persona in one place and everything that imports it recompiles.

The part that matters for agents: @message

The same source can compile to the message array your SDK sends, using @message blocks:

@import "./_persona.mds" as p

@message system:
{p.reviewer("TypeScript")}
@end

@message user:
Review this diff:
{diff}
@end
Enter fullscreen mode Exit fullscreen mode

Running mds build review.mds produces JSON:

[
  { "role": "system", "content": "You are a senior TypeScript code reviewer. Flag correctness bugs first, then style. Keep it concise." },
  { "role": "user", "content": "Review this diff:\n..." }
]
Enter fullscreen mode Exit fullscreen mode

A template that contains @message blocks compiles to JSON. Everything else compiles to Markdown. The output format is decided by the content, not a command-line flag. So the prompt you compose in Markdown becomes the messages your code passes to the model, instead of concatenating strings in Python.

Generating harness configs

Because a persona or ruleset is just an import, one source can feed several targets. Your Claude Code file and your Cursor rules can both import the same _rules.mds, each adding only what's specific to them. Edit the shared rule once, rebuild, and both are correct. @extends and @block let a child template inherit a base and override only the parts that differ, which is how you collapse a family of near-identical agent files to one base plus small deltas.

The build also fails on things plain text never catches: undefined variables, import cycles, and functions called with the wrong number of arguments. A broken prompt becomes a failing build instead of a silent bug an agent reads at runtime.

Install

The CLI is Rust:

cargo install mds-cli
mds build system.mds        # writes system.md, or system.json if it has @message blocks
mds watch .                 # recompile on save
mds check .                 # validate without rendering
Enter fullscreen mode Exit fullscreen mode

If you'd rather stay in JS/TS, the compiler ships as a library and bundler plugins:

npm install @mdscript/mds
Enter fullscreen mode Exit fullscreen mode
import { compile } from '@mdscript/mds'
// or import a .mds file directly via the Vite/Webpack/Rollup/Rspack plugin:
import systemPrompt from './prompts/system.mds'
Enter fullscreen mode Exit fullscreen mode

What's next, honestly

It's v0.3 and a small project. No editor or LSP support yet, so you edit .mds without autocomplete. The function and conditional syntax is still minimal, and Python bindings aren't published yet. I've mostly run it against my own agents, so real projects will surface cases I haven't hit.

If you compose or generate prompts for agents today, whether by hand, with Jinja, or with a Makefile like wshobson's, I'd like to know whether compiling them ahead of time fits your workflow. The repo is github.com/dean0x/mdscript. Issues and stars both help me prioritize.

Top comments (0)