Skip to content

Commit 2991bb4

Browse files
carderneTrigger.dev RepoOps
authored andcommitted
feat(webapp): add default and minimum schedule spread windows
Mono-RevId: 6be9d1ef1f2ed06bbd9a6511417cc33c33eb189e
1 parent 9944f44 commit 2991bb4

36 files changed

Lines changed: 1686 additions & 330 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@trigger.dev/core": patch
3+
---
4+
5+
Add an optional `appliedSchedulePolicy` field to the schedule API response. It is present only when a non-overridable plan policy applies a minimum window to a schedule (e.g. a free-plan schedule's minimum run interval); the configured `window` continues to be returned separately and unchanged.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: feature
4+
---
5+
6+
New schedules use a default CRON spread window when none is set, distributing runs after their scheduled time instead of starting them all at once. Set an explicit window to override the default.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: feature
4+
---
5+
6+
Schedules now support a configurable minimum spread window that applies even when a smaller window is requested.

apps/webapp/app/components/schedules/ScheduleInspector.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ type ScheduleInspectorData = {
5656
cronDescription: string;
5757
timezone: string;
5858
window?: string;
59+
windowSource?: "explicit" | "schedule_default";
5960
externalId: string | null;
6061
deduplicationKey: string | null;
6162
userProvidedDeduplicationKey: boolean;
@@ -145,7 +146,10 @@ export function ScheduleInspector({
145146
</Property.Item>
146147
<Property.Item>
147148
<Property.Label>Window</Property.Label>
148-
<Property.Value>{schedule.window ?? "-"}</Property.Value>
149+
<Property.Value>
150+
{schedule.window ?? "-"}
151+
{schedule.windowSource === "schedule_default" ? " · Default" : ""}
152+
</Property.Value>
149153
</Property.Item>
150154
<Property.Item className="gap-1">
151155
<Property.Label>Environment</Property.Label>

apps/webapp/app/env.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1837,7 +1837,7 @@ const EnvironmentSchema = z
18371837
SCHEDULE_WORKER_CRON_SPREAD_FRACTION: z.coerce
18381838
.number()
18391839
.catch(0)
1840-
.default(0)
1840+
.default(1)
18411841
.transform((value) => (Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 0)),
18421842

18431843
SCHEDULE_WORKER_REDIS_HOST: z

apps/webapp/app/presenters/v3/EditSchedulePresenter.server.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ export class EditSchedulePresenter {
127127
timezone: true,
128128
windowDurationSeconds: true,
129129
windowPercentage: true,
130+
defaultWindowDurationSeconds: true,
131+
minimumWindowDurationSeconds: true,
130132
taskIdentifier: true,
131133
instances: {
132134
select: {
@@ -147,7 +149,12 @@ export class EditSchedulePresenter {
147149
return {
148150
...schedule,
149151
cron: schedule.generatorExpression,
152+
// The form shows only the user-configured value; a blank field lets a captured default
153+
// surface through the placeholder copy rather than appearing as a typed value.
150154
window: formatScheduleWindow(schedule),
155+
// Whether this schedule carries a captured default, so the form copy can say "clearing
156+
// returns to the 60-minute default" only for the new cohort, never for a grandfathered row.
157+
hasCapturedDefaultWindow: schedule.defaultWindowDurationSeconds !== null,
151158
environments: schedule.instances.flatMap((instance) => {
152159
const environment = possibleEnvironments.find((env) => env.id === instance.environmentId);
153160
if (!environment) {

apps/webapp/app/presenters/v3/ScheduleListPresenter.server.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { getTaskIdentifiers } from "~/models/task.server";
55
import { getCurrentPlan, getPlans } from "~/services/platform.v3.server";
66
import { findCurrentWorkerFromEnvironment } from "~/v3/models/workerDeployment.server";
77
import { ServiceValidationError } from "~/v3/services/baseService.server";
8-
import { formatScheduleWindow } from "~/v3/scheduleWindow.server";
8+
import { formatResolvedScheduleWindow } from "~/v3/scheduleWindow.server";
9+
import { type ScheduleWindowSource } from "@internal/schedule-engine";
910
import { CheckScheduleService } from "~/v3/services/checkSchedule.server";
1011
import { resolveScheduleTimings } from "~/v3/scheduleTimings.server";
1112
import { env } from "~/env.server";
@@ -37,6 +38,8 @@ type ScheduleListItem = {
3738
cronDescription: string;
3839
timezone: string;
3940
window?: string;
41+
windowSource?: ScheduleWindowSource;
42+
minimumWindowDurationSeconds: number | null;
4043
externalId: string | null;
4144
nextRun: Date;
4245
nextRunEffectiveAt: Date;
@@ -223,6 +226,8 @@ export class ScheduleListPresenter extends BasePresenter {
223226
timezone: true,
224227
windowDurationSeconds: true,
225228
windowPercentage: true,
229+
defaultWindowDurationSeconds: true,
230+
minimumWindowDurationSeconds: true,
226231
externalId: true,
227232
instances: {
228233
select: {
@@ -300,6 +305,8 @@ export class ScheduleListPresenter extends BasePresenter {
300305
schedulePhase: instances[index].schedulePhase,
301306
windowDurationSeconds: schedule.windowDurationSeconds,
302307
windowPercentage: schedule.windowPercentage,
308+
defaultWindowDurationSeconds: schedule.defaultWindowDurationSeconds,
309+
minimumWindowDurationSeconds: schedule.minimumWindowDurationSeconds,
303310
active: schedule.active,
304311
updatedAt: schedule.updatedAt,
305312
})),
@@ -308,6 +315,7 @@ export class ScheduleListPresenter extends BasePresenter {
308315

309316
const schedules: ScheduleListItem[] = rawSchedules.map((schedule, index) => {
310317
const { nextRun, nextRunEffectiveAt, lastRun } = timings[index];
318+
const resolvedWindow = formatResolvedScheduleWindow(schedule);
311319

312320
return {
313321
id: schedule.id,
@@ -319,7 +327,9 @@ export class ScheduleListPresenter extends BasePresenter {
319327
cron: schedule.generatorExpression,
320328
cronDescription: schedule.generatorDescription,
321329
timezone: schedule.timezone,
322-
window: formatScheduleWindow(schedule),
330+
window: resolvedWindow.window,
331+
windowSource: resolvedWindow.source,
332+
minimumWindowDurationSeconds: schedule.minimumWindowDurationSeconds,
323333
active: schedule.active,
324334
externalId: schedule.externalId,
325335
lastRun,

apps/webapp/app/presenters/v3/ViewSchedulePresenter.server.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
55
import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server";
66
import { NextRunListPresenter } from "./NextRunListPresenter.server";
77
import { scheduleWhereClause } from "~/models/schedules.server";
8-
import { calculateNextScheduleRunTimes, formatScheduleWindow } from "~/v3/scheduleWindow.server";
8+
import {
9+
calculateNextScheduleRunTimes,
10+
formatResolvedScheduleWindow,
11+
} from "~/v3/scheduleWindow.server";
912
import { env } from "~/env.server";
1013

1114
type ViewScheduleOptions = {
@@ -40,6 +43,8 @@ export class ViewSchedulePresenter {
4043
timezone: true,
4144
windowDurationSeconds: true,
4245
windowPercentage: true,
46+
defaultWindowDurationSeconds: true,
47+
minimumWindowDurationSeconds: true,
4348
externalId: true,
4449
deduplicationKey: true,
4550
userProvidedDeduplicationKey: true,
@@ -101,6 +106,8 @@ export class ViewSchedulePresenter {
101106
phaseSecret: env.ENCRYPTION_KEY,
102107
windowDurationSeconds: schedule.windowDurationSeconds,
103108
windowPercentage: schedule.windowPercentage,
109+
defaultWindowDurationSeconds: schedule.defaultWindowDurationSeconds,
110+
minimumWindowDurationSeconds: schedule.minimumWindowDurationSeconds,
104111
count: 5,
105112
})
106113
: [];
@@ -120,7 +127,8 @@ export class ViewSchedulePresenter {
120127
timezone: schedule.timezone,
121128
cron: schedule.generatorExpression,
122129
cronDescription: schedule.generatorDescription,
123-
window: formatScheduleWindow(schedule),
130+
window: formatResolvedScheduleWindow(schedule).window,
131+
windowSource: formatResolvedScheduleWindow(schedule).source,
124132
nextRuns,
125133
runs,
126134
environments: schedule.instances.map((instance) => {
@@ -168,6 +176,13 @@ export class ViewSchedulePresenter {
168176
active: result.schedule.active,
169177
nextRun: result.schedule.nextRuns[0]?.nominalAt ?? null,
170178
nextRunEffectiveAt: result.schedule.nextRuns[0]?.effectiveAt ?? null,
179+
appliedSchedulePolicy:
180+
result.schedule.minimumWindowDurationSeconds !== null
181+
? {
182+
minimumWindowSeconds: result.schedule.minimumWindowDurationSeconds,
183+
reason: "free_schedule" as const,
184+
}
185+
: undefined,
171186
generator: {
172187
type: "CRON",
173188
expression: result.schedule.cron,

apps/webapp/app/routes/api.v1.schedules.$scheduleId.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
131131
environments: schedule.environments,
132132
nextRun: schedule.nextRun,
133133
nextRunEffectiveAt: schedule.nextRunEffectiveAt,
134+
appliedSchedulePolicy: schedule.appliedSchedulePolicy,
134135
};
135136

136137
return json(responseObject, { status: 200 });

apps/webapp/app/routes/api.v1.schedules.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export async function action({ request }: ActionFunctionArgs) {
7373
environments: schedule.environments,
7474
nextRun: schedule.nextRun,
7575
nextRunEffectiveAt: schedule.nextRunEffectiveAt,
76+
appliedSchedulePolicy: schedule.appliedSchedulePolicy,
7677
};
7778

7879
return json(responseObject, { status: 200 });
@@ -133,6 +134,13 @@ export async function loader({ request }: LoaderFunctionArgs) {
133134
active: schedule.active,
134135
nextRun: schedule.nextRun,
135136
nextRunEffectiveAt: schedule.nextRunEffectiveAt,
137+
appliedSchedulePolicy:
138+
schedule.minimumWindowDurationSeconds !== null
139+
? {
140+
minimumWindowSeconds: schedule.minimumWindowDurationSeconds,
141+
reason: "free_schedule" as const,
142+
}
143+
: undefined,
136144
environments: schedule.environments,
137145
})),
138146
pagination: {

0 commit comments

Comments
 (0)