@botpress/adk-cli 1.19.0-beta.3 → 1.19.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/ui-dist/assets/{index-CSTz-oTK.js → index-B3b7PPgV.js} +4 -4
- package/{dist/index-DNqXHLNi-27nvpkee.js → assets/ui-dist/assets/index-BESZeTO5.js} +2 -2
- package/{dist/index-DAztRXeA-vr85xnxs.css → assets/ui-dist/assets/index-DIFGtVmI.css} +1 -1
- package/{dist/index-DfjXY3Oo-szsanhnt.js → assets/ui-dist/assets/index-a3ZL0_Jr.js} +2 -2
- package/assets/ui-dist/index.html +2 -2
- package/dist/{README-wjb7z7f0.md → README-824wa4jj.md} +1 -1
- package/dist/SKILL-75ym6tcj.md +257 -0
- package/dist/SKILL-chnwzmt1.md +548 -0
- package/dist/SKILL-d4bs53y3.md +244 -0
- package/dist/SKILL-h4b2f1n9.md +114 -0
- package/dist/SKILL-hbw5nt95.md +111 -0
- package/dist/actions-h7hxxvxr.md +823 -0
- package/dist/adk-build-emhwd0tg.md +39 -0
- package/dist/adk-debug-yrrdtpmb.md +23 -0
- package/dist/adk-dev-console-7z9rtjpy.md +17 -0
- package/dist/adk-document-ras6zzzh.md +68 -0
- package/dist/adk-eval-vx76dwq0.md +17 -0
- package/dist/adk-explain-fcsaw852.md +22 -0
- package/dist/adk-init-9kmxmbbe.md +30 -0
- package/dist/adk-ship-dybzn884.md +29 -0
- package/dist/adk-test-xq67savs.md +27 -0
- package/dist/adk-validate-1p4skv4m.md +31 -0
- package/dist/advanced-patterns-z86m9h1a.md +669 -0
- package/dist/agent-config-1aqvkrgf.md +450 -0
- package/dist/agent-steps-zj4c1n9t.md +115 -0
- package/dist/agent0-assets/skills/adk/SKILL.md +5 -5
- package/dist/agent0-assets/skills/adk/references/cli.md +265 -24
- package/dist/agent0-assets/skills/adk-debugger/SKILL.md +5 -5
- package/dist/agent0-assets/skills/adk-debugger/references/common-failures.md +1 -1
- package/dist/agent0-assets/skills/adk-debugger/references/debug-workflow.md +3 -3
- package/dist/agent0-assets/skills/adk-debugger/references/traces-and-logs.md +15 -7
- package/dist/assets-q7jt8r2q.md +237 -0
- package/dist/autonomous-execution-fymc12e5.md +747 -0
- package/dist/cli-c31e06yr.md +1334 -0
- package/dist/cli.js +963 -548
- package/dist/commands.json +24 -3
- package/dist/common-failures-ht29w2db.md +231 -0
- package/dist/component-registry-bbpw0see.md +43 -0
- package/dist/context-api-760cvh0h.md +461 -0
- package/dist/conversation-analysis-8ka4nrm4.md +420 -0
- package/dist/conversation-lifecycle-4seqqjc1.md +311 -0
- package/dist/conversations-gwfvxaee.md +1111 -0
- package/dist/custom-components-y5wdgpey.md +167 -0
- package/dist/debug-workflow-vz84q80v.md +239 -0
- package/dist/desk-k54ktvsd.md +852 -0
- package/dist/doc-standards-ck6ax2jd.md +124 -0
- package/dist/eval-format-7n9brja4.md +354 -0
- package/dist/evals-y1zcr1mc.md +11 -0
- package/dist/explain-config-1yaq3zzd.md +424 -0
- package/dist/files-dzt0wxn3.md +918 -0
- package/dist/{index-gqxgqnyz.html → index-3t354nbw.html} +2 -2
- package/dist/{index-CSTz-oTK-mdfh47r8.js → index-B3b7PPgV-zetkb01w.js} +4 -4
- package/{assets/ui-dist/assets/index-DNqXHLNi.js → dist/index-BESZeTO5-2rpmcefa.js} +2 -2
- package/{assets/ui-dist/assets/index-DAztRXeA.css → dist/index-DIFGtVmI-d4dhjg97.css} +1 -1
- package/{assets/ui-dist/assets/index-DfjXY3Oo.js → dist/index-a3ZL0_Jr-q145h35p.js} +2 -2
- package/dist/integration-actions-xang7tma.md +659 -0
- package/dist/integrations-k5ss07f4.md +266 -0
- package/dist/interfaces-6t0bpxs4.md +97 -0
- package/dist/knowledge-bases-20rg8p5c.md +456 -0
- package/dist/llm-debugging-76ng8rzm.md +209 -0
- package/dist/mcp-server-n1asyzhv.md +112 -0
- package/dist/messages-gkkg0b54.md +769 -0
- package/dist/model-configuration-f273kgd0.md +201 -0
- package/dist/multi-agent-dashboard-1mdfjdts.md +70 -0
- package/dist/pages-xn18xn73.md +273 -0
- package/dist/patterns-mistakes-5vb99dhf.md +469 -0
- package/dist/plugins-ttnx8f49.md +280 -0
- package/dist/tables-x45t36dd.md +1136 -0
- package/dist/tags-46td3qrg.md +516 -0
- package/dist/test-patterns-hj47w4nc.md +355 -0
- package/dist/testing-workflow-2vkp932f.md +225 -0
- package/dist/tools-wk7bq24z.md +581 -0
- package/dist/trace-summarization-hdyx1c2e.md +313 -0
- package/dist/traces-and-logs-f2kjfh19.md +203 -0
- package/dist/triggers-4j2cc86x.md +719 -0
- package/dist/ui-server-entry.js +977 -162
- package/dist/workflow-steps-mrdrf1c5.md +851 -0
- package/dist/workflows-gadx3s69.md +1408 -0
- package/dist/zai-agent-reference-e6644aad.md +454 -0
- package/dist/zai-complete-guide-mnna5d6f.md +1408 -0
- package/package.json +4 -4
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
# Actions
|
|
2
|
+
|
|
3
|
+
Actions are callable, strongly-typed functions that encapsulate reusable logic accessible throughout your agent. They can be invoked from workflows, conversations, other actions, or exposed as tools for AI.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Actions enable you to:
|
|
8
|
+
- Create reusable business logic across your bot
|
|
9
|
+
- Call integration APIs with type safety
|
|
10
|
+
- Expose functionality as AI-callable tools
|
|
11
|
+
- Compose complex operations from simple functions
|
|
12
|
+
- Test logic independently from conversational flows
|
|
13
|
+
|
|
14
|
+
## File Location
|
|
15
|
+
|
|
16
|
+
- **Location**: `src/actions/*.ts`
|
|
17
|
+
- **Auto-registration**: Files in this directory automatically become available as `actions.{actionName}`
|
|
18
|
+
- **Export pattern**: Both `export const` and `export default` work
|
|
19
|
+
- **`export const`** (Recommended): Enables direct imports and passing to `execute()` as tools
|
|
20
|
+
- **`export default`**: Simpler for single-action files
|
|
21
|
+
|
|
22
|
+
## Basic Action Structure
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { Action, z } from "@botpress/runtime";
|
|
26
|
+
|
|
27
|
+
// Option 1: Named export (recommended)
|
|
28
|
+
export const myAction = new Action({
|
|
29
|
+
name: "myAction",
|
|
30
|
+
input: z.object({
|
|
31
|
+
userId: z.string(),
|
|
32
|
+
}),
|
|
33
|
+
output: z.object({
|
|
34
|
+
data: z.string(),
|
|
35
|
+
timestamp: z.string()
|
|
36
|
+
}),
|
|
37
|
+
handler: async ({ input, client }) {
|
|
38
|
+
// Your business logic here
|
|
39
|
+
const result = await fetchData(input.userId);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
data: result,
|
|
43
|
+
timestamp: new Date().toISOString()
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Option 2: Default export (also valid)
|
|
49
|
+
export default new Action({
|
|
50
|
+
name: "myAction",
|
|
51
|
+
// ... same configuration
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Why use `export const`?**
|
|
56
|
+
- Enables direct imports: `import { myAction } from "./actions/myAction"`
|
|
57
|
+
- Can pass to `execute()`: `tools: [myAction.asTool()]`
|
|
58
|
+
- Better for action composition and reusability
|
|
59
|
+
|
|
60
|
+
## Action Properties
|
|
61
|
+
|
|
62
|
+
| Property | Type | Required | Description |
|
|
63
|
+
|----------|------|----------|-------------|
|
|
64
|
+
| **name** | `string` | Yes | Unique alphanumeric identifier (no spaces or special characters) |
|
|
65
|
+
| **input** | `z.ZodTypeAny` | Yes | Zod schema defining input parameters |
|
|
66
|
+
| **output** | `z.ZodTypeAny` | Yes | Zod schema defining return type |
|
|
67
|
+
| **handler** | `function` | Yes | Async function implementing the action logic |
|
|
68
|
+
| **title** | `string` | No | Human-readable display name |
|
|
69
|
+
| **description** | `string` | No | Description of the action's functionality |
|
|
70
|
+
| **attributes** | `Record<string, string>` | No | Custom metadata for categorization |
|
|
71
|
+
| **cached** | `boolean` | No | Enable caching for identical inputs (defaults to `false`) |
|
|
72
|
+
|
|
73
|
+
## Handler Syntax
|
|
74
|
+
|
|
75
|
+
Action handlers receive a props object containing `input` and `client`. Both method syntax and arrow functions are supported.
|
|
76
|
+
|
|
77
|
+
### Method Syntax (Recommended)
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
export const fetchUser = new Action({
|
|
81
|
+
name: "fetchUser",
|
|
82
|
+
input: z.object({ userId: z.string() }),
|
|
83
|
+
output: z.object({
|
|
84
|
+
name: z.string(),
|
|
85
|
+
email: z.string()
|
|
86
|
+
}),
|
|
87
|
+
|
|
88
|
+
async handler({ input, client }) {
|
|
89
|
+
// input: validated input matching your input schema
|
|
90
|
+
// client: Botpress API client for making API calls
|
|
91
|
+
const { user } = await client.getUser({ id: input.userId });
|
|
92
|
+
return {
|
|
93
|
+
name: user.name,
|
|
94
|
+
email: user.tags.email
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Arrow Function Syntax
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
export const updateUser = new Action({
|
|
104
|
+
name: "updateUser",
|
|
105
|
+
input: z.object({
|
|
106
|
+
userId: z.string(),
|
|
107
|
+
name: z.string()
|
|
108
|
+
}),
|
|
109
|
+
output: z.object({ success: z.boolean() }),
|
|
110
|
+
|
|
111
|
+
handler: async ({ input, client }) => {
|
|
112
|
+
await client.updateUser({
|
|
113
|
+
id: input.userId,
|
|
114
|
+
name: input.name
|
|
115
|
+
});
|
|
116
|
+
return { success: true };
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Common Mistakes
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
// ❌ WRONG - Destructuring input fields directly
|
|
125
|
+
export const fetchUser = new Action({
|
|
126
|
+
name: "fetchUser",
|
|
127
|
+
input: z.object({ userId: z.string() }),
|
|
128
|
+
output: z.object({ name: z.string() }),
|
|
129
|
+
|
|
130
|
+
async handler({ userId }) { // ❌ Wrong! Must be { input }
|
|
131
|
+
return { name: userId };
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ✅ CORRECT - Use { input } wrapper
|
|
136
|
+
export const fetchUser = new Action({
|
|
137
|
+
name: "fetchUser",
|
|
138
|
+
input: z.object({ userId: z.string() }),
|
|
139
|
+
output: z.object({ name: z.string() }),
|
|
140
|
+
|
|
141
|
+
async handler({ input }) { // ✅ Correct
|
|
142
|
+
const { userId } = input; // Destructure inside handler
|
|
143
|
+
return { name: userId };
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Key points:**
|
|
149
|
+
- Handler receives `{ input, client }` props object
|
|
150
|
+
- Both method syntax and arrow functions work
|
|
151
|
+
- Destructure input fields **inside** the handler body, not in the parameter
|
|
152
|
+
- **Actions CANNOT destructure input fields directly** (unlike Tools)
|
|
153
|
+
- Omit `client` if not needed: `async handler({ input })`
|
|
154
|
+
|
|
155
|
+
## Calling Actions
|
|
156
|
+
|
|
157
|
+
### From Workflows
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
import { Workflow, actions } from "@botpress/runtime";
|
|
161
|
+
|
|
162
|
+
export default new Workflow({
|
|
163
|
+
name: "processOrder",
|
|
164
|
+
input: z.object({ orderId: z.string() }),
|
|
165
|
+
|
|
166
|
+
handler: async ({ input }) => {
|
|
167
|
+
// Call your custom actions
|
|
168
|
+
const order = await actions.fetchOrder({ orderId: input.orderId });
|
|
169
|
+
const result = await actions.processPayment({ order });
|
|
170
|
+
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### From Other Actions
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
import { Action, actions, z } from "@botpress/runtime";
|
|
180
|
+
|
|
181
|
+
export default new Action({
|
|
182
|
+
name: "compositeAction",
|
|
183
|
+
input: z.object({ userId: z.string() }),
|
|
184
|
+
output: z.object({ success: z.boolean() }),
|
|
185
|
+
|
|
186
|
+
async handler({ input }) {
|
|
187
|
+
// Compose multiple actions
|
|
188
|
+
const userData = await actions.fetchUser({ userId: input.userId });
|
|
189
|
+
const profile = await actions.buildProfile({ user: userData });
|
|
190
|
+
return { success: true };
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Integration Actions
|
|
196
|
+
|
|
197
|
+
Call integration actions from within your actions:
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
import { actions } from "@botpress/runtime";
|
|
201
|
+
|
|
202
|
+
// Inside action handler
|
|
203
|
+
await actions.slack.sendMessage({
|
|
204
|
+
channel: "#general",
|
|
205
|
+
text: "Message from bot"
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
await actions.linear.issueList({ first: 10 });
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
See **[Integration Actions](./integration-actions.md)** for complete guide.
|
|
212
|
+
|
|
213
|
+
## Accessing Additional Context
|
|
214
|
+
|
|
215
|
+
Actions can access additional runtime context through **direct imports** or the **Context API**.
|
|
216
|
+
|
|
217
|
+
Direct imports are best for the global proxies that actually exist at runtime, such as `user`, `bot`, `configuration`, and `adk`. For optional per-request values like `conversation` and `message`, use the Context API.
|
|
218
|
+
|
|
219
|
+
### Method 1: Direct Imports (Global Context Proxies)
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
import { Action, z, user, bot, adk } from "@botpress/runtime";
|
|
223
|
+
import { context } from "@botpress/runtime";
|
|
224
|
+
|
|
225
|
+
export default new Action({
|
|
226
|
+
name: "updateUserProfile",
|
|
227
|
+
input: z.object({ name: z.string() }),
|
|
228
|
+
output: z.object({ success: z.boolean() }),
|
|
229
|
+
|
|
230
|
+
async handler({ input }) {
|
|
231
|
+
// Access user state directly (when in conversation context)
|
|
232
|
+
user.state.profileName = input.name;
|
|
233
|
+
user.tags.profileComplete = "true";
|
|
234
|
+
|
|
235
|
+
// Access bot state
|
|
236
|
+
bot.state.totalUsers += 1;
|
|
237
|
+
|
|
238
|
+
// Access conversation (when in conversation context)
|
|
239
|
+
const conversation = context.get("conversation", { optional: true });
|
|
240
|
+
console.log(conversation?.id);
|
|
241
|
+
|
|
242
|
+
// Use ADK utilities
|
|
243
|
+
const extracted = await adk.zai.extract(
|
|
244
|
+
input.name,
|
|
245
|
+
z.object({ firstName: z.string(), lastName: z.string() })
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
return { success: true };
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Method 2: Context API (Explicit Retrieval)
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
import { Action, z, context } from "@botpress/runtime";
|
|
257
|
+
|
|
258
|
+
export default new Action({
|
|
259
|
+
name: "checkContext",
|
|
260
|
+
input: z.object({}),
|
|
261
|
+
output: z.object({ hasUser: z.boolean() }),
|
|
262
|
+
|
|
263
|
+
async handler({ input }) {
|
|
264
|
+
// Optional context (may not be available in all scenarios)
|
|
265
|
+
const user = context.get("user", { optional: true });
|
|
266
|
+
const conversation = context.get("conversation", { optional: true });
|
|
267
|
+
const message = context.get("message", { optional: true });
|
|
268
|
+
|
|
269
|
+
// Always available
|
|
270
|
+
const client = context.get("client");
|
|
271
|
+
const citations = context.get("citations");
|
|
272
|
+
|
|
273
|
+
if (user) {
|
|
274
|
+
console.log(`User: ${user.id}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return { hasUser: !!user };
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**Which method to use?**
|
|
283
|
+
|
|
284
|
+
- **Direct imports**: Use for `user`, `bot`, `adk` when you know they're available (simpler syntax)
|
|
285
|
+
- **Context API**: Use for optional context (`user`, `conversation`, `message`) or when you need `client` and `citations`
|
|
286
|
+
|
|
287
|
+
**Always available:** `bot`, `adk`, `client`, `context`
|
|
288
|
+
**Conditionally available:** `user`, `conversation`, `message` (only in conversation context)
|
|
289
|
+
|
|
290
|
+
See **[Context API](./context-api.md)** for complete details on all context keys and availability.
|
|
291
|
+
|
|
292
|
+
## Advanced Action Patterns
|
|
293
|
+
|
|
294
|
+
### Pattern 1: Integration Wrapper Pattern
|
|
295
|
+
|
|
296
|
+
**Purpose:** Wrap integration actions with bot-specific logic, data transformation, and local persistence.
|
|
297
|
+
|
|
298
|
+
**When to use:**
|
|
299
|
+
- Syncing external resources (products, issues, orders) to local tables
|
|
300
|
+
- Normalizing data from different integrations
|
|
301
|
+
- Adding business logic or validation
|
|
302
|
+
- Maintaining local cache of external data
|
|
303
|
+
|
|
304
|
+
**Abstract Structure:**
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
export default new Action({
|
|
308
|
+
name: "syncResource",
|
|
309
|
+
description: "Fetch and sync external resource to local storage",
|
|
310
|
+
|
|
311
|
+
input: z.object({
|
|
312
|
+
resourceId: z.string().describe("External resource identifier"),
|
|
313
|
+
}),
|
|
314
|
+
|
|
315
|
+
output: BotResourceSchema, // Your normalized schema
|
|
316
|
+
|
|
317
|
+
async handler({ input }) {
|
|
318
|
+
// 1. Call integration action
|
|
319
|
+
const externalData = await actions.integration.getResource({
|
|
320
|
+
id: input.resourceId
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// 2. Transform to bot schema
|
|
324
|
+
const normalizedData = transformToBotSchema(externalData);
|
|
325
|
+
|
|
326
|
+
// 3. Persist to table (prevents duplicates)
|
|
327
|
+
await ResourceTable.upsertRows({
|
|
328
|
+
rows: [normalizedData],
|
|
329
|
+
keyColumn: "id" // Unique identifier
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// 4. Return normalized data
|
|
333
|
+
return normalizedData;
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
**Real Example - Shopify: Sync Products**
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
import { Action, actions, z } from "@botpress/runtime";
|
|
342
|
+
import { ProductsTable } from "../tables/shopify";
|
|
343
|
+
|
|
344
|
+
export default new Action({
|
|
345
|
+
name: "syncProduct",
|
|
346
|
+
description: "Sync a Shopify product to local database",
|
|
347
|
+
|
|
348
|
+
input: z.object({
|
|
349
|
+
productId: z.string().describe("Shopify product ID"),
|
|
350
|
+
includeVariants: z.boolean().default(true)
|
|
351
|
+
}),
|
|
352
|
+
|
|
353
|
+
output: z.object({
|
|
354
|
+
id: z.string(),
|
|
355
|
+
title: z.string(),
|
|
356
|
+
price: z.number(),
|
|
357
|
+
inventory: z.number(),
|
|
358
|
+
lastSynced: z.string()
|
|
359
|
+
}),
|
|
360
|
+
|
|
361
|
+
async handler({ input }) {
|
|
362
|
+
// 1. Fetch from Shopify
|
|
363
|
+
const shopifyProduct = await actions.shopify.getProduct({
|
|
364
|
+
id: input.productId,
|
|
365
|
+
fields: ["id", "title", "variants", "inventory"]
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// 2. Transform to bot schema
|
|
369
|
+
const product = {
|
|
370
|
+
id: shopifyProduct.id,
|
|
371
|
+
title: shopifyProduct.title,
|
|
372
|
+
price: shopifyProduct.variants[0].price,
|
|
373
|
+
inventory: shopifyProduct.variants.reduce((sum, v) => sum + v.inventory_quantity, 0),
|
|
374
|
+
lastSynced: new Date().toISOString()
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
// 3. Upsert to table
|
|
378
|
+
await ProductsTable.upsertRows({
|
|
379
|
+
rows: [product],
|
|
380
|
+
keyColumn: "id"
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
return product;
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
**Key Benefits:**
|
|
389
|
+
- **Consistency**: All external data follows your bot's schema
|
|
390
|
+
- **Performance**: Local table queries are faster than API calls
|
|
391
|
+
- **Reliability**: Works even if integration is temporarily unavailable
|
|
392
|
+
- **Extensibility**: Easy to add business logic or validation
|
|
393
|
+
|
|
394
|
+
### Pattern 2: Resource State Management
|
|
395
|
+
|
|
396
|
+
**Purpose:** Update external resource state and keep local cache in sync.
|
|
397
|
+
|
|
398
|
+
**Real Example - Linear: Update Issue Status**
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
import { Action, actions, z } from "@botpress/runtime";
|
|
402
|
+
import { IssuesTable } from "../tables/linear";
|
|
403
|
+
|
|
404
|
+
export default new Action({
|
|
405
|
+
name: "updateIssueStatus",
|
|
406
|
+
|
|
407
|
+
input: z.object({
|
|
408
|
+
issueId: z.string(),
|
|
409
|
+
stateId: z.string(),
|
|
410
|
+
comment: z.string().optional()
|
|
411
|
+
}),
|
|
412
|
+
|
|
413
|
+
output: z.object({
|
|
414
|
+
id: z.string(),
|
|
415
|
+
title: z.string(),
|
|
416
|
+
state: z.string(),
|
|
417
|
+
updatedAt: z.string()
|
|
418
|
+
}),
|
|
419
|
+
|
|
420
|
+
async handler({ input }) {
|
|
421
|
+
// 1. Update in Linear
|
|
422
|
+
const issue = await actions.linear.updateIssue({
|
|
423
|
+
id: input.issueId,
|
|
424
|
+
stateId: input.stateId,
|
|
425
|
+
...(input.comment && {
|
|
426
|
+
commentBody: input.comment
|
|
427
|
+
})
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// 2. Transform and sync to table
|
|
431
|
+
const normalized = {
|
|
432
|
+
id: issue.id,
|
|
433
|
+
title: issue.title,
|
|
434
|
+
state: issue.state.name,
|
|
435
|
+
updatedAt: new Date().toISOString()
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
await IssuesTable.upsertRows({
|
|
439
|
+
rows: [normalized],
|
|
440
|
+
keyColumn: "id"
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
return normalized;
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
**When to use this pattern:**
|
|
449
|
+
- Tracking order fulfillment status
|
|
450
|
+
- Managing ticket/issue lifecycle
|
|
451
|
+
- Handling conversation states
|
|
452
|
+
- Any resource with state transitions
|
|
453
|
+
|
|
454
|
+
## Schema Design Best Practices
|
|
455
|
+
|
|
456
|
+
**Core Principles:**
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
import { z } from "@botpress/runtime";
|
|
460
|
+
|
|
461
|
+
// 1. Always export both type and schema
|
|
462
|
+
export type Product = z.infer<typeof ProductSchema>;
|
|
463
|
+
export const ProductSchema = z.object({
|
|
464
|
+
id: z.string(),
|
|
465
|
+
name: z.string(),
|
|
466
|
+
price: z.number()
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// 2. Use .describe() on every field (helps AI tools)
|
|
470
|
+
const schema = z.object({
|
|
471
|
+
productId: z.string().describe("Unique product identifier"),
|
|
472
|
+
price: z.number().describe("Price in cents"),
|
|
473
|
+
inStock: z.boolean().describe("Whether product is available")
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// 3. ISO strings for dates (NOT z.date())
|
|
477
|
+
const timeFields = z.object({
|
|
478
|
+
createdAt: z.string().describe("Creation date as ISO 8601 string"),
|
|
479
|
+
updatedAt: z.string().optional().describe("Last update date")
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// 4. Use .optional() for optional fields
|
|
483
|
+
const optionalFields = z.object({
|
|
484
|
+
name: z.string(), // Required
|
|
485
|
+
description: z.string().optional(), // Optional
|
|
486
|
+
metadata: z.record(z.unknown()).optional()
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// 5. Use .passthrough() for external API data
|
|
490
|
+
const externalData = z.object({
|
|
491
|
+
id: z.string(),
|
|
492
|
+
// Allow unknown fields from external API
|
|
493
|
+
}).passthrough();
|
|
494
|
+
|
|
495
|
+
// 6. Define nested objects explicitly
|
|
496
|
+
const structured = z.object({
|
|
497
|
+
product: z.object({ // ✅ Explicit structure
|
|
498
|
+
id: z.string(),
|
|
499
|
+
name: z.string()
|
|
500
|
+
}),
|
|
501
|
+
metadata: z.record(z.unknown()) // ❌ Use only when structure is truly unknown
|
|
502
|
+
});
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
**Key Patterns:**
|
|
506
|
+
1. **Export both type and schema** - TypeScript type from `z.infer<>`
|
|
507
|
+
2. **Describe every field** - Helps AI tools and documentation
|
|
508
|
+
3. **ISO strings for dates** - Use `z.string()` not `z.date()`
|
|
509
|
+
4. **Optional vs nullable** - Use `.optional()` for optional fields
|
|
510
|
+
5. **Passthrough for external APIs** - Use `.passthrough()` when structure varies
|
|
511
|
+
6. **Separate schemas file** - Keep complex schemas in `src/schemas.ts`
|
|
512
|
+
7. **Explicit nested objects** - Define structure instead of `z.unknown()`
|
|
513
|
+
|
|
514
|
+
## Converting Actions to Tools
|
|
515
|
+
|
|
516
|
+
Actions can be converted to AI-callable tools using the `.asTool()` method. This allows AI agents to autonomously call your actions during conversations.
|
|
517
|
+
|
|
518
|
+
### Basic Usage
|
|
519
|
+
|
|
520
|
+
```typescript
|
|
521
|
+
import { actions, Conversation } from "@botpress/runtime";
|
|
522
|
+
|
|
523
|
+
export default new Conversation({
|
|
524
|
+
handler: async ({ execute }) => {
|
|
525
|
+
await execute({
|
|
526
|
+
instructions: "Help the user with their request",
|
|
527
|
+
tools: [
|
|
528
|
+
// Convert existing actions to tools
|
|
529
|
+
actions.fetchUser.asTool(),
|
|
530
|
+
actions.sendEmail.asTool(),
|
|
531
|
+
actions.searchDatabase.asTool()
|
|
532
|
+
]
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### Tool Patterns: Blocking vs Non-Blocking
|
|
539
|
+
|
|
540
|
+
#### Pattern Comparison
|
|
541
|
+
|
|
542
|
+
| Pattern | Returns | Execution | Example |
|
|
543
|
+
|---------|---------|-----------|---------|
|
|
544
|
+
| **Direct Action** | Final result | Synchronous | `fetchUser()` |
|
|
545
|
+
| **Workflow Wrapper** | Workflow ID | Asynchronous | `startIndexing()` |
|
|
546
|
+
|
|
547
|
+
#### 1. Direct Actions
|
|
548
|
+
|
|
549
|
+
Actions that complete and return results immediately:
|
|
550
|
+
|
|
551
|
+
```typescript
|
|
552
|
+
import { Action, z } from "@botpress/runtime";
|
|
553
|
+
|
|
554
|
+
export default new Action({
|
|
555
|
+
name: "calculateShipping",
|
|
556
|
+
description: "Calculate shipping cost for an order",
|
|
557
|
+
|
|
558
|
+
input: z.object({
|
|
559
|
+
weight: z.number(),
|
|
560
|
+
destination: z.string()
|
|
561
|
+
}),
|
|
562
|
+
|
|
563
|
+
output: z.object({
|
|
564
|
+
cost: z.number(),
|
|
565
|
+
estimatedDays: z.number()
|
|
566
|
+
}),
|
|
567
|
+
|
|
568
|
+
async handler({ input }) {
|
|
569
|
+
// Business logic that completes immediately
|
|
570
|
+
const baseRate = 5.99;
|
|
571
|
+
const perKg = 2.50;
|
|
572
|
+
const cost = baseRate + (input.weight * perKg);
|
|
573
|
+
|
|
574
|
+
const estimatedDays = input.destination === "domestic" ? 3 : 7;
|
|
575
|
+
|
|
576
|
+
return { cost, estimatedDays };
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
// Use as tool - AI can call this during conversation
|
|
581
|
+
await execute({
|
|
582
|
+
instructions: "Help the user with shipping",
|
|
583
|
+
tools: [actions.calculateShipping.asTool()]
|
|
584
|
+
});
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
#### 2. Workflow Wrappers
|
|
588
|
+
|
|
589
|
+
Actions that start workflows and return workflow ID:
|
|
590
|
+
|
|
591
|
+
```typescript
|
|
592
|
+
import { Action, z } from "@botpress/runtime";
|
|
593
|
+
import { DataAnalysisWorkflow } from "../workflows/data-analysis";
|
|
594
|
+
|
|
595
|
+
export default new Action({
|
|
596
|
+
name: "startDataAnalysis",
|
|
597
|
+
description: "Start data analysis workflow",
|
|
598
|
+
|
|
599
|
+
input: z.object({
|
|
600
|
+
datasetId: z.string()
|
|
601
|
+
}),
|
|
602
|
+
|
|
603
|
+
output: z.object({
|
|
604
|
+
workflowId: z.string()
|
|
605
|
+
}),
|
|
606
|
+
|
|
607
|
+
async handler({ input }) {
|
|
608
|
+
// Calls workflow.start() - returns immediately
|
|
609
|
+
const instance = await DataAnalysisWorkflow.start({
|
|
610
|
+
datasetId: input.datasetId
|
|
611
|
+
});
|
|
612
|
+
return { workflowId: instance.id };
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// Companion action to check status
|
|
617
|
+
export const checkAnalysisStatus = new Action({
|
|
618
|
+
name: "checkAnalysisStatus",
|
|
619
|
+
description: "Check workflow status",
|
|
620
|
+
|
|
621
|
+
input: z.object({
|
|
622
|
+
workflowId: z.string()
|
|
623
|
+
}),
|
|
624
|
+
|
|
625
|
+
output: z.object({
|
|
626
|
+
status: z.string()
|
|
627
|
+
}),
|
|
628
|
+
|
|
629
|
+
async handler({ input, client }) {
|
|
630
|
+
const { workflow } = await client.getWorkflow({ id: input.workflowId });
|
|
631
|
+
return { status: workflow.status };
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
// Use both as tools
|
|
636
|
+
await execute({
|
|
637
|
+
instructions: "Help the user",
|
|
638
|
+
tools: [
|
|
639
|
+
actions.startDataAnalysis.asTool(),
|
|
640
|
+
actions.checkAnalysisStatus.asTool()
|
|
641
|
+
]
|
|
642
|
+
});
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### Custom Tool Conversion
|
|
646
|
+
|
|
647
|
+
```typescript
|
|
648
|
+
// Basic conversion
|
|
649
|
+
const shippingTool = actions.calculateShipping.asTool();
|
|
650
|
+
|
|
651
|
+
// With custom description
|
|
652
|
+
const analysisTool = actions.startDataAnalysis.asTool({
|
|
653
|
+
description: "Analyze dataset and return workflow ID"
|
|
654
|
+
});
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
> **See Also:** [Exposing Workflows as Tools](./workflows.md#exposing-workflows-as-tools-non-blocking-pattern) for comprehensive workflow wrapper patterns and tracking examples.
|
|
658
|
+
|
|
659
|
+
## Best Practices
|
|
660
|
+
|
|
661
|
+
1. **Use descriptive names**: `fetchUserProfile` not `getUser`
|
|
662
|
+
2. **Validate inputs thoroughly**: Use Zod's validation features
|
|
663
|
+
3. **Handle errors gracefully**: Catch and wrap errors with context
|
|
664
|
+
4. **Keep actions focused**: One action = one responsibility
|
|
665
|
+
5. **Document complex logic**: Add comments for non-obvious operations
|
|
666
|
+
6. **Use caching wisely**: Enable `cached: true` for deterministic operations
|
|
667
|
+
|
|
668
|
+
```typescript
|
|
669
|
+
export default new Action({
|
|
670
|
+
name: "fetchUserProfile",
|
|
671
|
+
description: "Fetches complete user profile with preferences",
|
|
672
|
+
|
|
673
|
+
input: z.object({
|
|
674
|
+
userId: z.string().uuid(), // Validate UUID format
|
|
675
|
+
includePreferences: z.boolean().default(false)
|
|
676
|
+
}),
|
|
677
|
+
|
|
678
|
+
output: z.object({
|
|
679
|
+
user: z.object({
|
|
680
|
+
id: z.string(),
|
|
681
|
+
name: z.string(),
|
|
682
|
+
email: z.string().email()
|
|
683
|
+
}),
|
|
684
|
+
preferences: z.object({
|
|
685
|
+
language: z.string(),
|
|
686
|
+
timezone: z.string()
|
|
687
|
+
}).optional()
|
|
688
|
+
}),
|
|
689
|
+
|
|
690
|
+
cached: true, // Cache for performance
|
|
691
|
+
|
|
692
|
+
async handler({ input, client }) {
|
|
693
|
+
// Validate user exists
|
|
694
|
+
const { user } = await client.getUser({ id: input.userId });
|
|
695
|
+
if (!user) {
|
|
696
|
+
throw new Error(`User ${input.userId} not found`);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Conditionally fetch preferences
|
|
700
|
+
const preferences = input.includePreferences
|
|
701
|
+
? await fetchUserPreferences(input.userId)
|
|
702
|
+
: undefined;
|
|
703
|
+
|
|
704
|
+
return {
|
|
705
|
+
user: {
|
|
706
|
+
id: user.id,
|
|
707
|
+
name: user.name,
|
|
708
|
+
email: user.tags.email
|
|
709
|
+
},
|
|
710
|
+
preferences
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
## Common Patterns
|
|
717
|
+
|
|
718
|
+
### Composition Pattern
|
|
719
|
+
|
|
720
|
+
Combine multiple actions for complex operations:
|
|
721
|
+
|
|
722
|
+
```typescript
|
|
723
|
+
export default new Action({
|
|
724
|
+
name: "processOrder",
|
|
725
|
+
input: z.object({ orderId: z.string() }),
|
|
726
|
+
output: z.object({ success: z.boolean() }),
|
|
727
|
+
|
|
728
|
+
async handler({ input }) {
|
|
729
|
+
// Compose multiple actions
|
|
730
|
+
const order = await actions.fetchOrder({ id: input.orderId });
|
|
731
|
+
const validated = await actions.validateOrder({ order });
|
|
732
|
+
|
|
733
|
+
if (validated.isValid) {
|
|
734
|
+
await actions.chargePayment({
|
|
735
|
+
amount: order.total,
|
|
736
|
+
customerId: order.customerId
|
|
737
|
+
});
|
|
738
|
+
await actions.sendConfirmation({ order });
|
|
739
|
+
await actions.updateInventory({ items: order.items });
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
return { success: validated.isValid };
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
### Error Recovery Pattern
|
|
748
|
+
|
|
749
|
+
Handle failures gracefully with retries:
|
|
750
|
+
|
|
751
|
+
```typescript
|
|
752
|
+
export default new Action({
|
|
753
|
+
name: "resilientFetch",
|
|
754
|
+
input: z.object({ url: z.string().url() }),
|
|
755
|
+
output: z.object({ data: z.unknown() }),
|
|
756
|
+
|
|
757
|
+
async handler({ input }) {
|
|
758
|
+
const maxRetries = 3;
|
|
759
|
+
let lastError;
|
|
760
|
+
|
|
761
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
762
|
+
try {
|
|
763
|
+
const response = await fetch(input.url);
|
|
764
|
+
if (!response.ok) {
|
|
765
|
+
throw new Error(`HTTP ${response.status}`);
|
|
766
|
+
}
|
|
767
|
+
return { data: await response.json() };
|
|
768
|
+
} catch (error) {
|
|
769
|
+
lastError = error;
|
|
770
|
+
if (attempt < maxRetries) {
|
|
771
|
+
// Exponential backoff
|
|
772
|
+
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)));
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
throw new Error(`Failed after ${maxRetries} attempts: ${lastError.message}`);
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
## Troubleshooting
|
|
783
|
+
|
|
784
|
+
### Common Issues
|
|
785
|
+
|
|
786
|
+
1. **"Cannot destructure property" error**
|
|
787
|
+
- **Cause**: Destructuring input fields directly in Action handler parameter
|
|
788
|
+
- **Wrong**: `async handler({ userId }) { ... }`
|
|
789
|
+
- **Correct**: `async handler({ input }) { const { userId } = input; ... }`
|
|
790
|
+
- **Note**: Tools can destructure directly, but Actions cannot
|
|
791
|
+
|
|
792
|
+
2. **"user is not defined" or context errors**
|
|
793
|
+
- **Cause**: Accessing conversation context outside of conversation context
|
|
794
|
+
- **Solution**: Use `context.get("user", { optional: true })` to check availability
|
|
795
|
+
- **Note**: `user`, `conversation`, `message` are only available in conversation contexts
|
|
796
|
+
|
|
797
|
+
3. **Action not found**
|
|
798
|
+
- Verify file is in `src/actions/` directory
|
|
799
|
+
- Check import: `import { actions } from "@botpress/runtime"`
|
|
800
|
+
- Ensure action name matches file export
|
|
801
|
+
|
|
802
|
+
4. **Type errors with inputs/outputs**
|
|
803
|
+
- Verify Zod schemas match actual data
|
|
804
|
+
- Use `.optional()` for optional fields
|
|
805
|
+
- Check for typos in property names
|
|
806
|
+
|
|
807
|
+
5. **State properties undefined**
|
|
808
|
+
- **Cause**: State properties must be defined in `agent.config.ts` schemas
|
|
809
|
+
- **Solution**: Add properties to `user.state` or `bot.state` schema in config
|
|
810
|
+
- **Example**: Can't use `user.state.characterName` unless defined in config
|
|
811
|
+
|
|
812
|
+
6. **Caching issues**
|
|
813
|
+
- **Issue**: Cached results not updating when expected
|
|
814
|
+
- **Solution**: Only use `cached: true` for deterministic functions
|
|
815
|
+
- **Tip**: Avoid caching for time-sensitive or user-specific data
|
|
816
|
+
|
|
817
|
+
## See Also
|
|
818
|
+
|
|
819
|
+
- [Tools](./tools.md) - AI-callable tools for autonomous execution
|
|
820
|
+
- [Workflows](./workflows.md) - Multi-step processes with state management
|
|
821
|
+
- [Context API](./context-api.md) - Access runtime context and services
|
|
822
|
+
- [Integration Actions](./integration-actions.md) - Using integration actions
|
|
823
|
+
- [Patterns & Common Mistakes](./patterns-mistakes.md) - Best practices for Zod schemas
|