Interactive Callbacks

Add interactive buttons to your agent - let users confirm payments, choose options, or trigger custom actions.

What are Callbacks?

When your agent needs a user decision, it can show interactive buttons. When clicked, your custom code executes.

Example: Agent asks "Confirm payment of $99?" with [Confirm] and [Cancel] buttons. User clicks [Confirm] → your payment handler runs → payment processed.

Quick Start

1. Create a button in your workflow

typescript
import { flutch } from '@flutch/sdk';

// Agent asks user to confirm
const token = await flutch.callbacks.issue({
  handler: 'confirm-payment',
  params: { amount: 99.99, orderId: 'abc123' }
});

return {
  content: 'Confirm payment of $99.99?',
  buttons: [
    { text: 'Confirm', callbackToken: token }
  ]
};

2. Create callback handler class

typescript
// src/callbacks/payment.callbacks.ts
import { Injectable } from '@nestjs/common';
import { Callback, ExtendedCallbackContext, CallbackResult } from '@flutch/sdk';

@Injectable()
export class PaymentCallbacks {
  @Callback('confirm-payment')
  async handleConfirmPayment(
    context: ExtendedCallbackContext
  ): Promise<CallbackResult> {
    const { amount, orderId } = context.params;

    // Your logic here
    await processPayment(orderId, amount);

    return {
      success: true,
      message: 'Payment confirmed!',
      patch: {
        text: '✓ Payment confirmed',
        disableButtons: true
      }
    };
  }
}

3. Register callbacks in builder

typescript
// src/graph/versions/v1.0.0/builder.ts
import { Injectable } from '@nestjs/common';
import { WithCallbacks, AbstractGraphBuilder } from '@flutch/sdk';
import { PaymentCallbacks } from '../../../callbacks/payment.callbacks';

@Injectable()
@WithCallbacks(PaymentCallbacks)
export class MyGraphV1Builder extends AbstractGraphBuilder<'1.0.0'> {
  readonly version = '1.0.0' as const;

  // builder implementation...
}

Done! When user clicks [Confirm], your confirmPayment function runs.

Handler Details

What you receive (context)

typescript
{
  userId: string;      // Who clicked
  threadId: string;    // Conversation ID
  params: {...};       // Data from issue()
  platform: 'web' | 'telegram' | 'whatsapp';
}

What you return

typescript
{
  success: boolean;
  message?: string;    // Optional success message
  error?: string;      // If success: false
  patch?: {...};       // Update the UI (optional)
}

UI Patches

Update the message after button click:

typescript
return {
  success: true,
  patch: {
    type: 'update-message',
    content: '✓ Payment confirmed',
    removeButtons: true
  }
};

Example: Order Confirmation

typescript
// In your workflow - show button
const token = await flutch.callbacks.issue({
  handler: 'confirm-order',
  params: { items: cart.items, total: 99.99 }
});

return {
  content: `Total: $99.99. Confirm order?`,
  buttons: [{ text: 'Confirm', callbackToken: token }]
};

// In your callback handler class
@Injectable()
export class OrderCallbacks {
  @Callback('confirm-order')
  async handleConfirmOrder(
    context: ExtendedCallbackContext
  ): Promise<CallbackResult> {
    const { params, userId } = context;

    // Your business logic
    const order = await createOrder(userId, params.items);
    await processPayment(order.id, params.total);

    // Return success + update UI
    return {
      success: true,
      patch: {
        text: `✓ Order #${order.id} confirmed!`,
        disableButtons: true
      }
    };
  }
}

Security

Flutch handles security automatically:

  • Token expiration - Tokens expire after 10 minutes (configurable with ttlSec)
  • User validation - Only the user who received the button can click it
  • Rate limiting - Prevents spam clicks
  • Idempotency - Duplicate clicks don't cause duplicate actions
  • Auto-retry - Failed handlers retry automatically with backoff

Best Practices

1. Handle errors

typescript
@Injectable()
export class MyCallbacks {
  @Callback('process-action')
  async handleAction(context: ExtendedCallbackContext): Promise<CallbackResult> {
    try {
      await riskyOperation();
      return { success: true };
    } catch (error) {
      return {
        success: false,
        error: 'Please try again later'
      };
    }
  }
}

2. Keep handlers fast

typescript
@Injectable()
export class MyCallbacks {
  // Good - returns quickly
  @Callback('start-job')
  async handleStartJob(context: ExtendedCallbackContext): Promise<CallbackResult> {
    const jobId = await startBackgroundJob(context.params);
    return { success: true, data: { jobId } };
  }

  // Bad - will timeout
  @Callback('slow-operation')
  async handleSlowOperation(context: ExtendedCallbackContext): Promise<CallbackResult> {
    await longOperation(); // ❌ Takes 30+ seconds
    return { success: true };
  }
}

Testing

bash
# 1. Register and start
flutch register
npm run dev

# 2. Test
flutch test "I want to pay"
# Click the button in console UI

View metrics in console: console.flutch.ai/agents/{agent-id}/callbacks

Next Steps

Learn more about Flutch development:

Build your interactive agent: