Prompt templates
Compose system prompts from Liquid templates with drover builtins.
@drover/prompt assembles system prompts from Liquid
templates instead of hand-concatenated strings. A template is markdown with two
kinds of directive:
{% builtin %}tags — drover-provided blocks resolved from run state (memory, skills, instruction files, date, …).{{ slot }}outputs — your values, supplied per render.
A template with no directives is just markdown: it renders verbatim. Directives are entirely opt-in.
The .md.liquid format
The convention is a double extension — system.md.liquid — where .liquid
marks the template language and .md the output. A plain .md file works too.
You are {% agent field: "name" %}, working in {% cwd %}.
{% instructions %}
{% skills %}
Keep responses concise and grounded in the conventions above.
Today is {% date %}.Builtins
| Tag | Volatility | Renders |
|---|---|---|
{% instructions %} | static | AGENTS.md / CLAUDE.md block |
{% skills %} | static | available-skills block |
{% subagents %} | static | spawnable-agents block (task tool + caps) |
{% mcp %} | static | connected MCP servers + their prefixed tools |
{% memory %} | volatile | recalled-memory index |
{% environment %} | volatile | cwd / model / sandbox / date facts |
{% agent %} | static | agent id (field: "name" → name) |
{% cwd %} | static | run working directory |
{% runId %} | volatile | run id |
{% model %} | static | resolved model id |
{% tools %} | static | composed tool ids (sep: joins) |
{% date %} / {% time %} | volatile | current date / time (format: arg) |
The {% subagents %}, {% mcp %}, {% memory %}, {% skills %}, and
{% instructions %} tags are capability fragments — each describes a
mechanism drover auto-wires. On the default (no-template) assembly path they
are injected automatically via the harness’s DEFAULT_PROMPT_TEMPLATE; a
custom template opts into whichever it wants.
A builtin whose backing run state is absent renders an empty string — drop
{% memory %} into any template without guarding it. Volatility drives the
cache analyzer.
Args are named: {% memory limit: 10 %}, {% date format: "iso" %},
{% agent field: "name" %}.
Wiring it to an agent
Set promptTemplate on the spec. The harness builds the render scope from run
state, renders the template, and uses it as the system prompt. When
promptTemplate is set the template fully defines the prompt — systemPrompt
is not used for assembly.
import { defineAgent } from "@drover/core";
defineAgent({
id: "writer",
systemPrompt: "(unused when promptTemplate is set)",
promptTemplate: { path: "prompts/system.md.liquid", autoReorder: true },
// ...
});promptTemplate accepts source (inline string) or path (a file; relative
paths resolve against the run cwd). autoReorder is covered under
prompt caching.
Standalone use
The engine works without the harness — useful for previewing or bespoke assembly:
import { createPromptEngine } from "@drover/prompt";
const engine = createPromptEngine();
const { text, cache } = await engine.render(templateSource, {
agent: { id: "writer", name: "Writer" },
vars: { audience: "external developers" },
});vars feeds {{ }} slots. Liquid control flow ({% if %}, {% for %},
filters) is available alongside the builtins.
Composition pattern
Liquid {% if %} lets one template serve multiple modes — build the dynamic
parts in TypeScript, pass them as vars, keep the template declarative:
{% instructions %}
{% if mode == "review" %}
Focus on correctness and edge cases.
{% else %}
Focus on shipping the smallest correct change.
{% endif %}