Output schema
outputSchema (TypeScript / Python: outputSchema / output_schema; Go: OutputSchema) asks the provider to constrain the model’s final assistant message to a JSON document conforming to a JSON Schema. Useful when the SDK feeds the reply directly into downstream code without LLM-flavoured prose to parse out — typed extraction, agent-to-agent handoffs, function-style RPCs, compatibility tests, etc.
The terminal result event still carries the reply as text: string. Each SDK ships a helper (parseRunOutput / parse_run_output / ParseRunOutput) that turns it into a typed value. Use strict enforcement when accepting an unconstrained provider fallback would be incorrect.
Wire shape
Section titled “Wire shape”"outputSchema": { "name": "weather_report", // optional; default "output"; /^[a-zA-Z0-9_-]{1,64}$/ "schema": { /* JSON Schema */ }, // required, root must be a JSON object "enforcement": "strict" // optional; default "best_effort"}| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | no | Stable identifier the server forwards to providers (OpenAI text.format.name, Anthropic synthetic-tool name). Defaults to "output". |
schema |
object | yes | JSON Schema describing the assistant text. Root must be a JSON object — most providers reject array / scalar roots in structured-output mode. Shipped verbatim; MANTYX does not validate the schema’s contents (the provider does). |
enforcement |
string | no | "best_effort" preserves the historical fallback behavior and is the default. "strict" fails unless MANTYX actually enforces the schema. |
Server-side limits (mirrored locally by every reference SDK so you get an early typed error):
| Constraint | Limit |
|---|---|
Serialised JSON size of the whole outputSchema |
≤ 32 KB |
name regex |
/^[a-zA-Z0-9_-]{1,64}$/ |
schema shape |
non-null, non-array JSON object |
Enforcement and observability
Section titled “Enforcement and observability”best_effort remains the default. If OpenAI or Gemini rejects a provider-facing
schema, MANTYX may retry without it so existing applications still receive an
answer. strict never makes that unconstrained retry and fails providers or
models that have no enforceable path.
Every new-server terminal result/error reports actual execution:
{ "schemaRequested": true, "schemaEnforced": true, "enforcementMechanism": "native_schema", "unconstrainedFallbackOccurred": false}The block is exposed as result.structuredOutput /
result.structured_output / result.StructuredOutput. Its fields are optional
at the SDK boundary for compatibility with older MANTYX servers. Strict
provider-compatibility tests should require schemaEnforced and reject any
unconstrainedFallbackOccurred. The fallback flag is run-level: once any
provider call falls back unconstrained, the terminal value remains true.
Per provider
Section titled “Per provider”| Provider | How the schema is enforced |
|---|---|
| OpenAI Responses (o-series, GPT-5.x, …) | text.format = { type: "json_schema", strict: true, name, schema } on every completeTurn (compatible with tool calls). |
| Gemini ≥ 2.5 | responseMimeType: "application/json" + responseJsonSchema on no-tools turns (Gemini rejects schemas alongside functionDeclarations). |
| Anthropic / Bedrock-Anthropic | Synthetic final_report tool whose input_schema is the supplied schema; tool_choice is forced on the no-tools finishing turn. The tool’s input is surfaced as the assistant text. |
| xAI Grok, others | Best effort may return unconstrained text; strict mode fails explicitly. |
outputSchema is independent of reasoningLevel: the model can think extensively and emit JSON.
Per-SDK syntax
Section titled “Per-SDK syntax”TypeScript
Section titled “TypeScript”import { z } from "zod";import { MantyxClient, parseRunOutput } from "@mantyx/sdk";
const client = new MantyxClient({ apiKey: "...", workspaceSlug: "acme" });
const WeatherJsonSchema = { type: "object", properties: { city: { type: "string" }, temperature_c: { type: "number" }, }, required: ["city", "temperature_c"], additionalProperties: false,} as const;
const Weather = z.object({ city: z.string(), temperature_c: z.number(),});
const result = await client.runAgent({ systemPrompt: "Return the weather as JSON.", prompt: "What's the weather in San Francisco right now?", outputSchema: { name: "weather_report", schema: WeatherJsonSchema, enforcement: "strict" },});
const report = parseRunOutput(result, (v) => Weather.parse(v));// ^? { city: string; temperature_c: number }parseRunOutput<T>(result, validator?) JSON-parses result.text, runs the optional validator (zod .parse, Ajv, anything callable), and throws a typed MantyxParseError on failure. The original raw text is preserved on err.text for logging.
Python
Section titled “Python”from pydantic import BaseModelfrom mantyx import MantyxClient, parse_run_output
WEATHER_SCHEMA = { "type": "object", "properties": { "city": {"type": "string"}, "temperature_c": {"type": "number"}, }, "required": ["city", "temperature_c"], "additionalProperties": False,}
class Weather(BaseModel): city: str temperature_c: float
client = MantyxClient(api_key="...", workspace_slug="acme")
result = client.run_agent( system_prompt="Return the weather as JSON.", prompt="What's the weather in San Francisco right now?", output_schema={"name": "weather_report", "schema": WEATHER_SCHEMA, "enforcement": "strict"},)
report = parse_run_output(result, Weather.model_validate)# report is a fully-typed Weather instance.The [OutputSchema] TypedDict from mantyx.tools is the type alias for the dict shape; pass any Mapping[str, Any] that conforms.
OutputSchema.Schema accepts either a map[string]any / json.RawMessage
JSON Schema or a Go struct (or pointer-to-struct). When given a struct,
the SDK reflects it via google/jsonschema-go — the same path
LocalToolSpec.Parameters uses — so a single Go type can drive both the
schema you ship to the provider and the typed receive shape you decode
into:
import ( "context" "errors" mantyx "github.com/mantyx-io/mantyx-sdk/go")
client := mantyx.NewClient(mantyx.Options{APIKey: "...", WorkspaceSlug: "acme"})
type WeatherReport struct { City string `json:"city" jsonschema:"City the report is for"` TemperatureC float64 `json:"temperature_c" jsonschema:"Current temperature in Celsius"`}
result, err := client.RunAgent(ctx, mantyx.RunSpec{ SystemPrompt: "Return the weather as JSON.", Prompt: "What's the weather in San Francisco right now?", OutputSchema: &mantyx.OutputSchema{ Name: "weather_report", Schema: &WeatherReport{}, Enforcement: mantyx.OutputSchemaEnforcementStrict, },})if err != nil { /* ... */ }
var report WeatherReportif err := mantyx.ParseRunOutput(result, &report); err != nil { var pe *mantyx.ParseError if errors.As(err, &pe) { log.Printf("model returned non-JSON text: %q", pe.Text) } return err}If you’d rather keep the schema explicit, the same call also accepts a
hand-rolled map[string]any (or json.RawMessage) containing the full
JSON Schema — both shapes are passed through verbatim:
weatherSchema := map[string]any{ "type": "object", "properties": map[string]any{ "city": map[string]any{"type": "string"}, "temperature_c": map[string]any{"type": "number"}, }, "required": []any{"city", "temperature_c"}, "additionalProperties": false,}
result, err := client.RunAgent(ctx, mantyx.RunSpec{ SystemPrompt: "Return the weather as JSON.", Prompt: "What's the weather in San Francisco right now?", OutputSchema: &mantyx.OutputSchema{ Name: "weather_report", Schema: weatherSchema, Enforcement: mantyx.OutputSchemaEnforcementStrict, },})Defaults and inheritance
Section titled “Defaults and inheritance”outputSchema works on both ephemeral runs (systemPrompt-defined) and agentId-backed runs — the runner applies it to whichever AgentSpec it built for the run. When the field is omitted, the run returns unconstrained plain text as before.
For session-scoped runs the inheritance rules are:
client.createSession({ outputSchema })(TS) /client.create_session(output_schema=...)(Python) /mantyx.SessionSpec{OutputSchema: ...}(Go) — sets the session-default applied to every subsequent message run.session.send(prompt, { outputSchema })(TS) /session.send(prompt, output_schema=...)(Python) /session.Send(ctx, prompt, mantyx.WithOutputSchema(...))(Go) — optional per-message override; applies to that one run only and does not mutate the session’s stored value.
const session = await client.createSession({ systemPrompt: "...", outputSchema: { schema: WeatherJsonSchema }, // default for every turn});
await session.send("Weather in Tokyo?"); // matches WeatherJsonSchemaawait session.send("Now summarise our chat in plain prose.", { outputSchema: undefined as never, // (illustrative)});Tip: to disable structured output for a single turn in a structured session, simply omit the option — the per-message override applies only when explicitly set; it does not “unset” the session default. If you need plain-text mid-session today, run that turn through a stateless
runAgenton the same client.
Error handling
Section titled “Error handling”Strict provider rejection raises a typed run error with
errorClass: "structured_output_schema_rejected". The provider’s human
message is preserved along with apiStatus / apiCode when available.
structured_output_not_supported means the selected provider/model has no
enforceable path; structured_output_not_enforced means a required synthetic
final-result tool was bypassed.
Even after provider enforcement, transient model errors (refusal text,
truncation under max_tokens pressure, exotic Unicode normalisation) can still
occasionally produce a string that fails to parse. The reference SDKs:
- Pass the schema through unchanged from your code to the wire.
- Run a
JSON.parse/json.loads/json.Unmarshalon the terminalresult.textonly when you callparseRunOutput/parse_run_output/ParseRunOutput. - Re-validate against your source-of-truth Zod / Pydantic / typed-struct schema.
- Surface a typed
MantyxParseError(*ParseErrorin Go) carrying the rawtextso you can log it for debugging.
import { MantyxParseError } from "@mantyx/sdk";
try { const report = parseRunOutput(result, Weather.parse.bind(Weather)); // happy path} catch (err) { if (err instanceof MantyxParseError) { console.warn("model returned non-conformant text:", err.text); } throw err;}Truncation salvage (errorClass: "truncation")
Section titled “Truncation salvage (errorClass: "truncation")”When the model hits the provider’s output-token budget mid-JSON, MANTYX
does not discard the bytes that already streamed. Instead the run
terminates with a MantyxRunError (*RunError in Go) whose
errorClass === "truncation" carries the partial output on
partialText (partial_text / PartialText). Catch this case before
parseRunOutput so callers see a clear “truncated reply — JSON likely
incomplete” surface instead of a bare JSON parse failure:
import { MantyxRunError } from "@mantyx/sdk";
try { const result = await client.runAgent({ /* … outputSchema */ }); return parseRunOutput(result, Weather.parse.bind(Weather));} catch (err) { if (err instanceof MantyxRunError && err.errorClass === "truncation") { // `err.partialText` carries the raw bytes; do NOT auto-fallback to it // as the final answer, since the JSON object is almost certainly // unclosed. Surface a "truncated — please rephrase or raise the budget" // banner instead. console.warn("output truncated:", err.partialText); throw err; } throw err;}The same salvage is also persisted on the run row — GET /agent-runs/{id}
returns { status: "failed", finalText: "<partial JSON>", error: "Model output was truncated …", failureReason: { errorClass: "truncation", finishReason: "max_tokens" } } — so SDKs that re-fetch the row after a
reconnect see both pieces consistently. See
Wire protocol §4.7 for the
full truncation contract.
See also
Section titled “See also”reasoningLevel— independent dial for thinking effort; combine the two to get deep-reasoning JSON outputs.- Run guards — loop detection and per-tool budgets that protect long agent loops, including the JSON-finalising turn that backs
outputSchemaunder the hood. - Local tools — structured output — the same JSON Schema affordance applied to a single local tool’s return value (forwarded as
outputSchemaon the wire). Pair withlongRunningwhen a tool may return apendingstatus and you do the polling yourself. - Wire protocol §7 — the canonical spec for the run-level
outputSchemawire shape, per-provider mapping, and SDK guidance. - Agent-runs protocol §4.5 — server-side validation contract and inheritance rules for sessions.
