Skip to content

Get structured JSON from AI agents with TypeScript and Zod

Validate an Agent’s JSON output with Zod and use the typed result in the next workflow step.

No credentials needed

Turn an <Agent /> response into a typed finding instead of passing unvalidated prose between workflow stages. This recipe follows the maintained structured.tsx example.

This page starts with a deterministic provider so you can inspect the dataflow without credentials. For complete workflows using real models, follow Pi issue triage or GLM repository analysis; both collect typed results before a second Agent runs.

  • an AML project configured through Getting started, with zod installed;
  • no model credentials or network access for the deterministic example.
import { type AML, Agent, AmlRuntime, evaluate } from "@aml-jsx/sdk"
import { DeterministicAgentProvider } from "@aml-jsx/sdk/testing"
import { z } from "zod"
const Finding = z.object({
severity: z.enum(["low", "high"]),
summary: z.string(),
})
const provider = new DeterministicAgentProvider({
respond(request) {
if (request.output?.type === "json") {
return {
structured: {
severity: "high",
summary: "authorization is checked after mutation",
},
text: "",
}
}
return { text: `synthesized:` + request.prompt }
},
})
const Review: AML.Component = async () => {
const finding = await evaluate(<Agent provider={provider}>Inspect the change.</Agent>, Finding)
return (
<Agent provider={provider}>
Explain this {finding.severity} finding: {finding.summary}
</Agent>
)
}
const result = await new AmlRuntime().evaluate(<Review />)
console.log(result)

Save the complete source as recipe.tsx in a project configured as shown in Getting started, then run exactly that file:

Terminal
npx vite-node recipe.tsx

From an AML repository checkout, the smaller maintained example exercises the same structured-result contract:

Terminal
npm run example -- structured
synthesized:Explain this high finding: authorization is checked after mutation

The deterministic provider returns a JSON-shaped value only when AML asks for JSON. The schema then validates that value before the typed finding is interpolated into the second <Agent />.

Use the Agent schema prop when a nested Agent should own validation without opening an imperative collection boundary:

import { Agent, FollowUp } from "@aml-jsx/sdk"
const workflow = (
<Agent provider={provider}>
Specialist finding:
<Agent provider={provider} schema={Finding}>
Inspect the change.
<FollowUp>Challenge the evidence, then submit the final finding.</FollowUp>
</Agent>
Synthesize the validated finding.
</Agent>
)

The specialist’s transformed result enters the parent prompt as canonical JSON text. FollowUps remain one ordered session; AML requests structured output only on the final authored turn. Use evaluate(value, Finding) instead when component code needs the schema-inferred value. The two forms are alternative schema owners and cannot be combined on one Agent.

  1. evaluate(value, Finding) requests structured output from the nearest Agent provider.
  2. AML converts the application-owned schema to JSON Schema for the provider request. Zod is one implementation of the Standard Schema contract.
  3. On the shared ACP path, the Agent submits candidates through AML’s aml_submit_result MCP tool. AML immediately validates each candidate and returns validation errors to the Agent so it can correct the result in the same turn.
  4. The first valid candidate is accepted. Later submissions are ignored. If the final authored turn completes normally (end_turn) without a valid candidate, AML sends one repair prompt with the Tool instruction and complete JSON Schema; a second omission rejects the evaluation.
  5. AML validates the accepted value at the application boundary before returning the schema’s output type.
  6. The second <Agent /> remains text-producing. In this example, structured output applies to the specialist, not to the final response.
  • On the shared ACP path, invalid candidates are recoverable Tool errors. A final authored turn that completes normally (end_turn) without an accepted candidate receives one schema-bearing repair prompt; missing output after that repair rejects the evaluation. Provider request errors, cancellation, refusal, and exhausted limits do not trigger repair.
  • After AML accepts the first valid candidate, repeated submissions cannot replace it. With tracing enabled, submissions appear as agent.output events with invalid, accepted, or ignored status.
  • The repository example uses a deterministic provider; it does not prove that every Agent provider supports the same native structured-output mechanism.
  • Do not treat a valid shape as authorization to perform an external side effect. Validate business rules before writing files, merging code, or sending messages.
  • If you need structured output from a final <FollowUp />, remember that the final turn is the output boundary; earlier turns remain session inputs.