feat: surface cron windows in webapp, cli, sdk - #4572
Conversation
🦋 Changeset detectedLatest commit: eb8c3af The changes in this PR will be included in the next version bump. This PR includes changesets to release 27 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughThe change adds configurable duration and percentage windows to scheduled tasks. The schedule engine calculates deterministic effective execution times while preserving nominal CRON timestamps. Database models store window configuration and schedule phases. APIs, deployment output, dashboard views, and forms expose the window and both timestamps. Queue and ClickHouse records now preserve effective queue timestamps. Tests cover timing, persistence, validation, API lifecycle, deployment output, and queue behavior. 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
.server-changes/schedule-windows.md (1)
6-6: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDescribe the user-visible capability.
Replace “server support” with what users can configure and see. This file is published as dashboard-facing release notes.
Proposed text
- Add server support for stable execution windows on scheduled tasks while preserving each occurrence's nominal timestamp. + Configure stable execution windows for scheduled tasks while preserving each occurrence's nominal CRON time.Based on learnings,
.server-changesentries are published verbatim as dashboard-facing release notes and must state user-visible behavior.Source: Learnings
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 85def57d-c643-48eb-9cca-1e2488cf029d
📒 Files selected for processing (53)
.changeset/smooth-schedule-windows.md.server-changes/schedule-windows.mdapps/webapp/app/components/schedules/ScheduleInspector.tsxapps/webapp/app/env.server.tsapps/webapp/app/presenters/v3/EditSchedulePresenter.server.tsapps/webapp/app/presenters/v3/ScheduleListPresenter.server.tsapps/webapp/app/presenters/v3/ViewSchedulePresenter.server.tsapps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.scheduled.$taskParam/route.tsxapps/webapp/app/routes/api.v1.deployments.$deploymentId.tsapps/webapp/app/routes/api.v1.schedules.$scheduleId.activate.tsapps/webapp/app/routes/api.v1.schedules.$scheduleId.deactivate.tsapps/webapp/app/routes/api.v1.schedules.$scheduleId.tsapps/webapp/app/routes/api.v1.schedules.tsapps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsxapps/webapp/app/services/runsReplicationService.server.tsapps/webapp/app/v3/scheduleEngine.server.tsapps/webapp/app/v3/scheduleWindow.server.tsapps/webapp/app/v3/schedules.tsapps/webapp/app/v3/services/checkSchedule.server.tsapps/webapp/app/v3/services/createBackgroundWorker.server.tsapps/webapp/app/v3/services/upsertTaskSchedule.server.tsapps/webapp/test/engine/triggerTask.test.tsapps/webapp/test/runsReplicationService.part1.test.tsapps/webapp/test/scheduleWindow.test.tsapps/webapp/test/schedules-api.e2e.full.test.tsinternal-packages/clickhouse/schema/037_add_queue_timestamp_to_task_runs_v2.sqlinternal-packages/clickhouse/src/taskRuns.test.tsinternal-packages/clickhouse/src/taskRuns.tsinternal-packages/database/prisma/migrations/20260810130446_add_cron_spread_fields/migration.sqlinternal-packages/database/prisma/schema.prismainternal-packages/run-engine/src/engine/systems/dequeueSystem.tsinternal-packages/run-engine/src/run-queue/index.tsinternal-packages/run-engine/src/run-queue/tests/enqueueMessage.test.tsinternal-packages/schedule-engine/src/engine/distributedScheduling.tsinternal-packages/schedule-engine/src/engine/index.tsinternal-packages/schedule-engine/src/engine/scheduleCalculation.test.tsinternal-packages/schedule-engine/src/engine/scheduleCalculation.tsinternal-packages/schedule-engine/src/engine/scheduleTiming.test.tsinternal-packages/schedule-engine/src/engine/scheduleTiming.tsinternal-packages/schedule-engine/src/engine/types.tsinternal-packages/schedule-engine/src/engine/workerCatalog.test.tsinternal-packages/schedule-engine/src/engine/workerCatalog.tsinternal-packages/schedule-engine/src/index.tsinternal-packages/schedule-engine/test/scheduleEngine.test.tsinternal-packages/schedule-engine/test/scheduleEngine2.test.tsinternal-packages/schedule-engine/test/scheduleRecovery.test.tspackages/cli-v3/src/commands/deploy.tspackages/cli-v3/src/deploy/schedules.test.tspackages/cli-v3/src/deploy/schedules.tspackages/core/src/v3/schemas/api.tspackages/core/src/v3/schemas/schemas.tspackages/trigger-sdk/src/v3/schedules/index.test.tspackages/trigger-sdk/src/v3/schedules/index.ts
| "trigger.dev": patch | ||
| --- | ||
|
|
||
| Define stable execution windows on declarative scheduled tasks. Schedule API responses now expose both the nominal CRON time and its assigned time, while deploy output and the dashboard show configured windows and upcoming assignments. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Document imperative schedule windows.
Line 7 limits execution windows to declarative schedules. This PR also adds imperative schedule window support through the API. Include both paths in the package release note.
Proposed text
- Define stable execution windows on declarative scheduled tasks.
+ Define stable execution windows on declarative scheduled tasks and imperative schedules.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Define stable execution windows on declarative scheduled tasks. Schedule API responses now expose both the nominal CRON time and its assigned time, while deploy output and the dashboard show configured windows and upcoming assignments. | |
| Define stable execution windows on declarative scheduled tasks and imperative schedules. Schedule API responses now expose both the nominal CRON time and its assigned time, while deploy output and the dashboard show configured windows and upcoming assignments. |
| const windowFields = normalizeScheduleWindow(task.schedule.window); | ||
| const [nextRun] = calculateNextScheduleRunTimes({ | ||
| cron: task.schedule.cron, | ||
| timezone: task.schedule.timezone, | ||
| deduplicationKey: task.id, | ||
| environmentId: authenticatedEnv.id, | ||
| schedulePhase: null, | ||
| phaseSecret: env.ENCRYPTION_KEY, | ||
| ...windowFields, | ||
| }); | ||
|
|
||
| return [ | ||
| { | ||
| task: task.id, | ||
| cron: task.schedule.cron, | ||
| timezone: task.schedule.timezone, | ||
| window: task.schedule.window, | ||
| nextRun: nextRun.nominalAt, | ||
| nextRunEffectiveAt: nextRun.effectiveAt, |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
ast-grep outline apps/webapp/app/v3/scheduleWindow.server.ts --items all
ast-grep outline apps/webapp/app/v3/services/createBackgroundWorker.server.ts --items all
ast-grep outline apps/webapp/app/routes/api.v1.deployments.\$deploymentId.ts --items all
rg -n -C 5 \
'deduplicationKey|schedulePhase|calculateNextScheduleRunTimes|registerNextTaskScheduleInstance' \
apps/webapp/app/v3/scheduleWindow.server.ts \
apps/webapp/app/v3/services/createBackgroundWorker.server.ts \
apps/webapp/app/routes/api.v1.deployments.\$deploymentId.ts \
internal-packages/database/prisma/schema.prismaRepository: triggerdotdev/trigger.dev
Length of output: 16698
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- route ---'
cat -n 'apps/webapp/app/routes/api.v1.deployments.$deploymentId.ts' | sed -n '1,125p'
printf '%s\n' '--- declarative schedule sync ---'
cat -n apps/webapp/app/v3/services/createBackgroundWorker.server.ts | sed -n '643,780p'
printf '%s\n' '--- schedule models and creation/update paths ---'
rg -n -C 8 \
'model TaskSchedule|model TaskScheduleInstance|schedulePhase|deduplicationKey|TaskScheduleInstance' \
internal-packages/database/prisma/schema.prisma \
apps/webapp/app/v3 apps/webapp/app/services apps/webapp/app/models \
-g '*.ts' -g '*.tsx' -g '*.prisma'
printf '%s\n' '--- registration implementation ---'
rg -n -C 12 \
'registerNextTaskScheduleInstance|nextRunEffectiveAt|effectiveAt|nominalAt' \
apps/webapp/app/v3 -g '*.ts'Repository: triggerdotdev/trigger.dev
Length of output: 50381
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- schedule engine registration ---'
rg -n -C 18 \
'registerNextTaskScheduleInstance|schedulePhase|deduplicationKey|calculateNextScheduleRunTimes|calculateEffectiveScheduleTime' \
apps/webapp/app/v3/scheduleEngine.server.ts \
apps/webapp/app/v3 -g '*.ts' \
| sed -n '1,260p'
printf '%s\n' '--- schedule-phase helpers and tests ---'
rg -n -C 12 \
'function calculateSchedulePhase|calculateSchedulePhase|calculateEffectiveScheduleTime|effectiveAt' \
apps packages internal-packages -g '*.ts' -g '*.tsx' -g '*.test.*' -g '*.spec.*' \
| sed -n '1,300p'
printf '%s\n' '--- exact declarative schedule lookup and route callers ---'
rg -n -C 10 \
'taskIdentifier.*task\.id|taskIdentifier.*taskIdentifier|environmentId.*authenticatedEnv|taskSchedule\.find|taskScheduleInstance\.find' \
apps/webapp/app -g '*.ts' -g '*.tsx' \
| sed -n '1,260p'Repository: triggerdotdev/trigger.dev
Length of output: 50381
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- phase algorithm ---'
cat -n internal-packages/schedule-engine/src/engine/scheduleTiming.ts | sed -n '168,225p'
printf '%s\n' '--- registration definitions ---'
rg -l 'registerNextTaskScheduleInstance' . -g '*.ts' -g '*.tsx' | while read -r file; do
printf '%s\n' "--- $file"
rg -n -C 20 'registerNextTaskScheduleInstance' "$file" | sed -n '1,100p'
done
printf '%s\n' '--- declarative schedule tests ---'
rg -n -C 10 \
'syncDeclarativeSchedules|DECLARATIVE|nextRunEffectiveAt|schedulePhase' \
apps/webapp -g '*.test.*' -g '*.spec.*' -g '*.ts' \
| sed -n '1,260p'
printf '%s\n' '--- static invariant check ---'
python3 - <<'PY'
from pathlib import Path
route = Path("apps/webapp/app/routes/api.v1.deployments.$deploymentId.ts").read_text()
sync = Path("apps/webapp/app/v3/services/createBackgroundWorker.server.ts").read_text()
upsert = Path("apps/webapp/app/v3/services/upsertTaskSchedule.server.ts").read_text()
checks = {
"route uses task.id as deduplicationKey": "deduplicationKey: task.id" in route,
"route passes null schedulePhase": "schedulePhase: null" in route,
"declarative create omits deduplicationKey": "const newSchedule = await prisma.taskSchedule.create" in sync
and "deduplicationKey" not in sync[sync.index("const newSchedule = await prisma.taskSchedule.create"):
sync.index("const newSchedule = await prisma.taskSchedule.create") + 1800],
"declarative instance is registered": "registerNextTaskScheduleInstance({ instanceId: instance.id })" in sync,
"canonical API calculation uses persisted values": "deduplicationKey: taskSchedule.deduplicationKey" in upsert
and "schedulePhase: instance.schedulePhase" in upsert,
}
for name, result in checks.items():
print(f"{name}: {'PASS' if result else 'FAIL'}")
PYRepository: triggerdotdev/trigger.dev
Length of output: 46376
Use the persisted schedule identity for nextRunEffectiveAt.
The declarative schedule uses a generated deduplicationKey, not task.id. Query the matching TaskSchedule and TaskScheduleInstance for authenticatedEnv.id, then pass their deduplicationKey and schedulePhase values to calculateNextScheduleRunTimes. Add coverage against the registered schedule instance.
| <Hint> | ||
| Assigns each run a stable time after its CRON time. Use minutes, hours, days, or a | ||
| percentage of the interval. Schedules always use at least a 60-second placement | ||
| range. |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Correct the default-window guidance.
The hint says every schedule uses a 60-second placement range. Explicit 0m and 0% windows are supported and assign the effective time at the nominal CRON time. State that 60 seconds is the default only when the window is omitted.
Proposed text
- percentage of the interval. Schedules always use at least a 60-second placement
- range.
+ percentage of the interval. If omitted, the window defaults to a 60-second placement
+ range. Use `0m` or `0%` to assign the CRON time.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <Hint> | |
| Assigns each run a stable time after its CRON time. Use minutes, hours, days, or a | |
| percentage of the interval. Schedules always use at least a 60-second placement | |
| range. | |
| <Hint> | |
| Assigns each run a stable time after its CRON time. Use minutes, hours, days, or a | |
| percentage of the interval. If omitted, the window defaults to a 60-second placement | |
| range. Use `0m` or `0%` to assign the CRON time. |
| const { | ||
| effectiveAt: candidateEffectiveAt, | ||
| effectiveRangeMs, | ||
| windowMs, | ||
| offsetMs: candidateDelayMs, | ||
| rangeWasClamped, | ||
| } = calculateEffectiveScheduleTime({ | ||
| nominalAt, | ||
| nextNominalAt, | ||
| schedulePhase, | ||
| window: scheduleWindow, | ||
| }); | ||
| const effectiveAt = this.options.cronSpreadEnabled ? candidateEffectiveAt : nominalAt; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Guard the effective-time calculation so a disabled feature flag cannot break registration.
calculateEffectiveScheduleTime runs before the cronSpreadEnabled check. The function throws a RangeError when intervalMs is not a positive integer or when a date is invalid. If cronSpreadEnabled is false, registration must behave as it did before this change, but it now fails for any generator expression whose next two nominal timestamps are not strictly increasing.
Skip the calculation when the flag is off, or wrap it so a failure falls back to nominalAt.
🛡️ Proposed fallback that keeps the flag-off path on the previous behavior
- const {
- effectiveAt: candidateEffectiveAt,
- effectiveRangeMs,
- windowMs,
- offsetMs: candidateDelayMs,
- rangeWasClamped,
- } = calculateEffectiveScheduleTime({
- nominalAt,
- nextNominalAt,
- schedulePhase,
- window: scheduleWindow,
- });
- const effectiveAt = this.options.cronSpreadEnabled ? candidateEffectiveAt : nominalAt;
+ const [timingError, timing] = await tryCatch(
+ Promise.resolve().then(() =>
+ calculateEffectiveScheduleTime({
+ nominalAt,
+ nextNominalAt,
+ schedulePhase,
+ window: scheduleWindow,
+ })
+ )
+ );
+
+ if (timingError) {
+ this.logger.error("Failed to calculate the effective schedule time", {
+ instanceId: params.instanceId,
+ error: timingError instanceof Error ? timingError.message : String(timingError),
+ });
+ span.setAttribute("effective_schedule_time_error", true);
+ }
+
+ const candidateEffectiveAt = timing?.effectiveAt ?? nominalAt;
+ const effectiveRangeMs = timing?.effectiveRangeMs ?? 0;
+ const windowMs = timing?.windowMs ?? 0;
+ const candidateDelayMs = timing?.offsetMs ?? 0;
+ const rangeWasClamped = timing?.rangeWasClamped ?? false;
+ const effectiveAt =
+ this.options.cronSpreadEnabled && timing ? candidateEffectiveAt : nominalAt;| export const ScheduleWindow = z.string().min(1); | ||
|
|
||
| export type ScheduleWindow = `${bigint}${"m" | "h" | "d" | "%"}`; | ||
|
|
||
| export const ScheduleMetadata = z.object({ | ||
| cron: z.string(), | ||
| timezone: z.string(), | ||
| environments: z.array(EnvironmentType).optional(), | ||
| window: ScheduleWindow.optional(), |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the schema definition and all public contracts inferred from it.
rg -n -C 3 \
'export const ScheduleWindow|export type ScheduleWindow|ScheduleWindow\.optional|z\.infer<typeof (CreateScheduleOptions|ScheduleObject)' \
packages/core/src/v3/schemas/schemas.ts \
packages/core/src/v3/schemas/api.tsRepository: triggerdotdev/trigger.dev
Length of output: 3438
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- schema definitions and documentation ---'
sed -n '165,198p' packages/core/src/v3/schemas/schemas.ts
printf '%s\n' '--- API schedule schemas ---'
sed -n '805,838p;1045,1110p' packages/core/src/v3/schemas/api.ts
printf '%s\n' '--- Zod dependency declarations ---'
rg -n '"zod"|zod@' package.json packages/*/package.json pnpm-lock.yaml yarn.lock package-lock.json 2>/dev/null || true
printf '%s\n' '--- ScheduleWindow references ---'
rg -n 'ScheduleWindow|window:\s*ScheduleWindow' packages/core/src packages/core 2>/dev/null | head -80
printf '%s\n' '--- deterministic contract probe ---'
python3 - <<'PY'
from pathlib import Path
import re
schema = Path("packages/core/src/v3/schemas/schemas.ts").read_text()
api = Path("packages/core/src/v3/schemas/api.ts").read_text()
assert re.search(r'export const ScheduleWindow\s*=\s*z\.string\(\)\.min\(1\)', schema)
assert re.search(r'export type ScheduleWindow\s*=\s*`\$\{bigint\}\$\{"m" \| "h" \| "d" \| "%"\}`', schema)
for name in ("ScheduleMetadata",):
assert re.search(rf'export const {name}\s*=\s*z\.object\([\s\S]*?window:\s*ScheduleWindow\.optional\(\)', schema)
for name in ("CreateScheduleOptions", "ScheduleObject"):
assert re.search(rf'window:\s*ScheduleWindow\.optional\(\)', api)
assert re.search(rf'export type {name}\s*=\s*z\.infer<typeof {name}>', api)
print("ScheduleWindow runtime schema: non-empty string")
print("ScheduleWindow declared TypeScript type: `${bigint}${'m'|'h'|'d'|'%'}`")
print("ScheduleMetadata, CreateScheduleOptions, and ScheduleObject infer window from the Zod schema")
print('The runtime schema accepts "30s": non-empty string')
print("The declared ScheduleWindow type excludes the suffix 's'")
PYRepository: triggerdotdev/trigger.dev
Length of output: 11616
Preserve ScheduleWindow in inferred public types.
z.string().min(1) infers string, so callers can pass unsupported values such as "30s" to ScheduleMetadata, CreateScheduleOptions, and ScheduleObject. Type the schema output as ScheduleWindow while retaining the non-empty-string runtime validation.
2ba481a to
eb8c3af
Compare
Observability mapAs of 19/100 over 417 measured of 433 entry points (base 18, up 1) What this PR changed
FIX FIRST
AUDIT 3 of 50 sensitive mutations record an actor. 47 without one. What the score is made ofThe score and findings here are report-only and never gate the merge. Separately, a required test suite keeps this tool's symbol and route lists in sync with the code they name, and can fail a pull request that renames or removes a symbol they reference, or that adds the first route with a segment they anticipate. Each failure names the list to edit. The rules and their reasons: internal-packages/observability-map/README.md. |
@trigger.dev/build
trigger.dev
@trigger.dev/core
@trigger.dev/python
@trigger.dev/react-hooks
@trigger.dev/redis-worker
@trigger.dev/rsc
@trigger.dev/schema-to-json
@trigger.dev/sdk
commit: |
Summary
Adds execution-window product surfaces for both declarative and imperative schedules.
windowthroughschedules.task(), with support for whole-minute, hour, day, and percentage values.nextRunas the nominal CRON time and exposenextRunEffectiveAtas the stable assigned time.Design
Window validation remains authoritative on the server and ensures each window is compatible with the schedule cadence. Omitting a window uses the default 60-second range, while explicit zero-duration windows remain supported.
Deployment summaries are derived from the deployment's stored task metadata, so they reflect the declarations associated with that deployment.