@botpress/adk-cli
Version:
Command-line interface for the Botpress Agent Development Kit (ADK)
515 lines (396 loc) • 12.9 kB
Markdown
Tags are key-value pairs that can be attached to bots, users, conversations, and workflows. They provide a flexible way to track metadata, categorize entities, and store cross-handler state.
- **Key-value pairs**: String keys with string values
- **Entity-scoped**: Available on bot, user, conversation, and workflow
- **Schema-defined**: Must be declared in `agent.config.ts`
- **Automatically persisted**: Changes are tracked and saved automatically
- **System vs Custom**: System tags (with `:`) are read-only
### Tag Types
| Type | Example Key | Description |
| ---------- | ---------------- | ------------------------------ |
| **Custom** | `tier`, `region` | User-defined, read-write |
| **System** | `webchat:owner` | Integration-managed, read-only |
## Defining Tags
Tags must be defined in your `agent.config.ts` to be persisted:
### User Tags
```typescript
import { defineConfig, z } from '@botpress/runtime'
export default defineConfig({
name: 'my-bot',
user: {
// User state (mutable object data)
state: z.object({
preferences: z.object({}).passthrough(),
}),
// User tags (key-value pairs for categorization)
// Tags are defined as { title: string, description?: string } objects, NOT Zod schemas
tags: {
tier: { title: 'Subscription Tier', description: 'User subscription level' },
region: { title: 'Region', description: 'User geographic region' },
source: { title: 'Acquisition Source' },
referralCode: { title: 'Referral Code' },
},
},
})
```
```typescript
export default defineConfig({
name: 'my-bot',
conversation: {
tags: {
category: { title: 'Category' },
priority: { title: 'Priority', description: 'Conversation priority level' },
assignedTo: { title: 'Assigned To' },
department: { title: 'Department' },
},
},
})
```
### Bot Tags
```typescript
export default defineConfig({
name: 'my-bot',
bot: {
state: z.object({
version: z.number().default(1),
}),
tags: {
environment: { title: 'Environment', description: 'Deployment environment' },
region: { title: 'Region' },
version: { title: 'Version' },
},
},
})
```
```typescript
export default defineConfig({
name: 'my-bot',
workflow: {
tags: {
type: { title: 'Workflow Type' },
priority: { title: 'Priority' },
createdBy: { title: 'Created By' },
},
},
})
```
## Using Tags
### In Conversations
```typescript
import { Conversation, user } from '@botpress/runtime'
export default new Conversation({
channel: 'webchat.channel',
// conversation is provided as a handler parameter — no need for context.get()
async handler({ message, conversation }) {
// Read user tags
console.log(`User tier: ${user.tags.tier}`)
console.log(`User region: ${user.tags.region}`)
// Set user tags
user.tags.tier = 'pro'
user.tags.source = 'website'
// Read conversation tags
console.log(`Category: ${conversation.tags.category}`)
// Set conversation tags
conversation.tags.priority = 'high'
conversation.tags.department = 'support'
// Conditional logic based on tags
if (user.tags.tier === 'enterprise') {
// Priority handling for enterprise users
conversation.tags.priority = 'urgent'
}
},
})
```
```typescript
import { Workflow, z, bot } from '@botpress/runtime'
export const ProcessingWorkflow = new Workflow({
name: 'processing',
input: z.object({ userId: z.string() }),
async handler({ input, workflow, step }) {
// Set workflow tags
workflow.tags.type = 'data-processing'
workflow.tags.priority = 'high'
// Access bot tags
console.log(`Environment: ${bot.tags.environment}`)
await step('process', async () => {
// Processing logic
})
return { success: true }
},
})
```
```typescript
import { Action, z, user, context } from '@botpress/runtime'
export default new Action({
name: 'upgradeUser',
input: z.object({
newTier: z.enum(['free', 'pro', 'enterprise']),
}),
output: z.object({ success: z.boolean() }),
async handler({ input }) {
const oldTier = user.tags.tier
const conversation = context.get('conversation', { optional: true })
// Update user tag
user.tags.tier = input.newTier
// Log the upgrade in conversation
if (conversation) {
conversation.tags.category = 'upgrade'
}
console.log(`Upgraded user from ${oldTier} to ${input.newTier}`)
return { success: true }
},
})
```
Understanding when to use tags versus state:
| Aspect | Tags | State |
| ------------- | ------------------------- | ----------------------------------- |
| **Structure** | Flat key-value pairs | Nested objects |
| **Types** | Strings only | Any type (objects, arrays, numbers) |
| **Use case** | Categorization, filtering | Complex data storage |
| **Queryable** | Yes (via API) | Limited |
| **Size** | Small values | Larger structures |
### When to Use Tags
```typescript
// ✅ Good for tags - simple categorization
user.tags.tier = 'pro'
user.tags.region = 'us-east'
conversation.tags.priority = 'high'
// ❌ Bad for tags - complex data belongs in state
user.tags.preferences = JSON.stringify({ theme: 'dark' }) // Don't do this!
```
```typescript
// ✅ Good for state - complex structures
user.state.preferences = {
theme: 'dark',
notifications: true,
language: 'en',
}
user.state.history = [
{ action: 'login', timestamp: new Date() },
{ action: 'purchase', timestamp: new Date() },
]
```
System tags are managed by integrations and are **read-only**. They contain a colon (`:`) in their key.
### Common System Tags
```typescript
// Webchat system tags (read-only)
console.log(user.tags['webchat:owner']) // User who initiated webchat
console.log(conversation.tags['webchat:sessionId']) // Session identifier
// Integration-specific system tags
console.log(user.tags['slack:userId']) // Slack user ID
console.log(conversation.tags['discord:channelId']) // Discord channel
```
### System Tag Behavior
```typescript
// System tags are silently ignored when you try to set them
user.tags['webchat:owner'] = 'new-value' // No error, but no effect
// Only custom tags (without ':') can be modified
user.tags.tier = 'enterprise' // ✅ Works
user.tags.customField = 'value' // ✅ Works
```
```typescript
import { Conversation, conversation, user } from '@botpress/runtime'
export default new Conversation({
channel: 'webchat.channel',
async handler({ message, execute }) {
// Route based on conversation priority
const priority = conversation.tags.priority
if (priority === 'urgent') {
// Fast-track urgent conversations
await execute({
instructions: 'This is an urgent request. Prioritize resolution.',
model: 'openai:gpt-4o', // Use best model
})
} else {
await execute({
instructions: 'Help the user with their request.',
model: 'openai:gpt-4o-mini', // Cost-efficient model
})
}
},
})
```
```typescript
import { Trigger } from '@botpress/runtime'
import { AnalyticsTable } from '../tables/Analytics'
export default new Trigger({
name: 'trackConversation',
events: ['conversation.ended'],
async handler({ event }) {
const { conversationId, tags } = event.payload
// Store conversation metrics by tags
await AnalyticsTable.createRows({
rows: [
{
conversationId,
category: tags.category || 'uncategorized',
priority: tags.priority || 'normal',
department: tags.department || 'general',
timestamp: new Date(),
},
],
})
},
})
```
```typescript
import { Conversation, user, adk } from '@botpress/runtime'
export default new Conversation({
channel: 'webchat.channel',
async handler({ message }) {
// Infer and set tags from conversation
if (!user.tags.region && message?.type === 'text') {
const region = await adk.zai.extract(message.payload.text, z.string().optional(), {
instructions: 'Extract geographic region if mentioned',
})
if (region) {
user.tags.region = region
}
}
// Set source tag if not already set
if (!user.tags.source) {
user.tags.source = 'webchat'
}
},
})
```
```typescript
import { Workflow, z, user } from '@botpress/runtime'
export const SupportWorkflow = new Workflow({
name: 'support',
input: z.object({
conversationId: z.string(),
issue: z.string(),
}),
async handler({ input, workflow, step }) {
// Inherit priority from user tier
const tier = user.tags.tier
if (tier === 'enterprise') {
workflow.tags.priority = 'high'
} else if (tier === 'pro') {
workflow.tags.priority = 'normal'
} else {
workflow.tags.priority = 'low'
}
// Set workflow type
workflow.tags.type = 'support-ticket'
await step('process', async () => {
// Processing based on priority
})
return { processed: true }
},
})
```
```typescript
// ✅ Good - clear purpose
tags: {
subscriptionTier: { title: "Subscription Tier", description: "User subscription level" },
acquisitionChannel: { title: "Acquisition Channel" },
supportPriority: { title: "Support Priority" },
}
// ❌ Bad - unclear
tags: {
t: { title: "T" },
lvl: { title: "Lvl" },
p: { title: "P" },
}
```
```typescript
// ✅ Good - descriptive titles
tags: {
status: { title: "Account Status", description: "Active, inactive, or suspended" },
tier: { title: "Subscription Tier", description: "Free, pro, or enterprise" },
}
// ❌ Bad - missing descriptions
tags: {
status: { title: "Status" }, // Unclear what values are expected
}
```
```typescript
export default defineConfig({
user: {
tags: {
tier: { title: 'Subscription Tier', description: 'Customer subscription level for feature gating' },
region: { title: 'Region', description: 'Geographic region for compliance and routing' },
acquisitionSource: { title: 'Acquisition Source', description: 'How the user discovered the bot' },
},
},
})
```
### 4. Set Tags Early
```typescript
// Set identification tags early in the conversation
async handler({ message, type }) {
if (type === "conversation_started") {
// Set source immediately
conversation.tags.source = "webchat";
conversation.tags.startedAt = new Date().toISOString();
}
}
```
```typescript
// ✅ Good - tags for filtering/categorization
user.tags.tier = 'enterprise'
conversation.tags.category = 'billing'
// ❌ Bad - storing complex data in tags
user.tags.preferences = JSON.stringify({ notifications: true }) // Use state instead
```
1. **Check schema definition**: Tags must be defined in `agent.config.ts`
2. **Verify tag key**: Custom tags cannot contain `:`
3. **Check value type**: Tags only accept string values
```typescript
// ✅ Correct
user.tags.tier = 'pro' // String value
// ❌ Wrong - will be ignored or cause errors
user.tags.count = 5 // Number (should be string)
user.tags.active = true // Boolean (should be string)
```
System tags (containing `:`) are managed by integrations:
```typescript
// These are read-only - modifications are silently ignored
user.tags['webchat:owner'] = 'new' // No effect
conversation.tags['slack:channel'] = 'new' // No effect
// Only custom tags can be modified
user.tags.myCustomTag = 'value' // ✅ Works
```
If you see errors about undefined tags, add them to your config:
```typescript
// Before: Tag not in schema
user.tags.newTag = 'value' // Warning: Tag not defined
// Fix: Add to agent.config.ts
export default defineConfig({
user: {
tags: {
newTag: { title: 'New Tag' }, // Now it will persist
},
},
})
```
## See Also
- **[Agent Configuration](./agent-config.md)** - Full agent.config.ts reference
- **[Conversations](./conversations.md)** - Using tags in conversation handlers
- **[Workflows](./workflows.md)** - Workflow tags and state management
- **[Context API](./context-api.md)** - Accessing bot, user, conversation context