diff --git a/docs.json b/docs.json
index 8f29093a8..c80f5c3c6 100644
--- a/docs.json
+++ b/docs.json
@@ -306,6 +306,7 @@
"pages": [
"sdk/guides/hello-world",
"sdk/guides/custom-tools",
+ "sdk/guides/structured-output",
"sdk/guides/mcp",
"sdk/guides/skill",
"sdk/guides/plugins",
diff --git a/sdk/guides/structured-output.mdx b/sdk/guides/structured-output.mdx
new file mode 100644
index 000000000..73daff372
--- /dev/null
+++ b/sdk/guides/structured-output.mdx
@@ -0,0 +1,51 @@
+---
+title: Structured Output
+description: Attach a schema to any tool so the LLM returns typed, validated fields alongside the tool's own arguments.
+---
+
+import RunExampleCode from "/sdk/shared-snippets/how-to-run-example.mdx";
+
+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
+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
+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`.
+
+
+`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.
+
+
+## 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
+# content is auto-synced
+```
+
+