- Core Concepts
- Tool Calling
Core Concepts
Tool Calling
How tool calling works: define tools, handle requests, submit outputs, and loop for multi-round calls.
Tool calling lets your assistant invoke custom functions you define. When the assistant needs external data or actions, it pauses and returns tool-call requests for your code to fulfill, then continues with the results.
How it works
1. You send a message with tool definitions
2. The model returns tool_calls → status: REQUIRES_ACTION
3. Your code executes the functions locally
4. You submit tool outputs → model generates a reply
5. If the model needs more tools, repeat from step 2
Step 1 — Send a message with tools
Pass tool definitions in the tools array. Tools follow the OpenAI function-calling schema.
import requests
import json
BASE = "https://app.backboard.io/api"
headers = {"X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json"}
weather_tool = {
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["city"],
},
},
}
response = requests.post(
f"{BASE}/threads/messages",
headers=headers,
json={
"content": "What's the weather in San Francisco?",
"tools": [weather_tool],
},
).json()
Step 2 — Handle the tool call
When status is REQUIRES_ACTION, the response contains a tool_calls array and no content:
{
"status": "REQUIRES_ACTION",
"thread_id": "thr_abc123",
"tool_calls": [
{
"id": "call_xyz789",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\": \"San Francisco\", \"unit\": \"fahrenheit\"}"
}
}
],
"content": null
}
Execute each tool call and collect the outputs:
if response.get("status") == "REQUIRES_ACTION":
tool_outputs = []
for tool_call in response["tool_calls"]:
name = tool_call["function"]["name"]
args = json.loads(tool_call["function"]["arguments"])
if name == "get_weather":
result = {"city": args["city"], "temperature": 72, "condition": "partly cloudy"}
else:
result = {"error": f"Unknown tool: {name}"}
tool_outputs.append({
"tool_call_id": tool_call["id"],
"output": json.dumps(result),
})
Step 3 — Submit tool outputs
final = requests.post(
f"{BASE}/threads/tool-outputs",
headers=headers,
json={
"thread_id": response["thread_id"],
"tool_outputs": tool_outputs,
},
).json()
print(final["content"])
Streaming with tool calls
When streaming, tool calls arrive as a tool_submit_required SSE event. Submit outputs and continue streaming:
Two earlier events, tool_call_start and tool_call_ready, let you display a tool call while the model is still streaming. They are advisory — tool_submit_required stays the authoritative list you must respond to. See Early tool-call events.
import json
import requests
response = requests.post(
f"{BASE}/threads/messages",
headers=headers,
json={
"content": "What's the weather in Tokyo?",
"tools": [weather_tool],
"stream": True,
},
stream=True,
)
thread_id = None
for line in response.iter_lines():
if not line:
continue
decoded = line.decode()
if not decoded.startswith("data: "):
continue
event = json.loads(decoded[6:])
if event.get("type") == "content_streaming":
print(event.get("content", ""), end="", flush=True)
elif event.get("type") == "tool_submit_required":
thread_id = event["thread_id"]
tool_outputs = []
for tc in event.get("tool_calls", []):
args = json.loads(tc["function"]["arguments"])
result = {"city": args["city"], "temperature": "72°F", "condition": "Sunny"}
tool_outputs.append({
"tool_call_id": tc["id"],
"output": json.dumps(result),
})
# Submit and stream the final reply
final = requests.post(
f"{BASE}/threads/tool-outputs?stream=true",
headers=headers,
json={"thread_id": thread_id, "tool_outputs": tool_outputs},
stream=True,
)
for final_line in final.iter_lines():
if final_line:
fd = final_line.decode()
if fd.startswith("data: "):
fe = json.loads(fd[6:])
if fe.get("type") == "content_streaming":
print(fe.get("content", ""), end="", flush=True)
Chained tool calls (multi-round loop)
After submitting outputs, the response may return another REQUIRES_ACTION with new tool calls. Keep looping:
while response.get("status") == "REQUIRES_ACTION" and response.get("tool_calls"):
tool_outputs = []
for tc in response["tool_calls"]:
args = json.loads(tc["function"]["arguments"])
result = dispatch_tool(tc["function"]["name"], args)
tool_outputs.append({
"tool_call_id": tc["id"],
"output": json.dumps(result),
})
response = requests.post(
f"{BASE}/threads/tool-outputs",
headers=headers,
json={
"thread_id": response["thread_id"],
"tool_outputs": tool_outputs,
},
).json()
print("Final answer:", response["content"])
Parallel tool calls
The model can request multiple tool calls in a single response. Each call has its own tool_call_id — execute all of them and submit all outputs together in one call.
You must submit outputs for all tool calls in the response. Submitting a partial set will cause an error.
Tool definition schema
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Always "function" |
function.name | string | Yes | Function name the model will call |
function.description | string | Recommended | What the function does |
function.parameters | object | Yes | JSON Schema for function parameters |
function.parameters.properties | object | Yes | Individual parameter definitions |
function.parameters.required | array | No | Names of required parameters |
Related
- Send Message — the main messaging endpoint
- Tool Calls (SDK) — SDK-level examples
- Submit Tool Outputs
- Submit Tool Outputs (Legacy)