@botpress/adk-cli
Version:
Command-line interface for the Botpress Agent Development Kit (ADK)
855 lines (671 loc) • 21.7 kB
Markdown
# Desk Integration
Desk is Botpress's AI-augmented support workspace where human agents and AI bots collaborate on customer tickets. This guide covers how to build ADK agents that participate in the Desk support workflow.
## Quick Start
Minimal working example for a Desk support bot:
```typescript
// src/conversations/desk.ts
import { Autonomous, context, Conversation, z } from '@botpress/runtime'
import { actions, joinMarkdownChildren } from '@botpress/runtime/runtime'
export default new Conversation({
channel: 'desk.ticket',
state: z.object({
lastSync: z.string().default(''),
}),
events: ['desk:ticketActivity'],
handler: async ({ conversation, execute, send }) => {
// CRITICAL: Only process tickets assigned to this bot
if (conversation.tags['desk:dassignedself'] !== 'true') {
console.log('Ticket not assigned to bot, ignoring...')
return
}
await execute({
tools: [],
instructions: 'You are a helpful support agent...',
})
},
})
```
### Configuration (agent.config.ts)
```typescript
import { defineConfig } from '@botpress/runtime'
export default defineConfig({
name: 'support-bot',
dependencies: {
integrations: {
desk: {
enabled: true,
configuration: {
displayName: 'Support Bot',
botHandle: 'SupportBot',
canBeAssigned: true,
},
},
},
},
})
```
## Overview
### What is Desk?
Desk is a unified support workspace that:
- Aggregates tickets from multiple sources (Intercom, Zendesk, native)
- Enables human agents and AI bots to collaborate on customer tickets
- Provides real-time activity feeds and assignment management
- Supports internal notes (staff-only) and customer-visible replies
### Bot Participation Modes
| Mode | `canBeAssigned` | Description |
| ------------- | --------------- | ------------------------------------------------------ |
| **Assigned** | `true` | Bot owns tickets and replies directly to customers |
| **Assisting** | `false` | Bot helps human agents with suggestions and automation |
### Multi-Bot Teams
Multiple bots can be registered with Desk, each with different capabilities:
- A triage bot that classifies and routes tickets
- A knowledge bot that searches documentation
- A specialist bot for specific product areas
Each bot is identified by its `botHandle` (e.g., "@SupportBot", "@TriageBot").
## Configuration
All Desk configuration lives in `agent.config.ts` inside the `dependencies.integrations.desk` block:
```typescript
import { defineConfig } from '@botpress/runtime'
export default defineConfig({
name: 'my-agent',
dependencies: {
integrations: {
desk: {
enabled: true,
configuration: {
displayName: 'My Support Bot',
botHandle: 'MySupportBot',
displayAvatarUrl: 'https://example.com/bot-avatar.png',
canBeAssigned: true,
},
},
},
},
})
```
### Configuration Options
| Option | Type | Required | Description |
| ------------------ | ------- | -------- | --------------------------------------------------------------------------------- |
| `displayName` | string | Yes | Name shown in Desk UI |
| `botHandle` | string | Yes | Handle for @mentions (3-20 chars, starts with letter, alphanumeric + underscores) |
| `displayAvatarUrl` | string | No | URL of avatar image |
| `canBeAssigned` | boolean | No | Whether bot can be assigned to tickets (default: `false`) |
### Handle Rules
The `botHandle` must:
- Be 3-20 characters
- Start with a letter
- Contain only letters, numbers, and underscores
Examples:
- `SupportBot`, `Triage_Bot`, `KB123`
- ❌ `Bot`, `123Bot`, `support-bot`
## Channel & Events
### Channel: desk.ticket
The Desk integration exposes a single channel: `desk.ticket`. All ticket interactions flow through this channel.
```typescript
export default new Conversation({
channel: 'desk.ticket',
// ...
})
```
### Event: desk:ticketActivity
Emitted when new activities (comments, notes, or system activities) are added to a ticket the bot is assigned to.
```typescript
export default new Conversation({
channel: 'desk.ticket',
events: ['desk:ticketActivity'],
handler: async ({ event }) => {
// event.payload contains:
// - ticketId: string
// - activityCount: number
},
})
```
**Important:** The event does NOT include activities created by the bot itself (prevents infinite loops).
### Conversation Tags
| Tag | Description | Example |
| ---------------------- | --------------------------- | ------------------------------- |
| `desk:did` | Desk ticket ID | `tkt_abc123` |
| `desk:status` | Ticket status | `open`, `closed`, `snoozed` |
| `desk:title` | Ticket title | `"Login issue"` |
| `desk:system` | Source system | `intercom`, `zendesk`, `native` |
| `desk:url` | URL to view in Desk | `https://desk.botpress.com/...` |
| `desk:snoozeduntil` | Wake time if snoozed | ISO timestamp |
| `desk:dtags` | Desk tags (comma-separated) | `urgent,billing` |
| `desk:dcustomerid` | Customer ID | `cus_xyz789` |
| `desk:dassignedid` | Assigned admin/bot ID | `adm_123`, `ibot_456` |
| `desk:dassignedself` | **Is this bot assigned?** | `"true"` or `"false"` |
| `desk:dsyncactivityid` | Last synced activity ID | `tka_abc123` |
### Message Tags
| Tag | Description |
| ------------------ | -------------------------- |
| `desk:did` | Activity ID (`tka_...`) |
| `desk:dassignedid` | Who was assigned when sent |
| `desk:dstatus` | Ticket status when sent |
## Message Types
The Desk channel supports three message types:
### 1. Reply (text) — Customer-Visible
Replies are visible to the customer and may be sent via email.
```typescript
await send({
type: 'text',
payload: {
format: 'markdown',
text: 'Hello! How can I help you today?',
},
})
```
**Schema:**
```typescript
{
format: "plain" | "html" | "markdown",
text: string,
attachments?: Array<{
id: string,
url: string,
name: string,
contentType: string,
size: number,
width?: number,
height?: number,
}>
}
```
### 2. Note — Internal Staff-Only
Notes are only visible to support staff (admins and bots), never to customers.
```typescript
await send({
type: 'note',
payload: {
format: 'markdown',
text: 'Internal note: Customer has billing issues in CRM',
},
})
```
**Schema:**
```typescript
{
format: "plain" | "html" | "markdown",
text: string,
mentions?: Array<{
id: string,
type: "admin" | "bot",
name: string,
}>
}
```
### 3. Activity — System Events
Activities represent system events like assignment changes, status changes, etc.
```typescript
await send({
type: 'activity',
payload: {
type: 'ticket.activity',
private: true,
activity: {
type: 'ticket.assigned',
assignedTo: {
type: 'bot',
id: 'ibot_abc123',
reply_as: 'SupportBot',
},
},
},
})
```
**Activity Types:** `ticket.snoozed`, `ticket.unsnoozed`, `ticket.assigned`, `ticket.unassigned`, `ticket.closed`, `ticket.reopened`, `ticket.renamed`, `ticket.tags_updated`
## Conversation Handler Pattern
### Complete Handler Structure
```typescript
import { Autonomous, context, Conversation, z } from '@botpress/runtime'
import { actions, joinMarkdownChildren, TranscriptItem } from '@botpress/runtime/runtime'
export default new Conversation({
channel: 'desk.ticket',
state: z.object({
customer: z.object({
id: z.string(),
name: z.string(),
email: z.string().optional(),
}),
admins: z.array(z.object({ id: z.string(), name: z.string(), email: z.string().optional() })),
ticket: z.object({
id: z.string(),
title: z.string(),
status: z.string(),
priority: z.string().optional(),
assignedTo: z.string().optional(),
createdAt: z.string(),
}),
lastSync: z.string().default(''),
}),
events: ['desk:ticketActivity'],
handler: async ({ type, conversation, message, state, event, execute, client }) => {
// ... handler logic
},
})
```
### Assignment Check Pattern (CRITICAL)
**This is the most important pattern.** Without this check, your bot will process ALL tickets in the workspace.
```typescript
handler: async ({ conversation }) => {
if (conversation.tags['desk:dassignedself'] !== 'true') {
console.log('Ticket not assigned to bot, ignoring...')
return
}
// Only process assigned tickets below this line
}
```
**Why?** The Desk integration fires events for ALL ticket activity. Without the assignment check:
- Your bot processes every ticket in the workspace
- Customers receive confusing or duplicate responses
- Unnecessary resource and API consumption
### Transcript Management
Sync messages to maintain conversation context:
```typescript
handler: async ({ conversation, state, client }) => {
// Check assignment first...
const chat = context.get('chat')
const transcript = await chat.fetchTranscript()
const messages = await client._inner.list
.messages({
conversationId: conversation.id,
afterDate: state.lastSync,
})
.collect({})
.then((messages) => messages.sort((a, b) => (a.createdAt < b.createdAt ? -1 : 1)))
const newMessages = [
...transcript,
...messages.map(
(m) =>
({
id: m.id,
content: JSON.stringify(m.payload),
createdAt: m.createdAt,
role: m.direction === 'incoming' ? 'user' : 'assistant',
attachments: [],
name: m.userId,
}) satisfies TranscriptItem
),
]
chat.setTranscript(newMessages)
state.lastSync = newMessages.filter((x) => x.role === 'user' || x.role === 'assistant').slice(-1)[0]?.createdAt
}
```
> **Note:** Use `context.enterWith()` when reassigning context in async operations. Do not use `context.set("chat", ...)` for full context reassignment.
## Reply and Note Components
For AI-driven responses, register custom components that map to Desk message types.
### Reply Component
```typescript
const Reply = new Autonomous.Component({
name: 'Reply',
description: 'Sends a reply to the customer, visible to them and via email.',
type: 'leaf',
leaf: { props: z.object({}) },
aliases: [],
examples: [
{
name: 'Reply',
description: 'Sends a reply to the customer',
code: `
yield <Message><Reply>
Some **markdown** reply to the customer!
</Reply></Message>`,
},
],
})
```
### Note Component
```typescript
const Note = new Autonomous.Component({
name: 'Note',
description: 'Adds an internal note to the ticket. Not visible to the customer.',
type: 'leaf',
leaf: { props: z.object({}) },
aliases: [],
examples: [
{
name: 'Note',
description: 'Adds an internal note to the ticket',
code: `yield <Message><Note>This is a **note** with markdown!</Note></Message>`,
},
],
})
```
### Component Registration
```typescript
handler: async ({ conversation }) => {
// ... assignment check ...
const chat = context.get('chat')
// Remove default message components
chat.removeComponent('MESSAGE')
chat.removeComponent('Text')
chat.removeComponent('TEXT')
// Register Reply component
chat.registerComponent({
component: Reply as any,
handler: async ({ props, children }) => {
await conversation.send({
type: 'text',
payload: { format: 'markdown', text: joinMarkdownChildren(children) },
})
},
})
// Register Note component
chat.registerComponent({
component: Note as any,
handler: async ({ props, children }) => {
await conversation.send({
type: 'note',
payload: { format: 'markdown', text: joinMarkdownChildren(children) },
})
},
})
// Update chat context with new components
const newChat = Object.assign(chat, {
getComponents: async () => [Reply, Note],
} satisfies Partial<typeof chat>)
context.enterWith({ ...context.getAll(), chat: newChat })
}
```
## Available Actions
The Desk integration provides 11 actions for ticket management.
### Ticket Queries
```typescript
// listTickets - List tickets with pagination
const result = await actions.desk.listTickets({
orderBy: 'updatedAt',
orderDirection: 'desc',
limit: 50,
cursor: undefined,
})
// Returns: { tickets: Ticket[], hasMore: boolean, nextCursor?: string }
// getTicket - Get a single ticket by ID
const result = await actions.desk.getTicket({ id: 'tkt_abc123' })
// listTicketActivities - List activities for a ticket
const result = await actions.desk.listTicketActivities({
ticketId: 'tkt_abc123',
limit: 50,
cursor: undefined,
})
```
### Assignment Actions
```typescript
// assignTicket - Assign to admin or bot
await actions.desk.assignTicket({
ticketId: 'tkt_abc123',
assigneeId: 'adm_xyz789', // or "ibot_..." for a bot
})
// unassignTicket - Remove assignment
await actions.desk.unassignTicket({ ticketId: 'tkt_abc123' })
```
### Lifecycle Actions
```typescript
// closeTicket
await actions.desk.closeTicket({ ticketId: 'tkt_abc123' })
// reopenTicket
await actions.desk.reopenTicket({ ticketId: 'tkt_abc123' })
// snoozeTicket
await actions.desk.snoozeTicket({
ticketId: 'tkt_abc123',
snoozedUntil: '2025-01-21T09:00:00Z',
})
// unsnoozeTicket
await actions.desk.unsnoozeTicket({ ticketId: 'tkt_abc123' })
```
### Metadata Actions
```typescript
// renameTicket
await actions.desk.renameTicket({
ticketId: 'tkt_abc123',
title: 'Updated: Login Issue with 2FA',
})
// addTags
await actions.desk.addTags({
ticketId: 'tkt_abc123',
tags: ['urgent', 'billing', 'enterprise'],
})
// removeTags
await actions.desk.removeTags({
ticketId: 'tkt_abc123',
tags: ['low-priority'],
})
```
## Converting Actions to Tools
Convert Desk actions to AI-callable tools using `asTool()` and `setStaticInputValues()`:
```typescript
handler: async ({ conversation, execute }) => {
// Check assignment...
const ticketId = conversation.tags['desk:did']
// Create tools with bound ticketId
const tools = [
actions.desk.unassignTicket.asTool().setStaticInputValues({ ticketId }),
actions.desk.closeTicket.asTool().setStaticInputValues({ ticketId }),
actions.desk.snoozeTicket.asTool().setStaticInputValues({ ticketId }),
actions.desk.addTags.asTool().setStaticInputValues({ ticketId }),
actions.desk.removeTags.asTool().setStaticInputValues({ ticketId }),
]
await execute({
tools,
instructions: 'You are a helpful support agent...',
})
}
```
## AI Execution with Desk
### Execute with Instructions
```typescript
handler: async ({ conversation, execute }) => {
if (conversation.tags['desk:dassignedself'] !== 'true') return
const ticketId = conversation.tags['desk:did']
const UnassignTool = actions.desk.unassignTicket.asTool().setStaticInputValues({ ticketId })
await execute({
iterations: 5,
tools: [UnassignTool],
model: 'anthropic:claude-sonnet-4-20250514',
instructions: `You are a helpful assistant for customer support tickets.
You can choose to:
- Reply to the customer using <Reply>
- Add an internal note using <Note>
- Take no action
## Rules
- Always use <Reply> for customer-visible messages
- Always use <Note> for internal staff communication
- Be polite, concise, and don't make up information
- Ask for human help if unsure
## Current Ticket
${JSON.stringify(conversation.tags, null, 2)}
`,
})
}
```
### System Prompt Guidelines
**DO:**
- Explain when to use Reply vs Note
- Set clear escalation criteria
- Include ticket context via tags
- Define what "taking no action" means
**DON'T:**
- Allow direct customer replies without components
- Let the bot make up information
- Process messages the bot sent itself
- Reply to every message (e.g., "thank you")
## Escalation Pattern
When a bot needs to hand off to a human agent, follow this 4-step pattern:
```typescript
async function escalateToHuman(conversation: any, reason: string): Promise<void> {
// Step 1: Internal note explaining why
await conversation.send({
type: 'note',
payload: {
format: 'markdown',
text: `Escalating to human: ${reason}`,
},
})
// Step 2: Customer reply
await conversation.send({
type: 'text',
payload: {
format: 'markdown',
text: "I've connected you with a member of our team who can help. They'll be with you shortly!",
},
})
// Step 3: Unassign bot
await actions.desk.unassignTicket({
ticketId: conversation.tags['desk:did'],
})
}
// In handler:
handler: async ({ conversation }) => {
if (conversation.tags['desk:dassignedself'] !== 'true') return
if (needsEscalation) {
await escalateToHuman(conversation, 'Complex billing issue')
return // Step 4: Stop processing
}
// ... continue normal processing
}
```
### When to Escalate
- Technical issues beyond basic troubleshooting
- Billing or account access issues
- Angry or frustrated customers
- Any uncertainty about the correct answer
## Data Schemas
### Ticket
```typescript
interface Ticket {
id: string // "tkt_abc123"
title: string
customer: Customer
priority: 'low' | 'medium' | 'high' | 'urgent'
system: 'native' | 'intercom' | 'zendesk'
systemTicketId: string
assignedTo?: Admin | Bot
status: 'open' | 'closed' | 'snoozed'
createdAt: string
updatedAt: string
closedAt?: string
snoozedUntil?: string
tags: string[]
metadata?: Record<string, any>
}
```
### Customer
```typescript
interface Customer {
id: string // "cus_xyz789"
name?: string
email?: string
phone?: string
avatarUrl?: string
}
```
### Admin
```typescript
interface Admin {
id: string // "adm_abc123"
name: string
email: string
avatarUrl?: string
}
```
### Bot
```typescript
interface Bot {
id: string // "ibot_def456"
displayName: string
displayAvatarUrl?: string
botHandle: string
canBeAssigned: boolean
}
```
### Activity
```typescript
interface Activity {
id: string // "tka_abc123"
ticketId: string
activityType: 'comment' | 'note' | 'system_public' | 'system_internal'
authorType: 'admin' | 'customer' | 'bot' | 'user'
authorId: string
body: string
bodyFormat: 'plain' | 'html' | 'markdown' | 'json'
isRedacted: boolean
createdAt: string
updatedAt: string
}
```
### Activity Event Types
```typescript
type TicketActivityType =
| { type: 'ticket.snoozed'; author: Author }
| { type: 'ticket.unsnoozed'; author: Author }
| { type: 'ticket.assigned'; assignedTo: { type: 'admin' | 'bot'; id: string; reply_as?: string } }
| { type: 'ticket.unassigned'; author: Author }
| { type: 'ticket.closed'; author: Author }
| { type: 'ticket.reopened'; author: Author }
| { type: 'ticket.renamed'; title: string }
| { type: 'ticket.tags_updated'; author: Author; added: string[]; removed: string[] }
```
## Common Mistakes
### Missing Assignment Check
```typescript
// ❌ WRONG - Processes ALL tickets
handler: async ({ conversation, execute }) => {
await execute({ ... });
}
// ✅ CORRECT - Check assignment first
handler: async ({ conversation, execute }) => {
if (conversation.tags["desk:dassignedself"] !== "true") return;
await execute({ ... });
}
```
### Using Reply When Should Use Note
```typescript
// ❌ WRONG - Customer sees internal info
await send({ type: 'text', payload: { text: 'Customer has overdue invoices in CRM' } })
// ✅ CORRECT - Note for internal communication
await send({ type: 'note', payload: { text: 'Customer has overdue invoices in CRM' } })
```
### Not Extracting ticketId for Tools
```typescript
// ❌ WRONG - Tool has no ticketId
const UnassignTool = actions.desk.unassignTicket.asTool()
// ✅ CORRECT - Bind ticketId
const ticketId = conversation.tags['desk:did']
const UnassignTool = actions.desk.unassignTicket.asTool().setStaticInputValues({ ticketId })
```
### Using Wrong Tag Names
```typescript
// ❌ WRONG
conversation.tags['deskId']
conversation.tags['desk:assignedSelf'] // wrong case
// ✅ CORRECT
conversation.tags['desk:did']
conversation.tags['desk:dassignedself'] // lowercase
```
### Replying to Every Message
```typescript
// ❌ WRONG
instructions: 'Always reply to every message from the customer'
// ✅ CORRECT
instructions: `You DON'T have to reply to every message.
If the customer says "thanks" or "got it", take no action.
Only reply when you have something meaningful to add.`
```
## See Also
- [./conversations.md](./conversations.md) — General conversation handling
- [./integration-actions.md](./integration-actions.md) — Using integration actions
- [./actions.md](./actions.md) — Actions and Tools reference
- [./tags.md](./tags.md) — Tags reference
- [./patterns-mistakes.md](./patterns-mistakes.md) — General best practices
- [./agent-config.md](./agent-config.md) — Configuration reference