Skip to content

Commit 26c3e27

Browse files
committed
fix(workflow-engine): replay completed void steps on restart
1 parent d7a894e commit 26c3e27

File tree

2 files changed

+50
-1
lines changed

2 files changed

+50
-1
lines changed

rivetkit-typescript/packages/workflow-engine/src/context.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,12 +380,27 @@ export class WorkflowContextImpl implements WorkflowContextInterface {
380380
return stepData.output as T;
381381
}
382382

383-
// Check if we should retry
383+
// Check if we should retry.
384+
//
385+
// Important: steps that *return undefined* (i.e. "void" steps) are valid and
386+
// must still be treated as completed on restart. Since JSON omits properties
387+
// with value `undefined`, we cannot rely on `stepData.output !== undefined`
388+
// to determine completion. Prefer metadata.status for completion tracking.
384389
const metadata = await loadMetadata(
385390
this.storage,
386391
this.driver,
387392
existing.id,
388393
);
394+
395+
if (metadata.status === "completed") {
396+
this.log("debug", {
397+
msg: "replaying completed step from metadata",
398+
step: config.name,
399+
key,
400+
});
401+
return stepData.output as T;
402+
}
403+
389404
const maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
390405

391406
if (metadata.attempts >= maxRetries) {

rivetkit-typescript/packages/workflow-engine/tests/steps.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,40 @@ for (const mode of modes) {
7272
expect(callCount).toBe(1);
7373
});
7474

75+
it("should treat void step outputs as completed on restart", async () => {
76+
let callCount = 0;
77+
78+
const workflow = async (ctx: WorkflowContextInterface) => {
79+
await ctx.step("void-step", async () => {
80+
callCount += 1;
81+
// return undefined (void)
82+
});
83+
return "done";
84+
};
85+
86+
const first = await runWorkflow(
87+
"wf-void",
88+
workflow,
89+
undefined,
90+
driver,
91+
{ mode },
92+
).result;
93+
expect(first.state).toBe("completed");
94+
expect(first.output).toBe("done");
95+
expect(callCount).toBe(1);
96+
97+
const second = await runWorkflow(
98+
"wf-void",
99+
workflow,
100+
undefined,
101+
driver,
102+
{ mode },
103+
).result;
104+
expect(second.state).toBe("completed");
105+
expect(second.output).toBe("done");
106+
expect(callCount).toBe(1);
107+
});
108+
75109
it("should execute multiple steps in sequence", async () => {
76110
const workflow = async (ctx: WorkflowContextInterface) => {
77111
const a = await ctx.step("step-a", async () => 1);

0 commit comments

Comments
 (0)