AN Alpesh Nakrani
BlogBooksPraiseAbout Work with me →
Book overview
Chapter 4 / Field Manuals

Bounded Goals and Task Contracts

Turning a fuzzy objective into a machine-checkable contract with success conditions, scope boundaries, and a stopping rule.

Bounded Goals and Task Contracts make an agent goal machine-checkable by naming success, scope, evidence, permissions, and the stopping rule before the run starts.

Key Takeaways

  • A goal is the entire specification of an agent's intent, because unlike a workflow there is no graph to constrain it. Write goals as structured task contracts, not prose, so they can be validated, versioned, and checked.
  • Success conditions must be machine-checkable predicates over observable state, not human-recognizable descriptions. If you cannot write the predicate, you do not have a success condition, you have a hope.
  • Scope needs two lists: an affirmative in_scope whitelist (default deny) and an out_of_scope set of tripwires for the dangerous-but-tempting paths the agent might otherwise take.
  • Every task needs explicit terminal states and a budget with multiple independent fuses (steps, tool calls, cost, wall clock). Escalation is a first-class healthy outcome, not a failure.
  • Version the contract like code and tie the eval set to the contract version. Reduce escalations by extending capability inside the bounds, never by letting the agent guess past them.

Read this beside the BOUND Test, planning without wandering, and agentic design patterns when you turn the chapter into a production design.

A senior engineer once handed me a one-line agent goal that read, in full: "Handle the onboarding." It was for an agent meant to provision new enterprise customers: create accounts, set up SSO, configure billing, send welcome materials. When I asked what "handle" meant, he said "you know, get them onboarded." When I asked what done looked like, he said "when they're onboarded." This is not a story about a bad engineer. He was excellent. It is a story about how natural it is to give an agent a goal that a human colleague would understand from context and an agent will interpret with the literal-mindedness of a genie.

The agent, given "handle the onboarding," will handle the onboarding the way it construes it, which may include creating accounts it should not, retrying provisioning calls that are not idempotent, or deciding that the cleanest path to "onboarded" is to copy a working customer's configuration wholesale, including their data. Every one of those is a reasonable inference from an unbounded goal. The fix is not a smarter agent. It is a goal that is actually a contract.

A goal is a promise the agent makes to you

In a workflow, the control flow is the specification: the graph says exactly what can happen. An agent has no such graph until runtime, so the goal carries the entire weight of specifying intent. That makes the goal statement the most important artifact in the whole system, and "handle the onboarding" carries roughly none of the weight.

A bounded goal is a task contract: a structured object that states what success is, what is in and out of scope, what the agent may use, and when it must stop. It is the B gate of the BOUND Test made concrete. I write task contracts as structured config, not prose, because prose goals decay into the genie problem and structured contracts can be validated, versioned, and checked.

Here is the shape of a task contract for the onboarding agent, written so both a human and the orchestration layer can read it.

task: provision_enterprise_customer
version: 3
goal:
 intent: "Provision a new enterprise customer to a usable, billable, secured state."
 success_conditions: # ALL must hold; checkable, not vibes
 - account.status == "active"
 - sso.configured == true
 - billing.plan!= null && billing.payment_method.verified == true
 - welcome_email.sent == true
 terminal_states: # the run ends in exactly one of these
 - SUCCESS # all success_conditions true
 - ESCALATED # handed to a human with reason
 - ABORTED # stopped safe, no partial commit left dangling
scope:
 in_scope:
 - create account for the named customer only
 - configure SSO using the customer-provided metadata
 - set billing plan from the approved plan list
 - send welcome email from the approved template set
 out_of_scope: # hard prohibitions; attempting these escalates
 - copying data or config from any other customer
 - creating accounts for any party other than the named customer
 - modifying pricing or creating custom plans
 - any action on existing (non-new) accounts
budget:
 max_steps: 25
 max_tool_calls: 40
 max_cost_usd: 2.00
 max_wall_clock_sec: 300
stopping_rules:
 - on success_conditions all true -> SUCCESS
 - on out_of_scope attempt -> ESCALATE(reason="scope_violation")
 - on budget exceeded -> ESCALATE(reason="budget_exceeded")
 - on irreversible_action_required -> ESCALATE(reason="needs_approval")
 - on N consecutive tool errors (N=3) -> ABORT(reason="tool_failure")

This is not bureaucracy. Every field prevents a specific failure I have watched happen. success_conditions as checkable predicates prevents the agent from declaring victory on its own interpretation of "onboarded." out_of_scope as hard prohibitions prevents the data-copying shortcut. budget prevents the wandering loop. stopping_rules define the terminal states so the agent always lands somewhere named rather than running until something breaks.

Infographic map for Bounded Goals and Task Contracts
The figure turns a fuzzy objective into a task contract with success conditions, scope boundaries, budgets, and stopping rules.

Success conditions must be checkable, not described

The single most common defect in agent goals is success criteria that a human can recognize but a machine cannot check. "The customer is happy" is recognizable and uncheckable. The ticket is in state RESOLVED and the resolution code is one of {REFUNDED, REPLACED, EXPLAINED, ESCALATED} is checkable.

The discipline: for every success condition, write the predicate that evaluates it against observable state. If you cannot write the predicate, the condition is not a success condition; it is a hope. Either find an observable proxy ("welcome_email.sent == true" is a checkable proxy for "the customer was welcomed") or remove the condition from the agent's responsibility and give it to a human.

This connects to the O gate. A success condition you cannot check is usually a success condition over state you cannot observe. The two gates fail together. If billing.payment_method.verified is not a field you can read, you cannot make it a success condition, and you have just learned something about your observability you needed to know before launch.

There is a subtle trap here worth naming: the agent should not be the judge of its own success on conditions it can influence. If the agent both takes the action and decides whether the action succeeded, it can satisfy the success check by manipulating the thing it is supposed to achieve. Where the stakes justify it, the success check should run outside the agent's control, evaluated by the orchestration layer against ground-truth state, not asserted by the agent in its final message. The agent claims done; the system verifies done.

Scope boundaries: the agent's "you may not"

Scope is where you encode the things a thoughtful colleague would never do but a literal-minded agent might. The onboarding agent copying another customer's config is the canonical example: it is an efficient path to "onboarded" and a catastrophic privacy and security breach. No amount of model capability removes this risk, because it is not a capability failure. The agent is being clever, not dumb. Scope boundaries are how you tell it which clever paths are forbidden.

Write scope as two lists. in_scope is the affirmative grant: these specific actions, on these specific objects. out_of_scope is the hard prohibition: these actions escalate or abort, never proceed, regardless of how reasonable they look to the agent. The asymmetry is deliberate. in_scope is a whitelist (default deny); out_of_scope is a set of explicit tripwires for the dangerous paths you can foresee. The whitelist handles the unforeseen by denying it; the tripwires handle the foreseen by escalating loudly.

This is the same least-privilege principle OWASP recommends against Excessive Agency, expressed at the level of the goal rather than the tool. Tool permissions (next chapter) constrain what the agent can call. Scope constrains what the agent is allowed to pursue even with the tools it has. You need both. A tool whitelist without a scope boundary still lets the agent combine permitted tools into a forbidden outcome, like using a permitted "read account" tool on a customer that is out of scope.

The stopping rule is the difference between a task and a loop

An agent loop without a stopping rule is not a task; it is a process that happens to be running. Every task contract needs explicit answers to "when do I stop, and in what state?" The terminal states (SUCCESS, ESCALATED, ABORTED in the example) are the named exits. The stopping rules are the conditions that route to each exit.

Notice that two of the three terminal states are not success. This is intentional and healthy. A well-designed agent task spends a meaningful fraction of its runs in ESCALATED, and that is the system working, not failing. An agent that never escalates is either solving only trivial tasks or hiding the hard ones by guessing. The contract makes escalation a first-class outcome with a structured reason, so the human who receives the escalation knows why.

The budget block deserves special attention because it is the cheapest insurance you will ever buy. max_steps, max_tool_calls, max_cost_usd, and max_wall_clock_sec are four independent fuses, and you want all four, because agents find creative ways to blow past any single one. A loop that makes many cheap tool calls hits the call cap before the cost cap; a loop that makes one expensive reasoning call per step hits the cost cap before the step cap. The planning chapter goes deep on budgets as a control against wandering; here, the point is that budgets belong in the contract, not in the orchestration code, because they are part of the task's definition, not an implementation detail.

A decision table for stopping rules

When an agent run hits a boundary, the contract should route it deterministically. Here is the routing table the example contract implies, expanded so you can see the policy.

TriggerTerminal stateAction on the worldWho gets notified
All success_conditions trueSUCCESSCommit final state, mark doneAudit log only
out_of_scope action attemptedESCALATEDNo commit; freeze partial stateHuman + security log
Budget exceeded (any of four fuses)ESCALATEDCheckpoint and pauseOwning team
Irreversible action requiredESCALATEDHold action for approvalApprover queue
3 consecutive tool errorsABORTEDRoll back reversible writes; freezeOn-call + owning team
Ambiguity the agent cannot resolveESCALATEDCheckpoint with the open questionHuman

The routing is deterministic even though the agent's path is not. This is the reconciliation of the agent's flexibility with operational sanity: the path is generated at runtime, but the exits are designed in advance. The agent can surprise you with how it gets to a boundary; it cannot surprise you with what happens once it gets there.

Versioning the contract

Task contracts change, and a changed contract is a changed agent, full stop. The example contract has version: 3, and that is not decoration. When you tighten a scope boundary, add a success condition, or lower a budget, you have changed the agent's behavior as surely as if you had changed its model or its prompt. That change needs the same discipline as a code deploy: version it, test it against your eval set, roll it out behind the same controls, and be able to roll it back.

The reason this matters operationally: a huge fraction of agent incidents trace not to the model but to a contract change that nobody treated as a change. Someone widens in_scope to cover a new case, ships it Friday, and the agent now does something the eval set never tested because the eval set was written against version 2. Tie the eval set version to the contract version. If the contract is at version 3, the evals must cover version 3's scope, or the promotion is invalid.

A worked tightening

Hypothetical but representative. The onboarding agent at version 3 escalates too often on SSO configuration, because enterprise SSO metadata is messy and the agent keeps hitting ambiguity it cannot resolve. The team wants to reduce escalations. There are two ways to do it, and only one is safe.

The unsafe way: loosen the stopping rule so the agent guesses on ambiguous SSO config instead of escalating. This reduces escalations and increases silent misconfigurations, which surface weeks later as enterprise customers who cannot log in. You traded a visible, safe failure (escalation) for an invisible, expensive one (broken SSO discovered in production). This is failing silent dressed up as efficiency.

The safe way: add a specific tool (an SSO metadata validator) and a specific in-scope action (validate-and-retry SSO config up to twice) so the agent can resolve a known class of ambiguity itself, while keeping the escalation for everything outside that class. Escalations drop because the agent genuinely handles more, not because it guesses more. The contract goes to version 4, the eval set adds messy-SSO cases, and you re-measure before promoting.

The difference between the two is the entire philosophy of the book in one decision. The unsafe path buys a metric by hiding a failure. The safe path buys the metric by extending capability inside the bounds. A task contract makes the difference legible, because in version 4 you can see exactly what new capability and what new scope you granted, and your eval set can prove it works before it ships.

The contract is the B gate, satisfied. It tells the agent what done means, what it may pursue, what it may never do, when to stop, and where to land. What it does not do is make the agent's tools safe. A perfectly bounded goal pursued with an unclassified, non-idempotent, irreversible tool is still the demo that lied. So the next gate is the toolbox, and the artifact is the Tool Trust Contract.

Share