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.

liquid
You are {% agent field: "name" %}, working in {% cwd %}.

{% instructions %}

{% skills %}

Keep responses concise and grounded in the conventions above.

Today is {% date %}.

Builtins

TagVolatilityRenders
{% instructions %}staticAGENTS.md / CLAUDE.md block
{% skills %}staticavailable-skills block
{% subagents %}staticspawnable-agents block (task tool + caps)
{% mcp %}staticconnected MCP servers + their prefixed tools
{% memory %}volatilerecalled-memory index
{% environment %}volatilecwd / model / sandbox / date facts
{% agent %}staticagent id (field: "name" → name)
{% cwd %}staticrun working directory
{% runId %}volatilerun id
{% model %}staticresolved model id
{% tools %}staticcomposed tool ids (sep: joins)
{% date %} / {% time %}volatilecurrent 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.

ts
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:

ts
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:

liquid
{% instructions %}

{% if mode == "review" %}
Focus on correctness and edge cases.
{% else %}
Focus on shipping the smallest correct change.
{% endif %}

Type to search…

↑↓ navigate open esc close