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

# Get typed answers

> Get the agent's final answer as typed, schema-validated data instead of free-form text.

export const StructuredOutput = () => {
  const stroke = {
    fill: "none",
    stroke: "currentColor",
    strokeWidth: 1.75,
    strokeLinecap: "round",
    strokeLinejoin: "round"
  };
  const S = c => ({
    className: c,
    ...stroke
  });
  const icons = {
    schema: c => <svg viewBox="0 0 24 24" {...S(c)}><path d="M10 12.5 8 15l2 2.5" /><path d="m14 12.5 2 2.5-2 2.5" /><path d="M14 2v4a2 2 0 0 0 2 2h4" /><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z" /></svg>,
    agent: c => <svg viewBox="0 0 24 24" {...S(c)}><path d="M12 8V4H8" /><rect width="16" height="12" x="4" y="8" rx="2" /><path d="M2 14h2M20 14h2M15 13v2M9 13v2" /></svg>,
    code: c => <svg viewBox="0 0 24 24" {...S(c)}><path d="m16 18 6-6-6-6" /><path d="m8 6-6 6 6 6" /></svg>
  };
  const Card = ({icon, title, sub, children}) => <div className="flex shrink-0 flex-col self-center rounded-xl border border-zinc-200 bg-white p-5 dark:border-zinc-800 dark:bg-zinc-950">
      <div className="flex items-center gap-2.5">
        <span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-200">{icon("h-5 w-5")}</span>
        <div>
          <div className="whitespace-nowrap text-base font-semibold leading-6 text-zinc-900 dark:text-zinc-100">{title}</div>
          <div className="whitespace-nowrap text-sm text-zinc-500 dark:text-zinc-400">{sub}</div>
        </div>
      </div>
      {children}
    </div>;
  const Arrow = ({top}) => <div className="flex min-w-[104px] flex-1 flex-col items-stretch justify-center gap-1 px-3 text-center text-xs leading-4 text-zinc-500 dark:text-zinc-400">
      <span className="whitespace-nowrap">{top}</span>
      <div className="flex items-center text-zinc-400 dark:text-zinc-600">
        <span className="h-px flex-1 bg-current" />
        <svg className="-ml-px h-3 w-2 shrink-0" viewBox="0 0 8 12" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M1 1.5 6 6l-5 4.5" /></svg>
      </div>
    </div>;
  const Chip = ({children}) => <span className="whitespace-nowrap rounded-md bg-zinc-100 px-2 py-0.5 font-mono text-xs text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300">{children}</span>;
  return <div className="not-prose my-8 overflow-x-auto">
      <div className="flex min-w-[680px] items-stretch">
        <Card icon={icons.schema} title="Your schema" sub="the answer's shape">
          <div className="mt-4 flex gap-1.5">
            <Chip>Pydantic</Chip>
            <Chip>Zod</Chip>
            <Chip>JSON Schema</Chip>
          </div>
        </Card>

        <Arrow top="answer_format" />

        <Card icon={icons.agent} title="Agent" sub="answers in that shape">
          <div className="mt-4 text-sm text-zinc-600 dark:text-zinc-400">Off-shape answers<br />are retried</div>
        </Card>

        <Arrow top="validated" />

        <Card icon={icons.code} title="Your code" sub="typed, not a string">
          <div className="mt-4">
            <Chip>result.answer</Chip>
          </div>
        </Card>
      </div>
    </div>;
};

export const YouTube = ({id, title}) => <iframe className="aspect-video w-full rounded-xl border-0" src={`https://www.youtube-nocookie.com/embed/${id}?rel=0&modestbranding=1`} title={title} loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowFullScreen />;

<StructuredOutput />

By default the agent's answer is free-form text. Give it a schema and the `answer` you read from [`changes`](/agents-api/sessions/changes) becomes a JSON object that conforms to it.

| Where you set it                                                                                                                   | What you pass                                                                     | What you get back           |
| ---------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------- |
| [`answer_format`](/agents-api/agents/overview) on the agent, or per run via [`overrides`](/agents-api/sessions/overview#overrides) | A JSON Schema                                                                     | A JSON object               |
| `answer_schema` / `answerSchema` in the SDKs                                                                                       | A [Pydantic](https://docs.pydantic.dev) model or [Zod v4](https://zod.dev) schema | A validated, typed instance |

With the SDK schema, the SDK derives the JSON Schema and parses the final answer for you. Rules:

* A `completed` session whose answer is missing or doesn't match raises `AnswerValidationError` with the raw payload attached.
* The raw wire value always stays on `final_changes` / `finalChanges`, next to the parsed answer.
* Passing both a schema and an `agent.answer_format` override is rejected: they set the same field.
* Runs that end without reaching `completed` (an `idle` session that hasn't answered, a failed one) skip validation; the answer passes through as-is, `None` / `undefined` when absent.

<CodeGroup>
  ```bash CLI theme={"system"}
  # `hai run` prints the structured answer once it lands
  hai run "Top 3 stories on Hacker News right now?" \
    --agent h/web-surfer-flash \
    --override 'agent.answer_format={"type":"object","properties":{"stories":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"url":{"type":"string"}},"required":["title","url"]}}},"required":["stories"]}'
  ```

  ```bash cURL theme={"system"}
  # Launch with a schema...
  SESSION=$(curl -sX POST https://agp.eu.hcompany.ai/api/v2/sessions \
    -H "Authorization: Bearer $HAI_API_KEY" -H "Content-Type: application/json" \
    -d '{
      "agent": "h/web-surfer-flash",
      "messages": [{"type": "user_message", "message": "Top 3 stories on Hacker News right now?"}],
      "overrides": {
        "agent.answer_format": {
          "type": "object",
          "properties": {
            "stories": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {"title": {"type": "string"}, "url": {"type": "string"}},
                "required": ["title", "url"]
              }
            }
          },
          "required": ["stories"]
        }
      }
    }' | jq -r .id)

  # ...then read the structured answer once it lands.
  curl -s "https://agp.eu.hcompany.ai/api/v2/sessions/$SESSION/changes" \
    -H "Authorization: Bearer $HAI_API_KEY" | jq .answer
  ```

  ```python Python theme={"system"}
  from pydantic import BaseModel
  from hai_agents import Client

  class Story(BaseModel):
      title: str
      url: str

  class Stories(BaseModel):
      stories: list[Story]

  client = Client()
  result = client.run_session(
      agent="h/web-surfer-flash",
      messages="Top 3 stories on Hacker News right now?",
      answer_schema=Stories,
  )

  for story in result.answer.stories:  # result.answer is a Stories instance
      print(story.title, story.url)
  ```

  ```typescript TypeScript theme={"system"}
  import { HaiAgentsClient } from "hai-agents";
  import { z } from "zod";

  const Stories = z.object({
    stories: z.array(z.object({ title: z.string(), url: z.string() })),
  });

  const client = new HaiAgentsClient();
  const result = await client.runSession({
    agent: "h/web-surfer-flash",
    messages: "Top 3 stories on Hacker News right now?",
    answerSchema: Stories,
  });

  for (const story of result.answer?.stories ?? []) {
    console.log(story.title, story.url); // typed via z.infer
  }
  ```
</CodeGroup>

## See it in action

Describe the data you want and the shape you want it in; the agent browses and hands back exactly that.

<Frame>
  <YouTube id="gNjNmPYPvwk" title="Computer Use Agents: Web Extraction" />
</Frame>

## Chaining agents

With a typed answer, an agent behaves like any other function: call it, get data back, build on it. Here one agent gathers sources and others read them in parallel:

<CodeGroup>
  ```python Python theme={"system"}
  import asyncio
  from pydantic import BaseModel
  from hai_agents import AsyncClient

  class Source(BaseModel):
      title: str
      url: str
      excerpt: str

  class Sources(BaseModel):
      sources: list[Source]

  class Brief(BaseModel):
      url: str
      summary: str
      key_facts: list[str]

  async def main() -> None:
      client = AsyncClient()

      scout = await client.run_session(
          agent="h/web-surfer-flash",
          messages="Find the 5 highest-value sources on EU AI Act enforcement",
          answer_schema=Sources,
      )

      readers = await asyncio.gather(*(
          client.run_session(
              agent="h/web-surfer-flash",
              messages=f"Read this source and extract the key facts: {source.url}",
              overrides={"agent.environments[kind=web].start_url": source.url},
              answer_schema=Brief,
          )
          for source in scout.answer.sources
      ))

      for reader in readers:
          print(reader.answer.url, reader.answer.key_facts)

  asyncio.run(main())
  ```

  ```typescript TypeScript theme={"system"}
  import { HaiAgentsClient } from "hai-agents";
  import { z } from "zod";

  const Sources = z.object({
    sources: z.array(z.object({ title: z.string(), url: z.string(), excerpt: z.string() })),
  });

  const Brief = z.object({
    url: z.string(),
    summary: z.string(),
    keyFacts: z.array(z.string()),
  });

  const client = new HaiAgentsClient();

  const scout = await client.runSession({
    agent: "h/web-surfer-flash",
    messages: "Find the 5 highest-value sources on EU AI Act enforcement",
    answerSchema: Sources,
  });

  const readers = await Promise.all(
    (scout.answer?.sources ?? []).map((source) =>
      client.runSession({
        agent: "h/web-surfer-flash",
        messages: `Read this source and extract the key facts: ${source.url}`,
        overrides: { "agent.environments[kind=web].start_url": source.url },
        answerSchema: Brief,
      }),
    ),
  );

  for (const reader of readers) {
    console.log(reader.answer?.url, reader.answer?.keyFacts);
  }
  ```
</CodeGroup>

Parallel sessions count against your [concurrency quota](/agents-api/sessions/quota).

## Next steps

<CardGroup cols={2}>
  <Card title="Configure an agent" icon="robot" href="/agents-api/agents/overview#configure-an-agent">
    Set `answer_format` on the agent itself.
  </Card>

  <Card title="Parallelize work with subagents" icon="diagram-project" href="/agents-api/multi-agent">
    Fan work out, then merge typed answers.
  </Card>
</CardGroup>
