From 6f9a0f846bde53bee7bbc82b870e8daf897f0992 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Tue, 18 Aug 2026 12:20:55 +0100 Subject: [PATCH 1/4] docs: document cron schedule windows --- docs/tasks/scheduled.mdx | 45 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/docs/tasks/scheduled.mdx b/docs/tasks/scheduled.mdx index 3731ec04103..c754ef46ec4 100644 --- a/docs/tasks/scheduled.mdx +++ b/docs/tasks/scheduled.mdx @@ -140,6 +140,50 @@ To use imperative schedules you need to do two things: 1. Define a task in your code using `schedules.task()`. 2. Attach 1+ schedules to the task either using the dashboard or the SDK. +## Spreading runs with windows + +By default a schedule runs at its exact cron time. When many schedules share the same cron pattern — for example a daily `0 0 * * *` across thousands of users — they all fire at the same instant and create a load spike. A **window** spreads those runs out: each schedule is assigned a stable time within the window after its cron time, so the load is smoothed while each schedule keeps firing at a predictable, repeatable moment. + +The assigned time is deterministic. A given schedule always lands at the same offset for a given interval, so runs don't jump around between occurrences. + +Set a `window` as either: + +- **An absolute duration** in whole minutes or hours, up to 24 hours: `"30m"`, `"2h"`, `"24h"`. Absolute windows are capped at the next cron time, so a run is never delayed past its following occurrence. +- **A percentage** of the interval between runs: `"30%"`, `"100%"`. A `"50%"` window on an hourly schedule spreads runs across the first 30 minutes of each hour. + +Set `"0m"` (or `"0%"`) for no spreading — the run fires at its exact cron time. + +Declarative schedules set the window on the `cron` object: + +```ts +export const dailyReport = schedules.task({ + id: "daily-report", + cron: { + pattern: "0 0 * * *", + // spread this run across the 30 minutes after midnight + window: "30m", + }, + run: async (payload) => {}, +}); +``` + +Imperative schedules set it when creating or updating a schedule, either in the dashboard form or through the SDK: + +```ts +const createdSchedule = await schedules.create({ + task: dailyReport.id, + cron: "0 0 * * *", + window: "30m", + deduplicationKey: "user_123456-daily-report", +}); +``` + + + The payload `timestamp` and `upcoming` values are always the nominal cron times, not the assigned + times. When you retrieve a schedule, `nextRun` is the nominal cron time and `nextRunEffectiveAt` + is the assigned time the run will actually start. + + ## Supported cron syntax ``` @@ -198,6 +242,7 @@ These are the options when creating a schedule: | Task | The id of the task you want to attach to. | | Cron pattern | The schedule in cron format. You can also describe it in natural language and press "Generate" to fill this in. | | Timezone | The timezone the schedule will run in. Defaults to "UTC" | +| Window | An optional [window](#spreading-runs-with-windows) to spread runs after their cron time, e.g. `30m`, `2h`, or `50%`. | | External id | An optional external id, usually you'd use a userId. | | Deduplication key | An optional deduplication key. If you pass the same value, it will update rather than create. Scoped per project, not per environment. | | Environments | The environments this schedule will run in. | From 29e705c8f479cf4f6696e3b4f68485efdea6bc1f Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Tue, 18 Aug 2026 12:27:56 +0100 Subject: [PATCH 2/4] update doc --- docs/tasks/scheduled.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tasks/scheduled.mdx b/docs/tasks/scheduled.mdx index c754ef46ec4..ec1519da838 100644 --- a/docs/tasks/scheduled.mdx +++ b/docs/tasks/scheduled.mdx @@ -142,9 +142,9 @@ To use imperative schedules you need to do two things: ## Spreading runs with windows -By default a schedule runs at its exact cron time. When many schedules share the same cron pattern — for example a daily `0 0 * * *` across thousands of users — they all fire at the same instant and create a load spike. A **window** spreads those runs out: each schedule is assigned a stable time within the window after its cron time, so the load is smoothed while each schedule keeps firing at a predictable, repeatable moment. +By default a schedule runs at its exact cron time. When many schedules share the same cron pattern, such as a daily `0 9 * * *`, they all fire at the same time and load your downstream systems as well as ours. A **window** spreads those runs out: each schedule is assigned a stable time within the window after its cron time, so the load is smoothed while each schedule keeps firing at a predictable, repeatable moment. -The assigned time is deterministic. A given schedule always lands at the same offset for a given interval, so runs don't jump around between occurrences. +The assigned time is deterministic. A given schedule always lands at the same offset for a given interval, so runs don't jump around between occurrences. And you can always see exactly when the next run will start. Set a `window` as either: From 741c63e44652f50cf81fad0bd6ac2d51daefc849 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Tue, 18 Aug 2026 12:33:22 +0100 Subject: [PATCH 3/4] docs: add schedule window fields to management API spec --- docs/v3-openapi.yaml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/v3-openapi.yaml b/docs/v3-openapi.yaml index a97ae70307e..1607d36ffb8 100644 --- a/docs/v3-openapi.yaml +++ b/docs/v3-openapi.yaml @@ -5744,6 +5744,10 @@ components: type: string example: "America/New_York" description: Defaults to "UTC". In IANA format ("America/New_York"). If set then it will trigger at the CRON frequency in that timezone and respect daylight savings time. + window: + type: string + example: "30m" + description: Optionally spread runs by assigning each schedule a stable time within this window after its cron time. Use a whole duration in minutes or hours up to 24 hours ("30m", "2h", "24h"), or a percentage of the interval between runs ("30%", "100%"). Use "0m" for no spreading. required: - task - cron @@ -5761,6 +5765,10 @@ components: type: string example: "America/New_York" description: Defaults to "UTC". In IANA format ("America/New_York"). If set then it will trigger at the CRON frequency in that timezone and respect daylight savings time. + window: + type: string + example: "30m" + description: Optionally spread runs by assigning each schedule a stable time within this window after its cron time. Use a whole duration in minutes or hours up to 24 hours ("30m", "2h", "24h"), or a percentage of the interval between runs ("30%", "100%"). Use "0m" for no spreading. required: - task - cron @@ -5812,11 +5820,20 @@ components: type: string example: "America/New_York" description: Defaults to UTC. In IANA format, if set then it will trigger at the CRON frequency in that timezone and respect daylight savings time. + window: + type: string + example: "30m" + description: The configured window used to spread runs after their cron time, if set. A whole duration such as "30m" or "2h", or a percentage such as "30%". nextRun: type: string format: date-time - description: The next time the schedule will run + description: The next nominal cron time the schedule will run example: "2024-04-01T00:00:00Z" + nextRunEffectiveAt: + type: string + format: date-time + description: The stable assigned time the next run will actually start. Equals `nextRun` when no window is set. + example: "2024-04-01T00:12:00Z" environments: type: array items: From 182ee5d488cce0dfb2b156dcc9411fbb91b18383 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Tue, 18 Aug 2026 12:34:43 +0100 Subject: [PATCH 4/4] format --- docs/v3-openapi.yaml | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/docs/v3-openapi.yaml b/docs/v3-openapi.yaml index 1607d36ffb8..a97ae70307e 100644 --- a/docs/v3-openapi.yaml +++ b/docs/v3-openapi.yaml @@ -5744,10 +5744,6 @@ components: type: string example: "America/New_York" description: Defaults to "UTC". In IANA format ("America/New_York"). If set then it will trigger at the CRON frequency in that timezone and respect daylight savings time. - window: - type: string - example: "30m" - description: Optionally spread runs by assigning each schedule a stable time within this window after its cron time. Use a whole duration in minutes or hours up to 24 hours ("30m", "2h", "24h"), or a percentage of the interval between runs ("30%", "100%"). Use "0m" for no spreading. required: - task - cron @@ -5765,10 +5761,6 @@ components: type: string example: "America/New_York" description: Defaults to "UTC". In IANA format ("America/New_York"). If set then it will trigger at the CRON frequency in that timezone and respect daylight savings time. - window: - type: string - example: "30m" - description: Optionally spread runs by assigning each schedule a stable time within this window after its cron time. Use a whole duration in minutes or hours up to 24 hours ("30m", "2h", "24h"), or a percentage of the interval between runs ("30%", "100%"). Use "0m" for no spreading. required: - task - cron @@ -5820,20 +5812,11 @@ components: type: string example: "America/New_York" description: Defaults to UTC. In IANA format, if set then it will trigger at the CRON frequency in that timezone and respect daylight savings time. - window: - type: string - example: "30m" - description: The configured window used to spread runs after their cron time, if set. A whole duration such as "30m" or "2h", or a percentage such as "30%". nextRun: type: string format: date-time - description: The next nominal cron time the schedule will run + description: The next time the schedule will run example: "2024-04-01T00:00:00Z" - nextRunEffectiveAt: - type: string - format: date-time - description: The stable assigned time the next run will actually start. Equals `nextRun` when no window is set. - example: "2024-04-01T00:12:00Z" environments: type: array items: