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
38 changes: 38 additions & 0 deletions .changeset/workflows-schedule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
"wrangler": minor
---

Add `schedule` property to Workflow bindings for cron-based triggering

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.

```jsonc
// wrangler.json
{
"workflows": [
{
"binding": "MY_WORKFLOW",
"name": "my-workflow",
"class_name": "MyWorkflow",
"schedule": "0 9 * * 1",
},
],
}
```

Multiple schedules can be provided as an array:

```jsonc
{
"workflows": [
{
"binding": "MY_WORKFLOW",
"name": "my-workflow",
"class_name": "MyWorkflow",
"schedule": ["0 9 * * 1", "0 17 * * 5"],
},
],
}
```

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.
2 changes: 2 additions & 0 deletions packages/workers-utils/src/config/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,8 @@ export type WorkflowBinding = {
/** Maximum number of steps a Workflow instance can execute */
steps?: number;
};
/** Optional cron schedule(s) for automatically triggering workflow instances */
schedule?: string | string[];
};

/**
Expand Down
38 changes: 38 additions & 0 deletions packages/workers-utils/src/config/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2657,6 +2657,43 @@ const validateWorkflowBinding: ValidatorFn = (diagnostics, field, value) => {
isValid = false;
}

if (hasProperty(value, "schedule") && value.schedule !== undefined) {
if (typeof value.schedule === "string") {
if (value.schedule.length === 0) {
diagnostics.errors.push(
`"${field}" bindings "schedule" field must not be an empty string.`
);
isValid = false;
}
} else if (Array.isArray(value.schedule)) {
if (value.schedule.length === 0) {
diagnostics.errors.push(
`"${field}" bindings "schedule" field must not be an empty array.`
);
isValid = false;
} else if (!value.schedule.every((s: unknown) => typeof s === "string")) {
diagnostics.errors.push(
`"${field}" bindings should, optionally, have a string or array of strings "schedule" field but got ${JSON.stringify(
value
)}.`
);
isValid = false;
} else if (value.schedule.some((s: unknown) => s === "")) {
diagnostics.errors.push(
`"${field}" bindings "schedule" field must not contain empty strings.`
);
isValid = false;
}
} else {
diagnostics.errors.push(
`"${field}" bindings should, optionally, have a string or array of strings "schedule" field but got ${JSON.stringify(
value
)}.`
);
isValid = false;
}
}

if (hasProperty(value, "limits") && value.limits !== undefined) {
if (
typeof value.limits !== "object" ||
Expand Down Expand Up @@ -2705,6 +2742,7 @@ const validateWorkflowBinding: ValidatorFn = (diagnostics, field, value) => {
"script_name",
"remote",
"limits",
"schedule",
]);

return isValid;
Expand Down
1 change: 1 addition & 0 deletions packages/workers-utils/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ export interface CfWorkflow {
limits?: {
steps?: number;
};
schedule?: string | string[];
}

export interface CfQueue {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4956,6 +4956,52 @@ describe("normalizeAndValidateConfig()", () => {
expect(diagnostics.hasWarnings()).toBe(false);
});

it("should accept valid workflow bindings with schedule as a string", ({
expect,
}) => {
const { diagnostics } = normalizeAndValidateConfig(
{
workflows: [
{
binding: "MY_WORKFLOW",
name: "my-workflow",
class_name: "MyWorkflow",
schedule: "*/5 * * * *",
},
],
} as unknown as RawConfig,
undefined,
undefined,
{ env: undefined }
);

expect(diagnostics.hasErrors()).toBe(false);
expect(diagnostics.hasWarnings()).toBe(false);
});

it("should accept valid workflow bindings with schedule as an array of strings", ({
expect,
}) => {
const { diagnostics } = normalizeAndValidateConfig(
{
workflows: [
{
binding: "MY_WORKFLOW",
name: "my-workflow",
class_name: "MyWorkflow",
schedule: ["*/5 * * * *", "0 9 * * 1"],
},
],
} as unknown as RawConfig,
undefined,
undefined,
{ env: undefined }
);

expect(diagnostics.hasErrors()).toBe(false);
expect(diagnostics.hasWarnings()).toBe(false);
});

it("should error if workflow bindings are not valid", ({ expect }) => {
const { diagnostics } = normalizeAndValidateConfig(
{
Expand Down Expand Up @@ -5036,6 +5082,130 @@ describe("normalizeAndValidateConfig()", () => {
`);
});

it("should error if schedule has wrong type", ({ expect }) => {
const { diagnostics } = normalizeAndValidateConfig(
{
workflows: [
{
binding: "MY_WORKFLOW",
name: "my-workflow",
class_name: "MyWorkflow",
schedule: 123,
},
],
} as unknown as RawConfig,
undefined,
undefined,
{ env: undefined }
);

expect(diagnostics.hasErrors()).toBe(true);
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
"Processing wrangler configuration:
- "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}."
`);
});

it("should error if schedule is an empty string", ({ expect }) => {
const { diagnostics } = normalizeAndValidateConfig(
{
workflows: [
{
binding: "MY_WORKFLOW",
name: "my-workflow",
class_name: "MyWorkflow",
schedule: "",
},
],
} as unknown as RawConfig,
undefined,
undefined,
{ env: undefined }
);

expect(diagnostics.hasErrors()).toBe(true);
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
"Processing wrangler configuration:
- "workflows[0]" bindings "schedule" field must not be an empty string."
`);
});

it("should error if schedule is an empty array", ({ expect }) => {
const { diagnostics } = normalizeAndValidateConfig(
{
workflows: [
{
binding: "MY_WORKFLOW",
name: "my-workflow",
class_name: "MyWorkflow",
schedule: [],
},
],
} as unknown as RawConfig,
undefined,
undefined,
{ env: undefined }
);

expect(diagnostics.hasErrors()).toBe(true);
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
"Processing wrangler configuration:
- "workflows[0]" bindings "schedule" field must not be an empty array."
`);
});

it("should error if schedule is an array containing non-strings", ({
expect,
}) => {
const { diagnostics } = normalizeAndValidateConfig(
{
workflows: [
{
binding: "MY_WORKFLOW",
name: "my-workflow",
class_name: "MyWorkflow",
schedule: ["*/5 * * * *", 123],
},
],
} as unknown as RawConfig,
undefined,
undefined,
{ env: undefined }
);

expect(diagnostics.hasErrors()).toBe(true);
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
"Processing wrangler configuration:
- "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]}."
`);
});

it("should error if schedule is an array containing empty strings", ({
expect,
}) => {
const { diagnostics } = normalizeAndValidateConfig(
{
workflows: [
{
binding: "MY_WORKFLOW",
name: "my-workflow",
class_name: "MyWorkflow",
schedule: ["*/5 * * * *", ""],
},
],
} as unknown as RawConfig,
undefined,
undefined,
{ env: undefined }
);

expect(diagnostics.hasErrors()).toBe(true);
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
"Processing wrangler configuration:
- "workflows[0]" bindings "schedule" field must not contain empty strings."
`);
});

it("should error if limits is not an object", ({ expect }) => {
const { diagnostics } = normalizeAndValidateConfig(
{
Expand Down
Loading
Loading