Writing good prompts
An agent’s soul.md (see Writing good agents)
is its stable identity — who it is and how it behaves, every run. A step’s
prompt_template is the opposite: what’s true this run — this alert, this
service, whatever the previous step handed off. Mixing the two up is the
most common source of a prompt that’s both bloated and strangely rigid. This
guide is about the prompt_template half. For the full templating
mechanics — the Jinja2 context, session keys — see Prompt
construction.
Keep run-varying data in the prompt, not the agent
Section titled “Keep run-varying data in the prompt, not the agent”If a fact changes between runs — which service, what the alert said, what a
prior step found — it belongs in prompt_template, rendered from
{{labels.service}}, {{summary}}, {{steps.x.field}}, or a pipeline
vars: entry. If it’s true of the agent regardless of which run it’s
handling — its job, its output format, what confidence means for it — that
belongs in soul.md instead. A prompt that repeats the agent’s job
description every run is wasted tokens; a soul.md that hardcodes one run’s
specifics is an agent that only works for that one alert.
State the output contract every time, exactly
Section titled “State the output contract every time, exactly”confidence, summary, and next_step_context are mandatory on every
response — see the LLMOutput contract. Show
the model the exact JSON shape it needs to return, close with “Return ONLY
this JSON, no other text”, and mean it — a single stray sentence before or
after the JSON breaks parsing, because the runner expects the whole response
body to parse as one object. Include reasoning: {supports, contradicts, assumptions} too, even though it’s optional — a critic-mode
verifier and grounding
both read it, and so will you, six months from now, trying to work out why a
step decided what it decided.
prompt_template: | A {{severity}} alert fired for {{labels.service}} in {{labels.environment}}. Summary: {{summary}}
1. <task-specific instruction> 2. <task-specific instruction>
Return ONLY this JSON, no other text: { "confidence": 0.0, "summary": "One sentence: what's happening and what you found", "next_step_context": "Focused brief for the next step, or \"\" if terminal", "reasoning": { "supports": "Evidence that makes this credible", "contradicts": "Evidence that suggests otherwise", "assumptions": "What you're assuming in the absence of data" } }Include only what this step actually needs
Section titled “Include only what this step actually needs”context_template.include decides what’s auto-injected into every step’s
prompt; vars: adds pipeline-level constants. Both are easy to let grow
into a kitchen sink — every field anyone might conceivably want, included
everywhere, “just in case.” Resist it. A prompt with ten fields the model
never uses is ten more things it might latch onto irrelevantly, and ten more
things a reader has to mentally filter out to see what the step actually
depends on. Include what this step’s task genuinely needs; a later step can
include something different.
Reference prior-step output precisely
Section titled “Reference prior-step output precisely”{{steps.step_name.field}} pulls a mandatory or extra field from any
already-completed step; {{artifacts.step_name.key}} pulls the full text of
a stored artifact. The one real gotcha: hyphens in step names become
underscores in the reference — a step named first-line-triage is
{{steps.first_line_triage.summary}}, not
{{steps.first-line-triage.summary}}. This silently renders as an empty
Jinja2 lookup rather than erroring loudly, so a typo here reads at first
like the prior step “didn’t return anything,” not like a template mistake.
See Prompt construction for the full context
reference, including {{pipeline_run_id}}, {{current_step}}, and
{{labels.*}}.
An edited prompt is a reset, not a tweak
Section titled “An edited prompt is a reset, not a tweak”prompt_template’s content hash is part of every calibration bucket’s
key — editing it starts a fresh bucket at zero, discarding the measured
accuracy history for that exact step, same as editing an agent’s soul.md
does on the Gateway side. See Editing a prompt or an agent resets the
measurement
for exactly what you’ll see when it happens. It’s not a reason to avoid
editing prompts — it’s a reason to batch changes deliberately rather than
tweak wording casually on a step with real production history.
Where next
Section titled “Where next”- Prompt construction & session keys — the full Jinja2 context reference.
- Writing good agents — the soul.md half of this same split.
- LLMOutput: the step contract — every mandatory and optional response field.