Prompt versioning
`definePrompt` makes a prompt an identified, versioned artefact — and stamps that identity onto the step row, the spend ledger and a `_voltro_prompts` table, so "which prompt produced this run" is a lookup.
Your schema is versioned. Your rows carry provenance. Your prompts — the part of an AI feature that changes weekly and is edited by whoever is nearest — were code with no identity at all.
definePrompt closes that asymmetry. It reuses the digest aiStep already recorded on step rows rather than inventing a second identity scheme.
Declaring a prompt
import { definePrompt } from '@voltro/ai'
export const triage = definePrompt({
id: 'support.triage',
system: 'You triage support tickets. Answer only with the category.',
template: 'Ticket:\n{{body}}\n\nCategories: {{categories}}',
label: 'v3-shorter-system',
})idis the stable identity across revisions, dotted like a descriptor tag.templateis a string with{{name}}placeholders — not a function. A function has no stable content to hash, so a function-built prompt could only be versioned by hashing its output, which would make every distinct customer message a new "version".digestis computed from the template + system at definition time. It is the version, and it exists before any database does — a test, an eval, or a CLI can identify a prompt version with no connection.labelis metadata and deliberately not part of the digest, so renaming a revision does not fork it.
Render it to get the text plus a stamp:
const rendered = yield* triage.render({ body: ticket.body, categories: 'hardware, software, billing' })
// { promptId: 'support.triage', digest: 'sha256:…', prompt: 'Ticket:\n…', system: '…' }A missing variable fails rather than sending the literal {{body}} to a model.
Provenance you get by using the primitive
Pass the rendered prompt straight to aiStep — the string form still works, it just carries no stamp:
const result = yield* aiStep({
name: 'triage-ticket',
prompt: rendered,
store: ctx.store,
offload: true,
})That one call now writes the same promptId + digest to three places:
| Where | What it answers |
|---|---|
_voltro_workflow_run_steps.input |
Which prompt version produced this run |
_voltro_ai_usage |
What each prompt version cost |
_voltro_prompts |
What the template was, at that version |
Nothing has to be remembered at call time, and offloaded calls are covered too — the stamp rides across the suspend on the queue row, so the dispatcher attributes the spend to the same version.
recordPrompt: 'none' still records the provenance. That is deliberate: the reason to record nothing about a prompt is that its text is sensitive, and an id plus a content digest is neither the text nor derivable from it. Suppressing the identity along with the content would mean the most privacy-conscious setting is also the one where you cannot tell which prompt version ran.
Reading it back
import { aiSpendUsd, promptVersionByDigest, promptVersionsFor } from '@voltro/ai'
// From a run's step row → the artefact.
const version = yield* promptVersionByDigest(ctx.store, stepInput.promptVersion)
version?.template // the template, as it was
// The history of one prompt, newest revision first.
const history = yield* promptVersionsFor(ctx.store, 'support.triage')
// Did revision 4 cost more than revision 3?
const spend = yield* aiSpendUsd(ctx.store, { promptDigest: version.digest })recordPromptVersion assigns the human-facing revision (1, 2, 3 …) on first sight of a (promptId, digest) pair and is idempotent afterwards. Under a two-replica race two different versions can land on the same revision number — accepted deliberately: the digest is the exact identity, the revision is a label for humans, and order by firstSeenAt when you need the true sequence.
Retention
_voltro_prompts is bounded by the standard retention sweep on lastUsedAt, defaulting to 365 days and tunable with VOLTRO_AI_PROMPTS_TTL_HOURS. The table is self-healing under it: a version that ages out is one nothing has run in a year, and the next run re-registers it.
The registration is framework-precedence, so an app that registers its own window for the table wins without having to know the framework's exists.
The table
_voltro_prompts
promptId stable identity across revisions
digest content digest of template + system — the version
revision human-facing counter within a promptId
label the author's name for this revision
template the TEMPLATE (code) — never a rendered prompt (data)
system
firstSeenAt / lastUsedAtStoring the template is safe precisely because it is code — it is in your repository already. The rendered prompt, which may contain a customer's message, is never written here.