Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/experimental-config-workflow-exports.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"wrangler": minor
"@cloudflare/vite-plugin": minor
---

Add experimental config support for declarative Workflow exports and cross-Worker Workflow bindings

`cloudflare.config.ts` can now declare owned Workflows with `exports.workflow()`, including step limits and schedules, and create typed external bindings with a referenced Worker's `workflow()` helper. Wrangler deploys and locally simulates these exports, while the Vite plugin recognizes them as `WorkflowEntrypoint` exports.
68 changes: 66 additions & 2 deletions packages/config/src/__tests__/convert.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { describe, it } from "vitest";
import { bindings as bindingConfig } from "../bindings";
import { convertToWranglerConfig } from "../convert";
import { exports as exportConfig } from "../exports";
import { defineWorker } from "../worker-definition";

const baseConfig = { name: "worker", compatibilityDate: "2026-06-01" } as const;

Expand Down Expand Up @@ -782,6 +784,28 @@ describe("convertToWranglerConfig", () => {
});
});

it("passes Workflow exports through", ({ expect }) => {
const result = convertToWranglerConfig({
...baseConfig,
exports: {
MyWorkflow: exportConfig.workflow({
name: "my-workflow",
limits: { steps: 25_000 },
schedules: ["0 * * * *"],
}),
},
});

expect(result.exports).toEqual({
MyWorkflow: {
type: "workflow",
name: "my-workflow",
limits: { steps: 25_000 },
schedules: ["0 * * * *"],
},
});
});

it("emits no exports key when the map is empty", ({ expect }) => {
const result = convertToWranglerConfig({ ...baseConfig, exports: {} });
expect("exports" in (result as object)).toBe(false);
Expand All @@ -791,16 +815,56 @@ describe("convertToWranglerConfig", () => {
const config = {
...baseConfig,
exports: {
FutureExport: { type: "workflow" },
FutureExport: { type: "container" },
},
} as unknown as Parameters<typeof convertToWranglerConfig>[0];

expect(() => convertToWranglerConfig(config)).toThrow(
/Unknown export types found: - FutureExport : workflow/
/Unknown export types found: - FutureExport : container/
);
});
});

describe("Workflow bindings", () => {
it("maps an export-based Workflow binding", ({ expect }) => {
const workflowWorker = defineWorker({
...baseConfig,
name: "workflow-worker",
exports: {
MyWorkflow: exportConfig.workflow({ name: "my-workflow" }),
},
});
const result = convertToWranglerConfig({
...baseConfig,
env: {
MY_WORKFLOW: workflowWorker.workflow({
workerName: "workflow-worker",
exportName: "MyWorkflow",
remote: true,
}),
UNTYPED_WORKFLOW: bindingConfig.workflow({
workerName: "other-worker",
exportName: "OtherWorkflow",
}),
},
});

expect(result.workflows).toEqual([
{
binding: "MY_WORKFLOW",
class_name: "MyWorkflow",
script_name: "workflow-worker",
remote: true,
},
{
binding: "UNTYPED_WORKFLOW",
class_name: "OtherWorkflow",
script_name: "other-worker",
},
]);
});
});

describe("triggers", () => {
it("maps scheduled triggers to triggers.crons", ({ expect }) => {
const result = convertToWranglerConfig({
Expand Down
42 changes: 42 additions & 0 deletions packages/config/src/__tests__/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,48 @@ describe("InputWorkerSchema", () => {
});
});

describe("Workflows", () => {
it("accepts Workflow exports and bindings", ({ expect }) => {
const result = InputWorkerSchema.safeParse({
...baseConfig,
env: {
MY_WORKFLOW: {
type: "workflow",
workerName: "workflow-worker",
exportName: "MyWorkflow",
remote: true,
},
},
exports: {
MyWorkflow: {
type: "workflow",
name: "my-workflow",
limits: { steps: 25_000 },
schedules: ["0 * * * *"],
},
},
});

expect(result.success).toBe(true);
});

it("rejects invalid Workflow export configuration", ({ expect }) => {
const result = InputWorkerSchema.safeParse({
...baseConfig,
exports: {
MyWorkflow: {
type: "workflow",
name: "",
limits: { steps: 25_001 },
schedules: [],
},
},
});

expect(result.success).toBe(false);
});
});

describe("unknown property rejection", () => {
it("rejects unknown top-level keys (typo)", ({ expect }) => {
const result = InputWorkerSchema.safeParse({
Expand Down
16 changes: 7 additions & 9 deletions packages/config/src/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -828,13 +828,12 @@ export interface Bindings {
worker(options: WorkerBindingOptions): WorkerBinding;
/** Binding to a Worker Loader. */
workerLoader(): WorkerLoaderBinding;
// TODO: re-enable when workflow bindings return.
// /**
// * Create a Workflow binding.
// * `workerName` must match a known config's name (or any `string` for untyped bindings).
// * `exportName` must be a valid `WorkflowEntrypoint` export for the given Worker.
// */
// workflow(options: WorkflowBindingOptions): WorkflowBinding;
/**
* Create a Workflow binding.
* `workerName` must match a known config's name (or any `string` for untyped bindings).
* `exportName` must be a valid `WorkflowEntrypoint` export for the given Worker.
*/
workflow(options: WorkflowBindingOptions): WorkflowBinding;
}

export const bindings = {
Expand Down Expand Up @@ -885,6 +884,5 @@ export const bindings = {
webSearch: (options) => ({ type: "web-search", ...options }),
worker: (options) => ({ type: "worker", ...options }),
workerLoader: () => ({ type: "worker-loader" }),
// TODO: re-enable when workflow bindings return.
// workflow: (options) => ({ type: "workflow", ...options }),
workflow: (options) => ({ type: "workflow", ...options }),
} as Bindings;
27 changes: 15 additions & 12 deletions packages/config/src/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,18 +543,17 @@ function convertBindingsAndAssets(
workerLoaders.push({ binding: name });
break;
}
// TODO: re-enable when workflow bindings return.
// case "workflow": {
// workflows.push(
// omitUndefined({
// binding: name,
// class_name: binding.exportName,
// script_name: binding.workerName,
// remote: binding.remote,
// })
// );
// break;
// }
case "workflow": {
workflows.push(
omitUndefined({
binding: name,
class_name: binding.exportName,
script_name: binding.workerName,
remote: binding.remote,
})
);
break;
}
}
}

Expand Down Expand Up @@ -683,6 +682,10 @@ function convertExports(
converted[exportName] = value;
continue;
}
if (value.type === "workflow") {
converted[exportName] = value;
continue;
}

if (value.type !== "durable-object") {
unknownExports[exportName] = value;
Expand Down
25 changes: 25 additions & 0 deletions packages/config/src/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,23 @@ export interface WorkerEntrypointExport extends WorkerEntrypointExportOptions {
type: "worker";
}

export interface WorkflowExportOptions {
/** The account-unique name of the Workflow. */
name: string;
/** Optional limits for the Workflow. */
limits?: {
/** Maximum number of steps a Workflow instance can execute. */
steps?: number;
};
/** Optional cron schedules for automatically triggering Workflow instances. */
schedules?: string[];
}

/** Declares a Workflow defined by this Worker. */
export interface WorkflowExport extends WorkflowExportOptions {
type: "workflow";
}

/**
* Configuration for named exports declared by the Worker. Each entry's
* key is the exported class name; the value configures the export.
Expand Down Expand Up @@ -179,6 +196,9 @@ export interface Exports {

/** Declares a WorkerEntrypoint export defined by this Worker. */
worker(options?: WorkerEntrypointExportOptions): WorkerEntrypointExport;

/** Declares a WorkflowEntrypoint export defined by this Worker. */
workflow(options: WorkflowExportOptions): WorkflowExport;
}

function durableObject(
Expand Down Expand Up @@ -213,6 +233,10 @@ function worker(
return { type: "worker", ...options };
}

function workflow(options: WorkflowExportOptions): WorkflowExport {
return { type: "workflow", ...options };
}

/**
* Exports builder for configuring Worker exports.
*
Expand All @@ -234,4 +258,5 @@ function worker(
export const exports: Exports = {
durableObject,
worker,
workflow,
};
2 changes: 2 additions & 0 deletions packages/config/src/public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ export type {
DurableObjectExpectingTransferExport,
WorkerEntrypointExport,
WorkerEntrypointExportOptions,
WorkflowExport,
WorkflowExportOptions,
} from "./exports";
export { exports } from "./exports";
export type {
Expand Down
27 changes: 14 additions & 13 deletions packages/config/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,13 +186,12 @@ const KnownBindingSchema = z.discriminatedUnion("type", [
remote: z.boolean().optional(),
}),
z.strictObject({ type: z.literal("worker-loader") }),
// TODO: support Workflows
// z.strictObject({
// type: z.literal("workflow"),
// workerName: z.string(),
// exportName: z.string(),
// remote: z.boolean().optional(),
// }),
z.strictObject({
type: z.literal("workflow"),
workerName: z.string(),
exportName: z.string(),
remote: z.boolean().optional(),
}),
]);

const UnsafeBindingSchema = z.looseObject({
Expand Down Expand Up @@ -321,12 +320,14 @@ const ExportSchema = z.union([
type: z.literal("worker"),
cache: z.strictObject({ enabled: z.boolean() }).optional(),
}),
// TODO: support Workflows
// z.strictObject({
// type: z.literal("workflow"),
// name: z.string(),
// limits: z.strictObject({ steps: z.number().optional() }).optional(),
// }),
z.strictObject({
type: z.literal("workflow"),
name: z.string().min(1),
limits: z
.strictObject({ steps: z.number().int().min(1).max(25_000).optional() })
.optional(),
schedules: z.array(z.string().min(1)).min(1).max(100).optional(),
}),
]);

const LimitsSchema = z.strictObject({
Expand Down
13 changes: 6 additions & 7 deletions packages/config/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ import type {
WebSearchBinding,
WorkerBinding,
WorkerLoaderBinding,
// TODO: re-enable when workflow bindings return.
// WorkflowBinding,
WorkflowBinding,
} from "./bindings";
import type {
DurableObjectDeletedExport,
Expand All @@ -53,6 +52,7 @@ import type {
DurableObjectRenamedExport,
DurableObjectTransferredExport,
WorkerEntrypointExport,
WorkflowExport,
} from "./exports";
import type { WorkerModule } from "./inference";
import type {
Expand Down Expand Up @@ -100,9 +100,8 @@ type Binding =
| VpcServiceBinding
| WebSearchBinding
| WorkerBinding
| WorkerLoaderBinding;
// TODO: re-enable when workflow bindings return.
// | WorkflowBinding;
| WorkerLoaderBinding
| WorkflowBinding;

/**
* Union of all trigger definitions accepted in `triggers`.
Expand All @@ -120,8 +119,8 @@ type Export =
| DurableObjectRenamedExport
| DurableObjectTransferredExport
| DurableObjectExpectingTransferExport
| WorkerEntrypointExport;
// TODO: support Workflows
| WorkerEntrypointExport
| WorkflowExport;

/**
* Worker configuration. This is the input shape passed to
Expand Down
Loading
Loading