API
Structured output
Machine-to-machine callers usually want a typed object from an agent, not
prose. Structured output lets you attach a JSON Schema to a turn: Mobius makes
the agent submit its final result through a reserved tool, validates the
submission against your schema on the server, and stores the validated object
on the turn. You read it from one field. There is no fenced-JSON scraping, no
JSON.parse, and no ambiguity about whether the model actually met your
contract.
Use it when your backend consumes the result — enrichment, classification, extraction, option generation — and a malformed object would break the next step. For conversational replies your app renders as text, keep the normal turn output.
The contract is per turn, not part of the agent. You attach it on
invokeAgent or on a
start-turn call, so the same stored agent can run with a
schema on one turn and without one on the next, and two turns of a session can
declare different schemas.
Attach a schema
Add an output object with a schema to the request:
{
"agent_ref": { "name": "lexicon-augmenter" },
"session": { "mode": "new", "visibility": "private" },
"input": {
"content": [{ "type": "text", "text": "Augment: throughput" }]
},
"output": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": ["options"],
"properties": {
"options": {
"type": "array",
"minItems": 12,
"maxItems": 12,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["term", "definition"],
"properties": {
"term": { "type": "string", "minLength": 1 },
"definition": { "type": "string", "minLength": 1 }
}
}
}
}
}
}
}The schema root must be type: object. The object you get back is the value
that satisfies it.
Mobius appends a reserved mobius_submit_output tool whose input schema is the
schema you sent, and adds a short instruction telling the agent to call it once
with its final result. The tool is present even for agents that otherwise have
no tools, so a no-tool agent becomes a one-tool agent for that turn.
Read the validated object
The validated value lands on the completed turn as output. Read it from the
turn record — the REST turn read, listTurns, or the streaming turn.upsert
frame all carry it once the turn is completed:
{
"id": "turn_01J9...",
"status": "completed",
"output": {
"options": [{ "term": "throughput", "definition": "..." }]
},
"output_source": "tool"
}output is present only on a completed turn that declared a schema. A turn
with no schema, or a turn that failed, has no output.
output_source tells you where the value came from: tool when the agent
submitted it through mobius_submit_output (the normal path), or text when
Mobius accepted a schema-valid final message as a fallback. A steady stream of
text sources is a signal that the model is not using the tool reliably.
The SDKs expose a typed reader on the folded turn record —
turn.structuredOutput<T>() (TypeScript), turn.structured_output() (Python),
and turn.StructuredOutput() (Go). The generic is your own type assertion; the
server-side schema is the authority, and the SDK adds no second validation
layer. These SDK helpers ship in a follow-up release; the REST fields (output,
output_source) are available now.
Supported schema features
Mobius validates against the OpenAPI 3.0 schema subset. Author your schema to this table:
| Supported | Not supported |
|---|---|
type (root must be object) | $ref / $defs (inline instead) |
required | if / then / else |
enum | const (use a one-value enum) |
additionalProperties | patternProperties |
minItems / maxItems | prefixItems |
minLength / maxLength | |
pattern | |
numeric bounds (minimum, maximum, …) | |
| nested objects and arrays | |
oneOf / anyOf / allOf | |
nullable |
Mobius rejects an unsupported keyword at request time rather than silently
ignoring it. Because the validator would otherwise skip the keyword — leaving the
value it was meant to constrain unchecked — a schema that leans on one would
validate more permissively than you intended. Inline any $ref/$defs, and use
the supported forms (for example a one-value enum instead of const).
The schema must be self-contained and at most 32 KiB serialized. A single submitted value is capped at 256 KiB serialized; a larger submission is rejected at the tool boundary like any other validation failure.
When validation fails
A rejected submission comes back to the agent as an ordinary tool error inside the same turn, so the model can correct itself and submit again. This repair is bounded by the turn's tool-iteration budget and its timeout, and the extra tokens are billed as normal turn usage. You do not configure a retry count.
If the turn ends without a schema-valid object — the model never called the
tool, or every submission was invalid — the turn fails with
error_type: output_schema_unsatisfied, and error_message carries the final
validation error. An invalid result can never produce a completed turn.
Rejected submissions are ordinary tool_use / tool_result rows in the turn
transcript, so you can see exactly what the model tried and why it was rejected
without any extra diagnostics surface.
If a turn submits its output and then suspends — waiting on an event or a
long-running job — the in-memory submission is not carried across the resume. On
resume Mobius falls back to accepting a schema-valid final message, so a
tool-using turn that resumes with prose only can fail with
output_schema_unsatisfied. Structured-output workloads are usually no-tool or
tool-light and rarely suspend; if yours both uses tools and suspends, expect the
text fallback to govern the resumed turn.
Size the timeout for a repair round
Because repair happens inside the turn, size timeout_seconds for at least one
correction round, not just one clean generation. If a single generation takes
about 11 seconds, a 20-second timeout leaves no room to recover from a first
invalid submission; give the turn enough budget to submit, read the validation
error, and submit again.
Example: replace a hand-rolled parser
Before, a caller collected assistant text, stripped code fences, found the first
{, parsed it, and validated in application code — and still could not tell
whether the model produced the wrong shape or produced the right shape that
failed app-side validation.
With a schema attached, the turn either completes with a validated object or
fails with output_schema_unsatisfied:
const turn = await client.invokeAgent({
agentName: "lexicon-augmenter",
content: [{ type: "text", text: input }],
session: { mode: "new", visibility: "private" },
config: { effort: "low", timeout_seconds: 45, toolkits: [], skills: [] },
output: { schema: augmentationOutputSchema }, // e.g. minItems 12, maxItems 12
});
for await (const _ of turn.updates()) {
}
if (turn.status !== "completed") {
throw new Error(turn.errorMessage);
}
const value = turn.structuredOutput<AugmentationOutput>();The parser is gone. What remains in your app is whatever no schema can express — semantic checks like naming quality — not shape validation.