OpenAI Assistants API Function Calling Guide

OpenAI Assistants API
function calling
AI automation
Responses API migration
tool integration

OpenAI’s Assistants API lets you give a language model the ability to reach beyond text generation and interact with external systems. By defining custom functions, you turn a conversational assistant into an agent that can fetch live data, trigger workflows, send emails, or update databases—while the model decides when a function is needed and you handle the actual execution. This guide walks through the core concepts, practical implementation steps, and recommendations for building reliable, production‑ready assistants.


How Function Calling Works in the Assistants API

When you create an assistant you attach a list of tools. A tool of type "function" tells the model the name, description, and JSON‑Schema parameters of a callable routine. During a run, if the model determines that user intent matches one of your functions, it pauses the run and sets the status to requires_action. The response includes a tool_calls array that contains the function name and the arguments the model thinks you should supply.

Your responsibility is to:

  1. Detect the requires_action status (by polling or streaming).
  2. Extract each tool_call from required_action.submit_tool_outputs.tool_calls.
  3. Execute the corresponding function in your backend (calling an API, querying a database, sending email, etc.).
  4. Submit the result back with submitToolOutputs, providing the original tool_call_id and a stringified output.
  5. Continue polling until the run reaches completed, failed, cancelled, or expired.

Because the model never runs your code, you retain full control over authentication, error handling, rate limiting, and any business logic you need.


Defining a Function

A function definition lives inside the tools array when you create or update an assistant. The minimal schema looks like this:

{
  "type": "function",
  "function": {
    "name": "get_stock_price",
    "description": "Retrieve the latest closing price of a stock using its ticker symbol",
    "parameters": {
      "type": "object",
      "properties": {
        "symbol": {
          "type": "string",
          "description": "The ticker symbol of the stock"
        }
      },
      "required": ["symbol"]
    }
  }
}
  • name – Identifier you will use in your code to route the call.
  • description – Helps the model decide when the function is relevant; be concise yet specific.
  • parameters – JSON‑Schema object describing the expected inputs. List every required field in the required array.
  • Optional additions – You can add strict: true to enforce that the model’s arguments exactly match the schema (useful for avoiding type surprises).

You can attach many functions to a single assistant; the model picks the appropriate one based on the user message.


Typical Implementation Flow (Python‑style Pseudocode)

Below is a concise, language‑agnostic outline that mirrors the examples in the source material.

# 1. Create assistant with function tools
assistant = client.beta.assistants.create(
    name="Data Assistant",
    instructions="You are a helpful data assistant.",
    model="gpt-4o",
    tools=[stock_tool, email_tool]   # each tool as defined above
)

# 2. Start a thread and add a user message
thread = client.beta.threads.create()
client.beta.threads.messages.create(
    thread_id=thread.id,
    role="user",
    content="What is the current price of AAPL?"
)

# 3. Run the assistant
run = client.beta.threads.runs.create(thread_id=thread.id, assistant_id=assistant.id)

# 4. Poll for completion or action
while run.status in ("queued", "in_progress"):
    time.sleep(1)
    run = client.beta.threads.runs.retrieve(thread_id=thread.id, run_id=run.id)

if run.status == "requires_action":
    outputs = []
    for call in run.required_action.submit_tool_outputs.tool_calls:
        name = call.function.name
        args = json.loads(call.function.arguments)

        if name == "get_stock_price":
            result = fetch_stock_price(args["symbol"])   # your backend logic
            outputs.append({"tool_call_id": call.id, "output": json.dumps(result)})
        elif name == "send_email":
            result = send_email(args["to"], args["subject"], args["body"])
            outputs.append({"tool_call_id": call.id, "output": json.dumps(result)})

    # 5. Send the outputs back
    client.beta.threads.runs.submit_tool_outputs(
        thread_id=thread.id,
        run_id=run.id,
        tool_outputs=outputs
    )

    # 6. Poll again until final state
    while run.status not in ("completed", "failed", "cancelled", "expired"):
        time.sleep(1)
        run = client.beta.threads.runs.retrieve(thread_id=thread.id, run_id=run.id)

# 7. Retrieve the assistant's final reply
messages = client.beta.threads.messages.list(thread_id=thread.id)
assistant_reply = [m for m in messages.data if m.role == "assistant"][0].content[0].text.value
print(assistant_reply)

The same pattern works in Node.js, TypeScript, or any language that can call the OpenAI HTTP endpoints.


Real‑World Examples

1. Stock Price Lookup

A classic demo uses the yfinance library (or any market data API) to return the latest close price. The function takes a symbol string, calls the external service, and returns a number or a small JSON object. The assistant can then answer questions like “What is Apple’s stock price today?” with a precise figure.

2. Email Sending

As shown in the LobsterMail guide, defining a send_email function with to, subject, and body lets the assistant draft messages on your behalf. The critical part is the delivery backend: use a service that handles SPF/DKIM automatically (e.g., LobsterMail, SendGrid with verified domain, or an internal SMTP relay with proper authentication). For safety-critical emails, insert a human‑approval step before calling the send function—this is trivial because you can pause after extracting the arguments and only submit the tool output once approved.

3. Country Information

The tutorial from ReadMedium shows a function that calls restcountries.com. The assistant receives a country name, queries the API, and returns the full JSON payload. The model then incorporates that data into a friendly response, such as “Japan’s currency is the yen and its capital is Tokyo.”

4. Parallel Function Calls

Models released after November 2023 can return multiple tool_calls in a single turn. If a user asks, “Give me the weather in Paris and Tokyo,” the assistant may produce two calls to a get_weather function at once. Your execution loop simply iterates over the array, runs each function, and submits all outputs together. This reduces latency compared with sequential calls.


Best Practices for Reliable Assistants

AreaRecommendation
SecurityTreat every function as a privileged endpoint. Validate and sanitize inputs, enforce least‑privilege API keys, and consider short‑lived tokens or OAuth scopes.
Error HandlingIf your function fails, return a structured error object (e.g., {"error":"invalid_symbol","message":"Symbol XYZ not found"}) rather than throwing an exception. The assistant can surface a helpful message to the user.
Rate LimitingTrack calls per function and apply your own limits (token bucket, leaky bucket) to protect upstream services.
Human‑in‑the‑LoopFor actions with legal or financial consequences (sending emails, creating tickets, modifying data), pause after requires_action, present the proposed call to a human for approval, then submit the output only on confirmation.
LoggingLog every function name, arguments, timestamp, and result. This audit trail is invaluable for debugging and for satisfying compliance requirements.
TestingStart with a mock function that simply logs the arguments and returns a canned response. Verify argument extraction and edge cases (missing parameters, extra text) before plugging in the real backend.
DocumentationKeep your function descriptions up to date; they are part of the prompt and affect the model’s decision‑making.

Moving From Assistants API to Responses API

OpenAI has announced that the Assistants API will be retired on August 26, 2026. The recommended replacement is the Responses API, which adopts a stateless request‑response model. The core function‑calling idea stays the same: you describe functions as tools, the model decides when to call them, you execute and return results. The differences are:

  • No thread object; you pass the full conversation history with each request.
  • You manage the “run loop” yourself: send a request, inspect output for tool_calls, execute, then send another request that includes the results.
  • This gives you more control over persistence, branching, replay, branching, and custom state stores (Redis, database, etc.).

If you already have production assistants, begin by exporting your function definitions, building a simple conversation store, and replacing the poll‑for‑requires_action pattern with a straightforward request‑response loop. Run both implementations in parallel for a window to validate parity before cutover.


Leveraging OpenAPI Specs for Function Definitions

Many organizations already maintain OpenAPI (formerly Swagger) documents for their internal or external APIs. Rather than hand‑craft JSON‑Schema for each endpoint, you can convert an OpenAPI path into a function definition automatically:

  1. Identify the operation (GET, POST, etc.) you want to expose.
  2. Map path/query/header parameters to the function’s parameters object.
  3. Use the operation’s summary or description as the function description.
  4. Keep the requestBody (if any) as part of the parameters schema.

Tools like Runbear or open-source converters can perform this mapping, letting you paste the raw OpenAPI YAML/JSON and receive a ready‑to‑use function list for your assistant. This approach reduces boilerplate and keeps the API contract in sync with your assistant’s capabilities.


Combining Function Calling with Built‑In Tools

Assistants aren’t limited to functions alone. You can also enable:

  • Code Interpreter – lets the assistant run Python to analyze uploaded files, generate charts, or perform calculations.
  • File Search (Retrieval) – indexes PDFs, CSVs, or text files so the assistant can answer questions from your knowledge base without extra code.

A typical powerful setup includes all three: retrieval for documentation, code interpreter for ad‑hoc data work, and functions for actions that affect external systems (email, ticketing, database updates). The model decides which tool(s) to invoke in each turn, giving you a flexible, multi‑modal agent.


Practical Applications

  • Customer Support – Look up order status via an internal API, create a ticket in Zendesk, or fetch a knowledge‑base article.
  • Sales Enablement – Retrieve real‑time product pricing, log call notes to a CRM, or send follow‑up emails.
  • DevOps – Query monitoring services (Prometheus, Datadog), restart services via a secured webhook, or fetch logs.
  • Personal Productivity – Calendar management (create events, check availability), email drafting, or todo‑list synchronization.
  • Education – Provide up‑to‑date country facts, solve math problems via code interpreter, or fetch recent research papers from arXiv.

Each use case benefits from the assistant’s ability to maintain conversational context while reaching out to the right external system at the right moment.


Conclusion

Function calling is the bridge that turns a eloquent chatbot into a genuine automation partner. By defining clear, secure functions, handling the run lifecycle correctly, and layering in best practices like error handling, logging, and optional human approval, you can build assistants that are both useful and trustworthy.

While the Assistants API will eventually give way to the Responses API, the underlying pattern—model decides, your code executes—remains unchanged. Invest now in solid function definitions, reliable backend services, and clean orchestration code, and you’ll be well positioned to migrate smoothly when the time comes.

Start small—pick one external service, expose a simple function, and watch your assistant evolve from a conversational partner into a capable agent that can act on your behalf. Happy building!

Share this post:
OpenAI Assistants API Function Calling Guide