Skip to content

Commit cde5d99

Browse files
authored
Add cross-Worker Workflow bindings to the experimental configuration API (#15877)
1 parent 4c2993b commit cde5d99

11 files changed

Lines changed: 173 additions & 41 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@cloudflare/config": minor
3+
---
4+
5+
Add cross-Worker Workflow bindings to the experimental configuration API
6+
7+
Workflow bindings can now identify a Workflow by name and reference the Worker and exported `WorkflowEntrypoint` class that define it.

‎packages/config/src/__tests__/config-loader.test.ts‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,48 @@ describe("resolveAndParseConfig", () => {
181181
}
182182
});
183183

184+
it("resolves a Worker reference in a Workflow binding", async ({
185+
expect,
186+
}) => {
187+
const workflowWorker = defineWorker({
188+
name: "workflow-worker",
189+
compatibilityDate,
190+
exports: {
191+
GreetingWorkflow: workerExports.workflow({ name: "greeting" }),
192+
},
193+
});
194+
const config = defineConfig({
195+
worker: defineWorker({
196+
name: "web",
197+
compatibilityDate,
198+
env: {
199+
GREETING: bindings.workflow({
200+
name: "greeting",
201+
worker: workflowWorker,
202+
exportName: "GreetingWorkflow",
203+
}),
204+
},
205+
}),
206+
});
207+
208+
const result = await resolveAndParseConfig(config, {
209+
isPreview: false,
210+
mode: undefined,
211+
});
212+
213+
expect(result.success).toBe(true);
214+
if (result.success) {
215+
expect(result.data.worker?.env).toMatchObject({
216+
GREETING: {
217+
type: "workflow",
218+
name: "greeting",
219+
worker: "workflow-worker",
220+
exportName: "GreetingWorkflow",
221+
},
222+
});
223+
}
224+
});
225+
184226
it("allows a cross-project Worker factory to use an explicit context", async ({
185227
expect,
186228
}) => {

‎packages/config/src/__tests__/convert.test.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,32 @@ describe("convertToWranglerConfig", () => {
726726
});
727727
});
728728

729+
it("maps Workflow binding to workflows", ({ expect }) => {
730+
const result = convertToWranglerConfig({
731+
worker: {
732+
...baseWorker,
733+
env: {
734+
WORKFLOW: {
735+
type: "workflow",
736+
name: "greeting",
737+
worker: "workflow-worker",
738+
exportName: "GreetingWorkflow",
739+
},
740+
},
741+
},
742+
containers: [],
743+
});
744+
745+
expect(result.workflows).toEqual([
746+
{
747+
binding: "WORKFLOW",
748+
name: "greeting",
749+
class_name: "GreetingWorkflow",
750+
script_name: "workflow-worker",
751+
},
752+
]);
753+
});
754+
729755
it("maps logfwdr binding to logfwdr.bindings", ({ expect }) => {
730756
const result = convertToWranglerConfig({
731757
worker: {

‎packages/config/src/__tests__/schema.test.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,29 @@ describe("InputWorkerSchema", () => {
293293
});
294294
});
295295

296+
describe("workflow bindings", () => {
297+
const workflowBinding = {
298+
type: "workflow",
299+
name: "greeting",
300+
worker: "workflow-worker",
301+
exportName: "GreetingWorkflow",
302+
} as const;
303+
304+
it("accepts a cross-Worker Workflow binding", ({ expect }) => {
305+
expect(BindingSchema.safeParse(workflowBinding).success).toBe(true);
306+
});
307+
308+
it.for(["name", "worker", "exportName"] as const)(
309+
"requires %s",
310+
(field, { expect }) => {
311+
const binding: Record<string, unknown> = { ...workflowBinding };
312+
delete binding[field];
313+
314+
expect(BindingSchema.safeParse(binding).success).toBe(false);
315+
}
316+
);
317+
});
318+
296319
describe("entrypoint", () => {
297320
it("accepts a string entrypoint and passes it through unchanged", ({
298321
expect,

‎packages/config/src/__tests__/worker-references.test-d.ts‎

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1-
import { DurableObject, WorkerEntrypoint } from "cloudflare:workers";
1+
import {
2+
DurableObject,
3+
WorkerEntrypoint,
4+
WorkflowEntrypoint,
5+
} from "cloudflare:workers";
26
import { bindings } from "../bindings";
37
import { defineWorker } from "../definition";
48
import { exports as workerExports } from "../exports";
5-
import type { DurableObjectBinding, WorkerBinding } from "../bindings";
9+
import type {
10+
DurableObjectBinding,
11+
WorkerBinding,
12+
WorkflowBinding,
13+
} from "../bindings";
614
import type { InferEnv, UnwrapConfig } from "../inference";
715

816
class Admin extends WorkerEntrypoint {
@@ -17,10 +25,13 @@ class Counter extends DurableObject {
1725
}
1826
}
1927

28+
class GreetingWorkflow extends WorkflowEntrypoint {}
29+
2030
const entrypoint = {
2131
default: { fetch: () => new Response() },
2232
Admin,
2333
Counter,
34+
GreetingWorkflow,
2435
};
2536

2637
const auxiliary = defineWorker({
@@ -29,6 +40,7 @@ const auxiliary = defineWorker({
2940
entrypoint,
3041
exports: {
3142
Counter: workerExports.durableObject({ storage: "sqlite" }),
43+
GreetingWorkflow: workerExports.workflow({ name: "greeting" }),
3244
},
3345
});
3446

@@ -56,6 +68,11 @@ const config = defineWorker({
5668
worker: auxiliary,
5769
exportName: "Counter",
5870
}),
71+
GREETING: bindings.workflow({
72+
name: "greeting",
73+
worker: auxiliary,
74+
exportName: "GreetingWorkflow",
75+
}),
5976
DIRECT_ADMIN: {
6077
type: "worker",
6178
worker: auxiliary,
@@ -80,6 +97,13 @@ bindings.durableObject({
8097
exportName: "Admin",
8198
});
8299

100+
bindings.workflow({
101+
name: "greeting",
102+
worker: auxiliary,
103+
// @ts-expect-error Only configured Workflow exports are accepted.
104+
exportName: "Admin",
105+
});
106+
83107
type Equal<T, U> =
84108
(<V>() => V extends T ? 1 : 2) extends <V>() => V extends U ? 1 : 2
85109
? true
@@ -94,10 +118,15 @@ export type WorkerExportNameTest = Assert<
94118
export type DurableObjectExportNameTest = Assert<
95119
Equal<DurableObjectBinding<Auxiliary>["exportName"], "Counter">
96120
>;
121+
export type WorkflowExportNameTest = Assert<
122+
Equal<WorkflowBinding<Auxiliary>["exportName"], "GreetingWorkflow">
123+
>;
97124
// @ts-expect-error Worker binding export names come from the referenced Worker.
98125
export type InvalidWorkerExportNameTest = WorkerBinding<Auxiliary, "Counter">;
99126
// @ts-expect-error Durable Object export names come from the referenced Worker.
100127
export type InvalidDoExportNameTest = DurableObjectBinding<Auxiliary, "Admin">;
128+
// @ts-expect-error Workflow export names come from the referenced Worker.
129+
export type InvalidWorkflowExportNameTest = WorkflowBinding<Auxiliary, "Admin">;
101130
export type AdminBindingTest = Assert<Equal<Env["ADMIN"], Fetcher<Admin>>>;
102131
export type DefaultBindingTest = Assert<Equal<Env["DEFAULT"], Fetcher>>;
103132
export type FactoryAdminBindingTest = Assert<
@@ -109,4 +138,7 @@ export type DirectAdminBindingTest = Assert<
109138
export type CounterBindingTest = Assert<
110139
Equal<Env["COUNTER"], DurableObjectNamespace<Counter>>
111140
>;
141+
export type GreetingBindingTest = Assert<
142+
Env["GREETING"] extends Workflow ? true : false
143+
>;
112144
export type ExternalBindingTest = Assert<Equal<Env["EXTERNAL"], Fetcher>>;

‎packages/config/src/bindings.ts‎

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -635,16 +635,18 @@ interface WorkflowBindingOptions<
635635
TWorker extends WorkerReference = WorkerReference,
636636
TExportName extends WorkflowExportName<TWorker> = WorkflowExportName<TWorker>,
637637
> {
638+
/** The name of the Workflow. */
639+
name: string;
638640
/** The name or config of the Worker that defines the Workflow. */
639641
worker: TWorker;
640642
/** The exported class name of the Workflow. */
641643
exportName: TExportName;
642644
}
643645

644646
/**
645-
* Binding to a Workflow. `worker` is the name or config of the Worker that
646-
* defines the Workflow; `exportName` is the exported `WorkflowEntrypoint`
647-
* class name.
647+
* Binding to a Workflow. `name` identifies the Workflow, `worker` is the name
648+
* or config of the Worker that defines it, and `exportName` is the exported
649+
* `WorkflowEntrypoint` class name.
648650
*/
649651
export interface WorkflowBinding<
650652
TWorker extends WorkerReference = WorkerReference,
@@ -851,13 +853,17 @@ export interface Bindings {
851853
): WorkerBinding<TWorker, NoInfer<TExportName>>;
852854
/** Binding to a Worker Loader. */
853855
workerLoader(): WorkerLoaderBinding;
854-
// TODO: re-enable when workflow bindings return.
855-
// /**
856-
// * Create a Workflow binding.
857-
// * `worker` may be a Worker config reference or a Worker name.
858-
// * `exportName` must be a valid `WorkflowEntrypoint` export for the given Worker.
859-
// */
860-
// workflow(options: WorkflowBindingOptions): WorkflowBinding;
856+
/**
857+
* Create a Workflow binding.
858+
* `worker` may be a Worker config reference or a Worker name.
859+
* `exportName` must be a valid `WorkflowEntrypoint` export for the given Worker.
860+
*/
861+
workflow<
862+
TWorker extends WorkerReference,
863+
TExportName extends WorkflowExportName<TWorker>,
864+
>(
865+
options: WorkflowBindingOptions<TWorker, TExportName>
866+
): WorkflowBinding<TWorker, NoInfer<TExportName>>;
861867
}
862868

863869
export const bindings = {
@@ -907,6 +913,5 @@ export const bindings = {
907913
vpcNetwork: (options) => ({ type: "vpc-network", ...options }),
908914
worker: (options) => ({ type: "worker", ...options }),
909915
workerLoader: () => ({ type: "worker-loader" }),
910-
// TODO: re-enable when workflow bindings return.
911-
// workflow: (options) => ({ type: "workflow", ...options }),
916+
workflow: (options) => ({ type: "workflow", ...options }),
912917
} as Bindings;

‎packages/config/src/convert.ts‎

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -725,17 +725,15 @@ function convertBindingsAndAssets(
725725
workerLoaders.push({ binding: name });
726726
break;
727727
}
728-
// TODO: re-enable when workflow bindings return.
729-
// case "workflow": {
730-
// workflows.push(
731-
// omitUndefined({
732-
// binding: name,
733-
// class_name: binding.exportName,
734-
// script_name: binding.worker,
735-
// })
736-
// );
737-
// break;
738-
// }
728+
case "workflow": {
729+
workflows.push({
730+
binding: name,
731+
name: binding.name,
732+
class_name: binding.exportName,
733+
script_name: binding.worker,
734+
});
735+
break;
736+
}
739737
}
740738
}
741739

‎packages/config/src/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export {
3030
validateSingletonBindings,
3131
WorkerBindingSchema,
3232
WorkerEntrypointExportSchema,
33+
WorkflowBindingSchema,
3334
WorkflowExportSchema,
3435
} from "./schema";
3536
export { generateTypes } from "./generate";

‎packages/config/src/schema.ts‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ export const WorkerBindingSchema = z.strictObject({
3535
dev: RemoteBindingDevSchema.optional(),
3636
});
3737

38+
export const WorkflowBindingSchema = z.strictObject({
39+
type: z.literal("workflow"),
40+
name: z.string(),
41+
worker: z.string(),
42+
exportName: z.string(),
43+
});
44+
3845
export const D1BindingSchema = z.strictObject({
3946
type: z.literal("d1"),
4047
name: z.string().optional(),
@@ -230,12 +237,7 @@ export const KnownBindingSchema = z.discriminatedUnion("type", [
230237
),
231238
WorkerBindingSchema,
232239
z.strictObject({ type: z.literal("worker-loader") }),
233-
// TODO: support Workflows
234-
// z.strictObject({
235-
// type: z.literal("workflow"),
236-
// worker: z.string(),
237-
// exportName: z.string(),
238-
// }),
240+
WorkflowBindingSchema,
239241
]);
240242

241243
export const UnsafeBindingSchema = z.looseObject({

‎packages/config/src/types.ts‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,7 @@ import type {
4242
VpcServiceBinding,
4343
WorkerBinding,
4444
WorkerLoaderBinding,
45-
// TODO: re-enable when workflow bindings return.
46-
// WorkflowBinding,
45+
WorkflowBinding,
4746
} from "./bindings";
4847
import type { ConfigInput } from "./definition";
4948
import type {
@@ -124,9 +123,8 @@ type Binding =
124123
| VpcNetworkBinding
125124
| VpcServiceBinding
126125
| WorkerBinding
127-
| WorkerLoaderBinding;
128-
// TODO: re-enable when workflow bindings return.
129-
// | WorkflowBinding;
126+
| WorkerLoaderBinding
127+
| WorkflowBinding;
130128

131129
/**
132130
* Union of all trigger definitions accepted in `triggers`.

0 commit comments

Comments
 (0)