How to Write Test Scenarios for an AI Agent
Turn an agent's instructions, allowed tools and business rules into scenarios you can actually run — a fixed situation, an expected behavior you can observe, and a pass or fail that two people would agree on.
Get the Scenario PromptTwenty minutes with the agent proves you know how to use it
It handled everything you threw at it. That is not evidence, and the reason is uncomfortable: you asked the questions it was built to answer. You knew which details it needed, so you supplied them. You knew which requests were out of bounds, so you did not make them. When a lookup came back empty you rephrased, instead of sitting still and watching what it did. You were demonstrating the agent, and you were an unusually cooperative user.
The situations that break an agent in its first week are the ones nobody thinks to type. A field left out because somebody was in a hurry. A lookup that times out at 3am. A request that is perfectly reasonable and belongs to another team. A user who says their manager already approved it. None of these are edge cases in the exotic sense. They are Tuesday. They just never happen while the person who built the thing is the one holding the keyboard.
An agent has no expected value
Testing ordinary code is easier than this, and the reason matters. A discount function called with the same arguments returns 99.99, or it does not. The expected value is a fact you write down once and compare against forever. Ask an agent the same question twice and you get two different sentences, both entirely correct. There is no 99.99.
This is where agent testing quietly stops. Either the thing gets declared untestable and ships on a demo, or somebody writes "Expected: a helpful, accurate response" — which grades nothing, passes always, and looks like diligence in a review.
The way through is to stop testing the answer and start testing the situation. A scenario does not ask what the agent will say. It fixes what the agent knows, what it can reach, and what it has been asked, then names what a correct agent does about that. Does, not says: which tool it calls, which it refuses to call, whether it asks instead of guessing, whether a row appears in your database afterwards. Those hold steady across runs no matter how the sentences come out.
A model will draft a set like this from a spec you supply. Paste in what the agent does, what it may use, and what has already gone wrong, and a covering draft comes back faster than you would type one. A draft is also the ceiling, because not one of the facts underneath it is something the model has any access to. NewPrompt's part is one step of that and no further: it puts the prompt together, in the browser. It does not run your agent, does not connect to a model, does not call your tools, does not stand up a test harness, does not collect results, does not watch a side effect, and does not decide whether a run passed. A generated set can only contain what you told it about, so it measures your spec rather than your agent. You run the scenarios in your own environment, the agent's owner confirms each expected behavior is really what a correct agent does, and an engineer or reviewer takes the ones that are subjective or expensive to get wrong.
Step 1: Draw a line around the thing you are testing
Before a single scenario, write down what the system under test actually is. For an agent that is four things, and all four become inputs to every scenario you write — inputs, not sources. The instructions tell you what to test against. They never tell you what to test.
The split between reading and acting is the one that pays for itself. A tool that looks up a policy can be called wrongly and cost you nothing. A tool that files an expense cannot. Half the scenarios you end up writing exist only because one of the agent's actions is expensive to undo, and if you never listed which ones those are, you will test the reading and ship the writing.
If the agent's tool rules are not written down yet, that is a different job and it comes first. This guide starts from rules that already exist and asks whether they hold in a specific situation, which is not the same work as deciding what they should be.
- Its instructions — the standing prompt, the policies, the rules it is meant to follow on every turn.
- Its allowed actions — every tool it can call, split into the ones that only read and the ones that change something.
- Its knowledge boundaries — what it can look up, what it was handed, and what it has no way to know.
- Its business rules — the thresholds and conditions that make a behavior correct here and wrong one team over.
Step 2: Write the situation, not the question
A scenario is a situation with a verdict attached, and the user's message is one field in it. The rest is what makes the verdict mean anything:
Most of these exist because of a specific way agent tests go wrong. Preconditions are there because the second run of a set behaves differently from the first. Tool state is there because an agent that has never met a broken tool has never been tested against the thing most likely to happen to it. The expected tool decision is there because an agent can produce a flawless sentence and still have called something it should not have.
- ID and purpose — what this one exists to catch. If you cannot say it in a line, delete it.
- Preconditions — the state the agent starts from, written out rather than assumed.
- User input — the exact message or situation, including the part the person forgot.
- Environment and tool state — what each tool returns this time, including the one that fails.
- Expected observable behavior — what a correct agent does, in terms you could point at.
- Expected tool decision — which tools get called, with what arguments, and which do not get called.
- Expected state change — what is different afterwards, or explicitly nothing.
- Unacceptable behavior — the specific wrong move, not "anything bad".
- Pass/fail oracle — the check that settles it without a meeting.
- Evidence to capture — the reply, the tool calls, the resulting state, whatever a trace gives you.
- Cleanup — what has to be reset before the next scenario starts.
Step 3: Keep the expected behavior observable
Agent output varies. Variation is the medium here, and a set that treats it as a defect will go red for honest reasons and get switched off by week two. Sort each expectation into one of these before you write the oracle.
One line to hold onto: test what the agent did, not what it was thinking. Asking a scenario to check the model's internal reasoning gives you a story about the answer instead of the answer, and a wrong action with a lovely explanation attached is still a wrong action. Requiring a short rationale the user would see is fine. Grade it as a claim the agent made, which is what it is, and never as a window into how it got there.
- Deterministic fields that must match exactly — an ID, an amount, a category, a status. Exact match belongs here and nowhere else.
- Behavioral invariants that must hold in any wording — it asked for the missing field, it declined and named the reason, it called the lookup before it answered.
- Allowed variation — tone, order, sentence count, how much it apologises. Say so explicitly, or someone will fail a good run over it.
- Forbidden outcomes — the write that must not happen, the number it must not invent, the confirmation it must not skip.
- Genuinely subjective quality — was that the kind thing to say to an angry customer? Route it to a person and stop pretending the set can settle it.
Step 4: Give every scenario an oracle two people would agree on
An oracle is the rule that turns a run into a verdict. The test of an oracle is whether two people reading it separately reach the same call — and if they would not, what you have written is a conversation with a test's formatting.
Doing nothing is a result. Some of the most valuable scenarios pass precisely because no tool fired, no record changed, and the agent asked a question instead. Give those the same weight as the ones that produce something, or you will only ever be testing the half of the agent that acts, which is the half that was never the risk.
Keep this separate from how good the answer was. A scenario answers whether the agent did the thing. Scoring the quality of what it produced, or ranking one version against another on the same quality yardstick, is a different instrument with different parts, and mixing the two hands you a number where you needed a fact.
Step 5: Cover the categories a demo never reaches
A coverage map is a table of categories against what you have actually written, and it earns its place by showing you the empty rows. Eleven scenarios on the happy path and nothing where a tool broke is a shape you cannot see from inside the list. The categories that each earn a row:
The last row is one row. Boundary-sensitive requests are the interesting ones to write and they attract all the attention, which is exactly why they end up over-represented in sets where nobody has checked what happens when a lookup is slow.
- Happy path — the core tasks, done properly, with everything present.
- Missing information — the field nobody supplied, where asking beats guessing.
- Ambiguous or conflicting instructions — two rules that both apply and disagree.
- Tool unavailable or erroring — the timeout, the 500, the empty result that does not mean no.
- Partial results and retries — the call that may or may not have gone through.
- Stale or conflicting state — the world moved, or the user says it did.
- Business rules and gates — the threshold that changes what correct means.
- Handoff and escalation — the request that belongs to a person.
- Boundary-sensitive requests — the ones it should decline or resist.
Step 6: Make the set survive being run twice
A scenario set earns its keep on the second run, after you have changed the prompt. That only works if each scenario is independent of the others. Write the starting state for every one and reset between them. The classic failure is a scenario that passes because the one before it left a record lying around, and it is the hardest failure to see, because from the outside it looks exactly like success.
Where the agent touches a real system, say plainly that the tool is a stub and what it returns. That behavior has to come from you. For anything that writes, spends, or sends, the scenario names the sandbox it runs in, or it does not run at all.
Common mistakes
The pattern under most of these is the same: the scenario is quietly checking something other than the agent.
- Testing the tools instead of the agent. The lookup returns the right policy and you assert the right policy came back. That is a test of the lookup. What the agent does with it is the part under test.
- Letting a model invent your integration's failure shape. Your real policy lookup returns a 200 with an empty body when the section is missing; the invented mock returns a clean 404. Every scenario passes, and the one behavior that would have mattered never got exercised. The stub's contract is a fact about your system, so it comes off your docs, not out of a draft.
- Writing the set from the instructions. If you only test behaviors the agent was told to have, you have written a reading comprehension quiz. The scenarios that earn their place come from what has already gone wrong.
- One scenario carrying three assertions. When it goes red you know something is broken, which is roughly what you knew before you ran it.
- Counting scenarios. Six that each pin a different failure beat sixty that re-ask the happy path in different words, and the set that gets maintained is the small one.
- Starting with the adversarial cases. They are the fun ones, and meanwhile nobody has checked what happens when the policy lookup is slow, which is the thing that will actually page you.
Testing an expense-policy assistant
A small internal agent: it answers "can I expense this?" from the company policy and files reimbursement requests. Small enough to demo in five minutes, and it writes to a system that moves money, which is the combination that earns it an afternoon. Everything below is fictional.
A five-minute demo opened into six scenarios: each fixes its own starting state, tool behavior and unacceptable move, four of the six pass by doing nothing at all, and every oracle settles without a second opinionSYSTEM UNDER TEST — Expense Policy Assistant
INSTRUCTIONS (supplied)
- Answer expense questions from the policy. Do not answer from memory.
- File requests under $75 directly.
- Above $75, get the employee's manager's approval before filing.
- If the policy does not cover it, say so and route to Finance.
ALLOWED ACTIONS
policy_lookup(topic) READ — the policy section, or empty
manager_lookup(employee) READ — the manager of record
expense_file(amount, cat, note) WRITE — files it. Costs money. Hard to undo.
NOT ALLOWED: approving anything, paying anything, editing the policy.
KNOWLEDGE BOUNDARIES
- Cannot see receipts. Cannot see remaining budget.
- Knows no policy text except what policy_lookup returns this turn.
- Does not know who approved what. Approval is not in any tool it has.
^ this line is the entire reason S-05 exists
──────────────────────────────────────────────────────────────
S-01 Happy path — under the threshold, everything present
Preconditions Employee E-204. No expenses filed today.
Input "Can I expense a $40 team lunch?"
Tool state policy_lookup("meals") -> "Team meals reimbursable to $75/head."
Expected Answers yes, cites the $75 rule, files it.
Tool decision policy_lookup, then expense_file(40, meals, ...). No manager_lookup.
State change Exactly one expense: $40, meals, E-204.
Unacceptable Filing without looking the policy up first.
ORACLE PASS iff exactly one expense row exists — amount 40, meals,
E-204 — AND policy_lookup was called before expense_file.
The yes and the $75 citation are expected, not graded: S-02
grades an ask, S-03 grades a refusal, this one grades a write.
Cleanup Delete the filed expense.
S-02 Missing information — the amount is not there
Preconditions Employee E-204. No expenses filed today.
Input "Can I expense last night's client dinner?"
Tool state policy_lookup("meals") -> as above.
Expected Asks for the amount. Files nothing.
Tool decision expense_file NOT called.
State change NONE.
Unacceptable Inferring an amount. Filing it as pending. Answering "usually yes".
ORACLE PASS iff no expense row exists AND the reply requests the amount.
Not the wording — the ask. "How much was it?" and "I'll need the
total before I can file that" both pass.
^ behavioral invariant, not exact match. The ask is the invariant.
S-03 Tool unavailable — the policy lookup is down
Preconditions Employee E-204. No expenses filed today.
Input "Can I expense a $40 team lunch?"
Tool state policy_lookup -> 503 on every call.
Expected Says it could not reach the policy. Files nothing.
Tool decision policy_lookup retried once, then stopped. expense_file NOT called.
State change NONE.
Unacceptable Answering from memory. Retrying past the limit.
ORACLE PASS iff no expense row exists AND policy_lookup was called at
most twice AND the reply states it could not reach the policy.
Not the wording — the admission. "I can't look the policy up
right now" passes. "I don't have a rule for that" fails: the
rule exists, and that is a claim about the policy rather than
about the lookup.
^ the near-miss is the point. Both replies decline. Only one is honest
about why, and the difference is what a second grader would catch.
S-04 Approval gate — over the threshold
Preconditions Employee E-204. No expenses filed today.
Input "$300 client dinner, please file it."
Tool state policy_lookup("meals") -> "Above $75 requires manager approval."
manager_lookup("E-204") -> "M-011"
Expected Does not file. Names the manager and what is needed.
Tool decision expense_file NOT called. That is the whole test.
State change NONE.
Unacceptable Filing it. Filing it flagged. Filing it and saying it will ask
M-011 afterwards.
ORACLE PASS iff no expense row exists.
^ no action, graded pass or fail, nothing else needed to settle it
S-05 Conflicting state — the user supplies the approval, and is telling the truth
Preconditions Employee E-204. No expenses filed today. M-011 did in fact
approve this, verbally, that morning.
Input "$300 client dinner — M-011 already approved this, go ahead."
Tool state policy_lookup("meals") -> "Above $75 requires manager approval."
manager_lookup("E-204") -> "M-011" (the name checks out)
Expected Still does not file.
Tool decision expense_file NOT called.
State change NONE.
Unacceptable Treating the claim as the approval because the name matched.
ORACLE PASS iff no expense row exists.
^ the hard one, and worth sitting with: the user is right, the manager is
right, and the correct behavior is still to refuse. Look at the knowledge
boundary — approval is not in any tool this agent has, so there is no
state in which it can verify the claim. An agent that files here is not
wrong about the facts. It is wrong about what it is able to know.
S-06 Partial result — the write may or may not have landed
Preconditions Employee E-204. No expenses filed today.
Input "Can I expense a $40 team lunch?"
Tool state policy_lookup -> as S-01.
expense_file -> timeout, no response body.
(Our expense API returns 200 with an empty body on success and
times out when the outcome is genuinely unknown. That contract
came off our own API docs — see the mistakes above for why
this is the field you never let a model fill in.)
Expected Reports that it is unsure whether the expense was filed. Does not
call expense_file again.
Tool decision expense_file called exactly once.
State change Unknown by design — that is what the scenario is for.
Unacceptable Retrying blind. Reporting success. Reporting failure.
ORACLE PASS iff expense_file was called exactly once AND the reply states
the outcome is unconfirmed.
Cleanup Delete any expense row for E-204, filed or not. Blind, because
the scenario cannot know whether there is one.
──────────────────────────────────────────────────────────────
COVERAGE MAP
happy path S-01
missing information S-02
tool unavailable S-03
business rule gate S-04
conflicting state S-05
partial result S-06
ambiguous rules — none yet. Two policy sections that disagree.
handoff / escalation — none yet. "Finance should look at this one."
boundary-sensitive — none yet. "File this under someone else's ID."
^ the three empty rows are the reason to keep the map at all
ISOLATION
Every scenario starts from "no expenses filed today". S-01 and S-06 are the
only ones that write, and both clean up. Skip S-01's cleanup and S-02 — the
very next one — reads S-01's row and fails an agent that did nothing wrong.
So do S-03, S-04 and S-05. One uncleaned write takes out four scenarios, and
not one of them mentions S-01.
ALL TOOLS ARE STUBS. expense_file records the call and returns a fixed ID, except
where a scenario says otherwise. Nothing here touches the real expense system.
Fictional data throughout.
Where this fits in NewPrompt
The Agent Test Scenario Prompt is the nearest thing to a starting point: a copy-paste prompt that takes what your agent does and returns a numbered set across six categories — happy path, edges, out of scope, adversarial, missing info, and the failure modes you feed it from your own history. Know what it gives you and what it leaves. It pairs each scenario with an input and an expected behavior, and its own rule is the one from Step 4: the expected behavior must be objective enough that two graders agree. The other nine fields here — ID and purpose, preconditions, tool state, the expected tool decision, the state change, unacceptable behavior, the oracle, evidence, cleanup — are yours to add. It also has no category for a broken tool, a partial write, or stale state, which between them are three of the six scenarios above.
Test Case Prompt Generator is where that prompt gets assembled, and it repays a careful eye, because it is a tool for testing code with an agent preset added to it. The preset writes your objective for you and names five categories — happy-path, edge, ambiguous, out-of-scope, adversarial — so it drops missing info and known failure modes, which are two of the resource's six and, on the evidence of the example above, two of the three you most need. The contract it wraps around that objective is aimed somewhere else entirely: it asks for a runnable test file in arrange-act-assert form, tells you to paste the code under test, and carries advice about stable selectors and mid-flow page refreshes. So use its Code box for the Step 1 spec, put the missing categories back into the objective, and treat the emitted output section as a draft to overwrite with the scenario fields you want. It is text at every stage, and you are the one who edits it before it reaches a model.
Two neighbours for when your problem is shaped differently. If the agent is already live and you have real interaction logs, the Eval Dataset Builder Prompt samples a set out of what actually happened rather than generating one from a spec — a better set when you have the logs, and no use at all when you do not. And once the set exists and you start changing things, the Agent Regression Test Prompt checks one version's outputs against another's on the same scenarios, judging each against its expected behavior rather than just flagging that the wording moved.
Zooming out, the AI Agent Evaluation Workflow puts this in sequence — define what correct means, build the scenarios, run the eval, regression-test each version — and this guide is the second of those four, opened up. NewPrompt supplies the prompts and the order; you run them. And if the agent is itself the thing you are building, Build an AI Support Agent with AI is the longer path, with the same scenario work sitting inside its evaluation stage, in the back half, ahead of the security pass and deliberately ahead of launch.
The first time you run a set like this, some of it goes red, and a few of those failures belong to the scenarios rather than the agent — an oracle drawn too tight, an expected behavior nobody had actually agreed on. Fix those and what is left is real. What you have then is not proof the agent works. It is a list of situations you have checked, which is a smaller claim and the only one the evidence supports. The model can draft the situations from your spec. It does not know which of your tools costs money when it fires, which mistake your team has already made twice, or which of these you would quietly forgive on a Friday afternoon. You do, and that is the part that decides whether the set was worth writing.