The return contract is a schema. Not a formatting preference, a schema with required fields and enumerated values, so the caller can branch on the result without reading prose. Modern APIs will enforce this for you: the OpenAI structured outputs guide describes supplying a JSON Schema so the model always produces responses that adhere to it, with no omitted required key and no invalid enum value, and notes that this removes the need for strongly worded formatting prompts OpenAI's structured outputs guide. The Strands SDK docs show the same pattern at the agent level: define a model, pass it to the invocation, and read the validated object off the result the Strands structured output docs.
Four fields carry almost all the value:
status: one of done, partial, blocked. This is the routing signal. It must be honest, which is why the brief's stop conditions define what each value means.
result: the payload itself, shaped to the acceptance criteria. For a research subagent, a list of findings each with a claim and a source. For a code subagent, a diff plus the test command that was run.
evidence: the receipts. File paths, URLs, line numbers, query strings, test output. This field is what lets the orchestrator spot-check without redoing the work.
next_action: one sentence from the subagent about what the caller should do. It is advisory, not authoritative, but it reliably surfaces the cases where the subagent knows the result is shaky.
You can add fields for cost, elapsed time or confidence, but only if the caller actually branches on them. Every required field is a chance for the subagent to produce a technically compliant but useless value, so keep the schema small.
A schema that works in practice
{
"type": "object",
"required": ["status", "result", "evidence", "next_action"],
"additionalProperties": false,
"properties": {
"status": { "enum": ["done", "partial", "blocked"] },
"result": {
"type": "object",
"required": ["summary", "findings"],
"properties": {
"summary": { "type": "string", "maxLength": 600 },
"findings": {
"type": "array",
"items": {
"type": "object",
"required": ["claim", "source"],
"properties": {
"claim": { "type": "string" },
"source": { "type": "string" }
}
}
}
}
},
"evidence": { "type": "array", "items": { "type": "string" } },
"next_action": { "type": "string", "maxLength": 240 }
}
}
Two design choices in that schema are worth calling out. First, additionalProperties: false prevents the subagent from burying a caveat in an undeclared field the caller never reads. Second, the maxLength values are load-bearing: they force compression, and compression is the whole reason the subagent exists. Anthropic describes subagents as facilitating compression, condensing the most important tokens from a large exploration into something the lead agent can use Anthropic's engineering write-up. An unbounded summary field defeats that.
Worked example: a billing investigation
Take a support triage orchestrator with three subagents. The billing subagent receives this brief:
Task: Determine whether order 12345 was charged twice.
Inputs: order id 12345; customer id 8841; payments export at /data/payments_q3.csv (columns: order_id, customer_id, amount_cents, captured_at, status); refund policy: duplicate captures are refunded in full within 30 days of capture.
Constraints: read-only access to /data; do not contact the payment provider; quote amounts in cents; cite the exact rows you used.
Acceptance criteria: a duplicate determination with row-level evidence, and the refund amount in cents if a duplicate exists.
Stop conditions: if the export has no rows for this order, return blocked with the reason. If you have opened 200 rows and cannot decide, return partial.
A compliant return might read: status done, summary "two captures of 4900 cents on the same order 6 minutes apart, duplicate confirmed, refund of 4900 cents due," evidence naming the two CSV rows, next action "initiate refund and confirm with customer."
Now the counterfactual. If the brief had omitted the refund policy line, a competent subagent might still find the duplicate but return "refund amount unclear, needs policy." The orchestrator burns a round trip. If the brief had omitted the blocked condition, a subagent finding zero rows would likely return status done with an empty findings list, which the caller cannot distinguish from "no duplicate found." That distinction is the difference between a refund and a chargeback.