@botpress/adk-cli 1.19.0-beta.4 → 1.19.0-beta.6

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.
Files changed (87) hide show
  1. package/{dist/index-a3ZL0_Jr-q145h35p.js → assets/ui-dist/assets/index-BC_iUF2d.js} +2 -2
  2. package/assets/ui-dist/assets/index-BxpvDT_K.css +1 -0
  3. package/{dist/index-BESZeTO5-2rpmcefa.js → assets/ui-dist/assets/index-C7gAokRv.js} +2 -2
  4. package/assets/ui-dist/assets/{index-B3b7PPgV.js → index-exyr_Djl.js} +424 -424
  5. package/assets/ui-dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  6. package/assets/ui-dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  7. package/assets/ui-dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  8. package/assets/ui-dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  9. package/assets/ui-dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  10. package/assets/ui-dist/index.html +2 -2
  11. package/dist/{README-wjb7z7f0.md → README-824wa4jj.md} +1 -1
  12. package/dist/SKILL-75ym6tcj.md +257 -0
  13. package/dist/SKILL-chnwzmt1.md +548 -0
  14. package/dist/SKILL-d4bs53y3.md +244 -0
  15. package/dist/SKILL-h4b2f1n9.md +114 -0
  16. package/dist/SKILL-hbw5nt95.md +111 -0
  17. package/dist/actions-h7hxxvxr.md +823 -0
  18. package/dist/adk-build-emhwd0tg.md +39 -0
  19. package/dist/adk-debug-yrrdtpmb.md +23 -0
  20. package/dist/adk-dev-console-7z9rtjpy.md +17 -0
  21. package/dist/adk-document-ras6zzzh.md +68 -0
  22. package/dist/adk-eval-vx76dwq0.md +17 -0
  23. package/dist/adk-explain-fcsaw852.md +22 -0
  24. package/dist/adk-init-9kmxmbbe.md +30 -0
  25. package/dist/adk-ship-dybzn884.md +29 -0
  26. package/dist/adk-test-xq67savs.md +27 -0
  27. package/dist/adk-validate-1p4skv4m.md +31 -0
  28. package/dist/advanced-patterns-z86m9h1a.md +669 -0
  29. package/dist/agent-config-1aqvkrgf.md +450 -0
  30. package/dist/agent-steps-zj4c1n9t.md +115 -0
  31. package/dist/assets-q7jt8r2q.md +237 -0
  32. package/dist/autonomous-execution-fymc12e5.md +747 -0
  33. package/dist/cli-c31e06yr.md +1334 -0
  34. package/dist/cli.js +4241 -6449
  35. package/dist/commands.json +25 -4
  36. package/dist/common-failures-ht29w2db.md +231 -0
  37. package/dist/component-registry-bbpw0see.md +43 -0
  38. package/dist/context-api-760cvh0h.md +461 -0
  39. package/dist/conversation-analysis-8ka4nrm4.md +420 -0
  40. package/dist/conversation-lifecycle-4seqqjc1.md +311 -0
  41. package/dist/conversations-gwfvxaee.md +1111 -0
  42. package/dist/custom-components-y5wdgpey.md +167 -0
  43. package/dist/debug-workflow-vz84q80v.md +239 -0
  44. package/dist/desk-k54ktvsd.md +852 -0
  45. package/dist/doc-standards-ck6ax2jd.md +124 -0
  46. package/dist/eval-format-7n9brja4.md +354 -0
  47. package/dist/evals-y1zcr1mc.md +11 -0
  48. package/dist/explain-config-1yaq3zzd.md +424 -0
  49. package/dist/files-dzt0wxn3.md +918 -0
  50. package/dist/{index-3t354nbw.html → index-0qts7wfk.html} +2 -2
  51. package/{assets/ui-dist/assets/index-a3ZL0_Jr.js → dist/index-BC_iUF2d-3h9mqazs.js} +2 -2
  52. package/dist/index-BxpvDT_K-62864175.css +1 -0
  53. package/{assets/ui-dist/assets/index-BESZeTO5.js → dist/index-C7gAokRv-zr7dzkba.js} +2 -2
  54. package/dist/{index-B3b7PPgV-zetkb01w.js → index-exyr_Djl-ksys08wc.js} +424 -424
  55. package/dist/integration-actions-xang7tma.md +659 -0
  56. package/dist/integrations-k5ss07f4.md +266 -0
  57. package/dist/interfaces-6t0bpxs4.md +97 -0
  58. package/dist/jetbrains-mono-cyrillic-wght-normal-D73BlboJ-6vpq1jfh.woff2 +0 -0
  59. package/dist/jetbrains-mono-greek-wght-normal-Bw9x6K1M-jv5dqe09.woff2 +0 -0
  60. package/dist/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a-h2ysfc3f.woff2 +0 -0
  61. package/dist/jetbrains-mono-latin-wght-normal-B9CIFXIH-hj2e42wt.woff2 +0 -0
  62. package/dist/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq-wttk1pwp.woff2 +0 -0
  63. package/dist/knowledge-bases-20rg8p5c.md +456 -0
  64. package/dist/llm-debugging-76ng8rzm.md +209 -0
  65. package/dist/mcp-server-n1asyzhv.md +112 -0
  66. package/dist/messages-gkkg0b54.md +769 -0
  67. package/dist/model-configuration-f273kgd0.md +201 -0
  68. package/dist/multi-agent-dashboard-1mdfjdts.md +70 -0
  69. package/dist/pages-xn18xn73.md +273 -0
  70. package/dist/patterns-mistakes-5vb99dhf.md +469 -0
  71. package/dist/plugins-ttnx8f49.md +280 -0
  72. package/dist/tables-x45t36dd.md +1136 -0
  73. package/dist/tags-46td3qrg.md +516 -0
  74. package/dist/test-patterns-hj47w4nc.md +355 -0
  75. package/dist/testing-workflow-2vkp932f.md +225 -0
  76. package/dist/tools-wk7bq24z.md +581 -0
  77. package/dist/trace-summarization-hdyx1c2e.md +313 -0
  78. package/dist/traces-and-logs-f2kjfh19.md +203 -0
  79. package/dist/triggers-4j2cc86x.md +719 -0
  80. package/dist/ui-server-entry.js +1839 -791
  81. package/dist/workflow-steps-mrdrf1c5.md +851 -0
  82. package/dist/workflows-gadx3s69.md +1408 -0
  83. package/dist/zai-agent-reference-e6644aad.md +454 -0
  84. package/dist/zai-complete-guide-mnna5d6f.md +1408 -0
  85. package/package.json +4 -4
  86. package/assets/ui-dist/assets/index-DIFGtVmI.css +0 -1
  87. package/dist/index-DIFGtVmI-d4dhjg97.css +0 -1
@@ -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
+ ```