Skip to content

Commit 46892f9

Browse files
committed
[Workflows] Add dynamic workflows example docs
1 parent aaf5878 commit 46892f9

3 files changed

Lines changed: 198 additions & 0 deletions

File tree

315 KB
Loading
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
title: Dynamic Workers and Workflows
3+
description: Route Workflow runs into dynamically loaded Worker code while keeping durable Workflow steps and state.
4+
pcx_content_type: example
5+
external_link: /workflows/examples/dynamic-workflows/
6+
sidebar:
7+
order: 5
8+
---
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
---
2+
title: Dynamic Workflows
3+
description: Create and run Workflow logic dynamically while keeping Workflow steps durable.
4+
pcx_content_type: example
5+
sidebar:
6+
order: 3
7+
---
8+
9+
import { TypeScriptExample, WranglerConfig } from "~/components";
10+
11+
Use this pattern when your workflow logic is not known at deploy time, but you still need Workflow durability. In this example, we implement a simple Dynamic Workflow which allows you to run arbitrary workflow code at runtime.
12+
13+
## Understand the model
14+
15+
This setup has three parts:
16+
17+
- **Loader Worker**: the front door. It receives HTTP requests, knows how to load Dynamic Worker code, and can create Workflow instances.
18+
- **Workflow class**: the durable orchestrator. It persists state, survives restarts, and executes steps, including steps that pause for days while waiting for human input.
19+
- **Dynamic Worker**: the user-authored code that defines what the workflow actually does. It is loaded on-demand by script ID.
20+
21+
![Dynamic Workers and Workflows architecture diagram](~/assets/images/workflows/dynamic-workflows.png)
22+
23+
## Configure your Worker
24+
25+
Your Worker needs a Worker Loader binding and a Workflow binding. The Worker Loader creates Dynamic Workers at runtime, and the Workflow binding points to the durable Workflow class that runs them.
26+
27+
- A Worker Loader binding
28+
- A Workflow binding that points to the Workflow class
29+
30+
<WranglerConfig>
31+
32+
```toml
33+
name = "dynamic-workflow-loader"
34+
main = "src/index.ts"
35+
compatibility_date = "$today"
36+
37+
[[worker_loaders]]
38+
binding = "LOADER"
39+
40+
[[workflows]]
41+
name = "dynamic-workflow"
42+
binding = "WORKFLOW"
43+
class_name = "DynamicWorkflow"
44+
```
45+
46+
</WranglerConfig>
47+
48+
## Create your Worker entrypoint
49+
50+
Your Worker entrypoint accepts a script, creates a Workflow instance with that script in `params`, and returns the new instance ID. The same entrypoint can also look up the status of an existing Workflow instance.
51+
52+
<TypeScriptExample>
53+
54+
```ts
55+
export { DynamicWorkflow } from "./workflow";
56+
57+
export default {
58+
async fetch(request: Request, env: Env): Promise<Response> {
59+
const url = new URL(request.url);
60+
61+
if (url.pathname === "/api/run" && request.method === "POST") {
62+
try {
63+
const { script } = (await request.json()) as { script: string };
64+
65+
if (!script || typeof script !== "string") {
66+
return Response.json(
67+
{ error: "script is required" },
68+
{ status: 400 },
69+
);
70+
}
71+
72+
const instance = await env.WORKFLOW.create({
73+
params: { script },
74+
});
75+
76+
return Response.json({
77+
instanceId: instance.id,
78+
status: await instance.status(),
79+
});
80+
} catch (error) {
81+
return Response.json(
82+
{ error: error instanceof Error ? error.message : String(error) },
83+
{ status: 500 },
84+
);
85+
}
86+
}
87+
88+
if (url.pathname === "/api/status") {
89+
const instanceId = url.searchParams.get("instanceId");
90+
if (!instanceId) {
91+
return Response.json(
92+
{ error: "instanceId query param is required" },
93+
{ status: 400 },
94+
);
95+
}
96+
97+
try {
98+
const instance = await env.WORKFLOW.get(instanceId);
99+
return Response.json(await instance.status());
100+
} catch (error) {
101+
return Response.json(
102+
{ error: error instanceof Error ? error.message : String(error) },
103+
{ status: 500 },
104+
);
105+
}
106+
}
107+
108+
return new Response("Not Found", { status: 404 });
109+
},
110+
} satisfies ExportedHandler<Env>;
111+
```
112+
113+
</TypeScriptExample>
114+
115+
## Define the Workflow class
116+
117+
You still deploy one normal Workflow class. Its job is to load the dynamic code for the current run and call it with the `WorkflowStep` object. This is what keeps the [Workflows API](/workflows/build/workers-api/), including `step.do()`, `step.sleep()`, and `step.waitForEvent()`, working as normal.
118+
119+
Use `env.LOADER.get()` via the [Worker Loader API](/dynamic-workers/api-reference/#get) to load the Dynamic Worker for the current Workflow instance. If the loader already has a warm isolate for the same ID, it can reuse it. Otherwise, it calls the callback to create the Worker from the script in the workflow payload.
120+
121+
<TypeScriptExample>
122+
123+
```ts
124+
import {
125+
WorkflowEntrypoint,
126+
type WorkflowEvent,
127+
type WorkflowStep,
128+
} from "cloudflare:workers";
129+
130+
type Params = {
131+
script: string;
132+
};
133+
134+
export class DynamicWorkflow extends WorkflowEntrypoint<Env, Params> {
135+
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
136+
const { script } = event.payload;
137+
138+
const worker = this.env.LOADER.get(
139+
`dyn-wf-${event.instanceId}`,
140+
async () => ({
141+
mainModule: "index.js",
142+
modules: { "index.js": script },
143+
compatibilityDate: "2026-04-21",
144+
compatibilityFlags: ["nodejs_compat"],
145+
}),
146+
);
147+
148+
const entrypoint = worker.getEntrypoint() as unknown as {
149+
run(event: Record<string, unknown>, step: WorkflowStep): Promise<unknown>;
150+
};
151+
152+
return await entrypoint.run({}, step);
153+
}
154+
}
155+
```
156+
157+
</TypeScriptExample>
158+
159+
In this example, `event.payload.script` contains the workflow logic to run. The Workflow class loads that code as a Dynamic Worker, gets its entrypoint, and calls `run({}, step)` so the dynamic code can use the same durable step methods as any other Workflow.
160+
161+
## Trigger a dynamic workflow
162+
163+
Send a `POST` request to `/api/run` with the workflow code in the `script` field. The response includes an `instanceId` that you can use to check status later.
164+
165+
```bash
166+
curl -X POST http://localhost:8787/api/run \
167+
-H 'Content-Type: application/json' \
168+
--data-binary @- <<'EOF'
169+
{
170+
"script": "import { WorkerEntrypoint } from 'cloudflare:workers';\n\nexport default class extends WorkerEntrypoint {\n async run(event, step) {\n const result = await step.do('hello', async () => {\n return { message: 'Hello from API' };\n });\n\n return result;\n }\n}"
171+
}
172+
EOF
173+
```
174+
175+
## Check workflow status
176+
177+
Use the `instanceId` returned from `/api/run` to retrieve the current Workflow status.
178+
179+
```bash
180+
curl "http://localhost:8787/api/status?instanceId=YOUR_INSTANCE_ID"
181+
```
182+
183+
## Related resources
184+
185+
- [Workers API](/workflows/build/workers-api/)
186+
- [Trigger Workflows](/workflows/build/trigger-workflows/)
187+
- [Events and parameters](/workflows/build/events-and-parameters/)
188+
- [Dynamic Workers getting started](/dynamic-workers/getting-started/)
189+
- [Dynamic Worker Loaders](/workers/runtime-apis/bindings/worker-loader/)
190+
- [Bindings with Dynamic Workers](/dynamic-workers/usage/bindings/)

0 commit comments

Comments
 (0)