result.object is typed as z.output<typeof schema>, so zod defaults and
transforms apply. result.text keeps the raw JSON text the model wrote.
The schema can come from zod 3 or zod 4: it is rendered with z.toJSONSchema
for zod 4 and with the ai SDK’s converter for zod 3, and validated with the
schema’s own safeParse either way. output is typed as a Standard Schema,
the same as defineTool({ input }), so result.object is inferred without a
cast from a zod 3 schema, a zod 4 schema (zod/v4 on zod 3.25, or zod 4
itself) or any other Standard Schema that can produce JSON Schema:
Sessions
agent.session() on an agent with output returns a session whose
send() result (and stream()’s result) carries the typed object of each
turn. There is no per-call output override: the schema is the agent’s.
Sub-agents
A sub-agent created with its ownoutput schema answers the lead with its
validated object. The task tool result (and agent_await’s result for a
background task) is the object as JSON, then a blank line and the usual
[sub-agent '<name>': ... taskId '<id>'] footer, so the lead model sees the
object rather than a prose rendering. If the sub-agent’s reply is still invalid
after its repair step, the task call fails with the structured tool error
(kind: 'execution', a message naming the schema issues) and the lead can
retry or adapt.
A sub-agent does not inherit the lead’s output: each agent’s output is its
own, and a sub-agent without output returns text as before. A
remoteAgent() whose deployed agent has an
output schema returns the remote object the same way (JSON, then its footer),
read from the object of the remote stream’s run.done event.
How it works
- The system prompt gets an
## Output formatsection asking for the final answer as only a JSON object matching the schema, rendered as JSON Schema. - Every model call carries
responseFormat: { type: 'json', schema }onGenerateOptions. It is a hint: the built-inai-SDK providers turn on the model’s JSON mode (with the schema, for models that support structured outputs); a custom provider may use it or ignore it. - When the model replies without tool calls, the reply is parsed as JSON (a
```jsoncode fence around it is tolerated) and validated with the schema. - If it is invalid, the model gets one repair step: a user message
starting with
[output-invalid]that lists the issues, for example1 issue (tempC: Expected number, received string). The repair step counts againstmaxSteps, and there is none when the budget is spent. - Still invalid, the run resolves (it does not reject) with
finishReason: 'output-invalid', noobject, andoutputError:{ message, issues: [{ path, message }] }.
Streaming
agent.stream() works the same way: run.result resolves with object,
and the final run.done event carries object (JSON-encoded) when the
reply was valid. The repair step shows up as one more
step.start / step.done pair. See Streaming.
Without createAgent()
AgentExecutor.execute() and AgentExecutor.stream() take the same
output option. There result.object is unknown; parse it again with your
schema, or use createAgent() for the inferred type.
Testing
WithmockModel, script the JSON text the model would write: