# Approval card
URL: /elements/approval-card

Human in the loop: the agent asks before it takes a consequential action.

> For AI agents: a documentation index is available at [llms.txt](/llms.txt). Use `.md` for canonical markdown pages; `.mdx` is kept as a backwards-compatible alias on supported URL paths.

An approval card explains a consequential action before it happens. It can show a shell command, but it also works for actions with a short description and structured details, such as deleting records or publishing a release. With a runtime the decision flows through the tool call's own approval gate; standalone you hold the state and answer the callbacks yourself.

## Getting started

**With a runtime:**

A server-side tool approval gate keeps the request open until the user answers. Map the tool arguments to the card and send the selected response through `respondToApproval`.

1. ### Render the tool call

   ```
   "use client";

   import { Trash2Icon } from "lucide-react";
   import { defineToolkit, type ToolApprovalResponse } from "@assistant-ui/react";
   import { ApprovalCard } from "@/components/assistant-ui/elements/approval-card";

   export const toolkit = defineToolkit({
     manage_records: {
       type: "backend",
       render: ({ args, approval, respondToApproval, result }) => {
         const answer = async (response: ToolApprovalResponse) => {
           try {
             await respondToApproval(response);
           } catch (failure) {
             console.error(failure);
           }
         };
         const destructive = args.action === "delete";

         return (
           <ApprovalCard
             state={
               approval?.approved === false
                 ? "denied"
                 : approval?.approved === undefined
                   ? "request"
                   : result === undefined
                     ? "running"
                     : "done"
             }
             command={args.command}
             title={args.title}
             subtitle={args.subtitle}
             description={args.description}
             details={args.details}
             variant={destructive ? "destructive" : "default"}
             icon={destructive ? <Trash2Icon className="size-4" /> : undefined}
             onAllowOnce={() => void answer({ optionId: "once" })}
             onAlwaysAllow={() => void answer({ optionId: "always" })}
             onDeny={() => void answer({ optionId: "deny" })}
           />
         );
       },
     },
   });
   ```

   `approval.approved` is `undefined` until answered, so that is the only window in which `respondToApproval` is legal. Sending an `optionId` records which of the host's declared options was chosen rather than a plain yes or no. See [Approval options](/docs/tools/tool-ui#approval-options) for declaring `"once"`, `"always"`, and `"deny"` as `allow-once`, `allow-always`, and `reject-once`.

2. ### Register the toolkit

   ```
   import { AssistantRuntimeProvider, AuiConfig, Tools } from "@assistant-ui/react";
   import { toolkit } from "./toolkit";

   const config = AuiConfig({ tools: Tools({ toolkit }) });

   export function MyRuntimeProvider({ children }: { children: React.ReactNode }) {
     return (
       <AssistantRuntimeProvider runtime={runtime} config={config}>
         {children}
       </AssistantRuntimeProvider>
     );
   }
   ```

   Approval gates require a runtime that emits them; the AI SDK v7 runtime does for `toolApproval`-gated tools, and `LocalRuntime` does for gates your `ChatModelAdapter` emits.

**Standalone (no runtime):**

Standalone, the element is a controlled display: you own `state` and decide what each button does.

1. ### Hold the approval state

   ```
   "use client";

   import { useState } from "react";
   import {
     ApprovalCard,
     type ApprovalState,
   } from "@/components/assistant-ui/elements/approval-card";

   export function Approval() {
     const [state, setState] = useState<ApprovalState>("request");

     return (
       <ApprovalCard
         state={state}
         command="pnpm vitest run --changed"
         title="Run checks"
         subtitle="The agent wants to run a shell command"
         description="This runs the tests affected by the current changes."
         onAllowOnce={() => setState("running")}
         onAlwaysAllow={() => setState("running")}
         onDeny={() => setState("denied")}
       />
     );
   }
   ```

2. ### Resolve the action

   ```
   async function run() {
     await runChecks();
     setState("done");
   }
   ```

   `state="done"` reads "Finished" by default. Pass `statusLabel` when the completed action needs a more specific receipt.

## Anatomy

```
<div data-slot="approval-card" data-variant="default | destructive">
  <div>
    <span>{/* terminal icon or icon */}</span>
    <p>{/* title */}</p>
    <p>{/* subtitle */}</p>
  </div>
  <p>{/* optional description */}</p>
  <div>{/* optional command, monospace */}</div>
  <dl>{/* optional detail label and value pairs */}</dl>
  <div>{/* request actions or a status receipt */}</div>
</div>
```

The title labels the card for assistive technology, and `description` becomes its accessible description when present. The footer becomes a live status receipt once the state changes to `"running"`, `"done"`, or `"denied"`.

## Examples

### Approve a destructive action

`command` is optional, so an approval can describe an application action without pretending it is a terminal command. The destructive variant changes the icon tile and the primary action to the failure tone.

```
import { Trash2Icon } from "lucide-react";

<ApprovalCard
  state="request"
  variant="destructive"
  icon={<Trash2Icon className="size-4" />}
  title="Delete 12 archived conversations"
  subtitle="This action cannot be undone"
  description="The selected conversations and their generated files will be permanently removed."
  details={[
    { label: "Conversations", value: "12 archived" },
    { label: "Generated files", value: "38 files" },
  ]}
  allowOnceLabel="Delete conversations"
  alwaysAllowLabel="Always delete"
  denyLabel="Keep conversations"
  onAllowOnce={deleteConversations}
  onAlwaysAllow={alwaysDeleteConversations}
  onDeny={closeApproval}
/>
```

### Restyle the card

Both lanes take `className` on the root. The optional command and details panels use the `field` surface. Request buttons use `ghostButton` and `inkButton` from `surfaces.tsx`.

```
<ApprovalCard className="max-w-md" /* ... */ />
```

### Denying with a reason

**With a runtime:**

`respondToApproval` accepts a `reason` alongside the decision, which the host can show back to the model:

```
onDeny={() =>
  void answer({ optionId: "deny", reason: "not in this session" })
}
```

**Standalone (no runtime):**

The element has no reason field; collect one in your own UI before calling `onDeny`, or route the reason through your app state instead.

```
onDeny={() => {
  logDenyReason("not in this session");
  setState("denied");
}}
```

## API reference

**With a runtime:**

### Tool-call render props

| Source                         | Type                                                       | Description                                                                                                                          |
| ------------------------------ | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `args.title` / `args.subtitle` | `string`                                                   | Required framing copy for the card header.                                                                                           |
| `args.command`                 | `string \| undefined`                                      | Optional command displayed in the monospace panel.                                                                                   |
| `args.description`             | `string \| undefined`                                      | Optional summary mapped to `description`.                                                                                            |
| `args.details`                 | `readonly { label: string; value: string }[] \| undefined` | Optional structured detail rows mapped to `details`.                                                                                 |
| Renderer mapping               | `"default" \| "destructive"`, `ReactNode \| undefined`     | Choose `variant` and `icon` from the action type. An icon is renderer UI, not a tool argument.                                       |
| Renderer labels                | `string \| undefined`                                      | Map action-specific labels to `allowOnceLabel`, `alwaysAllowLabel`, and `denyLabel`; their card defaults are available when omitted. |
| Result mapping                 | `string \| undefined`                                      | Pass `statusLabel` when the running, finished, or denied receipt needs action-specific copy.                                         |
| `approval?.approved`           | `boolean \| undefined`                                     | `undefined` maps to `"request"`, `false` to `"denied"`, `true` to `"running"` or `"done"` depending on `result`.                     |
| `result`                       | `unknown`                                                  | Presence after approval maps to `"done"`.                                                                                            |
| `respondToApproval(response)`  | `(response: ToolApprovalResponse) => Promise<void>`        | Sends the decision. It is legal only while `approval.approved` is `undefined`.                                                       |

**Standalone (no runtime):**

### ApprovalCard

| Prop               | Type                                                       | Default             | Description                                                          |
| ------------------ | ---------------------------------------------------------- | ------------------- | -------------------------------------------------------------------- |
| `state`            | `"request" \| "running" \| "done" \| "denied"`             | required            | Selects the request controls or receipt.                             |
| `command`          | `string \| undefined`                                      |                     | Displays a monospace command panel when supplied.                    |
| `title`            | `string`                                                   | required            | Labels the card and names the action.                                |
| `subtitle`         | `string`                                                   | required            | Secondary header copy.                                               |
| `description`      | `string \| undefined`                                      |                     | Short explanation of what will happen.                               |
| `details`          | `readonly { label: string; value: string }[] \| undefined` |                     | Structured action details, displayed as label and value pairs.       |
| `variant`          | `"default" \| "destructive"`                               | `"default"`         | Uses the destructive tone for the icon tile and primary action.      |
| `icon`             | `ReactNode \| undefined`                                   | terminal icon       | Replaces the terminal icon in the header tile.                       |
| `allowOnceLabel`   | `string \| undefined`                                      | `"Allow once"`      | Label for the primary request action.                                |
| `alwaysAllowLabel` | `string \| undefined`                                      | `"Always allow"`    | Label for the persistent request action.                             |
| `denyLabel`        | `string \| undefined`                                      | `"Deny"`            | Label for the refusal action.                                        |
| `statusLabel`      | `string \| undefined`                                      | state-specific copy | Replaces the receipt text for any non-request state.                 |
| `onAllowOnce`      | `() => void`                                               |                     | Renders the primary request action and fires when it is selected.    |
| `onAlwaysAllow`    | `() => void`                                               |                     | Renders the persistent request action and fires when it is selected. |
| `onDeny`           | `() => void`                                               |                     | Renders the refusal action and fires when it is selected.            |
| `className`        | `string`                                                   |                     | Merged onto the root.                                                |

All other `div` props are forwarded to the root.