---
title: OpenAI SDK tools
description: The outbound tools as OpenAI function definitions for the Responses and Chat Completions APIs, with one function that runs a call from the model and returns the string to send back.
sidebar:
  label: OpenAI
related:
  - /docs/tools
  - /docs/tools/untrusted-content
  - /guides/agent-tools
---

`openaiTools(outbound)` from `outbound-sdk/tools` returns the [tools](/docs/tools) in the shape the OpenAI SDK takes. It is built without importing `openai`, so it adds no dependency, and it works with any client that accepts the same function-tool shape.

It returns three things:

| Property | What it is |
| --- | --- |
| `tools` | Function tools for the Responses API: `type`, `name`, `description`, `parameters`, `strict`. Pass as `tools` to `client.responses.create`. |
| `chatTools` | The same tools for Chat Completions, each nested under `function`. Pass as `tools` to `client.chat.completions.create`. |
| `handle(name, argumentsJson)` | Runs one function call. Takes the call's `name` and its `arguments` string, returns the result as a JSON string. |

`openaiTools` takes the same `only` and `extra` options as [`createTools`](/docs/tools#options).

## Build the tools and run a call

```ts title="openai.ts"
import { createOutbound } from "outbound-sdk";
import { demoData, memory } from "outbound-sdk/memory";
import { openaiTools } from "outbound-sdk/tools";

const outbound = createOutbound({
  adapter: memory({ data: demoData(), workspaceId: "acme" }),
});

const { chatTools, handle, tools } = openaiTools(outbound);

// `tools` is the Responses API shape, `chatTools` the Chat Completions shape.
const [first] = tools;

console.log(tools.length, "tools");

console.log({
  description: `${first?.description.slice(0, 40)}...`,
  name: first?.name,
  parameters: "(JSON Schema)",
  strict: first?.strict,
  type: first?.type,
});

console.log(Object.keys(chatTools[0] ?? {}), chatTools[0]?.function.name);

// `handle` takes the call's name and its `arguments` string, and returns the
// string to send back as the call's output.
console.log(await handle("campaign_overview", '{"status":"paused","limit":1}'));

// It never throws. A tool the set does not have and arguments that are not
// JSON are results too.
console.log(await handle("drop_database", "{}"));

console.log(await handle("sender_health", "{not json"));
```

```text title="Output"
9 tools
{
  description: 'How every campaign is doing right now: s...',
  name: 'campaign_overview',
  parameters: '(JSON Schema)',
  strict: false,
  type: 'function'
}
[ 'function', 'type' ] campaign_overview
{"data":{"campaigns":[{"campaign":"memory:acme:cmp_agencies","channel":"email","name":"Agency owners","metrics":{"bounce_rate":0.0667,"bounced":4,"connections_accepted":null,"connections_sent":null,"contacted":60,"opened":18,"positive":0,"positive_reply_rate":0,"replied":1,"reply_rate":0.0167,"reply_rate_basis":"contacted","sent":60},"status":"paused"}]},"meta":{"evidence":"exact","provider":"memory","requests":1,"today":"2026-11-14"},"next":"Use campaign_trend for one campaign over time, or winning_steps to see which step drives replies.","ok":true}
{"error":{"code":"Invalid","message":"Unknown tool drop_database"},"next":"Call one of the listed tools with a JSON object.","ok":false}
{"error":{"code":"Invalid","message":"The arguments are not valid JSON"},"next":"Call one of the listed tools with a JSON object.","ok":false}
```

- **`strict` is `false`.** The schemas have optional fields, which OpenAI's strict mode does not accept. The tool validates the arguments itself and reports anything wrong as a result.
- **An empty `arguments` string means no arguments.** `handle` reads it as `{}`.
- **`handle` never throws.** An unknown tool name and arguments that are not JSON come back as `{ ok: false }` with the code `Invalid`.

## Answer the model's calls

When a response contains `function_call` items, run each through `handle` and send the outputs back in the next request.

```ts title="dispatch.ts"
import { createOutbound } from "outbound-sdk";
import { demoData, memory } from "outbound-sdk/memory";
import { openaiTools } from "outbound-sdk/tools";

const outbound = createOutbound({
  adapter: memory({ data: demoData(), workspaceId: "acme" }),
});

const { handle } = openaiTools(outbound);

/** A `function_call` item from a Responses API response. */
interface FunctionCall {
  readonly type: "function_call";
  readonly call_id: string;
  readonly name: string;
  readonly arguments: string;
}

/** Runs each call and builds the `function_call_output` items to send back. */
const answer = (calls: readonly FunctionCall[]) =>
  Promise.all(
    calls.map(async (call) => ({
      call_id: call.call_id,
      output: await handle(call.name, call.arguments),
      type: "function_call_output" as const,
    }))
  );

// Written by hand in the shape a model emits. No model was called.
const outputs = await answer([
  {
    arguments: '{"limit":1}',
    call_id: "call_1",
    name: "sender_health",
    type: "function_call",
  },
]);

console.log(JSON.stringify(outputs, null, 2));
```

```text title="Output"
[
  {
    "call_id": "call_1",
    "output": "{\"data\":{\"senders\":[{\"daily_limit\":40,\"kind\":\"mailbox\",\"sender\":\"memory:acme:mbx_1\",\"status\":\"active\",\"warmup_enabled\":true,\"warmup_score\":96}],\"unhealthy\":0},\"meta\":{\"evidence\":\"exact\",\"provider\":\"memory\",\"requests\":0,\"today\":\"2026-11-14\"},\"next\":\"A sender address is not included. Look a sender up by its ref if you need more.\",\"ok\":true}",
    "type": "function_call_output"
  }
]
```

> **No model was called**
>
> The samples on this page build the tool set and run `handle` directly. The
> `function_call` item above was written by hand in the shape the Responses API
> documents. The round trip through an OpenAI model was not run by the test
> suite, and the `openai` package is not installed in this repository.

## Wiring it to a model

The loop around these pieces is yours:

1. Call `client.responses.create` with your model, your input and `tools`.
2. For each `function_call` item in the output, call `handle(item.name, item.arguments)`.
3. Append a `function_call_output` item with the same `call_id` and the returned string, and call the API again.
4. Stop when a response has no function calls.

With Chat Completions, pass `chatTools`, read `message.tool_calls`, and answer each with a `tool` message whose content is the string `handle` returned.

## Reply text

A model given `reply_samples` can ask for reply text. It comes back truncated and marked as untrusted. See [untrusted content](/docs/tools/untrusted-content).
