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

# Structured Output

> Attach a schema to any tool so the LLM returns typed, validated fields alongside the tool's own arguments.

export const path_to_script_0 = "examples/01_standalone_sdk/56_structured_output.py"

Pass a Pydantic model (or a JSON Schema dict) as a tool's `response_schema`. Its fields are merged into the schema the LLM sees, so the model must populate them when it calls that tool, and the reply is validated on receipt — no prompting for a format, no output parsing.

```python theme={null}
class ProjectFacts(BaseModel):
    description: str = Field(description="One-paragraph description of the project.")
    facts: list[str] = Field(description="Three concise, distinct facts.")


agent = Agent(
    llm=llm,
    tools=[Tool(name="FinishTool", params={"response_schema": ProjectFacts})],
)
```

The tool keeps its own arguments — `FinishTool` still takes `message`, now alongside `description` and `facts`. This works on any tool, including [custom](/sdk/guides/custom-tools) and [MCP](/sdk/guides/mcp) tools.

## Reading results

Resolved tools live on `agent.tools_map`. Use `parse_last_response()` for the most recent call, or `parse_response(action)` for a specific one:

```python theme={null}
finish_tool = agent.tools_map["finish"]
facts = cast(ProjectFacts | None, finish_tool.parse_last_response(conversation.state.events))
```

`parse_last_response()` returns `None` if the tool has not been called. With a JSON Schema dict instead of a model, both methods return a validated `dict`.

<Note>
  `parse_last_response()` re-reads the tool call, so it works after a conversation is persisted and reloaded. `action.structured_output` is in-memory only — it is not serialized with the event and comes back `None` after a round-trip, so prefer the parse methods.
</Note>

## Constraints

* **Reserved names.** A schema may not declare `kind`, `security_risk`, `structured_output`, or `summary`, nor reuse one of the tool's own field names (e.g. `message` on `FinishTool`). Both raise a `ValueError` when the tool is resolved.
* **One tool per spec.** A spec that resolves to a tool set is rejected; attach the schema to the individual tool instead.
* **Scoped to its tool.** A model may try to send the schema fields when calling *other* tools; those calls are rejected as unexpected arguments and the agent retries.

## Ready-to-run Example

```python icon="python" expandable examples/01_standalone_sdk/56_structured_output.py theme={null}
# content is auto-synced
```

You can run the example code as-is.

<Note>
  The model name should follow the [LiteLLM convention](https://models.litellm.ai/): `provider/model_name` (e.g., `anthropic/claude-sonnet-4-5-20250929`, `openai/gpt-4o`).
  The `LLM_API_KEY` should be the API key for your chosen provider.
</Note>

<CodeGroup>
  <CodeBlock language="bash" filename="Bring-your-own provider key" icon="terminal" wrap>
    {`export LLM_API_KEY="your-api-key"\nexport LLM_MODEL="anthropic/claude-sonnet-4-5-20250929"  # or openai/gpt-4o, etc.\ncd software-agent-sdk\nuv run python ${path_to_script_0}`}
  </CodeBlock>

  <CodeBlock language="bash" filename="OpenHands Cloud" icon="terminal" wrap>
    {`# https://app.all-hands.dev/settings/api-keys\nexport LLM_API_KEY="your-openhands-api-key"\nexport LLM_MODEL="openhands/claude-sonnet-4-5-20250929"\ncd software-agent-sdk\nuv run python ${path_to_script_0}`}
  </CodeBlock>
</CodeGroup>

<Tip>
  **ChatGPT Plus/Pro subscribers**: You can use `LLM.subscription_login()` to authenticate with your ChatGPT account and access Codex models without consuming API credits. See the [LLM Subscriptions guide](/sdk/guides/llm-subscriptions) for details.
</Tip>
