How to write a spec for an AI agent, before you prompt anything
Published 20 September 2026
Most agent projects start as one sentence in a meeting. The distance between that sentence and something you would let touch production is a short document, and its sections are not arbitrary: each one exists to stop a specific failure. One agent, one sentence, eight sections, and what each prevents.
Spec-first tooling already exists, and it is aimed at code
GitHub maintains Spec Kit, an open-source toolkit and CLI that installs slash commands (/speckit.specify, /speckit.plan, /speckit.tasks, /speckit.implement) into a coding agent, so a spec, a plan and a task list exist before implementation. AWS ships Kiro, whose docs define specs as "Structured artifacts that formalize the development process for features and bug fixes in your application" and whose workflow produces requirements.md, design.md and tasks.md.
Both are pointed at code. An agent needs different sections, because you are not specifying a feature with a known implementation path. Anthropic defines agents as systems where the model dynamically directs its own processes and tool usage, and frames them as suited to open-ended problems where the required number of steps is difficult or impossible to predict and you cannot hardcode a fixed path. So you fence the space instead. That is the spec.
The sentence
We need an agent that reads new support tickets and puts them in the right queue.
1. Identity, at a stated level
Give the agent a specific role at a stated level of seniority, so the model does not fall back on its own defaults.
Not "You are a helpful support assistant" but "You are a senior support operations specialist for a B2B billing product. You can tell a payment failure apart from a provisioning bug apart from a feature request, and you know which queue each belongs in."
What it prevents: the model falling back on its own defaults. Arize AI's field analysis of production agent failures names pre-training bias overriding retrieved context as one of its eight recurring modes, and argues that where training-learned knowledge conflicts with context supplied in the prompt, the trained-in behaviour often wins.
2. Mission, with what it does not do
The mission states the goal, the condition for success, and what the agent does not do.
GOAL Assign exactly one queue to every ticket opened in the last hour,
and record a one-line reason on the ticket.
SUCCESS Every ticket in the window has a queue and a non-empty reason,
or is marked escalated.
DOES NOT Reply to the customer. Issue refunds or credits. Close or merge
tickets. Change a queue a human has already set.
What it prevents: the slow expansion at the edges. Nobody writes "and also reply to the customer" into a prompt. It arrives because the model decided a reply was helpful and nothing said otherwise. That last line is the rule that usually gets written the day after it happens.
3. The reasoning loop, and its ceiling
The reasoning loop names the pattern the agent follows, a maximum number of iterations, and what it does when it reaches that ceiling.
PATTERN Observe the ticket, decide whether you have enough to route,
act by calling at most one tool, observe the result, repeat.
MAX_ITERATIONS 5
AT_CEILING Stop, escalate, attach everything gathered so far.
What it prevents: the loop that never ends. Arize's taxonomy includes recursive loops and inefficient trajectories, and it coins the term Polling Tax for an agent that checks status instead of waiting on a webhook. On why this survives monitoring, in its words: "You will see a stream of 200 OK responses." Arize puts the worst case at hundreds of API calls for a single task, the answer still arriving correct while the path taken makes the agent commercially unusable.
The ceiling alone is not enough. MAX_ITERATIONS with nothing after it produces an agent that silently stops mid-thought. Say what happens at the wall.
4. One contract per tool, failure path included
Every tool gets the same five parts. Here is one, in full:
TOOL get_customer_plan
WHAT Returns the billing plan and contract tier for one account.
WHEN Only when the ticket mentions pricing, invoices, seats or a plan
name, AND the ticket carries an account_id. Never speculatively.
RETURNS { account_id: string, plan: "starter"|"team"|"enterprise",
seats: int, contract_end: ISO-8601 date }
ON FAILURE Timeout: retry once. Second failure, or an empty result: route to
billing_general and note "plan lookup unavailable" on the ticket.
Never infer the plan from the ticket text.
EXAMPLE get_customer_plan(account_id: "acc_8812")
What it prevents: the failure that never announces itself. Arize's second failure mode is hallucinated arguments in tool calls, and its example is exact: an agent assumes a field is called user_id because that is what it saw in training, while the real schema requires customer_uuid. The database returns zero rows rather than an error, and the agent reports that it could not find any data. Arize calls that a silent hallucination in the intermediate tool logic. So RETURNS carries the real field names, and an empty result belongs in the failure path.
Anthropic makes the general case in an appendix to Building Effective AI Agents titled "Prompt engineering your tools": the agent-tool interface deserves the same care as a prompt written for a human.
5. The decision tree
Each branch tests something the agent can see, and each ends in one action.
IF ticket text matches card decline / payment failed
THEN queue = billing_urgent
ELSE IF ticket mentions invoices, seats or plan change
AND get_customer_plan returned plan = enterprise
THEN queue = billing_enterprise
ELSE IF ticket contains a stack trace or an error code
THEN queue = engineering_triage
ELSE queue = triage_human, reason = "no matching rule"
What it prevents: the branch you did not write. Without that final ELSE, an unmatched ticket goes wherever the model's judgement lands, and you find out when someone audits the queue.
6. Stop conditions a machine can check
"Stop when the ticket is handled" is a hope. A stop condition is a predicate you could write in code without asking a model to interpret it.
STOP WHEN queue is set AND reason is non-empty
STOP WHEN escalated == true
STOP WHEN iterations == 5
STOP WHEN ticket.status != "new" (a human has taken it)
Every one of those is a boolean. That is the bar.
7. The output contract
The output contract fixes the record the agent returns: the ticket id, one queue, a reason of at most 20 words, an escalated flag, and the list of tools called.
{ ticket_id: string,
queue: "billing_urgent"|"billing_enterprise"|"billing_general"
|"engineering_triage"|"triage_human",
reason: string, max 20 words,
escalated: boolean,
tools_called: [string] }
What it prevents: downstream code breaking on a shape change, and, less obviously, the inability to evaluate the agent at all. Once the output is a fixed record you can diff a week of routing against a human's and get a number. Without a contract you are reading transcripts.
8. Guardrails, each with its reason
Each guardrail states a rule and the reason for it. Cover at least four classes, each specific to this agent: data handling, scope, impersonation, and anything irreversible.
Never write the full ticket body into the routing log, because tickets
contain customer payment details subject to retention limits.
Never sign a note as a named teammate, because it misrepresents who
handled the ticket.
Never issue a credit or refund, because that moves money and cannot be
undone from inside this agent.
Never close or merge a ticket, because a merge destroys the original thread.
The reason is not decoration. It gives the model something to generalise from when it meets a case you did not list, and it tells the next engineer whether the rule still applies.
Be clear about what this section is, though. Arize's position is blunt: in its words, "Prompts are suggestions. They lack the rigidity of code." Safety, it argues, cannot rely on the model and demands a deterministic layer that inspects the payload and blocks it independently of the agent's reasoning. Anthropic warns similarly that the autonomous nature of agents brings higher costs and the potential for compounding errors, and recommends extensive testing in sandboxed environments along with appropriate guardrails. Your spec tells your reviewer what to enforce in code. It is not the enforcement.
The two sections that get skipped first
The not-doing clause and the machine-checkable stop conditions are the least satisfying to write. They also tend to cause the most trouble once the thing is running, because they are the only two that constrain the agent when it does something you did not anticipate. Every other section describes the happy path in more detail. If you write nothing else, write those two.
Two more earn their place once the agent is real: what it remembers between runs, and how it recovers per external surface it touches. Arize's list includes unhandled external API schema changes and instruction drift in long sessions, both memory and recovery problems more than reasoning problems. Neither belongs in a first draft of a small triage agent.
Where this fits if you use Bespoke Prompting
Bespoke Prompting's Agent build type emits these sections from a plain-language description, and two of its rules are the ones argued for here. Its tool registry will not accept a vague failure path: every tool needs a named fallback, retry once then fall back to Y, never "handle the error". Its guardrails must take the form "Never X because Y", covering data privacy, scope, impersonation and irreversible actions, made specific to that agent's own tools.
Sources
- GitHub, "Spec Kit": https://github.com/github/spec-kit
- AWS, "Kiro docs: Specs": https://kiro.dev/docs/specs/
- Anthropic, "Building Effective AI Agents": https://www.anthropic.com/engineering/building-effective-agents
- Arize AI, "Why AI Agents Break: A Field Analysis of Production Failures": https://arize.com/blog/common-ai-agent-failures/