---
title: Approvals
description: Pause a durable workflow until a human approves it in chat.
docs_index: /llms.txt
lastUpdated: 2026-10-27
type: guide
prerequisites:
  - /docs/cards
  - /docs/actions
related:
  - /docs/ai/ai-sdk-tools
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

`requestApproval` from `chat/workflow` posts a card with Approve/Deny buttons and suspends a [Workflow SDK](https://workflow-sdk.dev) workflow until a user clicks. The wait can last seconds or days and survives deploys and restarts. You don't need an approvals table, an `onAction` handler, or a polling loop.

```typescript title="workflows/deploy.ts" lineNumbers
import { requestApproval } from "chat/workflow";
import type { Thread } from "chat";

export async function deployApproval(opts: { thread: Thread; version: string }) {
  "use workflow";

  const { approved, user, timedOut } = await requestApproval(opts.thread, {
    title: `Deploy ${opts.version}?`,
    fields: { Version: opts.version },
    timeout: "24h",
  });

  if (approved) {
    await deploy(opts.version);
  }
}

async function deploy(version: string) {
  "use step";
  // Trigger your deploy here.
}
```

Start the workflow from any handler. `Thread` instances serialize across the workflow boundary automatically:

```typescript title="lib/bot.ts" lineNumbers
import { start } from "workflow/api";
import { deployApproval } from "@/workflows/deploy";

bot.onNewMention(async (thread, message) => {
  await start(deployApproval, [{ thread, version: "v1.2.3" }]);
});
```

## Setup

Install the optional `workflow` peer dependency and configure your framework for [Workflow SDK](https://workflow-sdk.dev/docs/getting-started). Then register your `Chat` instance as a singleton at startup so threads can be revived on the other side of the workflow boundary:

```typescript title="lib/bot.ts"
chat.registerSingleton();
```

## Options

| Option                       | Type                                                   | Description                                                                   |
| ---------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------- |
| `title`                      | `string`                                               | Card title (required)                                                         |
| `subtitle`                   | `string`                                               | Rendered under the title                                                      |
| `description`                | `string`                                               | Body text (markdown) above the fields                                         |
| `fields`                     | `Record<string, string>`                               | Key/value pairs rendered as a bold-label list                                 |
| `approveLabel` / `denyLabel` | `string`                                               | Button labels. Default: "Approve" / "Deny"                                    |
| `timeout`                    | `` number \| `${number}${"ms"\|"s"\|"m"\|"h"\|"d"}` `` | Give up after this long. Omit to wait indefinitely                            |
| `approvers`                  | `string[]`                                             | User IDs allowed to decide. Clicks from others post a notice and keep waiting |

## Result

| Property   | Type                            | Description                                  |
| ---------- | ------------------------------- | -------------------------------------------- |
| `approved` | `boolean`                       | True when the approve button was clicked     |
| `timedOut` | `boolean`                       | True when `timeout` elapsed with no decision |
| `user`     | `{ id: string; name?: string }` | Who decided. Absent on timeout               |

Once a decision lands (or the timeout elapses), the card is edited in place: the buttons are removed and replaced with an outcome line, leaving an audit trail in the thread and preventing stale clicks.

## Who can approve

Anyone who can see the card can decide the approval. When someone clicks, Chat SDK verifies the platform's signature on the event and stores a receipt in your state adapter that records which button was clicked, by whom, and on which thread. It then calls the workflow's webhook with that receipt. `requestApproval` takes the decision and `user` from the receipt rather than from the webhook request and accepts each receipt only once, so a POST to the webhook URL without a valid receipt is ignored and the workflow keeps waiting.

The `Chat` instance that handles the click and the one that runs the workflow must use the same state adapter, such as Redis or Postgres, so the workflow can find the receipt. The memory state adapter works only when both run in the same process. The thread you pass must also come from Chat SDK, such as the one a handler receives or one from `bot.thread()` or `bot.openDM()`. `requestApproval` throws when given a custom `Thread` implementation.

If only certain people should decide, pass `approvers` with their user IDs. When anyone else clicks, the bot posts a notice in the thread, refreshes the card's buttons so an approver can still decide, and keeps the workflow waiting:

```typescript
await requestApproval(thread, {
  title: `Deploy ${version}?`,
  approvers: ["U_ALICE", "U_BOB"],
});
```

Set `approvers` for anything consequential.

## Custom cards

The card builders are exported for cases the wrapper doesn't cover, such as posting an approval card whose buttons target a webhook you manage yourself:

```typescript
import { buildApprovalCard, buildResolvedCard } from "chat/workflow";

const card = buildApprovalCard(
  { title: "Deploy?", fields: { Version: "v1.2.3" } },
  webhookUrl
);
```

When the buttons target your own webhook, it receives the [callback payload](/docs/actions#callback-payload) directly, and nothing verifies its fields for you. Anyone who knows the URL can POST to it, so don't trust `user` or `actionId` from the body.

> The [Human-in-the-Loop guide](https://vercel.com/kb/guide/human-in-the-loop-with-chat-sdk-and-workflow-sdk)
> walks through the underlying pattern, including multi-stage approvals.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)