md-tmpl
Strongly-typed prompt templates for LLMs — markdown files with YAML frontmatter, validated at build time via proc macros, with a full runtime API for dynamic loading.
use include_template;
// Parses and validates the template at build time, generates typed structs + enums.
include_template!;
// Generated types:
// task_report::Params — typed struct
// task_report::ParamsPriority — enum(Critical, High, Medium, Low)
// task_report::ParamsTasksItem — struct { name, urgency }
// task_report::ParamsTasksItemUrgency — enum(Critical, High, Medium, Low)
let params = builder
.title
.priority
.tasks
.build;
let output = params.render.unwrap;
assert!;
assert!;
assert!;
The template behind it — a plain .tmpl.md markdown file:
name: task_report
description: A task report template with types
types:
-
params:
- - -
Priority: {{ kind(priority) }}
-
Rename a variant, add a field, remove a param — the compiler catches it immediately. No runtime surprises.
Why?
- Build-time validation — proc macros parse and validate syntax, types, and variable references at
cargo build. Typos, missing fields, and type mismatches are build errors. Templates can also be loaded and validated at runtime. - Markdown-native — prompts live in
.tmpl.mdfiles, readable in any editor or on GitHub. Compound types use()(never<>), control-flow tags use> {% %}blockquote prefixes. - Agent-safe — when an LLM edits prompts, the compiler catches drift immediately.
validate_template()enables hot-reload with contract enforcement.
Installation
Available on crates.io: https://crates.io/crates/md-tmpl (and https://crates.io/crates/md-tmpl-macros)
# macros are included by default — no extra dependency needed!
# (md-tmpl re-exports include_template! and template! macros)
MSRV: 1.85 (Rust 2024 edition) · no_std compatible (disable default std feature)
Template Syntax & Features
| Feature | Syntax / Example |
|---|---|
| Typed parameters | str, int, float, bool, list(…), struct(…), enum(…), option(…), tmpl(…) |
| Type aliases | types: block defines reusable named types (Priority = enum(High, Low)) |
| Cross-template imports | imports: pulls types via dotted paths (stem.TypeName) |
| Constants | consts: block for file-scoped immutable values |
| Environment variables | env: block for compile-time injection from the build environment |
| String interpolation | {{ expr }} inside all quoted strings — conditions, includes, panic messages |
| For loops & else | > {% for task in tasks %} … > {% else %} empty > {% /for %} |
| Conditionals | > {% if count > 0 %} … > {% elif active %} … > {% else %} … > {% /if %} |
| Enum dispatch | > {% match status %} > {% case Approved %} … > {% case Rejected %} … > {% /match %} |
| Includes as links | > {% include [widget](widget.tmpl.md) with title = "Hello" %} |
| Inline templates | > {% tmpl header %} … > {% /tmpl %} (call with {% include header %}) |
| Built-in functions | idx(b), len(x), kind(x), kinds(Type), has(x) |
| Filters | upper, lower, trim, fixed(N), join(sep), limit(N), add(N), sub(N) |
Build-Time Typed Structs
include_template!
Reads a .tmpl.md file at build time, validates it, and generates a typed
module:
use include_template;
// Generates: pub mod simple_greeting { pub struct Params { pub name: String } }
include_template!;
let output = Params .render.unwrap;
assert_eq!;
template!
Inline template strings — same validation, no file needed:
template!;
let output = Params
.render
.unwrap;
assert_eq!;
TypedBuilder Integration
Enable typed-builder for ergonomic builder patterns:
# (typed-builder is a default feature of md-tmpl)
# include_template!;
let params = builder
.name // setter(into): accepts &str or String
.count
.build; // `items` defaults to vec![]
let output = params.render.unwrap;
| Field type | Builder behaviour |
|---|---|
String |
setter(into) — accepts &str, String, or anything Into<String> |
Vec<…> |
default — omit the field to get an empty Vec |
Scalars (i64, f64, bool) |
Required |
Sub-structs also derive TypedBuilder:
# include_template!;
let item = builder
.label
.build;
let params = builder
.name
.count
.items
.build;
serde Integration
Render directly from any Serialize struct:
use Template;
use Serialize;
let tmpl = from_source.unwrap;
let output = tmpl.render.unwrap;
Runtime API
For dynamic or scripting use cases, parse templates at runtime.
ctx! Macro
Ergonomic context construction with nested structs and lists:
use ;
let tmpl = from_source.unwrap;
let output = tmpl.render_ctx.unwrap;
assert_eq!;
Runtime Loading
use load_template;
let tmpl = load_template.unwrap;
let mut ctx = new;
ctx.set;
let output = tmpl.render_ctx.unwrap;
assert!;
Environment Variables
Inject values at compile time from the build environment:
use ;
let = compile.unwrap;
let output = tmpl.render_ctx.unwrap;
assert!;
assert!; // default used
Hot-Reload
Load templates from disk at runtime while keeping type safety — iterate on prompt wording without recompiling:
# include_template!;
let tmpl = from_file.unwrap;
validate_template.unwrap;
let output = Params .render_reloaded.unwrap;
Caching
TemplateCache hashes file contents — unchanged files return cached
compilations. render_cached() extends this to included templates:
use TemplateCache;
let dir = tempdir.unwrap;
let path = dir.path.join;
write.unwrap;
let cache = new;
let tmpl = cache.load.unwrap;
let mut ctx = new;
ctx.set;
let output = tmpl.render_ctx_cached.unwrap;
assert_eq!;
Defaults & Extra Params
render_allowing_extra()
Extra context keys not declared in frontmatter are silently ignored:
use ;
let tmpl = from_source.unwrap;
let ctx = ctx! ;
assert_eq!;
defaults_context()
Returns a Context pre-filled with default values:
use Template;
let tmpl = from_source.unwrap;
let mut ctx = tmpl.defaults_context;
ctx.set; // count already has default 5
assert_eq!;
Performance
vs Competitors
Criterion benchmarks, render only (pre-parsed template + data → output). (source)
| Scenario | md-tmpl | Tera | MiniJinja |
Handlebars |
|---|---|---|---|---|
| simple | 164 ns 🏆 | 214 ns | 548 ns | 715 ns |
| loop | 499 ns 🏆 | 637 ns | 1.90 µs | 3.32 µs |
| conditional | 218 ns 🏆 | 369 ns | 598 ns | 1.39 µs |
| hero | 2.13 µs 🏆 | 2.18 µs | 7.58 µs | 24.01 µs |
| mega | 8.53 µs 🏆 | 10.63 µs | 28.46 µs | 90.35 µs |
Intel Xeon @ 2.60 GHz, 3 runs × 100 Criterion samples.
Full Reference
See SPEC.md for the complete syntax — control-flow tags, filters, built-in functions, whitespace control, and error diagnostics.
License
Apache-2.0 OR MIT