Agent Surface

Build Agents

Prompts and Configuration

Treating the system prompt as versioned, testable configuration built from trusted server context.

Last verified 2026-10-29

Summary

A system prompt built once and reused everywhere cannot adapt to who is asking, what platform they are on, or what just happened. The reliable pattern is a buildSystemPrompt(context) function that composes the prompt from trusted server-side context on every request: identity and locale, safety rules, tool routing, formatting rules, and platform-specific blocks. The result is testable, versioned, and reusable - the agent's own logic never branches on platform or context; the prompt does.

  • Dynamic composition - user/session context in, a finished prompt string out.
  • Safety rules as data - encoded once, applied through every generated prompt, not scattered through application code.
  • Testable - prompt output is a plain string; assert on its content in unit tests.
  • Platform-aware - one agent core, different prompt blocks per surface.
  • No branching in agent logic - the prompt varies; the code that runs the agent does not.

The Anti-Pattern

A static system prompt plus if (platform === "whatsapp") { ... } branching in application code puts platform logic in the wrong layer: it does not adapt to timezone, locale, or currency, it has no routing rules or confirmation workflow encoded anywhere, and a prompt change requires a code review rather than a prompt-file diff. Move all of that into the prompt builder.

The Pattern: A Dynamic Prompt Builder

export function buildSystemPrompt(ctx: UserContext): string {
  return `You are an assistant for ${ctx.teamName ?? "the user"}.

## User Context
- Name: ${ctx.fullName ?? "unknown"}
- Timezone: ${ctx.timezone}
- Locale: ${ctx.locale}
- Current time: ${ctx.localTime}

## Critical Rules
1. Never invent data. Every number, date, or ID must come from a tool call.
2. Confirm before destructive actions: state what will happen, ask "Confirm?", wait for explicit approval.
3. Pass ISO 8601 dates to tools regardless of the display locale.

## Tool Usage
Internal tools for the user's own data. Web search for real-time external facts.
If no tool fits, call \`search_tools\` with a short query.
`;
}

Compose from typed sections (identity, safety, tool routing, formatting) rather than one long template literal once the prompt grows - each section becomes independently testable and reusable across prompt variants.

Drafting the first version of a prompt is itself a task a model can help with: describe the task, the tools available, and the constraints, and ask the model to produce a first draft to edit rather than starting from a blank page. Treat the result as a draft to test and revise, not as a finished prompt.

Examples: Zero-, One- and Few-Shot

A prompt with no examples (zero-shot) relies entirely on the instruction being unambiguous, and works fine for simple, well-understood tasks. Add one example (one-shot) when the output format is non-obvious - a specific structure, tone, or level of detail the instruction alone under-specifies. Add several examples (few-shot) when the task has edge cases that a single example would not cover, or when outputs have been inconsistent in testing and a small set of examples narrows the range. Do not default to few-shot: each example costs on every call, and an instruction that is actually clear does not need one. Start at zero-shot, add examples only when testing shows the model needs them, and drop an example if a clearer instruction would do the same job.

Formatting: Tags vs Markdown

Whether a prompt reads more reliably with XML-style tags (<context>...</context>) or headings varies by model family and by task, and changes as models update. Do not assume one is universally better - test both against your actual prompts and pick whichever the scores favor, and re-check after a model upgrade rather than assuming the old answer still holds.

Prompt Caching

Providers that support prompt caching charge less, and respond faster, for the portion of a prompt that repeats unchanged across calls. Structure the prompt so static content - system instructions, safety rules, few-shot examples - sits in a stable prefix, and put the request-specific, variable content last. A prompt that interleaves static and variable content defeats caching, because any change earlier in the prompt invalidates everything after it. Caching applies only above a provider's minimum prefix length, and some providers charge extra for the first call that writes the cache, so check the thresholds before counting on savings for short prompts.

Safety And Confirmation Vocabulary

The rules that matter most are the ones an agent is most tempted to skip under pressure to be helpful. State them explicitly and keep the wording consistent across every prompt in the system, because tool design and guardrails pages assume this same vocabulary:

  • Never invent data - every number, date, name, or ID must come from a tool call; ask or call a tool rather than guess.
  • Confirm before destructive actions - before delete, cancel, send, or bulk update, state what will happen and ask "Confirm?"; wait for explicit approval before proceeding. This is the prompt-side half of the contract Human Approval covers at the tool surface.
  • No preamble, direct results - after a tool call returns, present results directly rather than narrating that you are about to.

Platform-Specific Composition

For a multi-platform agent, append a platform block after the shared core rather than forking the whole prompt:

export function buildSystemPromptForPlatform(ctx: UserContext, platform: Platform): string {
  return `${buildSystemPrompt(ctx)}\n\n${getPlatformRules(platform)}`;
}

Typical platform differences: a dashboard can render markdown tables and clickable entity links; WhatsApp and Telegram need plain text, numbered lists instead of tables, and a single confirmation message per action because of SMS-length and rendering limits; Slack supports block-kit formatting and longer messages. See Channels for the adapter layer that carries these platform-shaped concerns; this page is only about what changes in the prompt text itself.

DateTime Normalization

Force ISO 8601 at the tool boundary rather than trusting the model to convert correctly:

export function normalizeDateTime(value: string): string {
  const withTz = /[Zz]|[+-]\d{2}:\d{2}$/.test(value) ? value : `${value}Z`;
  const parsed = new Date(withTz);
  if (Number.isNaN(parsed.getTime())) throw new Error(`Invalid datetime: "${value}"`);
  return parsed.toISOString();
}

Pre-computing date ranges ("this month," "this quarter") in the prompt context eliminates a whole class of errors where the model calculates a relative range incorrectly across a month or year boundary.

Dynamic Agent Configuration

The same context-driven approach that builds the prompt text can also resolve configuration beyond the prompt: which tools are available, how deep memory recall goes, and which model handles the request, all resolved from runtime context - user role, plan tier, account status - instead of maintaining a separate hard-coded agent per segment. A single dynamically configured agent is easier to keep consistent than a family of near-duplicate agents that drift apart as each is patched independently.

This trades away predictability: a statically configured agent behaves the same way for everyone, which is easier to reason about, test, and explain when something goes wrong. A dynamically configured one can behave differently for two users on the same request, which is more efficient but harder to fully enumerate in testing. Weigh that trade-off against how much the segments genuinely differ - a small, well-understood set of runtime branches is worth it; branching on every field in the user record is not.

Structured Output

When a step's job is to produce data that fits a fixed schema rather than free text - classification, extraction, a form with fixed fields - constrain the model to return exactly that shape rather than parsing prose after the fact. Schema-constrained output removes a whole class of parsing failures, and because the task is narrow and well-specified, it is often a good job to hand to a cheaper model rather than the one handling open-ended reasoning elsewhere in the same agent.

Versioning And Testing

Treat prompts as versioned code, not prose that lives only in a string literal:

  • Write unit tests asserting the prompt contains expected content for a given context (user name, safety rules, platform-specific formatting instructions).
  • Version the builder function in git and review prompt changes the same way as any other logic change.
  • Measure prompt effectiveness against real traffic rather than assuming a wording change is an improvement.

Checklist

  • buildSystemPrompt(context) composes from trusted server context, not model-controlled input.
  • Identity, locale, timezone, and currency are included, not hard-coded.
  • Safety rules - never invent data, confirm before destructive actions - are present in every prompt variant.
  • Tool routing guidance matches the tools actually available to that agent.
  • Platform-specific rules are a block appended to the shared core, not a forked prompt.
  • Prompt output has unit tests.

On this page