Guidance versus enforcement
A system prompt that says “always verify the customer before processing a refund” will be followed most of the time. The guide's point is that most of the time is not good enough when the rule is a compliance requirement: prompt instructions have a non-zero failure rate. When ordering must be deterministic, enforce it programmatically.
Anthropic's Claude Code guidance makes the same distinction in its own words: instructions in CLAUDE.md are advisory, while hooks are deterministic and guarantee the action happens. The same logic applies to any agent. If you can state a rule as a condition your code can check — a verified id exists, an amount is under a limit, a status is “approved” — then check it in code and let the prompt handle everything that is a matter of judgement.
| Prompt-based guidance | Programmatic enforcement | |
|---|---|---|
| How it works | Instruction in the system prompt | A hook or prerequisite gate in code |
| Reliability | Probabilistic | Deterministic |
| Right for | Style, tone, preferred order | Identity checks, financial limits, compliance |
| Failure looks like | Occasional skipped step | Blocked call with a clear reason |
Instruction or enforcement?
- Money moves or law is brokenGate it in codeblock the tool until the prerequisite holds
- Wrong person's data is exposedGate it in codecheck identity before any lookup
- The reply is slightly worsePrompt guidancetone, format, preferred order
Prerequisite gates
A gate blocks a downstream tool until an upstream step has completed. The canonical example: process_refund is refused unless get_customer has already returned a verified customer id in this session.
def run_tool(name, args, session):
if name == "process_refund":
if not session.verified_customer_id:
# returned to Claude as a tool_result with is_error set
return tool_error(
category="validation",
retryable=False,
message="Verify the customer with get_customer before a refund.",
)
if args["customer_id"] != session.verified_customer_id:
return tool_error(
category="permission",
retryable=False,
message="Refund target does not match the verified customer.",
)
result = TOOLS[name](**args)
if name == "get_customer" and result.get("verified"):
session.verified_customer_id = result["id"]
return resultThe blocked call returns a structured error the model can act on — here, by calling get_customer first. In the Messages API that means a tool_result with is_error set and a message that says what to do next; the tool-use documentation specifically recommends instructive errors over a bare “failed”. Structured tool errors are covered properly in 2.2, and the Agent SDK's hook-based version of the same gate is covered in 1.5.
Notice two details. The gate records state from a successful upstream call (verified_customer_id) rather than trusting the model's claim that it verified someone. And it checks that the refund is for the same customer who was verified — a gate that only asks “has anyone been verified?” can be satisfied by the wrong person.
The gate in action
process_refund(C-20931, 2499)get_customer(C-20931)process_refund(C-20931, 2499)Multi-concern requests
Customers bundle problems: a late delivery, a double charge and an address change in one message. Decompose them into distinct items, investigate each — in parallel where they are independent, using shared context such as the customer record — then synthesise one resolution. Handling them as a single blob tends to resolve the loudest issue and drop the rest.
Structured handoff
When the agent escalates to a human mid-process, the human usually cannot see the transcript. A handoff that says “customer is upset about a refund” forces them to start again. A structured summary lets them act immediately.
{
"customer_id": "C-20931",
"issue": "Charged twice for order 88412",
"root_cause": "Payment retried after a gateway timeout; both captures succeeded",
"actions_taken": ["Verified identity", "Confirmed duplicate capture"],
"recommended_action": "Refund one capture of INR 2,499",
"why_escalated": "Refund exceeds the agent's automatic limit"
}Checking a weak handoff note
- Missing: Who the customer isno customer id
- Check: What the issue is“refund” — for which order?
- Missing: Root cause found so far
- Missing: Actions already takenwas identity verified?
- Missing: Recommended next action
- Missing: Why it was escalatedlimit, policy, or anger?
- Passes: Customer sentimentthe one thing it does say
Traps the wrong answers are built from
| Tempting but wrong | Do this instead |
|---|---|
| Relying on the system prompt for a compliance-critical order of steps | Block the downstream tool until the prerequisite has completed. |
| Escalating with a one-line summary | Hand off customer id, root cause, actions taken and a recommended action. |
| Treating a multi-issue message as one problem | Split into items, investigate each, then synthesise one reply. |
| A gate that trusts the model's claim that a step happened | Record state from the upstream tool's actual result and check it. |
| A bare “failed” when the gate blocks a call | Return an instructive error naming the step that must come first. |
You should now be able to
- Implement prerequisite gates that block a tool until an earlier step has succeeded.
- Decompose multi-concern requests and investigate the parts before one resolution.
- Compile structured handoff summaries for humans who lack the transcript.
- Decide when a rule needs enforcement rather than instruction.
- Write gate errors that let the model recover by itself.