@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,1408 @@
|
|
|
1
|
+
# Workflows
|
|
2
|
+
|
|
3
|
+
Workflows are multi-step, resumable processes that maintain state across executions. They're perfect for complex operations, scheduled tasks, and long-running processes that may need to pause for user input or external events.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
- [Basic Concepts](#basic-concepts)
|
|
7
|
+
- [Creating Workflows](#creating-workflows)
|
|
8
|
+
- [Managing Instances](#managing-instances)
|
|
9
|
+
- [State, Input, and Output](#state-input-and-output)
|
|
10
|
+
- [Step Mechanics](#step-mechanics)
|
|
11
|
+
- [Communication Patterns](#communication-patterns)
|
|
12
|
+
- [AI/LLM Integration](#aillm-integration)
|
|
13
|
+
- [Exposing Workflows as Tools (Non-Blocking Pattern)](#exposing-workflows-as-tools-non-blocking-pattern)
|
|
14
|
+
- [Scheduled Workflows](#scheduled-workflows)
|
|
15
|
+
- [Best Practices](#best-practices)
|
|
16
|
+
|
|
17
|
+
## Basic Concepts
|
|
18
|
+
|
|
19
|
+
### What are Workflows?
|
|
20
|
+
- **Resumable**: Can pause and resume from where they left off
|
|
21
|
+
- **Stateful**: Maintain state across steps and restarts
|
|
22
|
+
- **Scheduled**: Can run on cron schedules
|
|
23
|
+
- **Interactive**: Can request data from conversations
|
|
24
|
+
|
|
25
|
+
### File Location
|
|
26
|
+
- **Location**: `src/workflows/*.ts`
|
|
27
|
+
- **Auto-registration**: Files automatically become available as workflows
|
|
28
|
+
|
|
29
|
+
## Creating Workflows
|
|
30
|
+
|
|
31
|
+
### Basic Structure
|
|
32
|
+
|
|
33
|
+
> ⚠️ **CRITICAL: State Access Pattern**
|
|
34
|
+
>
|
|
35
|
+
> Workflow state is passed as a **parameter** to the handler, not accessed via `this.state`!
|
|
36
|
+
> The handler receives `{ input, state, step, client, execute }` where `state` is a mutable parameter that's automatically tracked and persisted.
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { Workflow, z } from "@botpress/runtime";
|
|
40
|
+
|
|
41
|
+
export const MyWorkflow = new Workflow({
|
|
42
|
+
name: "myWorkflow",
|
|
43
|
+
description: "Description of what this workflow does",
|
|
44
|
+
|
|
45
|
+
// Optional: Set workflow timeout (default: 5 minutes)
|
|
46
|
+
timeout: "6h", // Can be: "30m", "2h", "1d", etc.
|
|
47
|
+
|
|
48
|
+
// Optional: Schedule for automatic execution
|
|
49
|
+
schedule: "0 9 * * *", // Cron format (daily at 9 AM)
|
|
50
|
+
|
|
51
|
+
// Input schema - data required to start the workflow
|
|
52
|
+
input: z.object({
|
|
53
|
+
userId: z.string(),
|
|
54
|
+
data: z.string()
|
|
55
|
+
}),
|
|
56
|
+
|
|
57
|
+
// State schema - mutable data that persists across steps
|
|
58
|
+
state: z.object({
|
|
59
|
+
currentStep: z.number().default(0),
|
|
60
|
+
processedItems: z.array(z.string()).default([])
|
|
61
|
+
}),
|
|
62
|
+
|
|
63
|
+
// Output schema - what the workflow returns when complete
|
|
64
|
+
output: z.object({
|
|
65
|
+
result: z.boolean(),
|
|
66
|
+
itemsProcessed: z.number()
|
|
67
|
+
}),
|
|
68
|
+
|
|
69
|
+
// Handler receives state as a parameter
|
|
70
|
+
async handler({ input, state, step, client, execute }) {
|
|
71
|
+
// Access and modify state directly (it's automatically tracked)
|
|
72
|
+
state.currentStep = 1;
|
|
73
|
+
|
|
74
|
+
// Execute steps
|
|
75
|
+
const data = await step("fetch-data", async () => {
|
|
76
|
+
return await fetchSomething(input.userId);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
state.processedItems.push(data);
|
|
80
|
+
|
|
81
|
+
// Return output
|
|
82
|
+
return {
|
|
83
|
+
result: true,
|
|
84
|
+
itemsProcessed: state.processedItems.length
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Handler Parameters
|
|
91
|
+
|
|
92
|
+
The handler receives these parameters:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
async handler({ input, state, step, client, execute }) {
|
|
96
|
+
// input: z.infer<Input> - Read-only input data
|
|
97
|
+
// state: z.infer<State> - Mutable workflow state (automatically tracked and persisted)
|
|
98
|
+
// step: TypedWorkflowStep - Step execution functions
|
|
99
|
+
// client: BotClient - Botpress API client
|
|
100
|
+
// execute: ExecuteFn - AI execution function for autonomous operations
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### Using AI Execute in Workflows
|
|
105
|
+
|
|
106
|
+
The `execute` function enables AI operations within workflows:
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
export const AnalyzeWorkflow = new Workflow({
|
|
110
|
+
name: "analyze",
|
|
111
|
+
async handler({ input, step, execute }) {
|
|
112
|
+
const result = await step("ai-analysis", async () => {
|
|
113
|
+
return await execute({
|
|
114
|
+
instructions: "Analyze the provided data",
|
|
115
|
+
model: "openai:gpt-4o",
|
|
116
|
+
temperature: 0,
|
|
117
|
+
input: { data: input.data }
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return { analysis: result };
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Managing Instances
|
|
127
|
+
|
|
128
|
+
### Creating and Retrieving Workflows
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
// 1. Start a new workflow (always creates new instance)
|
|
132
|
+
const instance = await MyWorkflow.start({
|
|
133
|
+
userId: user.id,
|
|
134
|
+
data: "some data"
|
|
135
|
+
});
|
|
136
|
+
console.log(instance.id); // "wflw_abc123"
|
|
137
|
+
|
|
138
|
+
// 2. Get or create (idempotent - uses key for deduplication)
|
|
139
|
+
const instance = await MyWorkflow.getOrCreate({
|
|
140
|
+
key: user.id, // Unique key prevents duplicates
|
|
141
|
+
input: { userId: user.id, data: "x" }
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// 3. Check status via getOrCreate (no standalone get/list methods)
|
|
145
|
+
const instance = await MyWorkflow.getOrCreate({
|
|
146
|
+
key: user.id,
|
|
147
|
+
input: { userId: user.id, data: "x" }
|
|
148
|
+
});
|
|
149
|
+
console.log(instance.status); // "in_progress" | "completed" | "failed"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Important Notes
|
|
153
|
+
- There is NO `create()`, `get()`, or `list()` method - use `start()` or `getOrCreate()`
|
|
154
|
+
- `start()`: Always creates a new instance
|
|
155
|
+
- `getOrCreate()`: Creates only if no instance exists with the given key
|
|
156
|
+
|
|
157
|
+
### Instance Properties
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
const instance = await MyWorkflow.getOrCreate({ key: userId, input: { /* ... */ } });
|
|
161
|
+
|
|
162
|
+
// Available properties
|
|
163
|
+
console.log(instance.id); // "wflw_abc123"
|
|
164
|
+
console.log(instance.status); // "in_progress" | "completed" | "failed" | "cancelled"
|
|
165
|
+
console.log(instance.input); // Input data
|
|
166
|
+
console.log(instance.state); // Current state
|
|
167
|
+
console.log(instance.output); // Output (undefined if not complete)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Workflow Control Methods
|
|
171
|
+
|
|
172
|
+
#### Cancelling Workflows
|
|
173
|
+
|
|
174
|
+
Cancel a running workflow from outside:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
const instance = await MyWorkflow.getOrCreate({ key: userId, input: { /* ... */ } });
|
|
178
|
+
|
|
179
|
+
if (instance && instance.status === "in_progress") {
|
|
180
|
+
await instance.cancel();
|
|
181
|
+
console.log(instance.status); // "cancelled"
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Cancellation is also available via the ADK UI (localhost:3001) during development.
|
|
186
|
+
|
|
187
|
+
#### Completing Workflows Early
|
|
188
|
+
|
|
189
|
+
Complete a workflow immediately with output (from within the handler):
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
export const MyWorkflow = new Workflow({
|
|
193
|
+
name: "myWorkflow",
|
|
194
|
+
output: z.object({
|
|
195
|
+
result: z.string(),
|
|
196
|
+
skipped: z.boolean()
|
|
197
|
+
}),
|
|
198
|
+
|
|
199
|
+
async handler({ input, workflow, step }) {
|
|
200
|
+
// Check condition and complete early
|
|
201
|
+
if (input.skipProcessing) {
|
|
202
|
+
workflow.complete({
|
|
203
|
+
result: "Skipped",
|
|
204
|
+
skipped: true
|
|
205
|
+
});
|
|
206
|
+
// Code after complete() is never reached
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Normal processing continues if not completed early
|
|
210
|
+
const data = await step("process", async () => {
|
|
211
|
+
return await processData(input);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return { result: data, skipped: false };
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Note:** `workflow.complete()` can only be called from within the workflow handler. It immediately interrupts execution.
|
|
220
|
+
|
|
221
|
+
#### Failing Workflows
|
|
222
|
+
|
|
223
|
+
Fail a workflow with an error reason (from within the handler):
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
export const MyWorkflow = new Workflow({
|
|
227
|
+
name: "myWorkflow",
|
|
228
|
+
|
|
229
|
+
async handler({ input, workflow, step }) {
|
|
230
|
+
// Validate input
|
|
231
|
+
if (!input.data || input.data.length === 0) {
|
|
232
|
+
workflow.fail("Invalid input: data cannot be empty");
|
|
233
|
+
// Code after fail() is never reached
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Check external conditions
|
|
237
|
+
const isAvailable = await step("check-service", async () => {
|
|
238
|
+
return await checkServiceAvailability();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (!isAvailable) {
|
|
242
|
+
workflow.fail("External service unavailable");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Continue normal processing
|
|
246
|
+
return { success: true };
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Note:** `workflow.fail()` can only be called from within the workflow handler. It immediately interrupts execution and marks the workflow as failed.
|
|
252
|
+
|
|
253
|
+
#### Extending Workflow Timeouts
|
|
254
|
+
|
|
255
|
+
Extend or set a new timeout for long-running workflows:
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
export const LongRunningWorkflow = new Workflow({
|
|
259
|
+
name: "longRunning",
|
|
260
|
+
timeout: "1h", // Initial timeout
|
|
261
|
+
|
|
262
|
+
async handler({ input, workflow, step }) {
|
|
263
|
+
// Extend timeout before a long operation
|
|
264
|
+
workflow.setTimeout({ in: "6h" }); // Relative: 6 hours from now
|
|
265
|
+
|
|
266
|
+
// Or set an absolute deadline
|
|
267
|
+
workflow.setTimeout({ at: "2025-12-31T23:59:59Z" });
|
|
268
|
+
|
|
269
|
+
// Long-running operation
|
|
270
|
+
for (const batch of batches) {
|
|
271
|
+
await step(`process-batch-${batch.id}`, async () => {
|
|
272
|
+
// Extend timeout for each batch if needed
|
|
273
|
+
workflow.setTimeout({ in: "30m" });
|
|
274
|
+
return await processBatch(batch);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return { completed: true };
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Duration formats:** `"30m"`, `"1h"`, `"6 hours"`, `"1d"`, etc.
|
|
284
|
+
|
|
285
|
+
#### Control Methods Summary
|
|
286
|
+
|
|
287
|
+
| Method | Location | Purpose |
|
|
288
|
+
|--------|----------|---------|
|
|
289
|
+
| `instance.cancel()` | Outside handler | Cancel a workflow externally |
|
|
290
|
+
| `workflow.complete(output)` | Inside handler | Complete early with output |
|
|
291
|
+
| `workflow.fail(reason)` | Inside handler | Fail with error message |
|
|
292
|
+
| `workflow.setTimeout({ in })` | Inside handler | Extend timeout (relative) |
|
|
293
|
+
| `workflow.setTimeout({ at })` | Inside handler | Set absolute deadline |
|
|
294
|
+
|
|
295
|
+
## State, Input, and Output
|
|
296
|
+
|
|
297
|
+
### Understanding the Three Data Types
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
export const DataWorkflow = new Workflow({
|
|
301
|
+
name: "dataWorkflow",
|
|
302
|
+
|
|
303
|
+
// INPUT: Read-only data passed at creation
|
|
304
|
+
input: z.object({
|
|
305
|
+
userId: z.string(),
|
|
306
|
+
config: z.object({ mode: z.string() })
|
|
307
|
+
}),
|
|
308
|
+
|
|
309
|
+
// STATE: Mutable data persisted across steps
|
|
310
|
+
state: z.object({
|
|
311
|
+
currentStep: z.number().default(0),
|
|
312
|
+
items: z.array(z.string()).default([]),
|
|
313
|
+
error: z.string().optional()
|
|
314
|
+
}),
|
|
315
|
+
|
|
316
|
+
// OUTPUT: Final result returned on completion
|
|
317
|
+
output: z.object({
|
|
318
|
+
success: z.boolean(),
|
|
319
|
+
totalProcessed: z.number()
|
|
320
|
+
}),
|
|
321
|
+
|
|
322
|
+
async handler({ input, state, step }) {
|
|
323
|
+
// INPUT is read-only
|
|
324
|
+
console.log(input.userId); // ✅ Read
|
|
325
|
+
// input.userId = "new"; // ❌ TypeScript error
|
|
326
|
+
|
|
327
|
+
// STATE is mutable via the state parameter
|
|
328
|
+
state.currentStep = 1; // ✅ Automatically persisted
|
|
329
|
+
state.items.push("item1");
|
|
330
|
+
|
|
331
|
+
// Steps automatically persist state
|
|
332
|
+
await step("process", async () => {
|
|
333
|
+
state.currentStep = 2; // Saved even if workflow crashes
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// OUTPUT is returned at the end
|
|
337
|
+
return {
|
|
338
|
+
success: true,
|
|
339
|
+
totalProcessed: state.items.length
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## Step Mechanics
|
|
346
|
+
|
|
347
|
+
### Basic Steps
|
|
348
|
+
|
|
349
|
+
Steps are the fundamental unit of workflow execution with automatic persistence:
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
async handler({ input, state, step }) {
|
|
353
|
+
// Basic step - returns and caches the value
|
|
354
|
+
const userData = await step("fetch-user", async () => {
|
|
355
|
+
const data = await api.getUser(input.userId);
|
|
356
|
+
return data; // This value is cached and returned
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Use the returned value
|
|
360
|
+
console.log(userData.name);
|
|
361
|
+
|
|
362
|
+
// Nested steps with return values
|
|
363
|
+
const processedData = await step("process-data", async () => {
|
|
364
|
+
// Steps can contain other steps
|
|
365
|
+
const enriched = await step("enrich", async () => {
|
|
366
|
+
return await enrichData(userData);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const validated = await step("validate", async () => {
|
|
370
|
+
return await validateData(enriched);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
return { enriched, validated }; // Return multiple values
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// Step with retry logic
|
|
377
|
+
const result = await step("risky-op", async ({ attempt }) => {
|
|
378
|
+
console.log(`Attempt ${attempt}`);
|
|
379
|
+
if (attempt < 3) {
|
|
380
|
+
throw new Error("Retry me");
|
|
381
|
+
}
|
|
382
|
+
return "success";
|
|
383
|
+
}, {
|
|
384
|
+
maxAttempts: 5, // Retry up to 5 times
|
|
385
|
+
timeout: 10000 // 10 second timeout
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Accessing state inside steps
|
|
389
|
+
await step("update-state", async () => {
|
|
390
|
+
state.userData = userData; // State is accessible in the closure
|
|
391
|
+
state.processedData = processedData;
|
|
392
|
+
// No need to return if just updating state
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// Progress logging in steps (production pattern)
|
|
396
|
+
await step("final-processing", async () => {
|
|
397
|
+
console.log(`✓ Processing user ${userData.id}`);
|
|
398
|
+
const result = await finalProcess(processedData);
|
|
399
|
+
console.log(`✓ Completed: ${result.status}`);
|
|
400
|
+
return result;
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### Available Step Methods
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
// Sleep for specified milliseconds
|
|
409
|
+
await step.sleep("wait-5s", 5000);
|
|
410
|
+
|
|
411
|
+
// Sleep until specific time
|
|
412
|
+
await step.sleepUntil("wait-until", new Date("2025-12-31"));
|
|
413
|
+
|
|
414
|
+
// Update progress message
|
|
415
|
+
await step.progress("Processing items...");
|
|
416
|
+
|
|
417
|
+
// Execute child workflow
|
|
418
|
+
const result = await step.executeWorkflow(
|
|
419
|
+
"child",
|
|
420
|
+
ChildWorkflow,
|
|
421
|
+
{ input: data }
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
// Wait for workflow completion
|
|
425
|
+
await step.waitForWorkflow("wait-child", childWorkflowId);
|
|
426
|
+
|
|
427
|
+
// Request data from conversation (pauses workflow)
|
|
428
|
+
const answer = await step.request(
|
|
429
|
+
"confirmation",
|
|
430
|
+
"Do you want to proceed?"
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
// Process array in batches
|
|
434
|
+
await step.batch("batch-urls", urls, async (batch) => {
|
|
435
|
+
// Process batch
|
|
436
|
+
}, { batchSize: 20 });
|
|
437
|
+
|
|
438
|
+
// Map over array with parallel processing and retry logic
|
|
439
|
+
const results = await step.map(
|
|
440
|
+
"map-items",
|
|
441
|
+
items,
|
|
442
|
+
async (item) => {
|
|
443
|
+
return processItem(item);
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
concurrency: 10, // Process 10 items at a time
|
|
447
|
+
maxAttempts: 3 // Retry failed items up to 3 times
|
|
448
|
+
}
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
// ForEach with concurrency
|
|
452
|
+
await step.forEach("foreach-items", items, async (item) => {
|
|
453
|
+
await processItem(item);
|
|
454
|
+
}, { concurrency: 5 });
|
|
455
|
+
|
|
456
|
+
// Listen for events (pauses until event)
|
|
457
|
+
await step.listen("wait-for-event");
|
|
458
|
+
|
|
459
|
+
// Fail workflow with message
|
|
460
|
+
await step.fail("Workflow failed due to X");
|
|
461
|
+
|
|
462
|
+
// Abort workflow (no args, synchronous, does not mark as failed)
|
|
463
|
+
step.abort();
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### Step Rules
|
|
467
|
+
|
|
468
|
+
1. **Step names must be unique** within a workflow
|
|
469
|
+
2. **Step names must be stable** (don't use dynamic names)
|
|
470
|
+
3. **Steps execute sequentially**
|
|
471
|
+
4. **Completed steps are cached** and skipped on resume
|
|
472
|
+
5. **Step results are persisted**
|
|
473
|
+
|
|
474
|
+
```typescript
|
|
475
|
+
// ❌ BAD: Dynamic step names break resume
|
|
476
|
+
for (let i = 0; i < items.length; i++) {
|
|
477
|
+
await step(`process-${i}`, async () => { /* ... */ });
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ✅ GOOD: Single step for batch operations
|
|
481
|
+
await step("process-all-items", async () => {
|
|
482
|
+
for (const item of items) {
|
|
483
|
+
await processItem(item);
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
## Communication Patterns
|
|
489
|
+
|
|
490
|
+
### 1. Requesting User Input (Blocking)
|
|
491
|
+
|
|
492
|
+
Workflows can pause and request data from conversations:
|
|
493
|
+
|
|
494
|
+
```typescript
|
|
495
|
+
// In Workflow
|
|
496
|
+
export const InteractiveWorkflow = new Workflow({
|
|
497
|
+
name: "interactive",
|
|
498
|
+
|
|
499
|
+
// Define expected request types
|
|
500
|
+
requests: {
|
|
501
|
+
email: z.object({ email: z.string().email() }),
|
|
502
|
+
confirmation: z.object({ confirmed: z.boolean() })
|
|
503
|
+
},
|
|
504
|
+
|
|
505
|
+
async handler({ state, step }) {
|
|
506
|
+
// Request email (workflow pauses here)
|
|
507
|
+
const { email } = await step.request(
|
|
508
|
+
"email",
|
|
509
|
+
"Please enter your email address:"
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
// Request confirmation
|
|
513
|
+
const { confirmed } = await step.request(
|
|
514
|
+
"confirmation",
|
|
515
|
+
"Do you want to proceed?"
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
if (confirmed) {
|
|
519
|
+
// Continue processing
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// In Conversation - handle the request
|
|
525
|
+
export const Chat = new Conversation({
|
|
526
|
+
channel: "chat.channel",
|
|
527
|
+
|
|
528
|
+
async handler({ type, request, conversation }) {
|
|
529
|
+
if (type === "workflow_request") {
|
|
530
|
+
// Send prompt to user
|
|
531
|
+
await conversation.send({
|
|
532
|
+
type: "text",
|
|
533
|
+
payload: { text: request.workflow.prompt }
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// Later, provide data back
|
|
537
|
+
await request.workflow.provide("email", {
|
|
538
|
+
email: userInput
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
### 2. Sending Progress Updates (Non-blocking)
|
|
546
|
+
|
|
547
|
+
Send messages without pausing execution:
|
|
548
|
+
|
|
549
|
+
```typescript
|
|
550
|
+
export const NotificationWorkflow = new Workflow({
|
|
551
|
+
name: "notification",
|
|
552
|
+
input: z.object({
|
|
553
|
+
conversationId: z.string(), // CRITICAL: Required for messaging
|
|
554
|
+
userId: z.string()
|
|
555
|
+
}),
|
|
556
|
+
|
|
557
|
+
async handler({ input, state, client, step }) {
|
|
558
|
+
// Send progress update
|
|
559
|
+
await step("notify-start", async () => {
|
|
560
|
+
await client.createMessage({
|
|
561
|
+
conversationId: input.conversationId,
|
|
562
|
+
type: "text",
|
|
563
|
+
payload: { text: "🚀 Processing started..." }
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// Do work
|
|
568
|
+
const result = await step("process", async () => {
|
|
569
|
+
return await processData();
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
// Send completion
|
|
573
|
+
await step("notify-complete", async () => {
|
|
574
|
+
await client.createMessage({
|
|
575
|
+
conversationId: input.conversationId,
|
|
576
|
+
type: "text",
|
|
577
|
+
payload: { text: `✅ Complete: ${result}` }
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
### 3. Starting Workflows from Conversations
|
|
585
|
+
|
|
586
|
+
**CRITICAL**: Always pass `conversationId` for communication:
|
|
587
|
+
|
|
588
|
+
```typescript
|
|
589
|
+
// In Conversation handler
|
|
590
|
+
async handler({ state, conversation, message }) {
|
|
591
|
+
const instance = await MyWorkflow.start({
|
|
592
|
+
conversationId: conversation.id, // ← Essential!
|
|
593
|
+
userId: user.id,
|
|
594
|
+
data: message.payload.text
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// Store workflow ID for tracking
|
|
598
|
+
state.activeWorkflowId = instance.id;
|
|
599
|
+
}
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### 4. Handling Workflow Completion in Conversations
|
|
603
|
+
|
|
604
|
+
When a workflow completes, fails, is canceled, or times out, the associated conversation handler receives a `workflow_callback` event with a typed `completion` object:
|
|
605
|
+
|
|
606
|
+
```typescript
|
|
607
|
+
// In Conversation - handle workflow completion
|
|
608
|
+
export const Chat = new Conversation({
|
|
609
|
+
channel: "chat.channel",
|
|
610
|
+
|
|
611
|
+
async handler({ type, completion, conversation }) {
|
|
612
|
+
if (type === "workflow_callback") {
|
|
613
|
+
// completion.type — workflow name (e.g., "processOrder")
|
|
614
|
+
// completion.workflow — workflow instance
|
|
615
|
+
// completion.status — "completed" | "failed" | "canceled" | "timed_out"
|
|
616
|
+
// completion.output — workflow output (when completed)
|
|
617
|
+
// completion.error — error message (when failed)
|
|
618
|
+
|
|
619
|
+
if (completion.status === "completed") {
|
|
620
|
+
await conversation.send({
|
|
621
|
+
type: "text",
|
|
622
|
+
payload: {
|
|
623
|
+
text: `Processing finished! Result: ${JSON.stringify(completion.output)}`
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
} else if (completion.status === "failed") {
|
|
627
|
+
await conversation.send({
|
|
628
|
+
type: "text",
|
|
629
|
+
payload: { text: `Processing failed: ${completion.error}` }
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
This replaces the need to poll workflow status or use raw event type guards. See **[Conversations — Handling Workflow Callbacks](./conversations.md#handling-workflow-callbacks-completion-events)** for full examples.
|
|
638
|
+
|
|
639
|
+
### Communication Best Practices
|
|
640
|
+
|
|
641
|
+
1. **Always pass conversationId** for workflows that need to communicate
|
|
642
|
+
2. **Wrap client calls in steps** for persistence and retry
|
|
643
|
+
3. **Choose the right pattern**:
|
|
644
|
+
- `step.request()` for required user input
|
|
645
|
+
- `type === "workflow_callback"` for reacting to workflow completion/failure
|
|
646
|
+
- `client.createMessage()` for status updates
|
|
647
|
+
- Events for decoupled notifications
|
|
648
|
+
|
|
649
|
+
## Inter-Workflow Communication
|
|
650
|
+
|
|
651
|
+
### Calling and Waiting for Other Workflows
|
|
652
|
+
|
|
653
|
+
Workflows can start and wait for other workflows to complete:
|
|
654
|
+
|
|
655
|
+
```typescript
|
|
656
|
+
import { Workflow, WorkflowDefinitions } from "@botpress/runtime";
|
|
657
|
+
|
|
658
|
+
export const ParentWorkflow = new Workflow({
|
|
659
|
+
name: "parent",
|
|
660
|
+
input: z.object({
|
|
661
|
+
conversationId: z.string(),
|
|
662
|
+
uniqueKey: z.string()
|
|
663
|
+
}),
|
|
664
|
+
output: z.object({
|
|
665
|
+
childResult: z.any()
|
|
666
|
+
}),
|
|
667
|
+
|
|
668
|
+
async handler({ step, input }) {
|
|
669
|
+
// Start another workflow and wait for it
|
|
670
|
+
const { output } = await step("call-child", async () => {
|
|
671
|
+
const { id } = await ChildWorkflow.getOrCreate({
|
|
672
|
+
input: { data: "some data" },
|
|
673
|
+
statuses: ["pending", "in_progress", "completed"],
|
|
674
|
+
key: `child_${input.uniqueKey}`
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
// Wait for the workflow to complete
|
|
678
|
+
const result = await step.waitForWorkflow(
|
|
679
|
+
"ChildWorkflow",
|
|
680
|
+
id
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
return result.output as WorkflowDefinitions["child"]["output"];
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
return { childResult: output };
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
### Workflow Coordination Pattern
|
|
692
|
+
|
|
693
|
+
Use getOrCreate with unique keys to prevent duplicate workflows:
|
|
694
|
+
|
|
695
|
+
```typescript
|
|
696
|
+
export const ExtractTopicsWorkflow = new Workflow({
|
|
697
|
+
name: "topics",
|
|
698
|
+
input: z.object({
|
|
699
|
+
conversationId: z.string()
|
|
700
|
+
}),
|
|
701
|
+
|
|
702
|
+
async handler({ step, input }) {
|
|
703
|
+
// Get or create ensures only one workflow per conversation
|
|
704
|
+
const { id } = await InsightsWorkflow.getOrCreate({
|
|
705
|
+
input: { conversationId: input.conversationId },
|
|
706
|
+
statuses: ["pending", "in_progress", "completed"],
|
|
707
|
+
key: `insights_conversation_${input.conversationId}` // Unique key
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
// If workflow already exists with key, returns existing instance
|
|
711
|
+
const { output } = await step.waitForWorkflow(
|
|
712
|
+
"InsightsWorkflow",
|
|
713
|
+
id
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
return output;
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
### Workflow Synchronization Pattern
|
|
722
|
+
|
|
723
|
+
When you need to ensure certain operations complete before others:
|
|
724
|
+
|
|
725
|
+
```typescript
|
|
726
|
+
export const SyncWorkflow = new Workflow({
|
|
727
|
+
name: "sync",
|
|
728
|
+
|
|
729
|
+
async handler({ step, input }) {
|
|
730
|
+
// Step 1: Get or create dependent workflow
|
|
731
|
+
const topics = await step("find topics", async () => {
|
|
732
|
+
const { id } = await InsightsTopicsWorkflow.getOrCreate({
|
|
733
|
+
input: { conversationId: input.conversationId },
|
|
734
|
+
statuses: ["pending", "in_progress", "completed"],
|
|
735
|
+
key: `topics_conversation_${input.conversationId}`,
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
const { output } = await step.waitForWorkflow(
|
|
739
|
+
"InsightsTopicsWorkflow",
|
|
740
|
+
id
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
return output as WorkflowDefinitions["insights_topics"]["output"];
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
// Step 2: Use the topics in the main processing
|
|
747
|
+
const insights = await step("process insights", async () => {
|
|
748
|
+
// Use topics data from the dependent workflow
|
|
749
|
+
return processWithTopics(topics);
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
return { insights };
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
## AI/LLM Integration
|
|
758
|
+
|
|
759
|
+
Workflows have full access to AI capabilities through multiple approaches:
|
|
760
|
+
|
|
761
|
+
### 1. Using execute() Function (Autonomous Agent)
|
|
762
|
+
|
|
763
|
+
```typescript
|
|
764
|
+
import { Workflow, z, Autonomous } from "@botpress/runtime";
|
|
765
|
+
import { actions } from "@botpress/runtime";
|
|
766
|
+
|
|
767
|
+
export const AIWorkflow = new Workflow({
|
|
768
|
+
name: "aiWorkflow",
|
|
769
|
+
input: z.object({ question: z.string() }),
|
|
770
|
+
output: z.object({ answer: z.string() }),
|
|
771
|
+
|
|
772
|
+
handler: async ({ input, state, execute, step }) => {
|
|
773
|
+
// Define exits for structured responses
|
|
774
|
+
const AnswerExit = new Autonomous.Exit({
|
|
775
|
+
name: "Answer",
|
|
776
|
+
description: "Provide answer",
|
|
777
|
+
schema: z.object({
|
|
778
|
+
answer: z.string(),
|
|
779
|
+
confidence: z.number()
|
|
780
|
+
})
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
// Define tools for AI
|
|
784
|
+
const SearchTool = new Autonomous.Tool({
|
|
785
|
+
name: "Search",
|
|
786
|
+
description: "Search for info",
|
|
787
|
+
input: z.object({ query: z.string() }),
|
|
788
|
+
output: z.string(),
|
|
789
|
+
handler: async ({ query }) => {
|
|
790
|
+
return await actions.browser.webSearch({ query });
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
// Execute AI with tools
|
|
795
|
+
const result = await execute({
|
|
796
|
+
instructions: `Answer: "${input.question}"`,
|
|
797
|
+
tools: [SearchTool],
|
|
798
|
+
exits: [AnswerExit],
|
|
799
|
+
model: "openai:gpt-4o",
|
|
800
|
+
temperature: 0.7,
|
|
801
|
+
iterations: 10
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
if (result.is(AnswerExit)) {
|
|
805
|
+
return { answer: result.output.answer };
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
return { answer: "No answer found" };
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
### 2. Using execute() with Knowledge Bases
|
|
814
|
+
|
|
815
|
+
Provide knowledge bases directly to the AI for RAG-powered responses:
|
|
816
|
+
|
|
817
|
+
```typescript
|
|
818
|
+
import { Workflow, z, Autonomous } from "@botpress/runtime";
|
|
819
|
+
import { KnowledgeDocs } from "../knowledge/docs"; // Your knowledge base
|
|
820
|
+
|
|
821
|
+
export const KnowledgeWorkflow = new Workflow({
|
|
822
|
+
name: "answerFromKnowledge",
|
|
823
|
+
input: z.object({ question: z.string() }),
|
|
824
|
+
|
|
825
|
+
handler: async ({ input, execute }) => {
|
|
826
|
+
const AnswerExit = new Autonomous.Exit({
|
|
827
|
+
name: "Answer",
|
|
828
|
+
description: "Found answer in knowledge base",
|
|
829
|
+
schema: z.object({
|
|
830
|
+
answer: z.string(),
|
|
831
|
+
sources: z.array(z.string())
|
|
832
|
+
})
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
const NoAnswerExit = new Autonomous.Exit({
|
|
836
|
+
name: "NoAnswer",
|
|
837
|
+
description: "No relevant information found"
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
const result = await execute({
|
|
841
|
+
instructions: `Find answer to: "${input.question}"
|
|
842
|
+
Search the knowledge base and provide accurate information.
|
|
843
|
+
If no relevant information exists, use NoAnswer exit.`,
|
|
844
|
+
knowledge: [KnowledgeDocs], // Pass knowledge bases directly
|
|
845
|
+
exits: [AnswerExit, NoAnswerExit],
|
|
846
|
+
model: "openai:gpt-4o",
|
|
847
|
+
temperature: 0,
|
|
848
|
+
iterations: 5
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
if (result.is(AnswerExit)) {
|
|
852
|
+
// Access citations if needed
|
|
853
|
+
const citations = context.get("citations");
|
|
854
|
+
const [cleaned, found] = citations.removeCitationsFromObject(result.output.answer);
|
|
855
|
+
|
|
856
|
+
return {
|
|
857
|
+
answer: cleaned,
|
|
858
|
+
hasSources: found.length > 0
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
return { answer: "No information found", hasSources: false };
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
### 3. Using Cognitive Client (Raw LLM)
|
|
868
|
+
|
|
869
|
+
```typescript
|
|
870
|
+
import { Workflow, z, context } from "@botpress/runtime";
|
|
871
|
+
|
|
872
|
+
export const DirectLLMWorkflow = new Workflow({
|
|
873
|
+
name: "directLLM",
|
|
874
|
+
|
|
875
|
+
handler: async ({ input, state, step }) => {
|
|
876
|
+
const cognitive = context.get("cognitive");
|
|
877
|
+
|
|
878
|
+
const result = await step("generate", async () => {
|
|
879
|
+
const response = await cognitive.generateContent({
|
|
880
|
+
model: "openai:gpt-4o",
|
|
881
|
+
systemPrompt: "You are helpful",
|
|
882
|
+
messages: [
|
|
883
|
+
{ role: "user", content: input.prompt }
|
|
884
|
+
],
|
|
885
|
+
temperature: 0.7,
|
|
886
|
+
maxTokens: 1000
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
return response.choices[0].message.content;
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
return { response: result };
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
### 3. Using Zai (Structured Operations)
|
|
898
|
+
|
|
899
|
+
Use `adk.zai` for structured AI operations (extract, check, summarize, etc.) inside workflows. Always wrap Zai calls in `step()` for persistence.
|
|
900
|
+
|
|
901
|
+
```typescript
|
|
902
|
+
import { Workflow, z, adk } from "@botpress/runtime";
|
|
903
|
+
|
|
904
|
+
export const ZaiWorkflow = new Workflow({
|
|
905
|
+
name: "zaiWorkflow",
|
|
906
|
+
handler: async ({ input, state, step }) => {
|
|
907
|
+
const insights = await step("extract insights", async () =>
|
|
908
|
+
adk.zai.extract(input.text, z.array(z.object({
|
|
909
|
+
type: z.enum(["bug", "feature_request", "question"]),
|
|
910
|
+
description: z.string(),
|
|
911
|
+
priority: z.enum(["low", "medium", "high"]),
|
|
912
|
+
})))
|
|
913
|
+
);
|
|
914
|
+
const hasBugs = await step("check for bugs", async () =>
|
|
915
|
+
adk.zai.check(insights, "Are there any bug reports?")
|
|
916
|
+
);
|
|
917
|
+
return { insights, hasBugs };
|
|
918
|
+
},
|
|
919
|
+
});
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
### AI Approach Comparison
|
|
923
|
+
|
|
924
|
+
| Approach | Best For | Example Use Case |
|
|
925
|
+
|----------|----------|------------------|
|
|
926
|
+
| **execute()** | Multi-step AI tasks with tools | Research, support agents |
|
|
927
|
+
| **Cognitive Client** | Simple text generation | Content generation |
|
|
928
|
+
| **Zai** | Structured data extraction | Entity extraction |
|
|
929
|
+
| **Integration Actions** | Provider-specific features | Vision models |
|
|
930
|
+
|
|
931
|
+
### Important AI Notes
|
|
932
|
+
|
|
933
|
+
1. **Always wrap AI calls in step()** for persistence
|
|
934
|
+
2. **Model format**: Use `"provider:model"` (e.g., `"openai:gpt-4o"`)
|
|
935
|
+
3. **Cost awareness**: AI calls consume tokens
|
|
936
|
+
4. **Error handling**: AI calls can fail, use retries
|
|
937
|
+
|
|
938
|
+
> For the full list of Zai operations, everyday use cases, and edge cases, see [zai-agent-reference.md](./zai-agent-reference.md).
|
|
939
|
+
|
|
940
|
+
## Exposing Workflows as Tools (Non-Blocking Pattern)
|
|
941
|
+
|
|
942
|
+
Workflows can be converted to tools using `.asTool()`. This is useful when you want AI to start a long-running workflow without blocking on its completion.
|
|
943
|
+
|
|
944
|
+
### Direct Workflow Tool Pattern
|
|
945
|
+
|
|
946
|
+
```typescript
|
|
947
|
+
// Starts the workflow and returns immediately
|
|
948
|
+
await execute({
|
|
949
|
+
tools: [
|
|
950
|
+
MyWorkflow.asTool()
|
|
951
|
+
]
|
|
952
|
+
});
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
When a workflow is exposed this way, the tool starts the workflow and returns a lightweight result such as the workflow ID and current status.
|
|
956
|
+
|
|
957
|
+
### When to Still Use an Action Wrapper
|
|
958
|
+
|
|
959
|
+
An Action wrapper is still useful when you want to rename the tool, constrain inputs, add business rules, or hide workflow details from the AI:
|
|
960
|
+
|
|
961
|
+
```typescript
|
|
962
|
+
// File: /actions/start-processing.ts
|
|
963
|
+
import { Action, z } from "@botpress/runtime";
|
|
964
|
+
import { DataProcessingWorkflow } from "../workflows/data-processing";
|
|
965
|
+
|
|
966
|
+
export default new Action({
|
|
967
|
+
name: "startDataProcessing",
|
|
968
|
+
description: "Start data processing workflow",
|
|
969
|
+
|
|
970
|
+
input: z.object({
|
|
971
|
+
datasetId: z.string(),
|
|
972
|
+
options: z.object({
|
|
973
|
+
fullScan: z.boolean().default(false)
|
|
974
|
+
}).optional()
|
|
975
|
+
}),
|
|
976
|
+
|
|
977
|
+
output: z.object({
|
|
978
|
+
workflowId: z.string()
|
|
979
|
+
}),
|
|
980
|
+
|
|
981
|
+
async handler({ input }) {
|
|
982
|
+
// Calls workflow.start() - returns immediately
|
|
983
|
+
const instance = await DataProcessingWorkflow.start({
|
|
984
|
+
datasetId: input.datasetId,
|
|
985
|
+
options: input.options || { fullScan: false }
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
return { workflowId: instance.id };
|
|
989
|
+
}
|
|
990
|
+
});
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
Use the Action wrapper as a tool:
|
|
994
|
+
|
|
995
|
+
```typescript
|
|
996
|
+
import { Conversation, actions } from "@botpress/runtime";
|
|
997
|
+
|
|
998
|
+
export default new Conversation({
|
|
999
|
+
channel: "*",
|
|
1000
|
+
handler: async ({ execute }) => {
|
|
1001
|
+
await execute({
|
|
1002
|
+
instructions: "Help the user",
|
|
1003
|
+
tools: [
|
|
1004
|
+
actions.startDataProcessing.asTool()
|
|
1005
|
+
]
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
### Tracking Workflow Status
|
|
1012
|
+
|
|
1013
|
+
#### 1. Status Check Action
|
|
1014
|
+
|
|
1015
|
+
```typescript
|
|
1016
|
+
import { Action, z, context } from "@botpress/runtime";
|
|
1017
|
+
|
|
1018
|
+
export default new Action({
|
|
1019
|
+
name: "checkProcessingStatus",
|
|
1020
|
+
description: "Check workflow status",
|
|
1021
|
+
|
|
1022
|
+
input: z.object({
|
|
1023
|
+
workflowId: z.string()
|
|
1024
|
+
}),
|
|
1025
|
+
|
|
1026
|
+
output: z.object({
|
|
1027
|
+
status: z.string(),
|
|
1028
|
+
progress: z.number()
|
|
1029
|
+
}),
|
|
1030
|
+
|
|
1031
|
+
async handler({ workflowId }) {
|
|
1032
|
+
const client = context.get("client");
|
|
1033
|
+
const { workflow } = await client.getWorkflow({ id: workflowId });
|
|
1034
|
+
|
|
1035
|
+
const { state } = await client.getState({
|
|
1036
|
+
name: "workflowSteps",
|
|
1037
|
+
id: workflowId,
|
|
1038
|
+
type: "workflow"
|
|
1039
|
+
}).catch(() => ({ state: { payload: { steps: {} } } }));
|
|
1040
|
+
|
|
1041
|
+
const steps = state.payload.steps || {};
|
|
1042
|
+
const stepArray = Object.values(steps);
|
|
1043
|
+
const completed = stepArray.filter((s: any) => s.finishedAt).length;
|
|
1044
|
+
const progress = stepArray.length > 0
|
|
1045
|
+
? Math.round((completed / stepArray.length) * 100)
|
|
1046
|
+
: 0;
|
|
1047
|
+
|
|
1048
|
+
return {
|
|
1049
|
+
status: workflow.status,
|
|
1050
|
+
progress
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
});
|
|
1054
|
+
```
|
|
1055
|
+
|
|
1056
|
+
#### 2. Progress Events
|
|
1057
|
+
|
|
1058
|
+
Send progress events from workflow:
|
|
1059
|
+
|
|
1060
|
+
```typescript
|
|
1061
|
+
import { context } from "@botpress/runtime";
|
|
1062
|
+
|
|
1063
|
+
export const DataProcessingWorkflow = new Workflow({
|
|
1064
|
+
name: "dataProcessing",
|
|
1065
|
+
|
|
1066
|
+
async handler({ input, state, step, client }) {
|
|
1067
|
+
const workflow = context.get("workflow");
|
|
1068
|
+
const conversationId = workflow.conversationId;
|
|
1069
|
+
|
|
1070
|
+
if (conversationId) {
|
|
1071
|
+
await client.createEvent({
|
|
1072
|
+
type: "workflowProgress",
|
|
1073
|
+
conversationId,
|
|
1074
|
+
payload: {
|
|
1075
|
+
workflowId: workflow.id,
|
|
1076
|
+
stage: "processing",
|
|
1077
|
+
progress: 50
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
return { itemsProcessed: 0 };
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
```
|
|
1086
|
+
|
|
1087
|
+
### Key Differences
|
|
1088
|
+
|
|
1089
|
+
| Aspect | Action | Workflow Wrapper |
|
|
1090
|
+
|--------|--------|------------------|
|
|
1091
|
+
| **Returns** | Final result | Workflow ID |
|
|
1092
|
+
| **Execution** | Synchronous | Asynchronous |
|
|
1093
|
+
| **Example** | `fetchUser()` | `startIndexing()` |
|
|
1094
|
+
|
|
1095
|
+
> **See Also:** [Converting Actions to Tools](./actions.md#converting-actions-to-tools) for more details on using `.asTool()` with different action patterns.
|
|
1096
|
+
|
|
1097
|
+
## Scheduled Workflows
|
|
1098
|
+
|
|
1099
|
+
Run workflows automatically on a schedule:
|
|
1100
|
+
|
|
1101
|
+
```typescript
|
|
1102
|
+
import { bot, Workflow, z } from "@botpress/runtime";
|
|
1103
|
+
|
|
1104
|
+
export const DailySync = new Workflow({
|
|
1105
|
+
name: "dailySync",
|
|
1106
|
+
description: "Syncs data daily with incremental updates",
|
|
1107
|
+
|
|
1108
|
+
// Cron schedule (runs at 8 AM every day)
|
|
1109
|
+
schedule: "0 8 * * *",
|
|
1110
|
+
|
|
1111
|
+
// Long timeout for large sync operations
|
|
1112
|
+
timeout: "6h",
|
|
1113
|
+
|
|
1114
|
+
// Scheduled workflows can have optional input for manual triggering
|
|
1115
|
+
input: z.object({
|
|
1116
|
+
fullSync: z.boolean().default(false).describe("Force full sync")
|
|
1117
|
+
}),
|
|
1118
|
+
|
|
1119
|
+
// Track sync progress
|
|
1120
|
+
state: z.object({
|
|
1121
|
+
lastSyncDate: z.string().optional(),
|
|
1122
|
+
itemsProcessed: z.number().default(0)
|
|
1123
|
+
}),
|
|
1124
|
+
|
|
1125
|
+
async handler({ input, state, step }) {
|
|
1126
|
+
// Access global bot state to check if initial sync is done
|
|
1127
|
+
bot.state.syncStatus ??= { initialized: false };
|
|
1128
|
+
|
|
1129
|
+
const isInitialSync = input.fullSync || !bot.state.syncStatus.initialized;
|
|
1130
|
+
|
|
1131
|
+
const report = await step("generate", async () => {
|
|
1132
|
+
return await generateDailyReport(isInitialSync);
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
await step("send", async () => {
|
|
1136
|
+
await sendReportToTeam(report);
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
// Mark initial sync as complete in global state
|
|
1140
|
+
if (isInitialSync) {
|
|
1141
|
+
bot.state.syncStatus.initialized = true;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
return { sent: true };
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
```
|
|
1148
|
+
|
|
1149
|
+
### Cron Syntax Reference
|
|
1150
|
+
|
|
1151
|
+
```
|
|
1152
|
+
┌───────────── minute (0-59)
|
|
1153
|
+
│ ┌───────────── hour (0-23)
|
|
1154
|
+
│ │ ┌───────────── day of month (1-31)
|
|
1155
|
+
│ │ │ ┌───────────── month (1-12)
|
|
1156
|
+
│ │ │ │ ┌───────────── day of week (0-6, Sunday=0)
|
|
1157
|
+
│ │ │ │ │
|
|
1158
|
+
* * * * *
|
|
1159
|
+
```
|
|
1160
|
+
|
|
1161
|
+
### Common Patterns
|
|
1162
|
+
|
|
1163
|
+
```typescript
|
|
1164
|
+
const schedulePatterns = {
|
|
1165
|
+
everyMinute: "* * * * *",
|
|
1166
|
+
every5Minutes: "*/5 * * * *",
|
|
1167
|
+
everyHour: "0 * * * *",
|
|
1168
|
+
daily9AM: "0 9 * * *",
|
|
1169
|
+
weekdaysMorning: "0 9 * * 1-5", // Mon-Fri at 9am
|
|
1170
|
+
firstOfMonth: "0 0 1 * *"
|
|
1171
|
+
};
|
|
1172
|
+
```
|
|
1173
|
+
|
|
1174
|
+
## Accessing Global Bot State
|
|
1175
|
+
|
|
1176
|
+
Workflows can also access and modify global bot state (not just workflow-specific state):
|
|
1177
|
+
|
|
1178
|
+
```typescript
|
|
1179
|
+
import { bot, Workflow, z } from "@botpress/runtime";
|
|
1180
|
+
|
|
1181
|
+
export const AnalysisWorkflow = new Workflow({
|
|
1182
|
+
name: "analysis",
|
|
1183
|
+
schedule: "0 8 * * *", // Daily at 8 AM
|
|
1184
|
+
|
|
1185
|
+
async handler({ state, step }) {
|
|
1186
|
+
// Perform analysis
|
|
1187
|
+
const results = await step("analyze", async () => {
|
|
1188
|
+
return await performAnalysis();
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
// Store results in global bot state
|
|
1192
|
+
bot.state.analysisResults = results;
|
|
1193
|
+
bot.state.lastAnalysisDate = new Date().toISOString();
|
|
1194
|
+
|
|
1195
|
+
// Access existing bot state
|
|
1196
|
+
console.log(`Previous analysis: ${bot.state.lastAnalysisDate}`);
|
|
1197
|
+
|
|
1198
|
+
return { completed: true };
|
|
1199
|
+
}
|
|
1200
|
+
});
|
|
1201
|
+
```
|
|
1202
|
+
|
|
1203
|
+
This is useful for:
|
|
1204
|
+
- Storing cross-workflow shared data
|
|
1205
|
+
- Maintaining global configuration
|
|
1206
|
+
- Tracking system-wide metrics
|
|
1207
|
+
- Sharing state between workflows and conversations
|
|
1208
|
+
|
|
1209
|
+
## Best Practices
|
|
1210
|
+
|
|
1211
|
+
### 1. Use Meaningful Keys
|
|
1212
|
+
```typescript
|
|
1213
|
+
// Prevent duplicate workflows for same user
|
|
1214
|
+
await MyWorkflow.getOrCreate({
|
|
1215
|
+
key: userId, // Or composite: `${userId}-${date}`
|
|
1216
|
+
input: { userId }
|
|
1217
|
+
});
|
|
1218
|
+
```
|
|
1219
|
+
|
|
1220
|
+
### 2. Keep Step Names Stable
|
|
1221
|
+
```typescript
|
|
1222
|
+
// ✅ GOOD: Stable step names
|
|
1223
|
+
await step("process-batch", async () => {
|
|
1224
|
+
for (const item of items) {
|
|
1225
|
+
await processItem(item);
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
// ❌ BAD: Dynamic step names
|
|
1230
|
+
for (let i = 0; i < items.length; i++) {
|
|
1231
|
+
await step(`process-${i}`, async () => {});
|
|
1232
|
+
}
|
|
1233
|
+
```
|
|
1234
|
+
|
|
1235
|
+
### 3. Store Progress in State
|
|
1236
|
+
```typescript
|
|
1237
|
+
// Access state via the state parameter in workflow handlers
|
|
1238
|
+
state.progress = "Processing item 5 of 10";
|
|
1239
|
+
state.percentComplete = 50;
|
|
1240
|
+
// External systems can poll this state via workflow instance
|
|
1241
|
+
```
|
|
1242
|
+
|
|
1243
|
+
### 4. Pass State Values, Not References
|
|
1244
|
+
|
|
1245
|
+
```typescript
|
|
1246
|
+
// ❌ BAD - Passing state reference
|
|
1247
|
+
async handler({ state, step }) {
|
|
1248
|
+
const result = await processItems(state.items);
|
|
1249
|
+
// If state.items changes, processItems might see it!
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// ✅ GOOD - Pass a copy
|
|
1253
|
+
async handler({ state, step }) {
|
|
1254
|
+
const itemsCopy = [...state.items];
|
|
1255
|
+
const result = await processItems(itemsCopy);
|
|
1256
|
+
// Safe - function has its own copy
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// ✅ GOOD - Extract primitives directly
|
|
1260
|
+
async handler({ state, step }) {
|
|
1261
|
+
const count = state.count; // Primitives are safe
|
|
1262
|
+
const items = [...state.items]; // Arrays need copying
|
|
1263
|
+
const config = { ...state.config }; // Objects need copying
|
|
1264
|
+
|
|
1265
|
+
await step("process", async () => {
|
|
1266
|
+
return await processData(count, items, config);
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
```
|
|
1270
|
+
|
|
1271
|
+
### 5. Handle Errors Gracefully
|
|
1272
|
+
```typescript
|
|
1273
|
+
await step("risky-op", async () => {
|
|
1274
|
+
try {
|
|
1275
|
+
return await riskyOperation();
|
|
1276
|
+
} catch (error) {
|
|
1277
|
+
state.lastError = error.message; // Store error in state
|
|
1278
|
+
throw error; // Re-throw to mark step as failed
|
|
1279
|
+
}
|
|
1280
|
+
}, { maxAttempts: 3 });
|
|
1281
|
+
```
|
|
1282
|
+
|
|
1283
|
+
### 6. Use Appropriate Timeouts
|
|
1284
|
+
```typescript
|
|
1285
|
+
export const MyWorkflow = new Workflow({
|
|
1286
|
+
timeout: "5m", // Balance completion time and resources
|
|
1287
|
+
// ...
|
|
1288
|
+
});
|
|
1289
|
+
```
|
|
1290
|
+
|
|
1291
|
+
## Complete Example: Research Workflow
|
|
1292
|
+
|
|
1293
|
+
```typescript
|
|
1294
|
+
import { Workflow, z, Autonomous } from "@botpress/runtime";
|
|
1295
|
+
import { actions } from "@botpress/runtime";
|
|
1296
|
+
|
|
1297
|
+
export const ResearchWorkflow = new Workflow({
|
|
1298
|
+
name: "research",
|
|
1299
|
+
description: "Research a topic using web searches",
|
|
1300
|
+
|
|
1301
|
+
input: z.object({
|
|
1302
|
+
topic: z.string(),
|
|
1303
|
+
conversationId: z.string()
|
|
1304
|
+
}),
|
|
1305
|
+
|
|
1306
|
+
state: z.object({
|
|
1307
|
+
sources: z.array(z.string()).default([]),
|
|
1308
|
+
progress: z.string().default("Starting...")
|
|
1309
|
+
}),
|
|
1310
|
+
|
|
1311
|
+
output: z.object({
|
|
1312
|
+
report: z.string()
|
|
1313
|
+
}),
|
|
1314
|
+
|
|
1315
|
+
async handler({ input, state, execute, step, client }) {
|
|
1316
|
+
// Notify start
|
|
1317
|
+
await step("notify-start", async () => {
|
|
1318
|
+
await client.createMessage({
|
|
1319
|
+
conversationId: input.conversationId,
|
|
1320
|
+
type: "text",
|
|
1321
|
+
payload: { text: `🔍 Researching "${input.topic}"...` }
|
|
1322
|
+
});
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
// Define search tool
|
|
1326
|
+
const SearchTool = new Autonomous.Tool({
|
|
1327
|
+
name: "webSearch",
|
|
1328
|
+
description: "Search the web",
|
|
1329
|
+
input: z.object({ query: z.string() }),
|
|
1330
|
+
output: z.string(),
|
|
1331
|
+
async handler({ query }) {
|
|
1332
|
+
const results = await actions.browser.webSearch({
|
|
1333
|
+
query,
|
|
1334
|
+
count: 5
|
|
1335
|
+
});
|
|
1336
|
+
state.sources.push(...results.urls); // State accessible in closure
|
|
1337
|
+
throw new Autonomous.ThinkSignal("Found sources", results);
|
|
1338
|
+
}
|
|
1339
|
+
});
|
|
1340
|
+
|
|
1341
|
+
// Define report exit
|
|
1342
|
+
const ReportExit = new Autonomous.Exit({
|
|
1343
|
+
name: "Report",
|
|
1344
|
+
description: "Final research report",
|
|
1345
|
+
schema: z.string()
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
// Execute research
|
|
1349
|
+
const report = await step("research", async () => {
|
|
1350
|
+
state.progress = "Researching..."; // Update state
|
|
1351
|
+
|
|
1352
|
+
const result = await execute({
|
|
1353
|
+
instructions: `Research "${input.topic}".
|
|
1354
|
+
Use web searches to gather information.
|
|
1355
|
+
Compile findings into a comprehensive report.`,
|
|
1356
|
+
tools: [SearchTool],
|
|
1357
|
+
exits: [ReportExit],
|
|
1358
|
+
iterations: 15
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
if (result.is(ReportExit)) {
|
|
1362
|
+
return result.output;
|
|
1363
|
+
}
|
|
1364
|
+
return "Research incomplete";
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1367
|
+
// Send report
|
|
1368
|
+
await step("send-report", async () => {
|
|
1369
|
+
await client.createMessage({
|
|
1370
|
+
conversationId: input.conversationId,
|
|
1371
|
+
type: "text",
|
|
1372
|
+
payload: { text: `📄 Research Report:\n\n${report}` }
|
|
1373
|
+
});
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
return { report };
|
|
1377
|
+
}
|
|
1378
|
+
});
|
|
1379
|
+
```
|
|
1380
|
+
|
|
1381
|
+
## Workflow Lifecycle
|
|
1382
|
+
|
|
1383
|
+
### Status Values
|
|
1384
|
+
- `pending`: Created but not started
|
|
1385
|
+
- `in_progress`: Currently executing
|
|
1386
|
+
- `completed`: Finished successfully
|
|
1387
|
+
- `failed`: Terminated due to error
|
|
1388
|
+
- `cancelled`: Manually cancelled
|
|
1389
|
+
- `timedout`: Exceeded timeout limit
|
|
1390
|
+
|
|
1391
|
+
When a workflow reaches a terminal status (`completed`, `failed`, `cancelled`, `timedout`), the associated conversation handler receives a `workflow_callback` event. Use `type === "workflow_callback"` in the conversation handler to react to these transitions — see [Handling Workflow Completion in Conversations](#4-handling-workflow-completion-in-conversations).
|
|
1392
|
+
|
|
1393
|
+
### Managing Lifecycle
|
|
1394
|
+
```typescript
|
|
1395
|
+
// Check status
|
|
1396
|
+
const instance = await MyWorkflow.getOrCreate({ key: userId, input: { /* ... */ } });
|
|
1397
|
+
if (instance?.status === "failed") {
|
|
1398
|
+
// Retry by starting new instance
|
|
1399
|
+
await MyWorkflow.start(instance.input);
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// Handle cancellation in handler
|
|
1403
|
+
async handler({ state, step }) {
|
|
1404
|
+
if (state.shouldCancel) {
|
|
1405
|
+
return { cancelled: true };
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
```
|