AI prompts: code-defined, versioned, overridable

Your prompts live in code, versioned on every deploy. Change the text or model from the dashboard without redeploying, and trace every generation back to the exact version that produced it.

Matt Aitken

Matt Aitken

CEO, Trigger.dev

Here's a prompt. It has an id, a model, a set of variables typed with Zod, and a template. It's a normal export in a normal file, sitting next to the task that sends it.

trigger/support.ts

import { prompts } from "@trigger.dev/sdk";
import { z } from "zod";
export const supportPrompt = prompts.define({
id: "customer-support",
model: "claude-sonnet-4-5",
config: { temperature: 0.7 },
variables: z.object({ customerName: z.string(), plan: z.string() }),
content: `You're a support agent for {{customerName}} on the {{plan}} plan.`,
});

The template is Mustache, so {{customerName}} fills in at resolve time and {{#plan}}...{{/plan}} blocks turn on and off. The variables schema types the fill-in call, so a missing or wrong-typed variable is a compile error, not a broken generation you find in production.

Resolve it, then hand it to the model

resolve() interpolates the variables and returns the finished text, plus the model and the version it came from. Spread toAISDKTelemetry() into your call and the generation is wired to observability in the same line.

trigger/support.ts

const resolved = await supportPrompt.resolve({ customerName: "Alice", plan: "Pro" });
const result = await generateText({
model: anthropic(resolved.model ?? "claude-sonnet-4-5"),
system: resolved.text,
prompt: payload.message,
...resolved.toAISDKTelemetry(),
});

Versioned on every deploy

Every deploy snapshots your prompts, so each one has a history the dashboard lists with the commit that shipped it. resolve() uses the current deployed version by default, and you can pin a specific one with { version: 3 } or a named { label: "current" } when you want to.

Override without a redeploy

Sometimes you need to change the wording or swap the model right now, before the next deploy. An override does that from the dashboard or the SDK, and resolve() returns it while it's active.


await prompts.createOverride("customer-support", {
model: "claude-opus-4-8",
});

One override is active per prompt per environment, and it's scoped to that environment, so a change you pin in staging doesn't touch production. Remove it and the next resolve is back on the deployed version.

The management SDK is the whole surface: prompts.list(), prompts.versions(), prompts.promote(), and createOverride / updateOverride / removeOverride / reactivateOverride. It runs inside a task or from a plain script with an API client.

Every generation traces back

Because resolve() carries the version and toAISDKTelemetry() attaches it, each generation records which prompt version produced it. The dashboard shows the generations and the cost, tokens, and latency per version, so when an output goes bad you can see the exact template behind it and, if you overrode the model, which model actually ran.

It's the same wiring chat.agent uses: store a resolved prompt with chat.prompt.set() and chat.toStreamTextOptions() folds the text, model, config, and telemetry into every turn.

Try it

Read the Prompts docs for the full guide: templates, variable schemas, dashboard overrides, and the management SDK.

Ready to start building?

Build and deploy your first task in 3 minutes.

Get started now