Skip to content

Commit 7f923d2

Browse files
authored
feat(workflow): add execution primitives for step context (#1102)
* feat(workflow): add execution primitives for step context * fix(workflow): handle bail without result and close bail step span * fix(workflow): unify bail handling and type bail payload * fix(workflow): update bail state before step-end hook
1 parent 314ed40 commit 7f923d2

File tree

9 files changed

+799
-88
lines changed

9 files changed

+799
-88
lines changed

.changeset/fresh-otters-melt.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
---
2+
"@voltagent/core": minor
3+
---
4+
5+
feat: add workflow execution primitives (`bail`, `abort`, `getStepResult`, `getInitData`)
6+
7+
### What's New
8+
9+
Step execution context now includes four new primitives:
10+
11+
- `bail(result?)`: complete the workflow early with a custom final result
12+
- `abort()`: cancel the workflow immediately
13+
- `getStepResult(stepId)`: get a prior step output directly (returns `null` if not available)
14+
- `getInitData()`: get the original workflow input (stable across resume paths)
15+
16+
These primitives are available in all step contexts, including nested step flows.
17+
18+
### Example: Early Complete with `bail`
19+
20+
```ts
21+
const workflow = createWorkflowChain({
22+
id: "bail-demo",
23+
input: z.object({ amount: z.number() }),
24+
result: z.object({ status: z.string() }),
25+
})
26+
.andThen({
27+
id: "risk-check",
28+
execute: async ({ data, bail }) => {
29+
if (data.amount > 10_000) {
30+
bail({ status: "rejected" });
31+
}
32+
return { status: "approved" };
33+
},
34+
})
35+
.andThen({
36+
id: "never-runs-on-bail",
37+
execute: async () => ({ status: "approved" }),
38+
});
39+
```
40+
41+
### Example: Cancel with `abort`
42+
43+
```ts
44+
const workflow = createWorkflowChain({
45+
id: "abort-demo",
46+
input: z.object({ requestId: z.string() }),
47+
result: z.object({ done: z.boolean() }),
48+
})
49+
.andThen({
50+
id: "authorization",
51+
execute: async ({ abort }) => {
52+
abort(); // terminal status: cancelled
53+
},
54+
})
55+
.andThen({
56+
id: "never-runs-on-abort",
57+
execute: async () => ({ done: true }),
58+
});
59+
```
60+
61+
### Example: Use `getStepResult` + `getInitData`
62+
63+
```ts
64+
const workflow = createWorkflowChain({
65+
id: "introspection-demo",
66+
input: z.object({ userId: z.string(), value: z.number() }),
67+
result: z.object({ total: z.number(), userId: z.string() }),
68+
})
69+
.andThen({
70+
id: "step-1",
71+
execute: async ({ data }) => ({ partial: data.value + 1 }),
72+
})
73+
.andThen({
74+
id: "step-2",
75+
execute: async ({ getStepResult, getInitData }) => {
76+
const s1 = getStepResult<{ partial: number }>("step-1");
77+
const init = getInitData();
78+
79+
return {
80+
total: (s1?.partial ?? 0) + init.value,
81+
userId: init.userId,
82+
};
83+
},
84+
});
85+
```

packages/core/src/test-utils/mocks/workflows.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,19 @@ export function createMockWorkflowExecuteContext(
2424
data: overrides.data ?? ({} as DangerouslyAllowAny),
2525
state: overrides.state ?? ({} as DangerouslyAllowAny),
2626
getStepData: overrides.getStepData ?? (() => undefined),
27+
getStepResult: overrides.getStepResult ?? (() => null),
28+
getInitData: overrides.getInitData ?? (() => ({}) as DangerouslyAllowAny),
2729
suspend: overrides.suspend ?? vi.fn(),
30+
bail:
31+
overrides.bail ??
32+
(() => {
33+
throw new Error("WORKFLOW_BAIL_NOT_CONFIGURED");
34+
}),
35+
abort:
36+
overrides.abort ??
37+
(() => {
38+
throw new Error("WORKFLOW_ABORT_NOT_CONFIGURED");
39+
}),
2840
workflowState: overrides.workflowState ?? {},
2941
setWorkflowState: (() => undefined) as MockWorkflowExecuteContext["setWorkflowState"],
3042
logger: overrides.logger ?? {

packages/core/src/workflow/core.spec-d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,11 @@ describe("non-chaining API type inference", () => {
104104
(update: WorkflowStateUpdater) => void
105105
>();
106106
expectTypeOf<Context["suspend"]>().toBeFunction();
107+
expectTypeOf<Context["bail"]>().toBeFunction();
108+
expectTypeOf<Context["abort"]>().toBeFunction();
107109
expectTypeOf<Context["getStepData"]>().toBeFunction();
110+
expectTypeOf<Context["getStepResult"]>().toBeFunction();
111+
expectTypeOf<Context["getInitData"]>().toBeFunction();
108112
expectTypeOf<Context["logger"]>().not.toBeNever();
109113
expectTypeOf<Context["writer"]>().not.toBeNever();
110114
});

0 commit comments

Comments
 (0)