1. Core Concepts
  2. Tool Calling

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

FieldTypeRequiredDescription
typestringYesAlways "function"
function.namestringYesFunction name the model will call
function.descriptionstringRecommendedWhat the function does
function.parametersobjectYesJSON Schema for function parameters
function.parameters.propertiesobjectYesIndividual parameter definitions
function.parameters.requiredarrayNoNames of required parameters