Confirmation Flows
Some tools should ask the user for confirmation before executing — deleting records, making charges, sending messages. The server SDK has a built-in confirmation pattern: return a ConfirmationResponse from your execute handler, and Pillar renders a channel-appropriate confirmation UI.
How it works
- The agent calls your tool
- Your
executehandler returns aConfirmationResponseinstead of the final result - Pillar renders a confirmation card (buttons in Slack, a structured card in the web panel)
- The user clicks Confirm or Cancel
- On confirm, Pillar re-calls your tool with
ctx.confirmed = trueand theconfirm_payload - Your handler runs the actual operation and returns the result
Example
examples/server-sdks/confirmation-flow.ts
import { defineTool, type ConfirmationResponse } from '@pillar-ai/server';import { z } from 'zod';const deleteMember = defineTool({name: 'delete_member',description: 'Remove a member from the workspace',input: z.object({userId: z.string().describe('ID of the member to remove'),}),execute: async ({ userId }, ctx): Promise<ConfirmationResponse | { removed: boolean }> => {if (!ctx.confirmed) {const user = await db.getUser(userId);return {confirmation_required: true,title: 'Remove member',message: `Remove ${user.name} from the workspace?`,details: {'Member': user.name,'Email': user.email,'Role': user.role,},confirm_payload: { userId },};}await workspace.removeMember(userId);return { removed: true };},});
On the first call, ctx.confirmed is false, so the handler returns a confirmation card with the member's details. After the user confirms, Pillar re-calls the tool with ctx.confirmed = true and the handler proceeds with the deletion.
ConfirmationResponse fields
| Field | Type | Required | Description |
|---|---|---|---|
confirmation_required | boolean | Yes | Must be true |
title | string | No | Short headline. Defaults to the tool name |
message | string | No | One-line description shown before the buttons |
details | Record<string, string> | No | Key-value pairs rendered as a summary card |
confirm_payload | object | No | Data passed back to execute on confirm. Defaults to the original arguments |
The confirm_payload field
confirm_payload lets you control exactly what data is sent back when the user confirms. This is useful when:
- You want to include resolved data (like a user ID looked up from an email)
- You want to strip sensitive fields from the confirmation round-trip
- You want to add metadata that wasn't in the original arguments
If omitted, the original tool arguments are used.
typescript
execute: async ({ email }, ctx) => {if (!ctx.confirmed) {const user = await db.findByEmail(email);return {confirmation_required: true,title: 'Delete user',message: `Delete ${user.name}?`,confirm_payload: { userId: user.id }, // Pass the resolved ID, not the email};}// On confirm, `input` is `{ userId: "..." }` from confirm_payloadawait db.deleteUser(ctx.callId);return { deleted: true };},
Channel behavior
The confirmation UI adapts to the channel:
| Channel | Rendering |
|---|---|
| Web panel | Structured card with details table and Confirm/Cancel buttons |
| Slack | Block Kit message with buttons |
| API | JSON response with confirmation_required: true for the client to handle |
Next steps
- Defining Tools — Full tool definition reference
- Webhook Security — How request signatures work
- Testing — Test confirmation flows in your test suite