Skip to content

Commit 3a1fbed

Browse files
deloreyjpombosilva
andauthored
[wrangler] Add schedule property to Workflow bindings for cron-based triggering (#13467)
Co-authored-by: Olga Silva <osilva@cloudflare.com>
1 parent 65a4b4e commit 3a1fbed

8 files changed

Lines changed: 628 additions & 7 deletions

File tree

.changeset/workflows-schedule.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
"wrangler": minor
3+
---
4+
5+
Add `schedule` property to Workflow bindings for cron-based triggering
6+
7+
> **Note:** This is a configuration-only change. Scheduled triggering of Workflow instances is not yet available — adding `schedule` to a Workflow binding will not result in scheduled invocations at this time. This change lays the groundwork for an upcoming feature.
8+
9+
Workflow bindings in `wrangler.json` now accept an optional `schedule` field that configures one or more cron expressions to automatically trigger new workflow instances on a schedule.
10+
11+
```jsonc
12+
// wrangler.json
13+
{
14+
"workflows": [
15+
{
16+
"binding": "MY_WORKFLOW",
17+
"name": "my-workflow",
18+
"class_name": "MyWorkflow",
19+
"schedule": "0 9 * * 1",
20+
},
21+
],
22+
}
23+
```
24+
25+
Multiple schedules can be provided as an array:
26+
27+
```jsonc
28+
{
29+
"workflows": [
30+
{
31+
"binding": "MY_WORKFLOW",
32+
"name": "my-workflow",
33+
"class_name": "MyWorkflow",
34+
"schedule": ["0 9 * * 1", "0 17 * * 5"],
35+
},
36+
],
37+
}
38+
```
39+
40+
The schedule is sent to the Workflows control plane on `wrangler deploy`. Configuring `schedule` on a workflow binding that references an external `script_name` is an error — the schedule must be configured on the worker that defines the workflow.

packages/workers-utils/src/config/environment.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,8 @@ export type WorkflowBinding = {
728728
/** Maximum number of steps a Workflow instance can execute */
729729
steps?: number;
730730
};
731+
/** Optional cron schedule(s) for automatically triggering workflow instances */
732+
schedule?: string | string[];
731733
};
732734

733735
/**

packages/workers-utils/src/config/validation.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2699,6 +2699,43 @@ const validateWorkflowBinding: ValidatorFn = (diagnostics, field, value) => {
26992699
isValid = false;
27002700
}
27012701

2702+
if (hasProperty(value, "schedule") && value.schedule !== undefined) {
2703+
if (typeof value.schedule === "string") {
2704+
if (value.schedule.length === 0) {
2705+
diagnostics.errors.push(
2706+
`"${field}" bindings "schedule" field must not be an empty string.`
2707+
);
2708+
isValid = false;
2709+
}
2710+
} else if (Array.isArray(value.schedule)) {
2711+
if (value.schedule.length === 0) {
2712+
diagnostics.errors.push(
2713+
`"${field}" bindings "schedule" field must not be an empty array.`
2714+
);
2715+
isValid = false;
2716+
} else if (!value.schedule.every((s: unknown) => typeof s === "string")) {
2717+
diagnostics.errors.push(
2718+
`"${field}" bindings should, optionally, have a string or array of strings "schedule" field but got ${JSON.stringify(
2719+
value
2720+
)}.`
2721+
);
2722+
isValid = false;
2723+
} else if (value.schedule.some((s: unknown) => s === "")) {
2724+
diagnostics.errors.push(
2725+
`"${field}" bindings "schedule" field must not contain empty strings.`
2726+
);
2727+
isValid = false;
2728+
}
2729+
} else {
2730+
diagnostics.errors.push(
2731+
`"${field}" bindings should, optionally, have a string or array of strings "schedule" field but got ${JSON.stringify(
2732+
value
2733+
)}.`
2734+
);
2735+
isValid = false;
2736+
}
2737+
}
2738+
27022739
if (hasProperty(value, "limits") && value.limits !== undefined) {
27032740
if (
27042741
typeof value.limits !== "object" ||
@@ -2747,6 +2784,7 @@ const validateWorkflowBinding: ValidatorFn = (diagnostics, field, value) => {
27472784
"script_name",
27482785
"remote",
27492786
"limits",
2787+
"schedule",
27502788
]);
27512789

27522790
return isValid;

packages/workers-utils/src/worker.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ export interface CfWorkflow {
192192
limits?: {
193193
steps?: number;
194194
};
195+
schedule?: string | string[];
195196
}
196197

197198
export interface CfQueue {

packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5204,6 +5204,52 @@ describe("normalizeAndValidateConfig()", () => {
52045204
expect(diagnostics.hasWarnings()).toBe(false);
52055205
});
52065206

5207+
it("should accept valid workflow bindings with schedule as a string", ({
5208+
expect,
5209+
}) => {
5210+
const { diagnostics } = normalizeAndValidateConfig(
5211+
{
5212+
workflows: [
5213+
{
5214+
binding: "MY_WORKFLOW",
5215+
name: "my-workflow",
5216+
class_name: "MyWorkflow",
5217+
schedule: "*/5 * * * *",
5218+
},
5219+
],
5220+
} as unknown as RawConfig,
5221+
undefined,
5222+
undefined,
5223+
{ env: undefined }
5224+
);
5225+
5226+
expect(diagnostics.hasErrors()).toBe(false);
5227+
expect(diagnostics.hasWarnings()).toBe(false);
5228+
});
5229+
5230+
it("should accept valid workflow bindings with schedule as an array of strings", ({
5231+
expect,
5232+
}) => {
5233+
const { diagnostics } = normalizeAndValidateConfig(
5234+
{
5235+
workflows: [
5236+
{
5237+
binding: "MY_WORKFLOW",
5238+
name: "my-workflow",
5239+
class_name: "MyWorkflow",
5240+
schedule: ["*/5 * * * *", "0 9 * * 1"],
5241+
},
5242+
],
5243+
} as unknown as RawConfig,
5244+
undefined,
5245+
undefined,
5246+
{ env: undefined }
5247+
);
5248+
5249+
expect(diagnostics.hasErrors()).toBe(false);
5250+
expect(diagnostics.hasWarnings()).toBe(false);
5251+
});
5252+
52075253
it("should error if workflow bindings are not valid", ({ expect }) => {
52085254
const { diagnostics } = normalizeAndValidateConfig(
52095255
{
@@ -5284,6 +5330,130 @@ describe("normalizeAndValidateConfig()", () => {
52845330
`);
52855331
});
52865332

5333+
it("should error if schedule has wrong type", ({ expect }) => {
5334+
const { diagnostics } = normalizeAndValidateConfig(
5335+
{
5336+
workflows: [
5337+
{
5338+
binding: "MY_WORKFLOW",
5339+
name: "my-workflow",
5340+
class_name: "MyWorkflow",
5341+
schedule: 123,
5342+
},
5343+
],
5344+
} as unknown as RawConfig,
5345+
undefined,
5346+
undefined,
5347+
{ env: undefined }
5348+
);
5349+
5350+
expect(diagnostics.hasErrors()).toBe(true);
5351+
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
5352+
"Processing wrangler configuration:
5353+
- "workflows[0]" bindings should, optionally, have a string or array of strings "schedule" field but got {"binding":"MY_WORKFLOW","name":"my-workflow","class_name":"MyWorkflow","schedule":123}."
5354+
`);
5355+
});
5356+
5357+
it("should error if schedule is an empty string", ({ expect }) => {
5358+
const { diagnostics } = normalizeAndValidateConfig(
5359+
{
5360+
workflows: [
5361+
{
5362+
binding: "MY_WORKFLOW",
5363+
name: "my-workflow",
5364+
class_name: "MyWorkflow",
5365+
schedule: "",
5366+
},
5367+
],
5368+
} as unknown as RawConfig,
5369+
undefined,
5370+
undefined,
5371+
{ env: undefined }
5372+
);
5373+
5374+
expect(diagnostics.hasErrors()).toBe(true);
5375+
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
5376+
"Processing wrangler configuration:
5377+
- "workflows[0]" bindings "schedule" field must not be an empty string."
5378+
`);
5379+
});
5380+
5381+
it("should error if schedule is an empty array", ({ expect }) => {
5382+
const { diagnostics } = normalizeAndValidateConfig(
5383+
{
5384+
workflows: [
5385+
{
5386+
binding: "MY_WORKFLOW",
5387+
name: "my-workflow",
5388+
class_name: "MyWorkflow",
5389+
schedule: [],
5390+
},
5391+
],
5392+
} as unknown as RawConfig,
5393+
undefined,
5394+
undefined,
5395+
{ env: undefined }
5396+
);
5397+
5398+
expect(diagnostics.hasErrors()).toBe(true);
5399+
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
5400+
"Processing wrangler configuration:
5401+
- "workflows[0]" bindings "schedule" field must not be an empty array."
5402+
`);
5403+
});
5404+
5405+
it("should error if schedule is an array containing non-strings", ({
5406+
expect,
5407+
}) => {
5408+
const { diagnostics } = normalizeAndValidateConfig(
5409+
{
5410+
workflows: [
5411+
{
5412+
binding: "MY_WORKFLOW",
5413+
name: "my-workflow",
5414+
class_name: "MyWorkflow",
5415+
schedule: ["*/5 * * * *", 123],
5416+
},
5417+
],
5418+
} as unknown as RawConfig,
5419+
undefined,
5420+
undefined,
5421+
{ env: undefined }
5422+
);
5423+
5424+
expect(diagnostics.hasErrors()).toBe(true);
5425+
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
5426+
"Processing wrangler configuration:
5427+
- "workflows[0]" bindings should, optionally, have a string or array of strings "schedule" field but got {"binding":"MY_WORKFLOW","name":"my-workflow","class_name":"MyWorkflow","schedule":["*/5 * * * *",123]}."
5428+
`);
5429+
});
5430+
5431+
it("should error if schedule is an array containing empty strings", ({
5432+
expect,
5433+
}) => {
5434+
const { diagnostics } = normalizeAndValidateConfig(
5435+
{
5436+
workflows: [
5437+
{
5438+
binding: "MY_WORKFLOW",
5439+
name: "my-workflow",
5440+
class_name: "MyWorkflow",
5441+
schedule: ["*/5 * * * *", ""],
5442+
},
5443+
],
5444+
} as unknown as RawConfig,
5445+
undefined,
5446+
undefined,
5447+
{ env: undefined }
5448+
);
5449+
5450+
expect(diagnostics.hasErrors()).toBe(true);
5451+
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
5452+
"Processing wrangler configuration:
5453+
- "workflows[0]" bindings "schedule" field must not contain empty strings."
5454+
`);
5455+
});
5456+
52875457
it("should error if limits is not an object", ({ expect }) => {
52885458
const { diagnostics } = normalizeAndValidateConfig(
52895459
{

0 commit comments

Comments
 (0)