@bratsos/workflow-engine 0.0.11
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/README.md +986 -0
- package/package.json +112 -0
- package/skills/workflow-engine/SKILL.md +539 -0
- package/skills/workflow-engine/references/01-stage-definitions.md +535 -0
- package/skills/workflow-engine/references/02-workflow-builder.md +421 -0
- package/skills/workflow-engine/references/03-runtime-setup.md +497 -0
- package/skills/workflow-engine/references/04-ai-integration.md +587 -0
- package/skills/workflow-engine/references/05-persistence-setup.md +614 -0
- package/skills/workflow-engine/references/06-async-batch-stages.md +514 -0
- package/skills/workflow-engine/references/07-testing-patterns.md +546 -0
- package/skills/workflow-engine/references/08-common-patterns.md +503 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
# Runtime Setup
|
|
2
|
+
|
|
3
|
+
Complete guide for configuring and running WorkflowRuntime.
|
|
4
|
+
|
|
5
|
+
## Creating a Runtime
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { createWorkflowRuntime } from "@bratsos/workflow-engine";
|
|
9
|
+
import {
|
|
10
|
+
createPrismaWorkflowPersistence,
|
|
11
|
+
createPrismaJobQueue,
|
|
12
|
+
createPrismaAICallLogger,
|
|
13
|
+
} from "@bratsos/workflow-engine/persistence/prisma";
|
|
14
|
+
import { PrismaClient } from "@prisma/client";
|
|
15
|
+
|
|
16
|
+
const prisma = new PrismaClient();
|
|
17
|
+
|
|
18
|
+
// PostgreSQL (default)
|
|
19
|
+
const runtime = createWorkflowRuntime({
|
|
20
|
+
// Required
|
|
21
|
+
persistence: createPrismaWorkflowPersistence(prisma),
|
|
22
|
+
jobQueue: createPrismaJobQueue(prisma),
|
|
23
|
+
registry: {
|
|
24
|
+
getWorkflow: (id) => workflowMap[id] ?? null,
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
// Optional
|
|
28
|
+
aiCallLogger: createPrismaAICallLogger(prisma),
|
|
29
|
+
pollIntervalMs: 10000,
|
|
30
|
+
jobPollIntervalMs: 1000,
|
|
31
|
+
staleJobThresholdMs: 60000,
|
|
32
|
+
workerId: "worker-1",
|
|
33
|
+
getWorkflowPriority: (id) => priorityMap[id] ?? 5,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// SQLite - pass databaseType to persistence and job queue
|
|
37
|
+
const runtime = createWorkflowRuntime({
|
|
38
|
+
persistence: createPrismaWorkflowPersistence(prisma, { databaseType: "sqlite" }),
|
|
39
|
+
jobQueue: createPrismaJobQueue(prisma, { databaseType: "sqlite" }),
|
|
40
|
+
registry: { getWorkflow: (id) => workflowMap[id] ?? null },
|
|
41
|
+
aiCallLogger: createPrismaAICallLogger(prisma),
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## WorkflowRuntimeConfig
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
interface WorkflowRuntimeConfig {
|
|
49
|
+
/** Persistence implementation (required) */
|
|
50
|
+
persistence: WorkflowPersistence;
|
|
51
|
+
|
|
52
|
+
/** Job queue implementation (required) */
|
|
53
|
+
jobQueue: JobQueue;
|
|
54
|
+
|
|
55
|
+
/** Workflow registry (required) */
|
|
56
|
+
registry: WorkflowRegistry;
|
|
57
|
+
|
|
58
|
+
/** AI call logger for createAIHelper (optional) */
|
|
59
|
+
aiCallLogger?: AICallLogger;
|
|
60
|
+
|
|
61
|
+
/** Orchestration poll interval in ms (default: 10000) */
|
|
62
|
+
pollIntervalMs?: number;
|
|
63
|
+
|
|
64
|
+
/** Job dequeue interval in ms (default: 1000) */
|
|
65
|
+
jobPollIntervalMs?: number;
|
|
66
|
+
|
|
67
|
+
/** Worker ID (default: auto-generated) */
|
|
68
|
+
workerId?: string;
|
|
69
|
+
|
|
70
|
+
/** Stale job threshold in ms (default: 60000) */
|
|
71
|
+
staleJobThresholdMs?: number;
|
|
72
|
+
|
|
73
|
+
/** Function to determine workflow priority */
|
|
74
|
+
getWorkflowPriority?: (workflowId: string) => number;
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## WorkflowRegistry
|
|
79
|
+
|
|
80
|
+
The registry maps workflow IDs to workflow definitions:
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
interface WorkflowRegistry {
|
|
84
|
+
getWorkflow(workflowId: string): Workflow<any, any, any> | null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Simple implementation
|
|
88
|
+
const registry: WorkflowRegistry = {
|
|
89
|
+
getWorkflow: (id) => {
|
|
90
|
+
const workflows = {
|
|
91
|
+
"document-analysis": documentAnalysisWorkflow,
|
|
92
|
+
"data-processing": dataProcessingWorkflow,
|
|
93
|
+
};
|
|
94
|
+
return workflows[id] ?? null;
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// With type safety
|
|
99
|
+
const workflowMap: Record<string, Workflow<any, any, any>> = {
|
|
100
|
+
"document-analysis": documentAnalysisWorkflow,
|
|
101
|
+
"data-processing": dataProcessingWorkflow,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const registry: WorkflowRegistry = {
|
|
105
|
+
getWorkflow: (id) => workflowMap[id] ?? null,
|
|
106
|
+
};
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Lifecycle Methods
|
|
110
|
+
|
|
111
|
+
### start()
|
|
112
|
+
|
|
113
|
+
Start the runtime as a worker that processes jobs and polls for state changes.
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
await runtime.start();
|
|
117
|
+
// Runtime is now:
|
|
118
|
+
// - Polling for pending workflows
|
|
119
|
+
// - Processing jobs from the queue
|
|
120
|
+
// - Checking suspended stages
|
|
121
|
+
// - Handling graceful shutdown on SIGTERM/SIGINT
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### stop()
|
|
125
|
+
|
|
126
|
+
Stop the runtime gracefully.
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
runtime.stop();
|
|
130
|
+
// Stops polling and job processing
|
|
131
|
+
// Current job completes before stopping
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Creating and Running Workflows
|
|
135
|
+
|
|
136
|
+
### createRun(options)
|
|
137
|
+
|
|
138
|
+
Create a new workflow run. The runtime picks it up automatically on the next poll.
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
interface CreateRunOptions {
|
|
142
|
+
workflowId: string; // Required
|
|
143
|
+
input: Record<string, unknown>; // Required
|
|
144
|
+
config?: Record<string, unknown>; // Optional
|
|
145
|
+
priority?: number; // Optional (default: 5)
|
|
146
|
+
metadata?: Record<string, unknown>; // Optional domain-specific data
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const { workflowRunId } = await runtime.createRun({
|
|
150
|
+
workflowId: "document-analysis",
|
|
151
|
+
input: { documentUrl: "https://example.com/doc.pdf" },
|
|
152
|
+
config: {
|
|
153
|
+
extract: { maxLength: 5000 },
|
|
154
|
+
},
|
|
155
|
+
priority: 8, // Higher = more important
|
|
156
|
+
metadata: {
|
|
157
|
+
userId: "user-123",
|
|
158
|
+
requestId: "req-456",
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The method:
|
|
164
|
+
1. Validates the workflow exists in the registry
|
|
165
|
+
2. Validates input against the workflow's input schema
|
|
166
|
+
3. Merges provided config with workflow defaults
|
|
167
|
+
4. Validates merged config against all stage config schemas
|
|
168
|
+
5. Creates a WorkflowRun record with status PENDING
|
|
169
|
+
|
|
170
|
+
### transitionWorkflow(workflowRunId)
|
|
171
|
+
|
|
172
|
+
Manually trigger workflow state transition (usually handled automatically).
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
await runtime.transitionWorkflow(workflowRunId);
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### pollSuspendedStages()
|
|
179
|
+
|
|
180
|
+
Manually check suspended stages (usually handled automatically).
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
await runtime.pollSuspendedStages();
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## AI Helper Integration
|
|
187
|
+
|
|
188
|
+
### createAIHelper(topic, logContext?)
|
|
189
|
+
|
|
190
|
+
Create an AIHelper bound to the runtime's logger.
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
// Simple usage
|
|
194
|
+
const ai = runtime.createAIHelper("my-task");
|
|
195
|
+
|
|
196
|
+
// With log context (for batch operations)
|
|
197
|
+
const logContext = runtime.createLogContext(workflowRunId, stageRecordId);
|
|
198
|
+
const ai = runtime.createAIHelper(`workflow.${workflowRunId}`, logContext);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### createLogContext(workflowRunId, stageRecordId)
|
|
202
|
+
|
|
203
|
+
Create a log context for AIHelper (enables batch logging to persistence).
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
const logContext = runtime.createLogContext(workflowRunId, stageRecordId);
|
|
207
|
+
// { workflowRunId, stageRecordId, createLog: fn }
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Complete Setup Example
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
import {
|
|
214
|
+
createWorkflowRuntime,
|
|
215
|
+
WorkflowBuilder,
|
|
216
|
+
defineStage,
|
|
217
|
+
} from "@bratsos/workflow-engine";
|
|
218
|
+
import {
|
|
219
|
+
createPrismaWorkflowPersistence,
|
|
220
|
+
createPrismaJobQueue,
|
|
221
|
+
createPrismaAICallLogger,
|
|
222
|
+
} from "@bratsos/workflow-engine/persistence/prisma";
|
|
223
|
+
import { PrismaClient } from "@prisma/client";
|
|
224
|
+
import { z } from "zod";
|
|
225
|
+
|
|
226
|
+
// Initialize Prisma
|
|
227
|
+
const prisma = new PrismaClient();
|
|
228
|
+
|
|
229
|
+
// Define stages
|
|
230
|
+
const helloStage = defineStage({
|
|
231
|
+
id: "hello",
|
|
232
|
+
name: "Hello Stage",
|
|
233
|
+
schemas: {
|
|
234
|
+
input: z.object({ name: z.string() }),
|
|
235
|
+
output: z.object({ greeting: z.string() }),
|
|
236
|
+
config: z.object({ prefix: z.string().default("Hello") }),
|
|
237
|
+
},
|
|
238
|
+
async execute(ctx) {
|
|
239
|
+
return {
|
|
240
|
+
output: { greeting: `${ctx.config.prefix}, ${ctx.input.name}!` },
|
|
241
|
+
};
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Build workflow
|
|
246
|
+
const helloWorkflow = new WorkflowBuilder(
|
|
247
|
+
"hello-workflow",
|
|
248
|
+
"Hello Workflow",
|
|
249
|
+
"A simple greeting workflow",
|
|
250
|
+
z.object({ name: z.string() }),
|
|
251
|
+
z.object({ greeting: z.string() })
|
|
252
|
+
)
|
|
253
|
+
.pipe(helloStage)
|
|
254
|
+
.build();
|
|
255
|
+
|
|
256
|
+
// Create registry
|
|
257
|
+
const registry = {
|
|
258
|
+
getWorkflow: (id: string) => {
|
|
259
|
+
if (id === "hello-workflow") return helloWorkflow;
|
|
260
|
+
return null;
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// Create runtime
|
|
265
|
+
const runtime = createWorkflowRuntime({
|
|
266
|
+
persistence: createPrismaWorkflowPersistence(prisma),
|
|
267
|
+
jobQueue: createPrismaJobQueue(prisma),
|
|
268
|
+
aiCallLogger: createPrismaAICallLogger(prisma),
|
|
269
|
+
registry,
|
|
270
|
+
pollIntervalMs: 5000,
|
|
271
|
+
jobPollIntervalMs: 500,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Start runtime
|
|
275
|
+
async function main() {
|
|
276
|
+
console.log("Starting runtime...");
|
|
277
|
+
await runtime.start();
|
|
278
|
+
|
|
279
|
+
// Create a workflow run
|
|
280
|
+
const { workflowRunId } = await runtime.createRun({
|
|
281
|
+
workflowId: "hello-workflow",
|
|
282
|
+
input: { name: "World" },
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
console.log(`Created workflow run: ${workflowRunId}`);
|
|
286
|
+
|
|
287
|
+
// Runtime will automatically:
|
|
288
|
+
// 1. Pick up the pending workflow
|
|
289
|
+
// 2. Enqueue the first stage
|
|
290
|
+
// 3. Execute the stage
|
|
291
|
+
// 4. Mark workflow as completed
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
main().catch(console.error);
|
|
295
|
+
|
|
296
|
+
// Graceful shutdown
|
|
297
|
+
process.on("SIGTERM", () => {
|
|
298
|
+
runtime.stop();
|
|
299
|
+
prisma.$disconnect();
|
|
300
|
+
});
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## Rerunning Workflows from a Specific Stage
|
|
304
|
+
|
|
305
|
+
You can rerun a workflow starting from a specific stage, skipping earlier stages and using their persisted outputs. This is useful for:
|
|
306
|
+
- Retrying after a stage failure (fix the bug, rerun from the failed stage)
|
|
307
|
+
- Re-processing data with updated stage logic
|
|
308
|
+
- Testing specific stages in isolation
|
|
309
|
+
|
|
310
|
+
### Using WorkflowExecutor.execute() with fromStage
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
import { WorkflowExecutor } from "@bratsos/workflow-engine";
|
|
314
|
+
|
|
315
|
+
// Given: A workflow that has already been run (stages 1-4 completed)
|
|
316
|
+
const executor = new WorkflowExecutor(
|
|
317
|
+
workflow,
|
|
318
|
+
workflowRunId,
|
|
319
|
+
workflowType,
|
|
320
|
+
{ persistence, aiLogger }
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
// Rerun from stage 3 - skips stages 1-2, runs 3-4
|
|
324
|
+
const result = await executor.execute(
|
|
325
|
+
input, // Original input (not used when fromStage is set)
|
|
326
|
+
config,
|
|
327
|
+
{ fromStage: "stage-3" }
|
|
328
|
+
);
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### How It Works
|
|
332
|
+
|
|
333
|
+
1. **Finds the execution group** containing the specified stage
|
|
334
|
+
2. **Loads input** from the previous stage's persisted output (or workflow input if first stage)
|
|
335
|
+
3. **Rebuilds workflowContext** from all completed stages before the target group
|
|
336
|
+
4. **Deletes stage records** for the target stage and all subsequent stages (clean re-execution)
|
|
337
|
+
5. **Executes** from the target stage forward
|
|
338
|
+
|
|
339
|
+
### Requirements
|
|
340
|
+
|
|
341
|
+
- **Previous stages must have been executed** - their outputs must be persisted
|
|
342
|
+
- **Stage must exist** in the workflow definition
|
|
343
|
+
|
|
344
|
+
### Error Handling
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
// Error: Stage doesn't exist
|
|
348
|
+
await executor.execute(input, config, { fromStage: "non-existent" });
|
|
349
|
+
// Throws: Stage "non-existent" not found in workflow "my-workflow"
|
|
350
|
+
|
|
351
|
+
// Error: No prior execution
|
|
352
|
+
await executor.execute(input, config, { fromStage: "stage-3" });
|
|
353
|
+
// Throws: Cannot rerun from stage "stage-3": no completed stages found before execution group 3
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### Common Use Cases
|
|
357
|
+
|
|
358
|
+
**Retry After Failure:**
|
|
359
|
+
```typescript
|
|
360
|
+
// Stage 3 failed, you fixed the bug
|
|
361
|
+
await executor.execute(input, config, { fromStage: "stage-3" });
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
**Re-process with Updated Logic:**
|
|
365
|
+
```typescript
|
|
366
|
+
// Updated stage-2 implementation, want to rerun from there
|
|
367
|
+
await executor.execute(input, config, { fromStage: "stage-2" });
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
**Fresh Start from Beginning:**
|
|
371
|
+
```typescript
|
|
372
|
+
// Rerun entire workflow
|
|
373
|
+
await executor.execute(input, config, { fromStage: "stage-1" });
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### workflowContext Availability
|
|
377
|
+
|
|
378
|
+
When rerunning from a stage, `ctx.workflowContext` contains outputs from all stages **before** the target group:
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
// Rerunning from stage-3 (group 3)
|
|
382
|
+
// ctx.workflowContext contains:
|
|
383
|
+
// - "stage-1": { ... } // from group 1
|
|
384
|
+
// - "stage-2": { ... } // from group 2
|
|
385
|
+
// - NOT "stage-3" or later
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## Worker Deployment Patterns
|
|
389
|
+
|
|
390
|
+
### Single Worker
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
// worker.ts
|
|
394
|
+
const runtime = createWorkflowRuntime({ ... });
|
|
395
|
+
await runtime.start();
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Multiple Workers (Horizontal Scaling)
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
// Each worker gets a unique ID
|
|
402
|
+
const workerId = `worker-${process.env.POD_NAME || process.pid}`;
|
|
403
|
+
|
|
404
|
+
const runtime = createWorkflowRuntime({
|
|
405
|
+
...config,
|
|
406
|
+
workerId,
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
await runtime.start();
|
|
410
|
+
// Workers compete for jobs using atomic dequeue
|
|
411
|
+
// Each job is processed by exactly one worker
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### API Server + Separate Workers
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
// api-server.ts - Only creates runs, doesn't process
|
|
418
|
+
const runtime = createWorkflowRuntime({ ...config });
|
|
419
|
+
// Don't call runtime.start()
|
|
420
|
+
|
|
421
|
+
app.post("/workflows/:id/runs", async (req, res) => {
|
|
422
|
+
const { workflowRunId } = await runtime.createRun({
|
|
423
|
+
workflowId: req.params.id,
|
|
424
|
+
input: req.body,
|
|
425
|
+
});
|
|
426
|
+
res.json({ workflowRunId });
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// worker.ts - Only processes, doesn't create
|
|
430
|
+
const runtime = createWorkflowRuntime({ ...config });
|
|
431
|
+
await runtime.start();
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
## Configuration Recommendations
|
|
435
|
+
|
|
436
|
+
### Development
|
|
437
|
+
|
|
438
|
+
```typescript
|
|
439
|
+
const runtime = createWorkflowRuntime({
|
|
440
|
+
...config,
|
|
441
|
+
pollIntervalMs: 2000, // Fast polling for development
|
|
442
|
+
jobPollIntervalMs: 500, // Quick job pickup
|
|
443
|
+
staleJobThresholdMs: 30000, // Short timeout
|
|
444
|
+
});
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### Production
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
const runtime = createWorkflowRuntime({
|
|
451
|
+
...config,
|
|
452
|
+
pollIntervalMs: 10000, // Standard polling
|
|
453
|
+
jobPollIntervalMs: 1000, // Balance between latency and DB load
|
|
454
|
+
staleJobThresholdMs: 60000, // Allow for longer processing
|
|
455
|
+
workerId: `worker-${process.env.HOSTNAME}`,
|
|
456
|
+
});
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
### High-Throughput
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
const runtime = createWorkflowRuntime({
|
|
463
|
+
...config,
|
|
464
|
+
pollIntervalMs: 5000, // More frequent orchestration
|
|
465
|
+
jobPollIntervalMs: 100, // Aggressive job pickup
|
|
466
|
+
staleJobThresholdMs: 120000, // Longer timeout for long jobs
|
|
467
|
+
});
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
## Monitoring
|
|
471
|
+
|
|
472
|
+
The runtime logs key events to console:
|
|
473
|
+
|
|
474
|
+
```
|
|
475
|
+
[Runtime] Starting worker worker-12345-hostname
|
|
476
|
+
[Runtime] Poll interval: 10000ms, Job poll: 1000ms
|
|
477
|
+
[Runtime] Created WorkflowRun abc123 for document-analysis
|
|
478
|
+
[Runtime] Found 1 pending workflows
|
|
479
|
+
[Runtime] Started workflow abc123
|
|
480
|
+
[Runtime] Processing stage extract for workflow abc123
|
|
481
|
+
[Runtime] Worker worker-12345-hostname: processed 10 jobs
|
|
482
|
+
[Runtime] Workflow abc123 completed
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
For production monitoring, integrate with your observability stack:
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
// Custom logging
|
|
489
|
+
const originalLog = console.log;
|
|
490
|
+
console.log = (...args) => {
|
|
491
|
+
if (args[0]?.includes("[Runtime]")) {
|
|
492
|
+
metrics.increment("workflow.runtime.log");
|
|
493
|
+
logger.info(args.join(" "));
|
|
494
|
+
}
|
|
495
|
+
originalLog(...args);
|
|
496
|
+
};
|
|
497
|
+
```
|