---
title: AI SDK tools
description: The outbound tools as a tool set for the AI SDK, keyed by tool name and ready to pass to generateText or streamText.
sidebar:
  label: AI SDK
related:
  - /docs/tools
  - /docs/tools/untrusted-content
  - /guides/agent-tools
---

`aiSdkTools(outbound)` from `outbound-sdk/tools/ai-sdk` returns the [tools](/docs/tools) as one AI SDK tool per outbound tool, keyed by tool name. Pass the object as `tools` to `generateText` or `streamText`.

This subpath imports `ai`, which is an optional peer dependency: version 6 or 7. Install it only if you import this subpath. See [installation](/docs/installation).

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

## Build the tools and run one

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

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

// One AI SDK tool per outbound tool, keyed by tool name.
const tools = aiSdkTools(outbound, {
  only: ["campaign_overview", "sender_health"],
});

console.log(Object.keys(tools));

const overview = tools.campaign_overview;

console.log(overview?.description);

// What the AI SDK does when the model calls the tool, done by hand here.
const result = await overview?.execute?.(
  { status: "paused" },
  { context: undefined, messages: [], toolCallId: "call_1" }
);

console.log(JSON.stringify(result, null, 2));

// A failure is a result the model can read, not an exception.
const failed = await overview?.execute?.(
  { client: "acme" },
  { context: undefined, messages: [], toolCallId: "call_2" }
);

console.log(JSON.stringify(failed));
```

```text title="Output"
[ 'campaign_overview', 'sender_health' ]
How every campaign is doing right now: sends, replies, positive replies, bounces and reply rate for each campaign with the given status. One vendor request per campaign. Rates are computed from counts; a null means the vendor does not report that count.
{
  "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": "Pick another of the listed tools to go further.",
  "ok": true
}
{"error":{"code":"Invalid","message":"campaign_overview: \"client\" is not an input. It takes: from, limit, status, to"},"next":"Check the input and try again.","ok":false}
```

Each tool has a `description`, an `inputSchema` built from the tool's JSON Schema, and an `execute` function that returns the tool's result object. A failed tool returns `{ ok: false }` from `execute`. It does not throw, so the AI SDK passes the failure to the model as an ordinary tool result.

## Wiring it to a model

```ts title="generate.ts"
import { generateText, stepCountIs } from "ai";
import type { LanguageModel } from "ai";
import type { OutboundApi } from "outbound-sdk";
import { aiSdkTools } from "outbound-sdk/tools/ai-sdk";

/**
 * Asks a model a question about `outbound`. The model may call the tools,
 * read what they return and answer, in up to five steps.
 */
export const ask = (
  outbound: OutboundApi,
  model: LanguageModel,
  prompt: string
) =>
  generateText({
    model,
    prompt,
    stopWhen: stepCountIs(5),
    tools: aiSdkTools(outbound),
  });
```

`ask` takes the instance, a model and a question. `stopWhen: stepCountIs(5)` gives the model up to five steps, so it can call a tool, read the result and answer inside one `generateText` call.

## A real run

This function was run against a real model on 2026-11-14: `deepseek/deepseek-v4-flash`, through the Vercel AI Gateway, with `ai` 7. The outbound side was the in-memory adapter's demo data, so the run shows a model calling the tools and answering from what they return. It says nothing about a vendor. This is the transcript of that run: the question, each tool call the model made, and its answer.

```json title="Transcript of one live run"
{
  "answer": "There is currently one active campaign:\n\n**Q4 SaaS founders** (`cmp_founders`, email channel)\n- Sends: 90\n- Replies: 7 (7.8% reply rate)\n\nNotes: 52 of the 90 sends were opened, 3 were positive, and 1 bounced. The account has no other campaigns in the active state right now.\n\nWant me to dig into this campaign at all — e.g., what's driving the replies, deliverability, or how many leads it has left?",
  "model": "deepseek/deepseek-v4-flash",
  "prompt": "Which campaigns are active, and how many sends and replies has each had?",
  "ranAt": "2026-11-14",
  "steps": 2,
  "toolCalls": [
    {
      "input": {
        "status": "active"
      },
      "tool": "campaign_overview"
    }
  ],
  "via": "Vercel AI Gateway"
}
```

> **An example run, not a recorded output**
>
> A model does not answer the same way twice, so the test suite does not
> reproduce this transcript, and it is not checked on every commit the way the
> other outputs on this site are. The run is `site/test/live/ai-sdk.live.ts` in
> the repository; `npm run site:live` repeats it with an AI Gateway credential
> and asserts that the model called a tool and named the campaign from the
> tool's result. Only `ai` 7 was run; `ai` 6 is accepted by the peer range and
> was not.

The first live run found a problem that is now fixed. The model does not know today's date, so it asked for 2024 to 2026 and found no data. Every tool result now carries `meta.today`, and the date inputs tell the model to leave the range out for the whole history and not to guess.

## 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).
