workflow 4.1.0-beta.62 → 4.2.0-beta.64
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/dist/api-workflow.d.ts +1 -1
- package/dist/api-workflow.d.ts.map +1 -1
- package/dist/api-workflow.js +1 -1
- package/dist/api.d.ts +1 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +1 -1
- package/docs/api-reference/workflow/create-hook.mdx +1 -1
- package/docs/api-reference/workflow/create-webhook.mdx +13 -80
- package/docs/api-reference/workflow-api/get-run.mdx +43 -0
- package/docs/api-reference/workflow-api/resume-webhook.mdx +9 -156
- package/docs/deploying/index.mdx +2 -0
- package/docs/deploying/world/vercel-world.mdx +2 -0
- package/docs/foundations/hooks.mdx +19 -34
- package/docs/foundations/workflows-and-steps.mdx +2 -0
- package/docs/getting-started/astro.mdx +2 -0
- package/docs/getting-started/express.mdx +2 -0
- package/docs/getting-started/fastify.mdx +2 -0
- package/docs/getting-started/hono.mdx +2 -0
- package/docs/getting-started/nestjs.mdx +2 -0
- package/docs/getting-started/next.mdx +2 -0
- package/docs/getting-started/nitro.mdx +2 -0
- package/docs/getting-started/nuxt.mdx +2 -0
- package/docs/getting-started/sveltekit.mdx +2 -0
- package/docs/getting-started/vite.mdx +2 -0
- package/docs/meta.json +1 -0
- package/docs/testing/index.mdx +368 -0
- package/docs/testing/meta.json +4 -0
- package/package.json +11 -11
package/dist/api-workflow.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { Event, StartOptions, WorkflowReadableStreamOptions, WorkflowRun, } from '@workflow/core/runtime';
|
|
1
|
+
export type { Event, StartOptions, StopSleepOptions, StopSleepResult, WorkflowReadableStreamOptions, WorkflowRun, } from '@workflow/core/runtime';
|
|
2
2
|
export declare class Run {
|
|
3
3
|
constructor();
|
|
4
4
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-workflow.d.ts","sourceRoot":"","sources":["../src/api-workflow.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,KAAK,EACL,YAAY,EACZ,6BAA6B,EAC7B,WAAW,GACZ,MAAM,wBAAwB,CAAC;AAQhC,qBAAa,GAAG;;CAIf;AACD,eAAO,MAAM,MAAM,aAA+B,CAAC;AACnD,eAAO,MAAM,cAAc,aAAuC,CAAC;AACnE,eAAO,MAAM,UAAU,aAAmC,CAAC;AAC3D,eAAO,MAAM,aAAa,aAAsC,CAAC;AACjE,eAAO,MAAM,OAAO,aAAgC,CAAC;AACrD,eAAO,MAAM,KAAK,aAA8B,CAAC"}
|
|
1
|
+
{"version":3,"file":"api-workflow.d.ts","sourceRoot":"","sources":["../src/api-workflow.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,KAAK,EACL,YAAY,EACZ,gBAAgB,EAChB,eAAe,EACf,6BAA6B,EAC7B,WAAW,GACZ,MAAM,wBAAwB,CAAC;AAQhC,qBAAa,GAAG;;CAIf;AACD,eAAO,MAAM,MAAM,aAA+B,CAAC;AACnD,eAAO,MAAM,cAAc,aAAuC,CAAC;AACnE,eAAO,MAAM,UAAU,aAAmC,CAAC;AAC3D,eAAO,MAAM,aAAa,aAAsC,CAAC;AACjE,eAAO,MAAM,OAAO,aAAgC,CAAC;AACrD,eAAO,MAAM,KAAK,aAA8B,CAAC"}
|
package/dist/api-workflow.js
CHANGED
|
@@ -12,4 +12,4 @@ export const resumeHook = () => workflowStub('resumeHook');
|
|
|
12
12
|
export const resumeWebhook = () => workflowStub('resumeWebhook');
|
|
13
13
|
export const runStep = () => workflowStub('runStep');
|
|
14
14
|
export const start = () => workflowStub('start');
|
|
15
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
15
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXBpLXdvcmtmbG93LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2FwaS13b3JrZmxvdy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFTQSxNQUFNLFlBQVksR0FBRyxDQUFDLElBQVksRUFBRSxFQUFFO0lBQ3BDLE1BQU0sSUFBSSxLQUFLLENBQ2IsZ0VBQWdFLElBQUksMkZBQTJGLENBQ2hLLENBQUM7QUFDSixDQUFDLENBQUM7QUFFRixNQUFNLE9BQU8sR0FBRztJQUNkO1FBQ0UsWUFBWSxDQUFDLEtBQUssQ0FBQyxDQUFDO0lBQ3RCLENBQUM7Q0FDRjtBQUNELE1BQU0sQ0FBQyxNQUFNLE1BQU0sR0FBRyxHQUFHLEVBQUUsQ0FBQyxZQUFZLENBQUMsUUFBUSxDQUFDLENBQUM7QUFDbkQsTUFBTSxDQUFDLE1BQU0sY0FBYyxHQUFHLEdBQUcsRUFBRSxDQUFDLFlBQVksQ0FBQyxnQkFBZ0IsQ0FBQyxDQUFDO0FBQ25FLE1BQU0sQ0FBQyxNQUFNLFVBQVUsR0FBRyxHQUFHLEVBQUUsQ0FBQyxZQUFZLENBQUMsWUFBWSxDQUFDLENBQUM7QUFDM0QsTUFBTSxDQUFDLE1BQU0sYUFBYSxHQUFHLEdBQUcsRUFBRSxDQUFDLFlBQVksQ0FBQyxlQUFlLENBQUMsQ0FBQztBQUNqRSxNQUFNLENBQUMsTUFBTSxPQUFPLEdBQUcsR0FBRyxFQUFFLENBQUMsWUFBWSxDQUFDLFNBQVMsQ0FBQyxDQUFDO0FBQ3JELE1BQU0sQ0FBQyxNQUFNLEtBQUssR0FBRyxHQUFHLEVBQUUsQ0FBQyxZQUFZLENBQUMsT0FBTyxDQUFDLENBQUMifQ==
|
package/dist/api.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { type Event, getHookByToken, getRun, Run, resumeHook, resumeWebhook, runStep, type StartOptions, start, type WorkflowReadableStreamOptions, type WorkflowRun, } from '@workflow/core/runtime';
|
|
1
|
+
export { type Event, getHookByToken, getRun, Run, resumeHook, resumeWebhook, runStep, type StartOptions, start, type StopSleepOptions, type StopSleepResult, type WorkflowReadableStreamOptions, type WorkflowRun, } from '@workflow/core/runtime';
|
|
2
2
|
//# sourceMappingURL=api.d.ts.map
|
package/dist/api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,KAAK,EACV,cAAc,EACd,MAAM,EACN,GAAG,EACH,UAAU,EACV,aAAa,EACb,OAAO,EACP,KAAK,YAAY,EACjB,KAAK,EACL,KAAK,6BAA6B,EAClC,KAAK,WAAW,GACjB,MAAM,wBAAwB,CAAC"}
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,KAAK,EACV,cAAc,EACd,MAAM,EACN,GAAG,EACH,UAAU,EACV,aAAa,EACb,OAAO,EACP,KAAK,YAAY,EACjB,KAAK,EACL,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,6BAA6B,EAClC,KAAK,WAAW,GACjB,MAAM,wBAAwB,CAAC"}
|
package/dist/api.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { getHookByToken, getRun, Run, resumeHook, resumeWebhook, runStep, start, } from '@workflow/core/runtime';
|
|
2
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
2
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXBpLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2FwaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBRUwsY0FBYyxFQUNkLE1BQU0sRUFDTixHQUFHLEVBQ0gsVUFBVSxFQUNWLGFBQWEsRUFDYixPQUFPLEVBRVAsS0FBSyxHQUtOLE1BQU0sd0JBQXdCLENBQUMifQ==
|
|
@@ -100,7 +100,7 @@ export async function slackBotWorkflow(channelId: string) {
|
|
|
100
100
|
|
|
101
101
|
// Token constructed from channel ID
|
|
102
102
|
using hook = createHook<SlackMessage>({ // [!code highlight]
|
|
103
|
-
token: `
|
|
103
|
+
token: `slack_messages:${channelId}`, // [!code highlight]
|
|
104
104
|
}); // [!code highlight]
|
|
105
105
|
|
|
106
106
|
for await (const message of hook) {
|
|
@@ -120,46 +120,6 @@ async function processData(data: any) {
|
|
|
120
120
|
}
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
-
### Customizing Tokens
|
|
124
|
-
|
|
125
|
-
Tokens are used to identify a specific webhook. You can customize the token to be more specific to a use case.
|
|
126
|
-
|
|
127
|
-
```typescript lineNumbers
|
|
128
|
-
import { createWebhook, type RequestWithResponse } from "workflow"
|
|
129
|
-
|
|
130
|
-
async function sendAck(request: RequestWithResponse) {
|
|
131
|
-
"use step";
|
|
132
|
-
await request.respondWith(
|
|
133
|
-
new Response(JSON.stringify({ received: true }), {
|
|
134
|
-
headers: { "Content-Type": "application/json" }
|
|
135
|
-
})
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export async function githubWebhookWorkflow(repoName: string) {
|
|
140
|
-
"use workflow";
|
|
141
|
-
|
|
142
|
-
// Use a deterministic token based on the repository
|
|
143
|
-
using webhook = createWebhook({ // [!code highlight]
|
|
144
|
-
token: `github_webhook:${repoName}`, // [!code highlight]
|
|
145
|
-
}); // [!code highlight]
|
|
146
|
-
|
|
147
|
-
console.log("Configure GitHub webhook:", webhook.url);
|
|
148
|
-
|
|
149
|
-
const request = await webhook;
|
|
150
|
-
const event = await request.json();
|
|
151
|
-
|
|
152
|
-
await sendAck(request);
|
|
153
|
-
|
|
154
|
-
await deployCommit(event);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async function deployCommit(event: any) {
|
|
158
|
-
"use step";
|
|
159
|
-
// Deploy logic here
|
|
160
|
-
}
|
|
161
|
-
```
|
|
162
|
-
|
|
163
123
|
### Waiting for Multiple Requests
|
|
164
124
|
|
|
165
125
|
You can also wait for multiple requests by using the `for await...of` syntax.
|
|
@@ -167,62 +127,35 @@ You can also wait for multiple requests by using the `for await...of` syntax.
|
|
|
167
127
|
```typescript lineNumbers
|
|
168
128
|
import { createWebhook, type RequestWithResponse } from "workflow"
|
|
169
129
|
|
|
170
|
-
async function
|
|
130
|
+
async function sendAck(request: RequestWithResponse, message: string) {
|
|
171
131
|
"use step";
|
|
172
132
|
await request.respondWith(
|
|
173
|
-
|
|
174
|
-
JSON.stringify({
|
|
175
|
-
response_type: "in_channel",
|
|
176
|
-
text: message
|
|
177
|
-
}),
|
|
178
|
-
{ headers: { "Content-Type": "application/json" } }
|
|
179
|
-
)
|
|
133
|
+
Response.json({ received: true, message })
|
|
180
134
|
);
|
|
181
135
|
}
|
|
182
136
|
|
|
183
|
-
async function
|
|
137
|
+
async function processEvent(data: any) {
|
|
184
138
|
"use step";
|
|
185
|
-
|
|
186
|
-
new Response("Stopping workflow...")
|
|
187
|
-
);
|
|
139
|
+
console.log("Processing event:", data);
|
|
188
140
|
}
|
|
189
141
|
|
|
190
|
-
export async function
|
|
142
|
+
export async function eventCollectorWorkflow() {
|
|
191
143
|
"use workflow";
|
|
192
144
|
|
|
193
|
-
using webhook = createWebhook({
|
|
194
|
-
|
|
195
|
-
});
|
|
145
|
+
using webhook = createWebhook({ respondWith: "manual" });
|
|
146
|
+
console.log("Send events to:", webhook.url);
|
|
196
147
|
|
|
197
148
|
for await (const request of webhook) { // [!code highlight]
|
|
198
|
-
const
|
|
199
|
-
const command = formData.get("command");
|
|
200
|
-
const text = formData.get("text");
|
|
149
|
+
const data = await request.json();
|
|
201
150
|
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
await sendSlackResponse(request, "Checking status...");
|
|
205
|
-
|
|
206
|
-
// Process the command
|
|
207
|
-
const status = await checkSystemStatus();
|
|
208
|
-
await postToSlack(channelId, `Status: ${status}`);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (text === "stop") {
|
|
212
|
-
await sendStopResponse(request);
|
|
151
|
+
if (data.type === "done") {
|
|
152
|
+
await sendAck(request, "Workflow complete");
|
|
213
153
|
break;
|
|
214
154
|
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
async function checkSystemStatus() {
|
|
219
|
-
"use step";
|
|
220
|
-
return "All systems operational";
|
|
221
|
-
}
|
|
222
155
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
156
|
+
await sendAck(request, "Event received");
|
|
157
|
+
await processEvent(data);
|
|
158
|
+
}
|
|
226
159
|
}
|
|
227
160
|
```
|
|
228
161
|
|
|
@@ -47,6 +47,22 @@ import type { WorkflowReadableStreamOptions } from "workflow/api";
|
|
|
47
47
|
export default WorkflowReadableStreamOptions;`}
|
|
48
48
|
/>
|
|
49
49
|
|
|
50
|
+
#### StopSleepOptions
|
|
51
|
+
|
|
52
|
+
<TSDoc
|
|
53
|
+
definition={`
|
|
54
|
+
import type { StopSleepOptions } from "workflow/api";
|
|
55
|
+
export default StopSleepOptions;`}
|
|
56
|
+
/>
|
|
57
|
+
|
|
58
|
+
#### StopSleepResult
|
|
59
|
+
|
|
60
|
+
<TSDoc
|
|
61
|
+
definition={`
|
|
62
|
+
import type { StopSleepResult } from "workflow/api";
|
|
63
|
+
export default StopSleepResult;`}
|
|
64
|
+
/>
|
|
65
|
+
|
|
50
66
|
## Examples
|
|
51
67
|
|
|
52
68
|
### Basic Status Check
|
|
@@ -78,6 +94,33 @@ export async function GET(req: Request) {
|
|
|
78
94
|
}
|
|
79
95
|
```
|
|
80
96
|
|
|
97
|
+
### Wake Up a Sleeping Workflow
|
|
98
|
+
|
|
99
|
+
Interrupt pending `sleep()` calls to resume a workflow early. This is useful for testing workflows or building custom UIs that let users skip wait periods:
|
|
100
|
+
|
|
101
|
+
```typescript lineNumbers
|
|
102
|
+
import { getRun } from "workflow/api";
|
|
103
|
+
|
|
104
|
+
export async function POST(req: Request) {
|
|
105
|
+
const { runId } = await req.json();
|
|
106
|
+
const run = getRun(runId);
|
|
107
|
+
|
|
108
|
+
// Wake up all pending sleep calls
|
|
109
|
+
const { stoppedCount } = await run.wakeUp(); // [!code highlight]
|
|
110
|
+
|
|
111
|
+
return Response.json({ stoppedCount });
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
You can also target specific sleep calls by correlation ID:
|
|
116
|
+
|
|
117
|
+
```typescript lineNumbers
|
|
118
|
+
const run = getRun("my-run-id"); // @setup
|
|
119
|
+
const { stoppedCount } = await run.wakeUp({
|
|
120
|
+
correlationIds: ["wait_abc123"],
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
81
124
|
## Related Functions
|
|
82
125
|
|
|
83
126
|
- [`start()`](/docs/api-reference/workflow-api/start) - Start a new workflow and get its run ID.
|
|
@@ -56,11 +56,17 @@ Returns a `Promise<Response>` that resolves to:
|
|
|
56
56
|
|
|
57
57
|
Throws an error if the webhook token is not found or invalid.
|
|
58
58
|
|
|
59
|
-
##
|
|
59
|
+
## Usage Note
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
<Callout type="warn">
|
|
62
|
+
In most cases, you should not need to call `resumeWebhook()` directly. When you use `createWebhook()`, the framework automatically generates a random webhook token and provides a public URL at `/.well-known/workflow/v1/webhook/:token`. External systems can send HTTP requests directly to that URL.
|
|
63
|
+
|
|
64
|
+
For server-side hook resumption with deterministic tokens, use [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) with [`createHook()`](/docs/api-reference/workflow/create-hook) instead.
|
|
65
|
+
</Callout>
|
|
62
66
|
|
|
63
|
-
|
|
67
|
+
## Example
|
|
68
|
+
|
|
69
|
+
Forward an incoming HTTP request to a webhook by token:
|
|
64
70
|
|
|
65
71
|
```typescript lineNumbers
|
|
66
72
|
import { resumeWebhook } from "workflow/api";
|
|
@@ -82,159 +88,6 @@ export async function POST(request: Request) {
|
|
|
82
88
|
}
|
|
83
89
|
```
|
|
84
90
|
|
|
85
|
-
### GitHub Webhook Handler
|
|
86
|
-
|
|
87
|
-
Handle GitHub webhook events and forward them to workflows:
|
|
88
|
-
|
|
89
|
-
```typescript lineNumbers
|
|
90
|
-
import { resumeWebhook } from "workflow/api";
|
|
91
|
-
import { verifyGitHubSignature } from "@/lib/github";
|
|
92
|
-
|
|
93
|
-
export async function POST(request: Request) {
|
|
94
|
-
// Extract repository name from URL
|
|
95
|
-
const url = new URL(request.url);
|
|
96
|
-
const repo = url.pathname.split("/").pop();
|
|
97
|
-
|
|
98
|
-
// Verify GitHub signature
|
|
99
|
-
const signature = request.headers.get("x-hub-signature-256");
|
|
100
|
-
const isValid = await verifyGitHubSignature(request, signature);
|
|
101
|
-
|
|
102
|
-
if (!isValid) {
|
|
103
|
-
return new Response("Invalid signature", { status: 401 });
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Construct deterministic token
|
|
107
|
-
const token = `github_webhook:${repo}`;
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
const response = await resumeWebhook(token, request); // [!code highlight]
|
|
111
|
-
return response;
|
|
112
|
-
} catch (error) {
|
|
113
|
-
return new Response("Workflow not found", { status: 404 });
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
### Slack Slash Command Handler
|
|
119
|
-
|
|
120
|
-
Process Slack slash commands and route them to workflow webhooks:
|
|
121
|
-
|
|
122
|
-
```typescript lineNumbers
|
|
123
|
-
import { resumeWebhook } from "workflow/api";
|
|
124
|
-
|
|
125
|
-
export async function POST(request: Request) {
|
|
126
|
-
const formData = await request.formData();
|
|
127
|
-
const channelId = formData.get("channel_id") as string;
|
|
128
|
-
const command = formData.get("command") as string;
|
|
129
|
-
|
|
130
|
-
// Verify Slack request signature
|
|
131
|
-
const slackSignature = request.headers.get("x-slack-signature");
|
|
132
|
-
if (!slackSignature) {
|
|
133
|
-
return new Response("Unauthorized", { status: 401 });
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Construct token from channel ID
|
|
137
|
-
const token = `slack_command:${channelId}`;
|
|
138
|
-
|
|
139
|
-
try {
|
|
140
|
-
const response = await resumeWebhook(token, request); // [!code highlight]
|
|
141
|
-
return response;
|
|
142
|
-
} catch (error) {
|
|
143
|
-
// If no workflow is listening, return a default response
|
|
144
|
-
return new Response(
|
|
145
|
-
JSON.stringify({
|
|
146
|
-
response_type: "ephemeral",
|
|
147
|
-
text: "No active workflow for this channel"
|
|
148
|
-
}),
|
|
149
|
-
{
|
|
150
|
-
headers: { "Content-Type": "application/json" }
|
|
151
|
-
}
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
### Multi-Tenant Webhook Router
|
|
158
|
-
|
|
159
|
-
Route webhooks to different workflows based on tenant/organization:
|
|
160
|
-
|
|
161
|
-
```typescript lineNumbers
|
|
162
|
-
import { resumeWebhook } from "workflow/api";
|
|
163
|
-
|
|
164
|
-
export async function POST(request: Request) {
|
|
165
|
-
const url = new URL(request.url);
|
|
166
|
-
|
|
167
|
-
// Extract tenant and webhook ID from path
|
|
168
|
-
// e.g., /api/webhooks/tenant-123/webhook-abc
|
|
169
|
-
const [, , , tenantId, webhookId] = url.pathname.split("/");
|
|
170
|
-
|
|
171
|
-
if (!tenantId || !webhookId) {
|
|
172
|
-
return new Response("Invalid webhook URL", { status: 400 });
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Verify API key for tenant
|
|
176
|
-
const apiKey = request.headers.get("authorization");
|
|
177
|
-
const isAuthorized = await verifyTenantApiKey(tenantId, apiKey);
|
|
178
|
-
|
|
179
|
-
if (!isAuthorized) {
|
|
180
|
-
return new Response("Unauthorized", { status: 401 });
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Construct namespaced token
|
|
184
|
-
const token = `tenant:${tenantId}:webhook:${webhookId}`;
|
|
185
|
-
|
|
186
|
-
try {
|
|
187
|
-
const response = await resumeWebhook(token, request); // [!code highlight]
|
|
188
|
-
return response;
|
|
189
|
-
} catch (error) {
|
|
190
|
-
return new Response("Webhook not found or expired", { status: 404 });
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
async function verifyTenantApiKey(tenantId: string, apiKey: string | null) {
|
|
195
|
-
// Verify API key logic
|
|
196
|
-
return apiKey === process.env[`TENANT_${tenantId}_API_KEY`];
|
|
197
|
-
}
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
### Server Action (Next.js)
|
|
201
|
-
|
|
202
|
-
Use `resumeWebhook` in a Next.js server action:
|
|
203
|
-
|
|
204
|
-
```typescript lineNumbers
|
|
205
|
-
"use server";
|
|
206
|
-
|
|
207
|
-
import { resumeWebhook } from "workflow/api";
|
|
208
|
-
|
|
209
|
-
export async function triggerWebhook(
|
|
210
|
-
token: string,
|
|
211
|
-
payload: Record<string, any>
|
|
212
|
-
) {
|
|
213
|
-
// Create a Request object from the payload
|
|
214
|
-
const request = new Request("http://localhost/webhook", {
|
|
215
|
-
method: "POST",
|
|
216
|
-
headers: {
|
|
217
|
-
"Content-Type": "application/json",
|
|
218
|
-
},
|
|
219
|
-
body: JSON.stringify(payload),
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
try {
|
|
223
|
-
const response = await resumeWebhook(token, request);
|
|
224
|
-
|
|
225
|
-
// Parse and return the response
|
|
226
|
-
const contentType = response.headers.get("content-type");
|
|
227
|
-
if (contentType?.includes("application/json")) {
|
|
228
|
-
return await response.json();
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return await response.text();
|
|
232
|
-
} catch (error) {
|
|
233
|
-
throw new Error("Webhook not found");
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
```
|
|
237
|
-
|
|
238
91
|
## Related Functions
|
|
239
92
|
|
|
240
93
|
- [`createWebhook()`](/docs/api-reference/workflow/create-webhook) - Create a webhook in a workflow
|
package/docs/deploying/index.mdx
CHANGED
|
@@ -339,53 +339,36 @@ Like hooks, webhooks support iteration:
|
|
|
339
339
|
```typescript lineNumbers
|
|
340
340
|
import { createWebhook, type RequestWithResponse } from "workflow";
|
|
341
341
|
|
|
342
|
-
async function
|
|
342
|
+
async function sendAck(request: RequestWithResponse, message: string) {
|
|
343
343
|
"use step";
|
|
344
344
|
|
|
345
345
|
await request.respondWith(
|
|
346
|
-
|
|
347
|
-
JSON.stringify({ response_type: "in_channel", text }),
|
|
348
|
-
{ headers: { "Content-Type": "application/json" } }
|
|
349
|
-
)
|
|
346
|
+
Response.json({ received: true, message })
|
|
350
347
|
);
|
|
351
348
|
}
|
|
352
349
|
|
|
353
|
-
|
|
354
|
-
"use
|
|
350
|
+
async function processEvent(data: any) {
|
|
351
|
+
"use step";
|
|
352
|
+
console.log("Processing event:", data);
|
|
353
|
+
}
|
|
355
354
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
respondWith: "manual"
|
|
359
|
-
});
|
|
355
|
+
export async function eventCollectorWorkflow() {
|
|
356
|
+
"use workflow";
|
|
360
357
|
|
|
361
|
-
|
|
358
|
+
using webhook = createWebhook({ respondWith: "manual" });
|
|
359
|
+
console.log("Send events to:", webhook.url);
|
|
362
360
|
|
|
363
361
|
for await (const request of webhook) {
|
|
364
|
-
const
|
|
365
|
-
const command = formData.get("command");
|
|
366
|
-
const text = formData.get("text");
|
|
367
|
-
|
|
368
|
-
if (command === "/status") {
|
|
369
|
-
await respondToSlack(request, "Checking status...");
|
|
370
|
-
const status = await checkSystemStatus();
|
|
371
|
-
await postToSlack(channelId, `Status: ${status}`);
|
|
372
|
-
}
|
|
362
|
+
const data = await request.json();
|
|
373
363
|
|
|
374
|
-
if (
|
|
375
|
-
await
|
|
364
|
+
if (data.type === "done") {
|
|
365
|
+
await sendAck(request, "Workflow complete");
|
|
376
366
|
break;
|
|
377
367
|
}
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
368
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
async function postToSlack(channelId: string, message: string) {
|
|
387
|
-
"use step";
|
|
388
|
-
// Post message to Slack
|
|
369
|
+
await sendAck(request, "Event received");
|
|
370
|
+
await processEvent(data);
|
|
371
|
+
}
|
|
389
372
|
}
|
|
390
373
|
```
|
|
391
374
|
|
|
@@ -464,7 +447,9 @@ This pattern is especially valuable in larger applications where the workflow an
|
|
|
464
447
|
|
|
465
448
|
### Token Design
|
|
466
449
|
|
|
467
|
-
|
|
450
|
+
Custom tokens are available for `createHook()` with server-side `resumeHook()` only. Webhooks (`createWebhook()`) always use randomly generated tokens to prevent unauthorized access to public webhook endpoints.
|
|
451
|
+
|
|
452
|
+
When using custom tokens with `createHook()`:
|
|
468
453
|
|
|
469
454
|
- **Make them deterministic**: Base them on data the external system can reconstruct (like channel IDs, user IDs, etc.)
|
|
470
455
|
- **Use namespacing**: Prefix tokens to avoid conflicts (e.g., `slack:${channelId}`, `github:${repoId}`)
|
|
@@ -122,6 +122,8 @@ Keep in mind that calling a step function outside of a workflow function will no
|
|
|
122
122
|
|
|
123
123
|
Workflow functions have the ability to automatically suspend while they wait on asynchronous work. While suspended, the workflow's state is stored via the [event log](/docs/how-it-works/event-sourcing) and no compute resources are used until the workflow resumes execution.
|
|
124
124
|
|
|
125
|
+
<FluidComputeCallout />
|
|
126
|
+
|
|
125
127
|
There are multiple ways a workflow can suspend:
|
|
126
128
|
|
|
127
129
|
- Waiting on a step function: the workflow yields while the step runs in the step runtime.
|
|
@@ -225,6 +225,8 @@ npx workflow inspect runs
|
|
|
225
225
|
|
|
226
226
|
Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and needs no special configuration.
|
|
227
227
|
|
|
228
|
+
<FluidComputeCallout />
|
|
229
|
+
|
|
228
230
|
To deploy your Astro project to Vercel, ensure that the [Astro Vercel adapter](https://docs.astro.build/en/guides/integrations-guide/vercel) is configured:
|
|
229
231
|
|
|
230
232
|
```bash
|
|
@@ -258,6 +258,8 @@ npx workflow inspect runs
|
|
|
258
258
|
|
|
259
259
|
Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and needs no special configuration.
|
|
260
260
|
|
|
261
|
+
<FluidComputeCallout />
|
|
262
|
+
|
|
261
263
|
Check the [Deploying](/docs/deploying) section to learn how your workflows can be deployed elsewhere.
|
|
262
264
|
|
|
263
265
|
## Next Steps
|
|
@@ -245,6 +245,8 @@ npx workflow inspect runs # add '--web' for an interactive Web based UI
|
|
|
245
245
|
|
|
246
246
|
Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and needs no special configuration.
|
|
247
247
|
|
|
248
|
+
<FluidComputeCallout />
|
|
249
|
+
|
|
248
250
|
Check the [Deploying](/docs/deploying) section to learn how your workflows can be deployed elsewhere.
|
|
249
251
|
|
|
250
252
|
## Next Steps
|
|
@@ -240,6 +240,8 @@ npx workflow inspect runs
|
|
|
240
240
|
|
|
241
241
|
Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and needs no special configuration.
|
|
242
242
|
|
|
243
|
+
<FluidComputeCallout />
|
|
244
|
+
|
|
243
245
|
Check the [Deploying](/docs/deploying) section to learn how your workflows can be deployed elsewhere.
|
|
244
246
|
|
|
245
247
|
## Next Steps
|
|
@@ -327,6 +327,8 @@ WorkflowModule.forRoot({
|
|
|
327
327
|
|
|
328
328
|
Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and needs no special configuration.
|
|
329
329
|
|
|
330
|
+
<FluidComputeCallout />
|
|
331
|
+
|
|
330
332
|
Check the [Deploying](/docs/deploying) section to learn how your workflows can be deployed elsewhere.
|
|
331
333
|
|
|
332
334
|
## Next Steps
|
|
@@ -260,6 +260,8 @@ npx workflow inspect runs
|
|
|
260
260
|
|
|
261
261
|
Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and need no special configuration.
|
|
262
262
|
|
|
263
|
+
<FluidComputeCallout />
|
|
264
|
+
|
|
263
265
|
Check the [Deploying](/docs/deploying) section to learn how your workflows can be deployed elsewhere.
|
|
264
266
|
|
|
265
267
|
## Troubleshooting
|
|
@@ -224,6 +224,8 @@ npx workflow inspect runs
|
|
|
224
224
|
|
|
225
225
|
Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and needs no special configuration.
|
|
226
226
|
|
|
227
|
+
<FluidComputeCallout />
|
|
228
|
+
|
|
227
229
|
Check the [Deploying](/docs/deploying) section to learn how your workflows can be deployed elsewhere.
|
|
228
230
|
|
|
229
231
|
## Next Steps
|
|
@@ -225,6 +225,8 @@ npx workflow inspect runs
|
|
|
225
225
|
|
|
226
226
|
Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and needs no special configuration.
|
|
227
227
|
|
|
228
|
+
<FluidComputeCallout />
|
|
229
|
+
|
|
228
230
|
Check the [Deploying](/docs/deploying) section to learn how your workflows can be deployed elsewhere.
|
|
229
231
|
|
|
230
232
|
## Next Steps
|
|
@@ -224,6 +224,8 @@ npx workflow inspect runs
|
|
|
224
224
|
|
|
225
225
|
Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and needs no special configuration.
|
|
226
226
|
|
|
227
|
+
<FluidComputeCallout />
|
|
228
|
+
|
|
227
229
|
Check the [Deploying](/docs/deploying) section to learn how your workflows can be deployed elsewhere.
|
|
228
230
|
|
|
229
231
|
## Next Steps
|
|
@@ -230,6 +230,8 @@ npx workflow inspect runs
|
|
|
230
230
|
|
|
231
231
|
Workflow DevKit apps currently work best when deployed to [Vercel](https://vercel.com/home) and needs no special configuration.
|
|
232
232
|
|
|
233
|
+
<FluidComputeCallout />
|
|
234
|
+
|
|
233
235
|
Check the [Deploying](/docs/deploying) section to learn how your workflows can be deployed elsewhere.
|
|
234
236
|
|
|
235
237
|
## Next Steps
|
package/docs/meta.json
CHANGED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Testing
|
|
3
|
+
description: Unit test individual steps and integration test entire workflows using Vitest and the Vite plugin.
|
|
4
|
+
type: conceptual
|
|
5
|
+
summary: Learn how to unit test steps and integration test workflows using Vitest.
|
|
6
|
+
prerequisites:
|
|
7
|
+
- /docs/foundations/workflows-and-steps
|
|
8
|
+
- /docs/getting-started/vite
|
|
9
|
+
related:
|
|
10
|
+
- /docs/foundations/hooks
|
|
11
|
+
- /docs/api-reference/workflow-api/start
|
|
12
|
+
- /docs/api-reference/workflow-api/resume-hook
|
|
13
|
+
- /docs/api-reference/workflow-api/get-run
|
|
14
|
+
- /docs/observability
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
Testing is a critical part of building reliable workflows. Because steps are just functions annotated with directives, they can be unit tested like any other JavaScript function. Workflow DevKit also provides a [Vite plugin](/docs/getting-started/vite) that integrates with [Vitest](https://vitest.dev), enabling full integration tests against a real workflow runtime.
|
|
18
|
+
|
|
19
|
+
This guide covers two approaches:
|
|
20
|
+
|
|
21
|
+
1. **Unit testing** - Test individual steps as plain functions, without the workflow runtime.
|
|
22
|
+
2. **Integration testing** - Test entire workflows against a real workflow setup using the Vite plugin. Required for workflows that use [hooks](/docs/foundations/hooks), webhooks, [`sleep()`](/docs/api-reference/workflow/sleep), or retries.
|
|
23
|
+
|
|
24
|
+
## Unit Testing Steps
|
|
25
|
+
|
|
26
|
+
Without the workflow compiler, the `"use step"` directive is a no-op. Your step functions run as regular JavaScript functions, making them straightforward to unit test with no special configuration.
|
|
27
|
+
|
|
28
|
+
### Example Steps
|
|
29
|
+
|
|
30
|
+
Given a workflow file with step functions like this:
|
|
31
|
+
|
|
32
|
+
```typescript title="workflows/user-signup.ts" lineNumbers
|
|
33
|
+
import { sleep } from "workflow";
|
|
34
|
+
|
|
35
|
+
export async function handleUserSignup(email: string) {
|
|
36
|
+
"use workflow";
|
|
37
|
+
|
|
38
|
+
const user = await createUser(email);
|
|
39
|
+
await sendWelcomeEmail(user);
|
|
40
|
+
|
|
41
|
+
await sleep("5d");
|
|
42
|
+
await sendOnboardingEmail(user);
|
|
43
|
+
|
|
44
|
+
return { userId: user.id, status: "onboarded" };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function createUser(email: string) {
|
|
48
|
+
"use step"; // [!code highlight]
|
|
49
|
+
return { id: crypto.randomUUID(), email };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function sendWelcomeEmail(user: { id: string; email: string }) {
|
|
53
|
+
"use step"; // [!code highlight]
|
|
54
|
+
// Send email logic
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function sendOnboardingEmail(user: { id: string; email: string }) {
|
|
58
|
+
"use step"; // [!code highlight]
|
|
59
|
+
// Send email logic
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Writing Unit Tests for Steps
|
|
64
|
+
|
|
65
|
+
You can import and test step functions directly with Vitest. No special configuration or workflow plugin is needed:
|
|
66
|
+
|
|
67
|
+
```typescript title="workflows/user-signup.test.ts" lineNumbers
|
|
68
|
+
import { describe, it, expect } from "vitest";
|
|
69
|
+
import { createUser, sendWelcomeEmail } from "./user-signup"; // [!code highlight]
|
|
70
|
+
|
|
71
|
+
describe("createUser step", () => {
|
|
72
|
+
it("should create a user with the given email", async () => {
|
|
73
|
+
const user = await createUser("test@example.com");
|
|
74
|
+
|
|
75
|
+
expect(user.email).toBe("test@example.com");
|
|
76
|
+
expect(user.id).toBeDefined();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("sendWelcomeEmail step", () => {
|
|
81
|
+
it("should send a welcome email without throwing", async () => {
|
|
82
|
+
const user = { id: "user-1", email: "test@example.com" };
|
|
83
|
+
await expect(sendWelcomeEmail(user)).resolves.not.toThrow();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
This approach is ideal for verifying the business logic inside individual steps in isolation.
|
|
89
|
+
|
|
90
|
+
<Callout type="info">
|
|
91
|
+
Unit testing works well for individual steps. A simple workflow that only calls steps can also be unit tested this way, since `"use workflow"` is similarly a no-op without the compiler. However, any workflow that uses runtime features like [`sleep()`](/docs/api-reference/workflow/sleep), [hooks](/docs/foundations/hooks), or [webhooks](/docs/foundations/hooks#understanding-webhooks) cannot be unit tested directly because those APIs require the workflow runtime. Use [integration testing](#integration-testing-with-the-vite-plugin) for testing entire workflows, especially those that depend on workflow-only features.
|
|
92
|
+
</Callout>
|
|
93
|
+
|
|
94
|
+
## Integration Testing with the Vite Plugin
|
|
95
|
+
|
|
96
|
+
For workflows that rely on runtime features like [hooks](/docs/foundations/hooks), [webhooks](/docs/foundations/hooks#understanding-webhooks), [`sleep()`](/docs/api-reference/workflow/sleep), or error retries, you need to test against a real workflow setup. The `workflow/vite` plugin integrates directly with Vitest, compiling your `"use workflow"` and `"use step"` directives so the full workflow runtime is active during tests.
|
|
97
|
+
|
|
98
|
+
### Vitest Configuration
|
|
99
|
+
|
|
100
|
+
Create a separate Vitest config for integration tests that includes the `workflow()` plugin and a `globalSetup` script:
|
|
101
|
+
|
|
102
|
+
```typescript title="vitest.integration.config.ts" lineNumbers
|
|
103
|
+
import { defineConfig } from "vitest/config";
|
|
104
|
+
import { workflow } from "workflow/vite"; // [!code highlight]
|
|
105
|
+
|
|
106
|
+
export default defineConfig({
|
|
107
|
+
plugins: [workflow()], // [!code highlight]
|
|
108
|
+
test: {
|
|
109
|
+
include: ["**/*.integration.test.ts"],
|
|
110
|
+
testTimeout: 60_000, // Workflows may take longer than default timeout
|
|
111
|
+
globalSetup: "./vitest.integration.setup.ts", // [!code highlight]
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
<Callout type="info">
|
|
117
|
+
Use a separate Vitest configuration and a distinct file naming convention (e.g. `*.integration.test.ts`) to keep unit tests and integration tests separate. Unit tests run with a standard Vitest config without the workflow plugin, while integration tests use the config above.
|
|
118
|
+
</Callout>
|
|
119
|
+
|
|
120
|
+
### Global Setup Script
|
|
121
|
+
|
|
122
|
+
Integration tests need a running server to execute workflow steps. The `globalSetup` script starts a [Nitro](https://v3.nitro.build) server as a sidecar process before tests run, and tears it down afterwards:
|
|
123
|
+
|
|
124
|
+
```typescript title="vitest.integration.setup.ts" lineNumbers
|
|
125
|
+
import { spawn } from "node:child_process";
|
|
126
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
127
|
+
import type { ChildProcess } from "node:child_process";
|
|
128
|
+
|
|
129
|
+
let server: ChildProcess | null = null;
|
|
130
|
+
const PORT = "4000";
|
|
131
|
+
|
|
132
|
+
export async function setup() { // [!code highlight]
|
|
133
|
+
console.log("Starting server for workflow execution...");
|
|
134
|
+
|
|
135
|
+
server = spawn("npx", ["nitro", "dev", "--port", PORT], {
|
|
136
|
+
stdio: "pipe",
|
|
137
|
+
detached: false,
|
|
138
|
+
env: process.env,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Wait for the server to be ready
|
|
142
|
+
const ready = await new Promise<boolean>((resolve) => {
|
|
143
|
+
const timeout = setTimeout(() => resolve(false), 15_000);
|
|
144
|
+
|
|
145
|
+
server?.stdout?.on("data", (data) => {
|
|
146
|
+
const output = data.toString();
|
|
147
|
+
console.log("[server]", output);
|
|
148
|
+
if (output.includes("listening") || output.includes("ready")) {
|
|
149
|
+
clearTimeout(timeout);
|
|
150
|
+
resolve(true);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
server?.stderr?.on("data", (data) => {
|
|
155
|
+
console.error("[server]", data.toString());
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
server?.on("error", (error) => {
|
|
159
|
+
console.error("Failed to start server:", error);
|
|
160
|
+
clearTimeout(timeout);
|
|
161
|
+
resolve(false);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (!ready) {
|
|
166
|
+
throw new Error("Server failed to start within 15 seconds");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
await delay(2_000); // Allow full initialization
|
|
170
|
+
|
|
171
|
+
// Point the workflow runtime at the local server
|
|
172
|
+
process.env.WORKFLOW_LOCAL_BASE_URL = `http://localhost:${PORT}`; // [!code highlight]
|
|
173
|
+
process.env.WORKFLOW_LOCAL_DATA_DIR = "./.workflow-data"; // [!code highlight]
|
|
174
|
+
|
|
175
|
+
console.log("Server ready for workflow execution");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function teardown() { // [!code highlight]
|
|
179
|
+
if (server) {
|
|
180
|
+
console.log("Stopping server...");
|
|
181
|
+
server.kill("SIGTERM");
|
|
182
|
+
await delay(1_000);
|
|
183
|
+
if (!server.killed) {
|
|
184
|
+
server.kill("SIGKILL");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
The setup script sets two environment variables that the workflow runtime reads:
|
|
191
|
+
|
|
192
|
+
- `WORKFLOW_LOCAL_BASE_URL` tells the runtime where to send step execution requests
|
|
193
|
+
- `WORKFLOW_LOCAL_DATA_DIR` tells the runtime where to persist workflow state locally
|
|
194
|
+
|
|
195
|
+
<Callout type="info">
|
|
196
|
+
You can use any server framework that supports the workflow runtime. The example above uses [Nitro](https://v3.nitro.build), but you could also use a [Next.js](https://nextjs.org), [Hono](https://hono.dev), or any other supported server.
|
|
197
|
+
</Callout>
|
|
198
|
+
|
|
199
|
+
### Running Integration Tests
|
|
200
|
+
|
|
201
|
+
Add a script to your `package.json`:
|
|
202
|
+
|
|
203
|
+
```json title="package.json"
|
|
204
|
+
{
|
|
205
|
+
"scripts": {
|
|
206
|
+
"test": "vitest",
|
|
207
|
+
"test:integration": "vitest --config vitest.integration.config.ts"
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Testing Hooks and Waits
|
|
213
|
+
|
|
214
|
+
The real power of integration testing comes when testing workflow-only features. Hooks and waits can be resumed programmatically using the [`workflow/api`](/docs/api-reference/workflow-api) functions, making it straightforward to simulate external events in your tests.
|
|
215
|
+
|
|
216
|
+
Given a workflow that waits for approval via a hook, then sleeps before publishing:
|
|
217
|
+
|
|
218
|
+
```typescript title="workflows/approval.ts" lineNumbers
|
|
219
|
+
import { createHook, sleep } from "workflow";
|
|
220
|
+
|
|
221
|
+
export async function approvalWorkflow(documentId: string) {
|
|
222
|
+
"use workflow";
|
|
223
|
+
|
|
224
|
+
const prepared = await prepareDocument(documentId);
|
|
225
|
+
|
|
226
|
+
using hook = createHook<{ approved: boolean; reviewer: string }>({ // [!code highlight]
|
|
227
|
+
token: `approval:${documentId}`, // [!code highlight]
|
|
228
|
+
}); // [!code highlight]
|
|
229
|
+
|
|
230
|
+
const decision = await hook; // [!code highlight]
|
|
231
|
+
|
|
232
|
+
if (decision.approved) {
|
|
233
|
+
// Wait 24 hours before publishing (e.g. grace period for retractions)
|
|
234
|
+
await sleep("24h"); // [!code highlight]
|
|
235
|
+
await publishDocument(prepared);
|
|
236
|
+
return { status: "published", reviewer: decision.reviewer };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return { status: "rejected", reviewer: decision.reviewer };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function prepareDocument(documentId: string) {
|
|
243
|
+
"use step";
|
|
244
|
+
return { id: documentId, content: "..." };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function publishDocument(doc: { id: string; content: string }) {
|
|
248
|
+
"use step";
|
|
249
|
+
console.log(`Publishing document ${doc.id}`);
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
You can write an integration test that starts the workflow, resumes the hook, and uses [`wakeUp()`](/docs/api-reference/workflow-api/get-run) to skip the sleep — so your tests don't have to wait for the full duration:
|
|
254
|
+
|
|
255
|
+
```typescript title="workflows/approval.integration.test.ts" lineNumbers
|
|
256
|
+
import { describe, it, expect } from "vitest";
|
|
257
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
258
|
+
import { start, getRun, resumeHook } from "workflow/api"; // [!code highlight]
|
|
259
|
+
import { approvalWorkflow } from "./approval";
|
|
260
|
+
|
|
261
|
+
describe("approvalWorkflow", () => {
|
|
262
|
+
it("should publish when approved", async () => {
|
|
263
|
+
const run = await start(approvalWorkflow, ["doc-123"]); // [!code highlight]
|
|
264
|
+
|
|
265
|
+
// Resume the hook programmatically, simulating an external approval
|
|
266
|
+
await resumeHook("approval:doc-123", { // [!code highlight]
|
|
267
|
+
approved: true, // [!code highlight]
|
|
268
|
+
reviewer: "alice", // [!code highlight]
|
|
269
|
+
}); // [!code highlight]
|
|
270
|
+
|
|
271
|
+
// Wait for the workflow to replay and reach the sleep() call
|
|
272
|
+
await delay(5_000); // [!code highlight]
|
|
273
|
+
|
|
274
|
+
// Skip the 24-hour sleep so the test completes immediately
|
|
275
|
+
await getRun(run.runId).wakeUp(); // [!code highlight]
|
|
276
|
+
|
|
277
|
+
const result = await run.returnValue;
|
|
278
|
+
expect(result).toEqual({
|
|
279
|
+
status: "published",
|
|
280
|
+
reviewer: "alice",
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("should reject when not approved", async () => {
|
|
285
|
+
const run = await start(approvalWorkflow, ["doc-456"]);
|
|
286
|
+
|
|
287
|
+
await resumeHook("approval:doc-456", {
|
|
288
|
+
approved: false,
|
|
289
|
+
reviewer: "bob",
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// No wakeUp() needed here — the rejected path has no sleep
|
|
293
|
+
const result = await run.returnValue;
|
|
294
|
+
expect(result).toEqual({
|
|
295
|
+
status: "rejected",
|
|
296
|
+
reviewer: "bob",
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
<Callout type="info">
|
|
303
|
+
[`start()`](/docs/api-reference/workflow-api/start), [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook), and [`getRun().wakeUp()`](/docs/api-reference/workflow-api/get-run) are the key API functions for integration testing. Use `start()` to trigger a workflow, `resumeHook()` to simulate external events, and `wakeUp()` to skip `sleep()` calls so tests run instantly. See the [API Reference](/docs/api-reference/workflow-api) for the full list of available functions.
|
|
304
|
+
</Callout>
|
|
305
|
+
|
|
306
|
+
## Debugging Test Runs
|
|
307
|
+
|
|
308
|
+
When integration tests fail, the [Workflow DevKit CLI and Web UI](/docs/observability) can help you inspect what happened. Because integration tests persist workflow state to `WORKFLOW_LOCAL_DATA_DIR`, you can use the same observability tools you would use in development.
|
|
309
|
+
|
|
310
|
+
Launch the Web UI to visually explore your test workflow runs:
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
npx workflow web
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Or use the CLI to inspect runs in the terminal:
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
# List recent workflow runs
|
|
320
|
+
npx workflow inspect runs
|
|
321
|
+
|
|
322
|
+
# Inspect a specific run
|
|
323
|
+
npx workflow inspect runs <run-id>
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
The Web UI shows each step, its inputs and outputs, retry attempts, hook state, and timing. This is especially useful for diagnosing issues with hooks that were not resumed, steps that failed unexpectedly, or workflows that timed out.
|
|
327
|
+
|
|
328
|
+

|
|
329
|
+
|
|
330
|
+
<Callout type="info">
|
|
331
|
+
See the [Observability](/docs/observability) docs for the full set of CLI commands and Web UI features.
|
|
332
|
+
</Callout>
|
|
333
|
+
|
|
334
|
+
## Best Practices
|
|
335
|
+
|
|
336
|
+
### Separate Unit and Integration Tests
|
|
337
|
+
|
|
338
|
+
Keep two test configurations:
|
|
339
|
+
|
|
340
|
+
- **Unit tests** - Standard Vitest config, no workflow plugin. Fast, no infrastructure required.
|
|
341
|
+
- **Integration tests** - Vitest config with `workflow()` plugin. Tests the full workflow lifecycle including hooks, sleeps, and retries.
|
|
342
|
+
|
|
343
|
+
### Use Custom Hook Tokens for Deterministic Testing
|
|
344
|
+
|
|
345
|
+
When testing workflows with hooks, use [custom tokens](/docs/foundations/hooks#custom-tokens-for-deterministic-hooks) based on predictable values (like document IDs or test identifiers). This makes it easy to resume the correct hook in your test code.
|
|
346
|
+
|
|
347
|
+
### Set Appropriate Timeouts
|
|
348
|
+
|
|
349
|
+
Workflows may take longer to execute than typical unit tests, especially when they involve multiple steps or retries. Set a generous `testTimeout` in your integration test config.
|
|
350
|
+
|
|
351
|
+
### Test Error and Retry Scenarios
|
|
352
|
+
|
|
353
|
+
Integration tests are the right place to verify that your workflows handle errors correctly, including retryable errors, fatal errors, and timeout scenarios.
|
|
354
|
+
|
|
355
|
+
## Further Reading
|
|
356
|
+
|
|
357
|
+
- [Hooks & Webhooks](/docs/foundations/hooks) - Pausing and resuming workflows with external data
|
|
358
|
+
- [`start()` API Reference](/docs/api-reference/workflow-api/start) - Start workflows programmatically
|
|
359
|
+
- [`resumeHook()` API Reference](/docs/api-reference/workflow-api/resume-hook) - Resume hooks with data
|
|
360
|
+
- [`getRun()` API Reference](/docs/api-reference/workflow-api/get-run) - Check workflow run status and wake up sleeping runs
|
|
361
|
+
- [Vite Integration](/docs/getting-started/vite) - Set up the Vite plugin
|
|
362
|
+
- [Observability](/docs/observability) - Inspect and debug workflow runs with the CLI and Web UI
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
<Callout type="info">
|
|
367
|
+
This guide was inspired by the testing approach described in Mux's article [*Launching durable AI workflows for video with @mux/ai*](https://www.mux.com/blog/launching-durable-ai-workflows-for-video-with-mux-ai#testing), which demonstrates how Mux uses the `workflow/vite` plugin with Vitest to integration test their durable AI video workflows built on Workflow DevKit.
|
|
368
|
+
</Callout>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "workflow",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0-beta.64",
|
|
4
4
|
"description": "Workflow DevKit - Build durable, resilient, and observable workflows",
|
|
5
5
|
"main": "dist/typescript-plugin.cjs",
|
|
6
6
|
"type": "module",
|
|
@@ -52,17 +52,17 @@
|
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"ms": "2.1.3",
|
|
55
|
-
"@workflow/astro": "4.0.0-beta.
|
|
56
|
-
"@workflow/cli": "4.
|
|
57
|
-
"@workflow/core": "4.
|
|
58
|
-
"@workflow/errors": "4.1.0-beta.
|
|
55
|
+
"@workflow/astro": "4.0.0-beta.38",
|
|
56
|
+
"@workflow/cli": "4.2.0-beta.64",
|
|
57
|
+
"@workflow/core": "4.2.0-beta.64",
|
|
58
|
+
"@workflow/errors": "4.1.0-beta.18",
|
|
59
59
|
"@workflow/typescript-plugin": "4.0.1-beta.5",
|
|
60
|
-
"@workflow/next": "4.0.1-beta.
|
|
61
|
-
"@workflow/nest": "0.0.0-beta.
|
|
62
|
-
"@workflow/nitro": "4.0.1-beta.
|
|
63
|
-
"@workflow/nuxt": "4.0.1-beta.
|
|
64
|
-
"@workflow/sveltekit": "4.0.0-beta.
|
|
65
|
-
"@workflow/rollup": "4.0.0-beta.
|
|
60
|
+
"@workflow/next": "4.0.1-beta.60",
|
|
61
|
+
"@workflow/nest": "0.0.0-beta.13",
|
|
62
|
+
"@workflow/nitro": "4.0.1-beta.59",
|
|
63
|
+
"@workflow/nuxt": "4.0.1-beta.48",
|
|
64
|
+
"@workflow/sveltekit": "4.0.0-beta.53",
|
|
65
|
+
"@workflow/rollup": "4.0.0-beta.21"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@types/ms": "2.1.0",
|