AI Development

OpenAI Agents API JSON Data Structures: How Agent, Tool Call, Tool Result, and Handoff Connect (2026)

About 16 min read

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_call and subagent_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_id is who asked. Done does not mean the subtask finished.
  • send / wait / interrupt: talk, wait, stop. agent_message may 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_id must 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

Changelog: First published