---
title: Structured decisions
description: Make a schema-validated structured decision without starting an agent session.
---

# Structured decisions

Use `generateStructuredOutput()` from `@bastani/atomic`, or the `structured_output` tool, when an integration needs one schema-validated semantic decision. Both use the same model contract. Neither starts a child agent, executes the caller's other tools, or authorizes an action. Neither reads `routerModel` or changes the selected chat model.

Automatic workflow-stage and subagent `model: "auto"` selection uses `routerModel` separately; it does not change calls to `generateStructuredOutput()`. See [Select the router](#select-the-router).

## Select the inference model

Pass an optional exact `provider/model` string, not a model object and not `auto`:

- `model` is the primary. It may be a chat language model or a classifier registered in the current model registry. `typesafe/jev-latest` is one example classifier, not a separate request type.
- `fallbackModels` is an ordered list of the same kind of exact IDs.
- Omit `model` to use the current stage or chat model as the primary.
- After the primary and every explicit fallback, Atomic tries the current chat model if it has not already been tried.

Image-generation models, model patterns, reasoning suffixes, surrounding whitespace, and IDs the registry does not list cannot be used. Atomic skips such an ID, records it with its error in `modelAttempts`, and still tries the remaining candidates, including the current chat model, so a mistyped `model` or fallback does not fail the call. The call fails with the first ID's error only when no candidate resolves. A classifier is never an execution `auto` candidate. A catalog entry does not prove that the provider can classify, that credentials work, or that quota remains. Setting `routerModel` does not change this selection.

A classifier can answer a finite Choice. Literal unions, enums, booleans, and flat objects whose required fields all use those types are Choice-compatible. Free-form text, unconstrained numbers, arrays, optional fields, and nested objects are not. Atomic compiles compatible schemas into generic choice questions and skips an incompatible classifier, recording the skip. A request whose entire chain is incompatible fails without inventing a decoder.

Pass the session `modelRegistry`. Classifier credentials come from that registry's normal provider login, such as `/login typesafe` or `TYPESAFE_API_KEY` for TypeSafe Jev. Do not copy a resolved key into `state`.

```typescript
import { Type } from "typebox";
import { generateStructuredOutput, ModelRegistry, ModelRuntime } from "@bastani/atomic";

const modelRuntime = await ModelRuntime.create();
const modelRegistry = new ModelRegistry(modelRuntime);
const currentModel = modelRegistry.getAvailable()[0];
if (!currentModel) throw new Error("Choose a configured chat model before making this request.");

const schema = Type.Object(
  { category: Type.Union([Type.Literal("question"), Type.Literal("statement")]) },
  { additionalProperties: false },
);
const result = await generateStructuredOutput({
  model: "typesafe/jev-latest",
  fallbackModels: [`${currentModel.provider}/${currentModel.id}`],
  currentModel,
  modelRegistry,
  schema,
  instructions: "Classify whether the message asks a question or makes a statement.",
  state: { message: "What time does the train leave?" },
});
console.log(result.value.category, result.model);
```

The returned `value` is the schema-validated decision, not the request arguments. `model` names the candidate that produced it. `modelAttempts` records skips and failed candidates when a chain was used. This decision API does not expose classifier billing usage: its classifier-produced `usage` is not a classifier bill. The lower-level classifier operation may report usage and cost separately. Chat usage is reported when a chat candidate supplies the accepted result. A valid shape is not proof that the judgment is correct. Validate current policy again before any later action.

The `structured_output` tool takes the same decision inputs from the calling model: required `instructions` and nonempty named `state`, plus optional `model` and `fallbackModels`. The registered schema is fixed by `createStructuredOutputTool({ schema })`. The tool resolves those IDs through the session registry and returns the inferred value. Omitting `model` uses the current stage or session chat model. The calling model cannot set a deadline; the tool's own [`timeoutMs`](#cancellation-and-failures) option does. See [Structured output final results](/sdk/reference#structured-output-final-results).

## Select the router

For workflow stages and subagents with `model: "auto"`, `routerModel` selects the decision provider: an explicit exact registered chat or classifier ID uses that model, while an unset or `auto` value uses the current chat model. Saved classifier credentials do not change this selection. An invalid explicit router selection fails instead of falling back. Set it through [/settings](/settings#routermodel); extensions can read the current value with `ctx.getRouterModel()`.

Chat routing gets an initial attempt plus up to three corrective retries for malformed or schema-invalid output, all within one [decision deadline](#cancellation-and-failures). A failed explicit classifier routing attempt switches to the current chat model immediately when one exists, including missing credentials, an unsupported classify operation, a provider size or context rejection, and a refusal. Cancellation does not fall back. The result includes `fallback: { from, to, reason }` when that hop is used. Transient provider retries follow the selected provider's operation and the request's retry settings. General `generateStructuredOutput()` advances its own `fallbackModels` chain; it does not read `routerModel`.

For [workflow-stage](/workflows/operations#automatic-stage-models) and [subagent](/subagents/reference#automatic-model-selection) `model: "auto"`, a complete routing-inference failure runs the stage or child on the current chat model with a recorded warning instead of failing, provided that model is available and satisfies every routing constraint.

## Prepare a decision

Instructions describe the judgment. Named `state` fields contain the actual task, relevant conversation, explicit constraints, and reference text. Paths and URLs may identify a source, but do not replace its content. Exclude secrets from state and instructions.

A general structured-output request supplies `instructions`, `state`, and `schema`. Atomic derives choice questions from a compatible schema and skips a classifier for other schemas. Keep fixed numeric limits in code. Include a no-match outcome in the schema when abstention is valid.

Automatic routing builds its own finite Choice questions from eligible execution models. Call `generateStructuredOutput()` directly for other decisions; it derives questions from your result schema instead of reading `routerModel`.

## Provider behavior and limits

Chat candidates receive one internal result tool whose parameters are the registered schema. Atomic uses provider-aware serialization, requests strict sampling where supported, and validates the returned arguments without coercing values or removing extra fields. Use `additionalProperties: false` for closed objects. A prose-only response, extra tool call, truncated response, or invalid result fails that candidate and advances the chain. The public `structured_output` tool is separate: its parameters are `instructions`, `state`, and optional model selectors, and its result is the inferred value.

Use `Type.Optional(...)` for result fields that may be omitted. When a strict provider represents an omitted optional object property as `null`, Atomic removes that placeholder if the property's schema rejects null, including declared properties inside nested objects and array items. If null is a meaningful result, explicitly allow it in the property schema; Atomic preserves those null values. Required properties and undeclared fields still follow the original schema.

Chat decisions use HTTP/SSE rather than WebSocket transport fallback and disable configured Anthropic server-side fallbacks for this request only. The chat decision layer retries transient failures according to the request's retry settings. Classifier calls use the registered provider's `classify` operation and forward the retry limit and cancellation signal; provider implementations own their transport retries.

A registered classifier receives shared state and choice questions through the generic classify operation. [TypeSafe Jev](/providers#typesafe-jev) is one such classifier. Router classification is one classify request. A provider that rejects the request size or context fails that candidate; routing then uses the current chat model when one exists.

The structured-decision path never trims supplied state. A general structured-output call skips an incompatible classifier candidate and continues with the next fallback. A provider size rejection advances the same way without repeating the rejected request. Supply concise context or select a chat model with enough capacity when needed.

Automatic subagent and workflow-stage model selection has a chat model read the task and sends a classifier only the resulting answers and candidate models; see [automatic model selection](/subagents/reference#automatic-model-selection).

Classifier response bodies are limited by the provider operation. Atomic validates that every question receives a known Choice option. Lower-level classify results may report usage and cost when the provider supplies them; this structured-decision API does not expose them as classifier billing usage. Reported model, probabilities, and confidence are advisory and never reject an otherwise valid decision.

## Cancellation and failures

One wall-clock deadline covers the whole call: every repair attempt, transport retry, and model-candidate fallback together, not each generation alone. `timeoutMs` sets it as a positive integer number of milliseconds. Without it, the `structured_output` tool uses the session's [`httpIdleTimeoutMs`](/settings#http) setting and every other call uses that setting's default of 10 minutes. A disabled idle timeout (`0` or `"disabled"`) leaves only the largest timer delay, about 24.8 days. Raise the deadline for slow, reasoning-heavy models or large `state`.

When the deadline passes, Atomic stops the in-flight generation, which also ends a provider that ignores its signal, and rejects the call with `Structured output decision exceeded its <n>ms deadline; no decision was accepted.` That failure is final: it does not start another repair or advance to a fallback model. Unlike cancellation, it names the deadline. The message ends with how each attempt finished and how long it took, such as `Attempts: 1) stopReason=stop 25m02s; 2) still generating 4m58s.`. The error for exhausted repairs ends the same way, so a slow wedge reads differently from several failed generations. Provider transport, credential-preparation and enclosing tool-request limits remain independent of this deadline.

Pass an `AbortSignal` to cancel sooner, for example `signal: AbortSignal.timeout(milliseconds)`. Cancellation rejects the whole call immediately, even if a provider ignores its signal. Output has no fixed token cap: a chat decision may use the selected model's full output limit. Pass a positive integer `maxTokens` only when you want a smaller bound.

No result is returned for missing state, invalid configuration, unrepaired malformed output or an unrecovered provider failure. Keep action admission after the awaited result and check cancellation again at that boundary. Neither API runs recursive agents or provider probes.

Provider errors report a static reason without raw response bodies or credentials. A failed classifier request, including a refusal or an unrecognized or missing Choice answer, advances to the next configured model or current chat model. Chat provider errors also advance, including explicit refusals. Ordinary text without a valid result gets up to three repairs before advancing; Atomic does not inspect the wording to guess whether it is a refusal. Cancellation stops the chain.
