Search documentation

Search documentation

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

  1. The agent calls your tool
  2. Your execute handler returns a ConfirmationResponse instead of the final result
  3. Pillar renders a confirmation card (buttons in Slack, a structured card in the web panel)
  4. The user clicks Confirm or Cancel
  5. On confirm, Pillar re-calls your tool with ctx.confirmed = true and the confirm_payload
  6. 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

FieldTypeRequiredDescription
confirmation_requiredbooleanYesMust be true
titlestringNoShort headline. Defaults to the tool name
messagestringNoOne-line description shown before the buttons
detailsRecord<string, string>NoKey-value pairs rendered as a summary card
confirm_payloadobjectNoData 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_payload
await db.deleteUser(ctx.callId);
return { deleted: true };
},

Channel behavior

The confirmation UI adapts to the channel:

ChannelRendering
Web panelStructured card with details table and Confirm/Cancel buttons
SlackBlock Kit message with buttons
APIJSON response with confirmation_required: true for the client to handle

Next steps