OpenAI Agents API JSON Data Structures: How Agent, Tool Call, Tool Result, and Handoff Connect (2026)
After the public beta the hard part is not “can it call a tool.” It is how four objects sit on one chain. There is no item typed handoff. The pins are IDs: session_id, turn_id, call_id, agent_id. This piece only unpacks that JSON chain.
Before you read
| Object | Official shape | The pin |
|---|---|---|
| Agent | Model, tool table, multi_agent flag | Written at session create |
| Tool Call | function_call inside required_actions | turn_id + call_id |
| Tool Result | agent.session.input.tool_result | Same pair written back |
| Handoff | create / send / wait / interrupt items | agent_id / subagent_id |
A function_call in history does not mean a result is still due. Pending work lives in required_actions.
Four objects join on IDs, not on a paragraph
In the Agents API, Agent is the config written when you create a session. Tool Call is a function_call sitting in required_actions when the session stops. Tool Result is agent.session.input.tool_result sent back on the event stream. “Handoff” here is a set of coordination items, not the SDK handoff object.
- The same turn_id / call_id pair: call and result must match. If they do not, the harness will not continue.
- History is not enough: a function_call in items only means it happened. Still waiting? Read
required_actions. - Handoff is not a type: look at
create_subagent_callandsubagent_id. Do not hunt a field named handoff.
How tools are called: OpenAI Agents API walkthrough. How a long job continues: Long-running Agents API. Who should own the loop: Agents API vs Responses API. If Agent and Function Calling are still one word, start with AI Agent complete guide.
Agent: a config object, not a reply
Create writes model, instructions, tools, and multi_agent once. That object is the subject of every later item. Official create is POST /v1/agents/sessions with OpenAI-Beta: agents=v1:
{
"agent": {
"model": "gpt-6-astra",
"instructions": "Look up orders. Confirm before any refund.",
"tools": [
{
"type": "function",
"name": "lookup_order",
"parameters": {
"type": "object",
"properties": {
"order_id": { "type": "string", "pattern": "^[A-Z]-\\d{4}$" }
},
"required": ["order_id"],
"additionalProperties": false
}
}
],
"multi_agent": { "enabled": true, "max_concurrent_subagents": 3 }
},
"environment": { "type": "openai_hosted" },
"input": [{ "role": "user", "content": [{ "type": "input_text", "text": "Look up A-1042." }] }]
}
Agent config stays with the session. Changing model or reasoning effort updates this session’s settings; a saved agent template applies only to new sessions. Environment is a separate resource: openai_hosted or your sandbox. Deleting the session does not clear the sandbox.
Tool Call: trust required_actions
When a function result is needed, the session emits agent.session.requires_action. Pending calls live in required_actions as a function_call with turn_id / call_id:
{
"type": "function_call",
"turn_id": "turn_2",
"call_id": "call_3c91",
"name": "lookup_order",
"arguments": { "order_id": "A-1042" }
}
Arguments must pass Schema. A wide object is no gate. See Why AI needs JSON Schema. Items history may also show function_call; official docs: history alone does not prove a result is still pending.
Tool Result: the same pair, written to the event stream
After you run the function, send agent.session.input.tool_result to the session events endpoint. Copy turn_id and call_id from the pending action. Success: success: true and a string output. Failure: success: false and error:
{
"type": "agent.session.input.tool_result",
"turn_id": "turn_2",
"call_id": "call_3c91",
"success": true,
"output": "{\"order_id\":\"A-1042\",\"status\":\"paid\"}"
}
Serialize objects to strings. If you already ran the function before a drop, resubmit the saved result with the same pair. Do not open a new session as a “retry.” The harness continues the turn only after required results arrive.
Handoff: coordination items, not a field name
With multi_agent.enabled, the harness supplies create, message, wait, and interrupt. You do not declare those tools. Coordination shows up as items:
{
"data": [
{
"type": "create_subagent_call",
"agent_id": "agent_main",
"task": "Compare A-1042 packing notes with the invoice."
},
{
"type": "send_subagent_input_call",
"subagent_id": "sub_7a12",
"input": "Focus on line items only."
},
{
"type": "agent_message",
"from": "sub_7a12",
"text": "Invoice lines match packing notes for A-1042."
}
]
}
- create_subagent_call: dispatch.
agent_idis who asked. Done does not mean the subtask finished. - send / wait / interrupt: talk, wait, stop.
agent_messagemay carry visible inter-agent text; the stream is not a full transcript. - The SDK handoff object: that lives in your process runner. It is not the same resource as these items.
What you actually hand to the next subject should be auditable JSON: order ID, intent, evidence. How to capture it as Schema: JSON Schema and agent workflows.
The chain holds only if the pins line up
Fold the four objects into a chain you can rebuild locally. Fictional order — no real secrets:
{
"session_id": "sess_8f21",
"links": [
{ "object": "agent", "id": "agent_main" },
{ "object": "turn", "id": "turn_2" },
{ "object": "function_call", "call_id": "call_3c91" },
{ "object": "tool_result", "call_id": "call_3c91" },
{ "object": "subagent", "id": "sub_7a12", "via": "create_subagent_call" }
]
}
| Object | Type you see | Pinned to | Usual mismatch |
|---|---|---|---|
| Agent | session.agent | session_id | A new session as “retry” |
| Tool Call | function_call | turn_id + call_id | Reading items history only |
| Tool Result | input.tool_result | The same pair | output still an object, not a string |
| Handoff | create_subagent_call | agent_id / subagent_id | Hunting a field named handoff |
| Turn | turn.completed / failed | turn_id; root subagent_id is null | Idle read as success |
An MCP row is another tool table. It does not invent a second ID scheme. Boundary: MCP Tool Schema vs Function Calling.
Review the chain by spreading two JSON trails
Most mismatches are drifting pins, not a dumber model.
- format required_actions next to the write-back event. The
call_idmust match exactly. - Use JSONPath extract to pull
turn_id,call_id,subagent_id. - JSON Diff the call and the result for the same order. If they drifted, look at IDs before the model.
- List the main agent’s items and a subagent’s items separately. Mixed on one page, coordination attaches to the wrong actor.
- Do not paste orders, sessions, or intranet MCP URLs into a public formatter. JSONSort runs in the browser locally.
If both trails rebuild on IDs, you are reading structure. Otherwise you are reading slogans.
Common traps
- Treating a function_call in items as still pending. Pending work is required_actions.
- Writing back a different ID pair. The harness cannot match; the turn stalls.
- Sending output as a raw object. Official docs want a string or a supported content array.
- Hunting type: handoff in the payload. The Agents API uses create / send / wait / interrupt.
- Sending your function to a subagent. Subagents do not support function tools. Lookups stay on the main agent.
- Treating an SDK handoff and session items as one resource. Deleting one does not clear the other.
FAQ
Must every Tool Call have a Tool Result?
Every pending call must be written back. A historical function_call that already finished does not need another result. The test is required_actions, not the length of the items list.
Is there a JSON field named handoff in the Agents API?
Not as a first-class item type. Coordination is create_subagent_call, send_subagent_input_call, wait_for_subagents_call, interrupt_subagent_call. The SDK handoff is a different object.
What is the difference between call_id and item_id?
call_id pins a function call to its result. item_id names the saved item. The stream uses item_id to attach text deltas to the same part. Two pins — do not mix them.
How can a turn complete if a tool failed?
Official docs: completed does not mean every tool succeeded. Also read success on the tool_result and the items for that turn.
Can I invent my own handoff JSON inside the app?
Yes, and you should. That is your business object: order ID, intent, evidence. Do not expect official items to remember amounts. Write it as Schema and store it apart from session items.
Conclusion
Agent, Tool Call, Tool Result, and Handoff are not four paragraphs. They are four objects on one JSON chain, pinned by IDs. Official write-back is an event; official transfer is a coordination item.
Write down whether you can match call and result from call_id alone. If yes, the structure holds. Models change; the pins should settle first.
Further reading
- OpenAI Agents API, complete walkthrough How Session, tool calling, and Schema connect
- Long-running Agents API Session, compact, subagents
- Agents API vs Responses API Who should run the loop
- Why AI needs JSON Schema Schema / Function Calling / Structured Output
- JSON Schema and agent workflows Capture tools and state as Schema
- JSONSort local toolbox Format, validate, Diff
- All posts More JSON and AI notes
Changelog: First published