> ## Documentation Index
> Fetch the complete documentation index at: https://internal.september.wtf/llms.txt
> Use this file to discover all available pages before exploring further.

# Structured outputs

> How to get JSON back from the agent that matches a schema.

Sometimes you don't want a paragraph; you want JSON that fits a specific
shape so your downstream code can use it directly. This page covers two
approaches the Engine supports and when each is the right call.

## Two approaches

### 1. Provider-native structured outputs

OpenAI and Anthropic both support a "give me JSON that matches this
schema" mode at the API level. The Engine surfaces this through a
request flag (when running on a provider that supports it) and the
provider enforces the schema during decoding. The output is guaranteed
to match the schema — no malformed JSON, no missing required fields.

This is the right answer when:

* You have a fixed schema you control.
* The schema is meaningful to the model (good field names, descriptions).
* You're on a provider that supports it for the model you're using.

### 2. Tool-as-extractor

Define a tool whose only purpose is to return the structured data. The
model "calls" the tool with the structured JSON as the input; you read
the JSON from the `tool_call.input` rather than executing anything:

```json theme={null}
{
  "name": "submit_summary",
  "description": "Submit the final structured summary.",
  "input_schema": {
    "type": "object",
    "properties": {
      "title": { "type": "string" },
      "key_points": { "type": "array", "items": { "type": "string" } },
      "sentiment": { "enum": ["positive", "neutral", "negative"] }
    },
    "required": ["title", "key_points", "sentiment"]
  }
}
```

The agent's system prompt instructs it: "When you've finished analyzing,
call `submit_summary` with the result. Do not write the summary as
text." When you see `tool_call.input` for `submit_summary`, that's your
structured output.

This works on every provider. It also gives you context — the model can
write some preamble (which you can ignore or surface as "thinking") and
then commit to the structured output as a "submit."

## Pick the right one

|                    | Provider-native                     | Tool-as-extractor                    |
| ------------------ | ----------------------------------- | ------------------------------------ |
| Provider support   | OpenAI, Anthropic (varies by model) | All                                  |
| Schema enforced    | Yes, at decode                      | At validation, not decode            |
| Mid-task usage     | Whole turn is structured            | Can interleave with text             |
| Streaming behavior | Field-by-field as decoded           | Comes through `tool_call`            |
| Best for           | Pure extractor tasks                | Multi-step tasks ending in structure |

If you want one structured object as the answer, native is cleaner. If
you want the agent to think, search, and finally commit a structure,
tool-as-extractor is better.

## Schema design

Whichever approach you pick, schema quality affects output quality.

### Use descriptive field names

```json theme={null}
"name": "user_name"           // ok
"name": "u"                   // bad
"name": "preferred_full_name" // better — tells the model what to fill
```

### Add descriptions

The model uses field descriptions like documentation. They guide the
fill:

```json theme={null}
{
  "type": "string",
  "description": "ISO 4217 currency code, uppercase. Example: USD, EUR."
}
```

### Use enums for closed sets

If a field has 3–5 valid values, use `enum`:

```json theme={null}
{
  "type": "string",
  "enum": ["high", "medium", "low"],
  "description": "Severity level."
}
```

The model picks one of the listed values. Without `enum`, you'll see
"Medium" vs. "medium" vs. "MEDIUM" inconsistently.

### Mark required fields

Optional fields that the model thinks are unnecessary will be omitted.
Required fields force the model to fill them. Use `required` to express
your contract.

### Avoid deeply nested structures

Models do well with one or two levels of nesting. Three levels deep
hurts accuracy. If your data is naturally hierarchical, consider:

* A flat structure with parent IDs.
* Separate calls for nested levels.
* Two passes: first the outline, then the details.

## Validation

The Engine does NOT silently accept invalid JSON. If you use
provider-native structured outputs, the provider guarantees validity. If
you use tool-as-extractor, the Engine validates the input against the
schema before emitting the `tool_call` event. Invalid input becomes a
`tool_error` and feeds back into context — the model can correct and
retry.

For your application code, treat the JSON as already validated against
the schema, but **still validate against your business rules**. The
schema can't enforce "the timestamp must be in the future" or "the
user\_id must exist."

## Pattern: extractor-with-confidence

Add a `confidence` field to your schema:

```json theme={null}
{
  "title": "...",
  "key_points": [...],
  "sentiment": "positive",
  "confidence": 0.85
}
```

The model self-reports how confident it is. Use this in your code to
route low-confidence outputs to a human review queue instead of acting
on them automatically. It's not perfectly calibrated, but it's a useful
signal in aggregate.

## Pattern: parsed-then-acted

For tasks where the structure is the input to a subsequent action, run
the agent in two turns:

1. Turn 1: agent calls `submit_summary` with structured JSON.
2. Your code reads the JSON, performs the action.
3. Turn 2: agent gets a `tool_result` describing what you did, replies
   with confirmation text to the user.

This separates "thinking and structuring" from "acting" cleanly, and
keeps your application in the loop for the destructive part.

## Pitfalls

* **Schema drift.** Updating the schema without updating the agent's
  system prompt or tool description leads to garbage output. Treat
  schemas like APIs — version and migrate.
* **Free-form fields in structured outputs.** A `string` field with no
  description ends up holding everything from a code snippet to a
  paragraph of explanation. Constrain with description and length hints.
* **Enums that grow.** If you keep adding to an enum, the model gets
  confused as the list lengthens. At some point, you want a
  classifier-as-tool instead.
* **Streaming partial JSON.** Provider-native structured outputs stream
  the JSON token-by-token. You can't parse it until it's complete unless
  you use a streaming JSON parser. Tool-as-extractor sends the full
  input in one event.

## See also

* [Tool use](./tool-use) — for the tool-as-extractor pattern.
* [Evaluation](../build-with-engine/evaluation) — how to measure
  structured-output accuracy.
* [Build a research agent](../guides/build-a-research-agent) — uses
  structured outputs for the final report.
