stackbone.prompts
stackbone.promptsreads and writes a versioned, key-addressed prompt catalog straight out of the agent's own Postgres (stackbone_platform.prompts/prompt_versions), over the same pool asstackbone.database. There is no control-plane round-trip — the agent owns its data, exactly likestackbone.secretsandstackbone.config. You reach it through the ambient client (import { stackbone } from '@stackbone/sdk'), the same handle you use from any agent tool or durable workflow step.
Each prompt is addressed by a workspace-unique key (summariser,
welcome_email, tool_describe_orders) and carries a human-friendly
name. Each update(...) appends a new immutable version under that
key; the latest version is what get(key) and compile(key, vars)
return by default. Pinning a specific version is supported via the
{ version } option.
Where it fits
A deep agent already has a static, always-on system prompt: the
systemPrompt string passed to defineDeepAgent({ ... }) in its
index.ts. That is the agent's persona and never changes at runtime, it
ships with the build. See Agents for the
authoring model.
stackbone.prompts is for the dynamic side: operator-editable,
versioned, A/B-routable templates your code fetches and compiles at
runtime, no redeploy. Reach for it when a prompt needs to be reviewed,
diffed, rolled back, or split-tested without rebuilding the container —
e.g. the body of a notification, a summarisation template, or a per-tool
instruction your agent renders on the fly.
It is agent-local and peer-free: it reads the agent's own database and pulls in no agent-authoring or workflow dependency, so a workflow-only project can import it without dragging the deep-agent runtime along.
Why it exists
Prompts are the most-edited, least-versioned piece of an agent's behaviour. Without a platform surface, every agent either hardcodes them, embeds JSON files in the project, or invents its own A/B routing. That defeats the "platform feature" promise: prompts must be reviewable, diffable and rollback-able without rebuilding the container.
No capability gate
Like stackbone.secrets and stackbone.config, this surface is
agent-local: it reads and writes the agent's own database tables
directly, so it is not gated on the contract handshake. The only
prerequisite is that the prompts schema has been applied. Those tables
are created when your project's migrations run — which happens
automatically when you boot your project with stackbone dev (and again
when you stackbone publish). A not-yet-migrated agent surfaces
prompts_not_configured until then.
API
Every method returns the standard Result<T> envelope
({ data, error: null } | { data: null, error: SdkError }). Import the
ambient client and call the accessor — the snippets below show the call
site inside an agent tool, but the same calls work verbatim inside a
durable workflow "use step".
get(key, options?)
Returns the stored prompt (template + version metadata). The latest
version by default; pin one via { version }.
import { tool } from '@langchain/core/tools';
import { stackbone, z } from '@stackbone/sdk';
const fetchSummariserTemplate = tool(
async () => {
const { data, error } = await stackbone.prompts.get('summariser');
if (error) throw error;
return JSON.stringify({
template: data.template, // raw template with {{var}} placeholders
version: data.version, // monotonically increasing per key
});
},
{
name: 'fetch_summariser_template',
description: 'Fetch the summariser template.',
schema: z.object({}),
},
);Pinning a version:
const { data } = await stackbone.prompts.get('summariser', { version: 3 });compile(key, vars, options?)
Fetches the prompt and runs the Mustache-subset compile engine against
vars. Returns { output, version } — the rendered string and the
version it ran against.
const { data: rendered, error } = await stackbone.prompts.compile('summariser', {
article: 'Stackbone makes agents shippable...',
});Pinning a version (e.g. for an A/B split):
await stackbone.prompts.compile('summariser', { article }, { version: 3 });Feeding a compiled prompt to a model
The point of compiling is to send the rendered string to an LLM. Pair
compile(...) with stackbone.ai, which routes the call
to a model through the
Vercel AI Gateway /
OpenRouter:
async function summarise(article: string) {
'use step';
const { data, error } = await stackbone.prompts.compile('summariser', { article });
if (error) throw error;
const completion = await stackbone.ai.chat.completions.create({
model: 'anthropic/claude-haiku-4.5',
messages: [{ role: 'user', content: data.output }],
});
return { summary: completion.choices[0]?.message?.content ?? '' };
}The compiled string is just text — feed it to stackbone.ai, to the
Vercel AI SDK
(generateText / streamText), or anywhere else you call a model.
list(options?)
Lists the current version of every (non-deleted) prompt, ordered by
key and capped by limit (1–100, default 50).
const { data } = await stackbone.prompts.list({ limit: 50 });
for (const prompt of data.items) {
console.log(prompt.key, '@v' + prompt.version);
}create(request)
Registers a new prompt at version 1. key and name are required; a
duplicate key errors with prompts_already_exists.
await stackbone.prompts.create({
key: 'welcome_email',
name: 'Welcome email',
template: 'Hello {{name}}, welcome to {{product}}.',
});update(key, options)
Pass a new template to append a new immutable version; name,
description and metadata can be updated in place without bumping the
version. History is append-only — past versions are kept for audit and
rollback. Concurrent updates to the same key are serialized, so two
parallel writers can never collide on the next version number.
await stackbone.prompts.update('welcome_email', {
template: 'Hi {{name}} — welcome to {{product}}.',
});delete(key, options?)
Soft-deletes the prompt — every version under key stops resolving but
the rows stay for audit. ({ version } is reserved for future
per-version deletes and is ignored today.) Returns { key, deleted },
where deleted is 1 when a live prompt was removed and 0 when the
key was already gone.
The compile engine
compile(...) runs a Mustache-subset. The only template syntax is
{{var}} where var matches [a-zA-Z_][a-zA-Z0-9_]*. There are no
conditionals, no loops, no helpers, no comments.
| Construct | Behaviour |
|---|---|
{{var}} |
Substituted with String(vars.var). |
{{#if x}}…{{/if}} |
Treated as a literal — not parsed. |
{{!comment}} |
Treated as a literal. |
{{var | filter}} |
Treated as a literal. |
Missing vars.var |
Errors with code prompts_missing_var. |
Extra fields in vars |
Silently ignored. |
This decision is frozen. Any expansion (conditionals, helpers) requires a documented contract change.
Errors
| Code | Meaning |
|---|---|
prompts_invalid_request |
A required field was missing or empty (key, name, template), or compile() hit a template that failed to compile for a reason other than a missing variable (reserved — unreachable with today's engine). |
prompts_not_found |
The prompt does not exist, was soft-deleted, or the pinned { version } does not exist. meta.key. |
prompts_already_exists |
create() was called with a key that is already registered. meta.key. |
prompts_missing_var |
compile() was called without a value for a {{var}}. meta.name. |
prompts_not_configured |
The prompts schema is not applied — boot your project with stackbone dev to run the migrations. |
prompts_unavailable |
The agent database handle was unavailable or a read/write failed for another reason. |
database_not_configured |
The agent has no Postgres pool (STACKBONE_POSTGRES_URL not injected). |
Where to go next
stackbone.ai— the model clientcompile(...)feeds into.stackbone.database— the Postgres that owns thepromptsandprompt_versionstables this surface reads and writes.stackbone.secretsandstackbone.config— the other agent-local surfaces that read straight from the agent's own database.- Agents — the deep-agent authoring model:
the inline
systemPromptas the static system prompt vs dynamic prompts served here.