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
7 changes: 7 additions & 0 deletions .changeset/smooth-schedule-windows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@trigger.dev/core": patch
"@trigger.dev/sdk": patch
"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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
29 changes: 19 additions & 10 deletions apps/webapp/app/components/schedules/ScheduleInspector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,14 @@ export type ScheduleInspectorData = {
cron: string;
cronDescription: string;
timezone: string;
window?: string;
externalId: string | null;
deduplicationKey: string | null;
userProvidedDeduplicationKey: boolean;
active: boolean;
environments: EnvironmentRow[];
runs: RunRow[];
nextRuns: Date[];
nextRuns: Array<{ nominalAt: Date; effectiveAt: Date }>;
};

type Props = {
Expand Down Expand Up @@ -142,6 +143,10 @@ export function ScheduleInspector({
<Property.Label>Timezone</Property.Label>
<Property.Value>{schedule.timezone}</Property.Value>
</Property.Item>
<Property.Item>
<Property.Label>Window</Property.Label>
<Property.Value>{schedule.window ?? "Default (60 seconds)"}</Property.Value>
</Property.Item>
<Property.Item className="gap-1">
<Property.Label>Environment</Property.Label>
<Property.Value>
Expand Down Expand Up @@ -195,12 +200,13 @@ export function ScheduleInspector({
/>
</div>
<div className="flex flex-col gap-1 pt-2">
<Header3 className="pb-1 pl-3">Next 5 runs</Header3>
<Header3 className="pb-1 pl-3">Next 5 scheduled runs</Header3>
<Table variant="bright">
<TableHeader>
<TableRow>
{!isUtc && <TableHeaderCell>{schedule.timezone}</TableHeaderCell>}
<TableHeaderCell>UTC</TableHeaderCell>
{!isUtc && <TableHeaderCell>CRON ({schedule.timezone})</TableHeaderCell>}
<TableHeaderCell>CRON (UTC)</TableHeaderCell>
<TableHeaderCell>Assigned (UTC)</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
Expand All @@ -210,21 +216,24 @@ export function ScheduleInspector({
<TableRow key={index}>
{!isUtc && (
<TableCell>
<DateTime date={run} timeZone={schedule.timezone} />
<DateTime date={run.nominalAt} timeZone={schedule.timezone} />
</TableCell>
)}
<TableCell>
<DateTime date={run} timeZone="UTC" />
<DateTime date={run.nominalAt} timeZone="UTC" />
</TableCell>
<TableCell>
<DateTime date={run.effectiveAt} timeZone="UTC" />
</TableCell>
</TableRow>
))
) : (
<TableBlankRow colSpan={isUtc ? 1 : 2}>
<TableBlankRow colSpan={isUtc ? 2 : 3}>
<PlaceholderText title="You found a bug" />
</TableBlankRow>
)
) : (
<TableBlankRow colSpan={isUtc ? 1 : 2}>
<TableBlankRow colSpan={isUtc ? 2 : 3}>
<PlaceholderText title="Schedule disabled" />
</TableBlankRow>
)}
Expand All @@ -249,8 +258,8 @@ export function ScheduleInspector({
}
panelClassName="max-w-full"
>
You can only edit a declarative schedule by updating your schedules.task and then
running the CLI dev and deploy commands.
You can only edit a declarative schedule, including its window, by updating your
schedules.task and then running the CLI dev and deploy commands.
</InfoPanel>
</div>
)}
Expand Down
4 changes: 4 additions & 0 deletions apps/webapp/app/presenters/v3/EditSchedulePresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { filterOrphanedEnvironments } from "~/utils/environmentSort";
import { getTimezones } from "~/utils/timezones.server";
import { findCurrentWorkerFromEnvironment } from "~/v3/models/workerDeployment.server";
import { ServiceValidationError } from "~/v3/services/baseService.server";
import { formatScheduleWindow } from "~/v3/scheduleWindow.server";

type EditScheduleOptions = {
userId: string;
Expand Down Expand Up @@ -124,6 +125,8 @@ export class EditSchedulePresenter {
deduplicationKey: true,
userProvidedDeduplicationKey: true,
timezone: true,
windowDurationSeconds: true,
windowPercentage: true,
taskIdentifier: true,
instances: {
select: {
Expand All @@ -144,6 +147,7 @@ export class EditSchedulePresenter {
return {
...schedule,
cron: schedule.generatorExpression,
window: formatScheduleWindow(schedule),
environments: schedule.instances.flatMap((instance) => {
const environment = possibleEnvironments.find((env) => env.id === instance.environmentId);
if (!environment) {
Expand Down
33 changes: 24 additions & 9 deletions apps/webapp/app/presenters/v3/ScheduleListPresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@ import { getTaskIdentifiers } from "~/models/task.server";
import { getCurrentPlan, getPlans } from "~/services/platform.v3.server";
import { findCurrentWorkerFromEnvironment } from "~/v3/models/workerDeployment.server";
import { ServiceValidationError } from "~/v3/services/baseService.server";
import { formatScheduleWindow } from "~/v3/scheduleWindow.server";
import { calculateNextScheduleRunTimes, formatScheduleWindow } from "~/v3/scheduleWindow.server";
import { CheckScheduleService } from "~/v3/services/checkSchedule.server";
import {
calculateNextScheduledTimestampFromNow,
previousScheduledTimestamp,
} from "~/v3/utils/calculateNextSchedule.server";
import { previousScheduledTimestamp } from "~/v3/utils/calculateNextSchedule.server";
import { env } from "~/env.server";
import { BasePresenter } from "./basePresenter.server";

type ScheduleListOptions = {
Expand All @@ -35,6 +33,7 @@ export type ScheduleListItem = {
window?: string;
externalId: string | null;
nextRun: Date;
nextRunEffectiveAt: Date;
lastRun: Date | undefined;
active: boolean;
environments: {
Expand Down Expand Up @@ -223,6 +222,7 @@ export class ScheduleListPresenter extends BasePresenter {
instances: {
select: {
environmentId: true,
schedulePhase: true,
},
},
active: true,
Expand Down Expand Up @@ -300,6 +300,23 @@ export class ScheduleListPresenter extends BasePresenter {
}
}

const instance = schedule.instances.find(
(instance) => instance.environmentId === environmentId
);
if (!instance) {
throw new Error(`Schedule instance not found for environment: ${environmentId}`);
}
const [nextRun] = calculateNextScheduleRunTimes({
cron: schedule.generatorExpression,
timezone: schedule.timezone,
deduplicationKey: schedule.deduplicationKey,
environmentId,
schedulePhase: instance.schedulePhase,
phaseSecret: env.ENCRYPTION_KEY,
windowDurationSeconds: schedule.windowDurationSeconds,
windowPercentage: schedule.windowPercentage,
});

return {
id: schedule.id,
type: schedule.type,
Expand All @@ -314,10 +331,8 @@ export class ScheduleListPresenter extends BasePresenter {
active: schedule.active,
externalId: schedule.externalId,
lastRun,
nextRun: calculateNextScheduledTimestampFromNow(
schedule.generatorExpression,
schedule.timezone
),
nextRun: nextRun.nominalAt,
nextRunEffectiveAt: nextRun.effectiveAt,
environments: schedule.instances.map((instance) => {
const environment = project.environments.find((env) => env.id === instance.environmentId);
if (!environment) {
Expand Down
31 changes: 26 additions & 5 deletions apps/webapp/app/presenters/v3/ViewSchedulePresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import type { PrismaClient } from "~/db.server";
import { prisma } from "~/db.server";
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server";
import { nextScheduledTimestamps } from "~/v3/utils/calculateNextSchedule.server";
import { NextRunListPresenter } from "./NextRunListPresenter.server";
import { scheduleWhereClause } from "~/models/schedules.server";
import { formatScheduleWindow } from "~/v3/scheduleWindow.server";
import { calculateNextScheduleRunTimes, formatScheduleWindow } from "~/v3/scheduleWindow.server";
import { env } from "~/env.server";

type ViewScheduleOptions = {
userId?: string;
Expand Down Expand Up @@ -52,6 +52,8 @@ export class ViewSchedulePresenter {
},
instances: {
select: {
environmentId: true,
schedulePhase: true,
environment: {
select: {
id: true,
Expand Down Expand Up @@ -82,8 +84,25 @@ export class ViewSchedulePresenter {
return;
}

const instance = schedule.instances.find(
(instance) => instance.environmentId === environmentId
);
if (!instance) {
return;
}

const nextRuns = schedule.active
? nextScheduledTimestamps(schedule.generatorExpression, schedule.timezone, new Date(), 5)
? calculateNextScheduleRunTimes({
cron: schedule.generatorExpression,
timezone: schedule.timezone,
deduplicationKey: schedule.deduplicationKey,
environmentId,
schedulePhase: instance.schedulePhase,
phaseSecret: env.ENCRYPTION_KEY,
windowDurationSeconds: schedule.windowDurationSeconds,
windowPercentage: schedule.windowPercentage,
count: 5,
})
: [];

const runs = includeRunHistory
Expand All @@ -101,6 +120,7 @@ export class ViewSchedulePresenter {
timezone: schedule.timezone,
cron: schedule.generatorExpression,
cronDescription: schedule.generatorDescription,
window: formatScheduleWindow(schedule),
nextRuns,
runs,
environments: schedule.instances.map((instance) => {
Expand Down Expand Up @@ -146,14 +166,15 @@ export class ViewSchedulePresenter {
type: result.schedule.type,
task: result.schedule.taskIdentifier,
active: result.schedule.active,
nextRun: result.schedule.nextRuns[0],
nextRun: result.schedule.nextRuns[0]?.nominalAt ?? null,
nextRunEffectiveAt: result.schedule.nextRuns[0]?.effectiveAt ?? null,
generator: {
type: "CRON",
expression: result.schedule.cron,
description: result.schedule.cronDescription,
},
timezone: result.schedule.timezone,
window: formatScheduleWindow(result.schedule),
window: result.schedule.window,
externalId: result.schedule.externalId ?? undefined,
deduplicationKey: result.schedule.userProvidedDeduplicationKey
? (result.schedule.deduplicationKey ?? undefined)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -972,8 +972,10 @@ type ScheduleRow = {
type: "DECLARATIVE" | "IMPERATIVE";
cron: string;
cronDescription: string;
window?: string;
externalId: string | null;
nextRun: Date;
nextRunEffectiveAt: Date;
lastRun: Date | undefined;
active: boolean;
};
Expand All @@ -993,7 +995,7 @@ function SchedulesMiniTable({
return (
<Table variant={variant} showTopBorder={showTopBorder}>
<TableBody>
<TableBlankRow colSpan={6}>
<TableBlankRow colSpan={9}>
<Paragraph variant="small" className="flex items-center justify-center">
No schedules attached to this task yet.
</Paragraph>
Expand All @@ -1009,9 +1011,11 @@ function SchedulesMiniTable({
<TableRow>
<TableHeaderCell>Schedule ID</TableHeaderCell>
<TableHeaderCell>Type</TableHeaderCell>
<TableHeaderCell>Cron</TableHeaderCell>
<TableHeaderCell>CRON</TableHeaderCell>
<TableHeaderCell>Window</TableHeaderCell>
<TableHeaderCell>External ID</TableHeaderCell>
<TableHeaderCell>Next run</TableHeaderCell>
<TableHeaderCell>Next CRON time</TableHeaderCell>
<TableHeaderCell>Next assigned time</TableHeaderCell>
<TableHeaderCell>Last run</TableHeaderCell>
<TableHeaderCell>Status</TableHeaderCell>
</TableRow>
Expand All @@ -1036,6 +1040,9 @@ function SchedulesMiniTable({
<TableCell onClick={open}>
<span className="font-mono text-xs">{schedule.cron}</span>
</TableCell>
<TableCell onClick={open}>
<span className="text-xs">{schedule.window ?? "Default (60s)"}</span>
</TableCell>
<TableCell onClick={open}>
{schedule.externalId ? (
<span className="font-mono text-xs">{schedule.externalId}</span>
Expand All @@ -1046,6 +1053,9 @@ function SchedulesMiniTable({
<TableCell onClick={open}>
<RelativeDateTime date={schedule.nextRun} />
</TableCell>
<TableCell onClick={open}>
<RelativeDateTime date={schedule.nextRunEffectiveAt} />
</TableCell>
<TableCell onClick={open}>
{schedule.lastRun ? (
<RelativeDateTime date={schedule.lastRun} />
Expand Down
Loading
Loading