# Tags 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. ## Basic Concepts ### What are Tags? - **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" }, }, }, }); ``` ### Conversation Tags ```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" }, }, }, }); ``` ### Workflow Tags ```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"; } }, }); ``` ### In Workflows ```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 }; }, }); ``` ### In Actions ```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 }; }, }); ``` ## Tags vs State 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! ``` ### When to Use State ```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 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 ``` ## Advanced Patterns ### Tag-Based Routing ```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 }); } }, }); ``` ### Tag-Based Analytics ```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(), }, ], }); }, }); ``` ### Progressive Tag Collection ```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"; } }, }); ``` ### Tag Inheritance in Workflows ```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 }; }, }); ``` ## Best Practices ### 1. Use Descriptive Tag Names ```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" }, } ``` ### 2. Use Enums for Constrained Values ```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 } ``` ### 3. Document Tag Purposes ```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(); } } ``` ### 5. Use Tags for Filtering, Not Storage ```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 ``` ## Troubleshooting ### Tags Not Persisting 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 Appearing Read-Only 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 ``` ### Tag Not in Schema Error 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