Schedule limits and timezone support (#1165)

* Added maximumScheduleInstancesLimit column to Org, default to 20

* Docs on the schedule limits and improved soft-limit communication

* Added limit info to the schedules list page

* Created a task that creates schedules, useful for testing

* Make deduplicationKey required when creating/updating a schedule using the SDK

* New schedule button shows an alert if you’re over the limit

* Added timezone to the form and db

* WIP on the timezone dropdown for the create/edit schedule form

* Use the new filter search for timezones

* Made the timezone dropdown faster by fixing the virtualization

* The preview table is working and added a nice message about daylight savings

* Created a page where you can view the full list of timezones

The URL is included in the error message if you send an invalid time using the SDK

* Creating tasks with the timezone

* Added timezone support the the scheduler and the schedules list

* Added timezone support to more of the schedules UI

* The timezone comes through to scheduled runs with nice JSDocs

* Allow setting the timezone from the SDK

* Always have a timezone on a schedule

* Updated jsdocs

* Updated catalog example

* Changed the column to be a string, not null. Added the timezone across the SDK

* API endpoint for getting the timezones

* Added an SDK function to get the list of timezones

* Added timezones to the docs

* Changeset: Added timezone support to schedules

* Added support for testing timezone

* Tidied up imports

* Imports

* Imports

* Update limits.mdx

* Fixed a couple type issues and use the already exported zodfetch

---------

Co-authored-by: Eric Allam <eallam@icloud.com>
This commit is contained in:
Matt Aitken
2024-06-14 13:31:27 +01:00
committed by GitHub
parent 3687fcb61e
commit c405ae7117
48 changed files with 1223 additions and 109 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/core": patch
---
Make deduplicationKey required when creating/updating a schedule
+6
View File
@@ -0,0 +1,6 @@
---
"@trigger.dev/sdk": patch
"@trigger.dev/core": patch
---
Added timezone support to schedules
@@ -6,6 +6,7 @@ type DateTimeProps = {
timeZone?: string; timeZone?: string;
includeSeconds?: boolean; includeSeconds?: boolean;
includeTime?: boolean; includeTime?: boolean;
showTimezone?: boolean;
}; };
export const DateTime = ({ export const DateTime = ({
@@ -13,6 +14,7 @@ export const DateTime = ({
timeZone, timeZone,
includeSeconds = true, includeSeconds = true,
includeTime = true, includeTime = true,
showTimezone = false,
}: DateTimeProps) => { }: DateTimeProps) => {
const locales = useLocales(); const locales = useLocales();
@@ -42,7 +44,12 @@ export const DateTime = ({
); );
}, [locales, includeSeconds, realDate]); }, [locales, includeSeconds, realDate]);
return <Fragment>{formattedDateTime.replace(/\s/g, String.fromCharCode(32))}</Fragment>; return (
<Fragment>
{formattedDateTime.replace(/\s/g, String.fromCharCode(32))}
{showTimezone ? ` (${timeZone ?? "UTC"})` : null}
</Fragment>
);
}; };
export function formatDateTime( export function formatDateTime(
@@ -8,6 +8,7 @@ import { ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys";
import { cn } from "~/utils/cn"; import { cn } from "~/utils/cn";
import { ShortcutKey } from "./ShortcutKey"; import { ShortcutKey } from "./ShortcutKey";
import { ChevronDown } from "lucide-react"; import { ChevronDown } from "lucide-react";
import { MatchSorterOptions, matchSorter } from "match-sorter";
const sizes = { const sizes = {
small: { small: {
@@ -75,7 +76,10 @@ export interface SelectProps<TValue extends string | string[], TItem>
showHeading?: boolean; showHeading?: boolean;
items?: TItem[] | Section<TItem>[]; items?: TItem[] | Section<TItem>[];
empty?: React.ReactNode; empty?: React.ReactNode;
filter?: (item: ItemFromSection<TItem>, search: string, title?: string) => boolean; filter?:
| boolean
| MatchSorterOptions<TItem>
| ((item: ItemFromSection<TItem>, search: string, title?: string) => boolean);
children: children:
| React.ReactNode | React.ReactNode
| (( | ((
@@ -129,18 +133,44 @@ export function Select<TValue extends string | string[], TItem>({
if (!items) return []; if (!items) return [];
if (!searchValue || !filter) return items; if (!searchValue || !filter) return items;
if (typeof filter === "function") {
if (isSection(items)) {
return items
.map((section) => ({
...section,
items: section.items.filter((item) =>
filter(item as ItemFromSection<TItem>, searchValue, section.title)
),
}))
.filter((section) => section.items.length > 0);
}
return items.filter((item) => filter(item as ItemFromSection<TItem>, searchValue));
}
if (typeof filter === "boolean" && filter) {
if (isSection(items)) {
return items
.map((section) => ({
...section,
items: matchSorter(section.items, searchValue),
}))
.filter((section) => section.items.length > 0);
}
return matchSorter(items, searchValue);
}
if (isSection(items)) { if (isSection(items)) {
return items return items
.map((section) => ({ .map((section) => ({
...section, ...section,
items: section.items.filter((item) => items: matchSorter(section.items, searchValue, filter),
filter(item as ItemFromSection<TItem>, searchValue, section.title)
),
})) }))
.filter((section) => section.items.length > 0); .filter((section) => section.items.length > 0);
} }
return items.filter((item) => filter(item as ItemFromSection<TItem>, searchValue)); return matchSorter(items, searchValue, filter);
}, [searchValue, items]); }, [searchValue, items]);
const enableItemShortcuts = allowItemShortcuts && matches.length === items?.length; const enableItemShortcuts = allowItemShortcuts && matches.length === items?.length;
@@ -0,0 +1,63 @@
import { useVirtualizer } from "@tanstack/react-virtual";
import { useRef } from "react";
import { SelectItem } from "../primitives/Select";
export function TimezoneList({ timezones }: { timezones: string[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const rowVirtualizer = useVirtualizer({
count: timezones.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 28,
});
return (
<div
ref={parentRef}
className="max-h-[calc(min(480px,var(--popover-available-height))-2.35rem)] overflow-y-auto overscroll-contain scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
>
<div
style={{
height: `${rowVirtualizer.getTotalSize()}px`,
width: "100%",
position: "relative",
}}
>
{rowVirtualizer.getVirtualItems().map((virtualItem) => (
<TimezoneCell
key={virtualItem.key}
size={virtualItem.size}
start={virtualItem.start}
timezone={timezones[virtualItem.index]}
/>
))}
</div>
</div>
);
}
function TimezoneCell({
timezone,
size,
start,
}: {
timezone: string;
size: number;
start: number;
}) {
return (
<SelectItem
value={timezone}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: `${size}px`,
transform: `translateY(${start}px)`,
}}
>
{timezone}
</SelectItem>
);
}
@@ -1,6 +1,7 @@
import { RuntimeEnvironmentType } from "@trigger.dev/database"; import { RuntimeEnvironmentType } from "@trigger.dev/database";
import { PrismaClient, prisma } from "~/db.server"; import { PrismaClient, prisma } from "~/db.server";
import { displayableEnvironment } from "~/models/runtimeEnvironment.server"; import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
import { getTimezones } from "~/utils/timezones.server";
type EditScheduleOptions = { type EditScheduleOptions = {
userId: string; userId: string;
@@ -74,6 +75,7 @@ export class EditSchedulePresenter {
return { return {
possibleTasks: possibleTasks.map((task) => task.slug), possibleTasks: possibleTasks.map((task) => task.slug),
possibleEnvironments, possibleEnvironments,
possibleTimezones: getTimezones(),
schedule: await this.#getExistingSchedule(friendlyId, possibleEnvironments), schedule: await this.#getExistingSchedule(friendlyId, possibleEnvironments),
}; };
} }
@@ -91,6 +93,7 @@ export class EditSchedulePresenter {
externalId: true, externalId: true,
deduplicationKey: true, deduplicationKey: true,
userProvidedDeduplicationKey: true, userProvidedDeduplicationKey: true,
timezone: true,
taskIdentifier: true, taskIdentifier: true,
instances: { instances: {
select: { select: {
@@ -22,6 +22,7 @@ export type ScheduleListItem = {
userProvidedDeduplicationKey: boolean; userProvidedDeduplicationKey: boolean;
cron: string; cron: string;
cronDescription: string; cronDescription: string;
timezone: string;
externalId: string | null; externalId: string | null;
nextRun: Date; nextRun: Date;
lastRun: Date | undefined; lastRun: Date | undefined;
@@ -36,7 +37,6 @@ export type ScheduleList = Awaited<ReturnType<ScheduleListPresenter["call"]>>;
export type ScheduleListAppliedFilters = ScheduleList["filters"]; export type ScheduleListAppliedFilters = ScheduleList["filters"];
export class ScheduleListPresenter extends BasePresenter { export class ScheduleListPresenter extends BasePresenter {
public async call({ public async call({
userId, userId,
projectId, projectId,
@@ -71,12 +71,23 @@ export class ScheduleListPresenter extends BasePresenter {
}, },
}, },
}, },
organization: {
select: {
maximumSchedulesLimit: true,
},
},
}, },
where: { where: {
id: projectId, id: projectId,
}, },
}); });
const schedulesCount = await this._prisma.taskSchedule.count({
where: {
projectId,
},
});
//get all possible scheduled tasks //get all possible scheduled tasks
const possibleTasks = await this._replica.backgroundWorkerTask.findMany({ const possibleTasks = await this._replica.backgroundWorkerTask.findMany({
distinct: ["slug"], distinct: ["slug"],
@@ -140,6 +151,7 @@ export class ScheduleListPresenter extends BasePresenter {
userProvidedDeduplicationKey: true, userProvidedDeduplicationKey: true,
generatorExpression: true, generatorExpression: true,
generatorDescription: true, generatorDescription: true,
timezone: true,
externalId: true, externalId: true,
instances: { instances: {
select: { select: {
@@ -218,10 +230,11 @@ export class ScheduleListPresenter extends BasePresenter {
userProvidedDeduplicationKey: schedule.userProvidedDeduplicationKey, userProvidedDeduplicationKey: schedule.userProvidedDeduplicationKey,
cron: schedule.generatorExpression, cron: schedule.generatorExpression,
cronDescription: schedule.generatorDescription, cronDescription: schedule.generatorDescription,
timezone: schedule.timezone,
active: schedule.active, active: schedule.active,
externalId: schedule.externalId, externalId: schedule.externalId,
lastRun: latestRun?.createdAt, lastRun: latestRun?.createdAt,
nextRun: calculateNextScheduledTimestamp(schedule.generatorExpression), nextRun: calculateNextScheduledTimestamp(schedule.generatorExpression, schedule.timezone),
environments: schedule.instances.map((instance) => { environments: schedule.instances.map((instance) => {
const environment = project.environments.find((env) => env.id === instance.environmentId); const environment = project.environments.find((env) => env.id === instance.environmentId);
if (!environment) { if (!environment) {
@@ -245,6 +258,10 @@ export class ScheduleListPresenter extends BasePresenter {
return displayableEnvironment(environment, userId); return displayableEnvironment(environment, userId);
}), }),
hasFilters, hasFilters,
limits: {
used: schedulesCount,
limit: project.organization.maximumSchedulesLimit,
},
filters: { filters: {
tasks, tasks,
environments, environments,
@@ -6,6 +6,7 @@ import {
TaskTriggerSource, TaskTriggerSource,
} from "@trigger.dev/database"; } from "@trigger.dev/database";
import { sqlDatabaseSchema, PrismaClient, prisma } from "~/db.server"; import { sqlDatabaseSchema, PrismaClient, prisma } from "~/db.server";
import { getTimezones } from "~/utils/timezones.server";
import { getUsername } from "~/utils/username"; import { getUsername } from "~/utils/username";
type TestTaskOptions = { type TestTaskOptions = {
@@ -37,6 +38,7 @@ export type TestTask =
| { | {
triggerSource: "SCHEDULED"; triggerSource: "SCHEDULED";
task: Task; task: Task;
possibleTimezones: string[];
runs: ScheduledRun[]; runs: ScheduledRun[];
}; };
@@ -61,6 +63,7 @@ export type ScheduledRun = Omit<RawRun, "number" | "payload"> & {
timestamp: Date; timestamp: Date;
lastTimestamp?: Date; lastTimestamp?: Date;
externalId?: string; externalId?: string;
timezone: string;
}; };
}; };
@@ -168,9 +171,11 @@ export class TestTaskPresenter {
), ),
}; };
case "SCHEDULED": case "SCHEDULED":
const possibleTimezones = getTimezones();
return { return {
triggerSource: "SCHEDULED", triggerSource: "SCHEDULED",
task: taskWithEnvironment, task: taskWithEnvironment,
possibleTimezones,
runs: ( runs: (
await Promise.all( await Promise.all(
latestRuns.map(async (r) => { latestRuns.map(async (r) => {
@@ -195,6 +200,9 @@ export class TestTaskPresenter {
async function getScheduleTaskRunPayload(run: RawRun) { async function getScheduleTaskRunPayload(run: RawRun) {
const payload = await parsePacket({ data: run.payload, dataType: run.payloadType }); const payload = await parsePacket({ data: run.payload, dataType: run.payloadType });
if (!payload.timezone) {
payload.timezone = "UTC";
}
const parsed = ScheduledTaskPayload.safeParse(payload); const parsed = ScheduledTaskPayload.safeParse(payload);
return parsed; return parsed;
} }
@@ -1,8 +1,8 @@
import { ScheduleObject } from "@trigger.dev/core/v3";
import { PrismaClient, prisma } from "~/db.server"; import { PrismaClient, prisma } from "~/db.server";
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
import { nextScheduledTimestamps } from "~/v3/utils/calculateNextSchedule.server"; import { nextScheduledTimestamps } from "~/v3/utils/calculateNextSchedule.server";
import { RunListPresenter } from "./RunListPresenter.server"; import { RunListPresenter } from "./RunListPresenter.server";
import { ScheduleObject } from "@trigger.dev/core/v3";
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
type ViewScheduleOptions = { type ViewScheduleOptions = {
userId?: string; userId?: string;
@@ -24,6 +24,7 @@ export class ViewSchedulePresenter {
friendlyId: true, friendlyId: true,
generatorExpression: true, generatorExpression: true,
generatorDescription: true, generatorDescription: true,
timezone: true,
externalId: true, externalId: true,
deduplicationKey: true, deduplicationKey: true,
userProvidedDeduplicationKey: true, userProvidedDeduplicationKey: true,
@@ -68,7 +69,7 @@ export class ViewSchedulePresenter {
} }
const nextRuns = schedule.active const nextRuns = schedule.active
? nextScheduledTimestamps(schedule.generatorExpression, new Date(), 5) ? nextScheduledTimestamps(schedule.generatorExpression, schedule.timezone, new Date(), 5)
: []; : [];
const runPresenter = new RunListPresenter(this.#prismaClient); const runPresenter = new RunListPresenter(this.#prismaClient);
@@ -82,6 +83,7 @@ export class ViewSchedulePresenter {
return { return {
schedule: { schedule: {
...schedule, ...schedule,
timezone: schedule.timezone,
cron: schedule.generatorExpression, cron: schedule.generatorExpression,
cronDescription: schedule.generatorDescription, cronDescription: schedule.generatorDescription,
nextRuns, nextRuns,
@@ -105,6 +107,7 @@ export class ViewSchedulePresenter {
expression: result.schedule.cron, expression: result.schedule.cron,
description: result.schedule.cronDescription, description: result.schedule.cronDescription,
}, },
timezone: result.schedule.timezone,
externalId: result.schedule.externalId ?? undefined, externalId: result.schedule.externalId ?? undefined,
deduplicationKey: result.schedule.userProvidedDeduplicationKey deduplicationKey: result.schedule.userProvidedDeduplicationKey
? result.schedule.deduplicationKey ?? undefined ? result.schedule.deduplicationKey ?? undefined
@@ -185,7 +185,8 @@ export default function Page() {
const location = useLocation(); const location = useLocation();
const organization = useOrganization(); const organization = useOrganization();
const project = useProject(); const project = useProject();
const user = useUser();
const isUtc = schedule.timezone === "UTC";
return ( return (
<div className="grid h-full max-h-full grid-rows-[2.5rem_1fr_3.25rem] overflow-hidden bg-background-bright"> <div className="grid h-full max-h-full grid-rows-[2.5rem_1fr_3.25rem] overflow-hidden bg-background-bright">
@@ -210,6 +211,7 @@ export default function Page() {
<Paragraph variant="small">{schedule.cronDescription}</Paragraph> <Paragraph variant="small">{schedule.cronDescription}</Paragraph>
</div> </div>
</Property> </Property>
<Property label="Timezone">{schedule.timezone}</Property>
<Property label="Environments"> <Property label="Environments">
<EnvironmentLabels size="small" environments={schedule.environments} /> <EnvironmentLabels size="small" environments={schedule.environments} />
</Property> </Property>
@@ -245,19 +247,21 @@ export default function Page() {
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
{!isUtc && <TableHeaderCell>{schedule.timezone}</TableHeaderCell>}
<TableHeaderCell>UTC</TableHeaderCell> <TableHeaderCell>UTC</TableHeaderCell>
<TableHeaderCell>Local time</TableHeaderCell>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{schedule.nextRuns.map((run, index) => ( {schedule.nextRuns.map((run, index) => (
<TableRow key={index}> <TableRow key={index}>
{!isUtc && (
<TableCell>
<DateTime date={run} timeZone={schedule.timezone} />
</TableCell>
)}
<TableCell> <TableCell>
<DateTime date={run} timeZone="UTC" /> <DateTime date={run} timeZone="UTC" />
</TableCell> </TableCell>
<TableCell>
<DateTime date={run} />
</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
@@ -21,7 +21,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
}; };
export default function Page() { export default function Page() {
const { schedule, possibleTasks, possibleEnvironments, showGenerateField } = const { schedule, possibleTasks, possibleEnvironments, possibleTimezones, showGenerateField } =
useTypedLoaderData<typeof loader>(); useTypedLoaderData<typeof loader>();
return ( return (
@@ -29,6 +29,7 @@ export default function Page() {
schedule={schedule} schedule={schedule}
possibleTasks={possibleTasks} possibleTasks={possibleTasks}
possibleEnvironments={possibleEnvironments} possibleEnvironments={possibleEnvironments}
possibleTimezones={possibleTimezones}
showGenerateField={showGenerateField} showGenerateField={showGenerateField}
/> />
); );
@@ -20,7 +20,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
}; };
export default function Page() { export default function Page() {
const { schedule, possibleTasks, possibleEnvironments, showGenerateField } = const { schedule, possibleTasks, possibleEnvironments, possibleTimezones, showGenerateField } =
useTypedLoaderData<typeof loader>(); useTypedLoaderData<typeof loader>();
return ( return (
@@ -29,6 +29,7 @@ export default function Page() {
possibleTasks={possibleTasks} possibleTasks={possibleTasks}
possibleEnvironments={possibleEnvironments} possibleEnvironments={possibleEnvironments}
showGenerateField={showGenerateField} showGenerateField={showGenerateField}
possibleTimezones={possibleTimezones}
/> />
); );
} }
@@ -4,12 +4,21 @@ import { Outlet, useLocation, useParams } from "@remix-run/react";
import { LoaderFunctionArgs } from "@remix-run/server-runtime"; import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { BlankstateInstructions } from "~/components/BlankstateInstructions"; import { BlankstateInstructions } from "~/components/BlankstateInstructions";
import { Feedback } from "~/components/Feedback";
import { AdminDebugTooltip } from "~/components/admin/debugTooltip"; import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
import { InlineCode } from "~/components/code/InlineCode"; import { InlineCode } from "~/components/code/InlineCode";
import { EnvironmentLabel, EnvironmentLabels } from "~/components/environments/EnvironmentLabel"; import { EnvironmentLabels } from "~/components/environments/EnvironmentLabel";
import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout"; import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout";
import { LinkButton } from "~/components/primitives/Buttons"; import { Button, LinkButton } from "~/components/primitives/Buttons";
import { DateTime } from "~/components/primitives/DateTime"; import { DateTime } from "~/components/primitives/DateTime";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTrigger,
} from "~/components/primitives/Dialog";
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader"; import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
import { PaginationControls } from "~/components/primitives/Pagination"; import { PaginationControls } from "~/components/primitives/Pagination";
import { Paragraph } from "~/components/primitives/Paragraph"; import { Paragraph } from "~/components/primitives/Paragraph";
@@ -78,6 +87,7 @@ export default function Page() {
possibleEnvironments, possibleEnvironments,
hasFilters, hasFilters,
filters, filters,
limits,
currentPage, currentPage,
totalPages, totalPages,
} = useTypedLoaderData<typeof loader>(); } = useTypedLoaderData<typeof loader>();
@@ -107,15 +117,43 @@ export default function Page() {
</PropertyTable> </PropertyTable>
</AdminDebugTooltip> </AdminDebugTooltip>
<LinkButton {limits.used >= limits.limit ? (
LeadingIcon={PlusIcon} <Dialog>
to={`${v3NewSchedulePath(organization, project)}${location.search}`} <DialogTrigger asChild>
variant="primary/small" <Button
shortcut={{ key: "n" }} LeadingIcon={PlusIcon}
disabled={possibleTasks.length === 0 || isShowingNewPane} variant="primary/small"
> shortcut={{ key: "n" }}
New schedule disabled={possibleTasks.length === 0 || isShowingNewPane}
</LinkButton> >
New schedule
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>You've exceeded your limit</DialogHeader>
<DialogDescription>
You've used {limits.used}/{limits.limit} of your schedules. You can request more
schedules.
</DialogDescription>
<DialogFooter>
<Feedback
button={<Button variant="primary/medium">Request more</Button>}
defaultValue="help"
/>
</DialogFooter>
</DialogContent>
</Dialog>
) : (
<LinkButton
LeadingIcon={PlusIcon}
to={`${v3NewSchedulePath(organization, project)}${location.search}`}
variant="primary/small"
shortcut={{ key: "n" }}
disabled={possibleTasks.length === 0 || isShowingNewPane}
>
New schedule
</LinkButton>
)}
</PageAccessories> </PageAccessories>
</NavBar> </NavBar>
<PageBody scrollable={false}> <PageBody scrollable={false}>
@@ -142,7 +180,21 @@ export default function Page() {
</div> </div>
<SchedulesTable schedules={schedules} hasFilters={hasFilters} /> <SchedulesTable schedules={schedules} hasFilters={hasFilters} />
<div className="mt-2 justify-end"> <div className="mt-2 justify-between">
<Paragraph variant="extra-small" className="mt-3">
<span className={limits.used >= limits.limit ? "text-warning" : ""}>
You've used {limits.used}/{limits.limit} of your schedules.
</span>{" "}
<Feedback
button={
<button className=" text-secondary transition hover:text-indigo-400">
Request more
</button>
}
defaultValue="help"
/>
.
</Paragraph>
<PaginationControls currentPage={currentPage} totalPages={totalPages} /> <PaginationControls currentPage={currentPage} totalPages={totalPages} />
</div> </div>
</div> </div>
@@ -236,12 +288,13 @@ function SchedulesTable({
<TableRow> <TableRow>
<TableHeaderCell>ID</TableHeaderCell> <TableHeaderCell>ID</TableHeaderCell>
<TableHeaderCell>Task ID</TableHeaderCell> <TableHeaderCell>Task ID</TableHeaderCell>
<TableHeaderCell>External ID</TableHeaderCell>
<TableHeaderCell>CRON</TableHeaderCell> <TableHeaderCell>CRON</TableHeaderCell>
<TableHeaderCell hiddenLabel>CRON description</TableHeaderCell> <TableHeaderCell hiddenLabel>CRON description</TableHeaderCell>
<TableHeaderCell>External ID</TableHeaderCell> <TableHeaderCell>Timezone</TableHeaderCell>
<TableHeaderCell>Next run</TableHeaderCell>
<TableHeaderCell>Last run</TableHeaderCell>
<TableHeaderCell>Deduplication key</TableHeaderCell> <TableHeaderCell>Deduplication key</TableHeaderCell>
<TableHeaderCell>Next run (UTC)</TableHeaderCell>
<TableHeaderCell>Last run (UTC)</TableHeaderCell>
<TableHeaderCell>Environments</TableHeaderCell> <TableHeaderCell>Environments</TableHeaderCell>
<TableHeaderCell>Enabled</TableHeaderCell> <TableHeaderCell>Enabled</TableHeaderCell>
</TableRow> </TableRow>
@@ -262,6 +315,9 @@ function SchedulesTable({
<TableCell to={path} className={cellClass}> <TableCell to={path} className={cellClass}>
{schedule.taskIdentifier} {schedule.taskIdentifier}
</TableCell> </TableCell>
<TableCell to={path} className={cellClass}>
{schedule.externalId ? schedule.externalId : ""}
</TableCell>
<TableCell to={path} className={cellClass}> <TableCell to={path} className={cellClass}>
{schedule.cron} {schedule.cron}
</TableCell> </TableCell>
@@ -269,17 +325,21 @@ function SchedulesTable({
{schedule.cronDescription} {schedule.cronDescription}
</TableCell> </TableCell>
<TableCell to={path} className={cellClass}> <TableCell to={path} className={cellClass}>
{schedule.externalId ? schedule.externalId : ""} {schedule.timezone}
</TableCell>
<TableCell to={path} className={cellClass}>
<DateTime date={schedule.nextRun} timeZone={schedule.timezone} />
</TableCell>
<TableCell to={path} className={cellClass}>
{schedule.lastRun ? (
<DateTime date={schedule.lastRun} timeZone={schedule.timezone} />
) : (
""
)}
</TableCell> </TableCell>
<TableCell to={path} className={cellClass}> <TableCell to={path} className={cellClass}>
{schedule.userProvidedDeduplicationKey ? schedule.deduplicationKey : ""} {schedule.userProvidedDeduplicationKey ? schedule.deduplicationKey : ""}
</TableCell> </TableCell>
<TableCell to={path} className={cellClass}>
<DateTime date={schedule.nextRun} timeZone="utc" />
</TableCell>
<TableCell to={path} className={cellClass}>
{schedule.lastRun ? <DateTime date={schedule.lastRun} timeZone="utc" /> : ""}
</TableCell>
<TableCell to={path} className={cellClass}> <TableCell to={path} className={cellClass}>
<EnvironmentLabels environments={schedule.environments} size="small" /> <EnvironmentLabels environments={schedule.environments} size="small" />
</TableCell> </TableCell>
@@ -26,8 +26,10 @@ import {
ResizablePanel, ResizablePanel,
ResizablePanelGroup, ResizablePanelGroup,
} from "~/components/primitives/Resizable"; } from "~/components/primitives/Resizable";
import { Select } from "~/components/primitives/Select";
import { TextLink } from "~/components/primitives/TextLink"; import { TextLink } from "~/components/primitives/TextLink";
import { TaskRunStatusCombo } from "~/components/runs/v3/TaskRunStatus"; import { TaskRunStatusCombo } from "~/components/runs/v3/TaskRunStatus";
import { TimezoneList } from "~/components/scheduled/timezones";
import { redirectBackWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server"; import { redirectBackWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
import { import {
ScheduledRun, ScheduledRun,
@@ -95,7 +97,13 @@ export default function Page() {
return <StandardTaskForm task={result.task} runs={result.runs} />; return <StandardTaskForm task={result.task} runs={result.runs} />;
} }
case "SCHEDULED": { case "SCHEDULED": {
return <ScheduledTaskForm task={result.task} runs={result.runs} />; return (
<ScheduledTaskForm
task={result.task}
runs={result.runs}
possibleTimezones={result.possibleTimezones}
/>
);
} }
} }
} }
@@ -215,12 +223,21 @@ function StandardTaskForm({ task, runs }: { task: TestTask["task"]; runs: Standa
); );
} }
function ScheduledTaskForm({ task, runs }: { task: TestTask["task"]; runs: ScheduledRun[] }) { function ScheduledTaskForm({
task,
runs,
possibleTimezones,
}: {
task: TestTask["task"];
runs: ScheduledRun[];
possibleTimezones: string[];
}) {
const lastSubmission = useActionData(); const lastSubmission = useActionData();
const [selectedCodeSampleId, setSelectedCodeSampleId] = useState(runs.at(0)?.id); const [selectedCodeSampleId, setSelectedCodeSampleId] = useState(runs.at(0)?.id);
const [timestampValue, setTimestampValue] = useState<Date | undefined>(); const [timestampValue, setTimestampValue] = useState<Date | undefined>();
const [lastTimestampValue, setLastTimestampValue] = useState<Date | undefined>(); const [lastTimestampValue, setLastTimestampValue] = useState<Date | undefined>();
const [externalIdValue, setExternalIdValue] = useState<string | undefined>(); const [externalIdValue, setExternalIdValue] = useState<string | undefined>();
const [timezoneValue, setTimezoneValue] = useState<string>("UTC");
//set initial values //set initial values
useEffect(() => { useEffect(() => {
@@ -233,11 +250,20 @@ function ScheduledTaskForm({ task, runs }: { task: TestTask["task"]; runs: Sched
setTimestampValue(initialRun.payload.timestamp); setTimestampValue(initialRun.payload.timestamp);
setLastTimestampValue(initialRun.payload.lastTimestamp); setLastTimestampValue(initialRun.payload.lastTimestamp);
setExternalIdValue(initialRun.payload.externalId); setExternalIdValue(initialRun.payload.externalId);
setTimezoneValue(initialRun.payload.timezone);
}, [selectedCodeSampleId]); }, [selectedCodeSampleId]);
const [ const [
form, form,
{ timestamp, lastTimestamp, externalId, triggerSource, taskIdentifier, environmentId }, {
timestamp,
lastTimestamp,
externalId,
triggerSource,
taskIdentifier,
environmentId,
timezone,
},
] = useForm({ ] = useForm({
id: "test-task-scheduled", id: "test-task-scheduled",
// TODO: type this // TODO: type this
@@ -314,6 +340,30 @@ function ScheduledTaskForm({ task, runs }: { task: TestTask["task"]; runs: Sched
</Hint> </Hint>
<FormError id={lastTimestamp.errorId}>{lastTimestamp.error}</FormError> <FormError id={lastTimestamp.errorId}>{lastTimestamp.error}</FormError>
</InputGroup> </InputGroup>
<InputGroup>
<Label htmlFor={timezone.id}>Timezone</Label>
<Select
{...conform.select(timezone)}
placeholder="Select a timezone"
defaultValue={timezoneValue}
value={timezoneValue}
setValue={(e) => {
if (Array.isArray(e)) return;
setTimezoneValue(e);
}}
items={possibleTimezones}
filter={{ keys: [(item) => item.replace(/\//g, " ").replace(/_/g, " ")] }}
dropdownIcon
variant="tertiary/medium"
>
{(matches) => <TimezoneList timezones={matches} />}
</Select>
<Hint>
The Timestamp and Last timestamp are in UTC so this just changes the timezone
string that comes through in the payload.
</Hint>
<FormError id={timezone.errorId}>{timezone.error}</FormError>
</InputGroup>
<InputGroup> <InputGroup>
<Label required={false} htmlFor={externalId.id}> <Label required={false} htmlFor={externalId.id}>
External ID External ID
@@ -0,0 +1,35 @@
import { Link } from "@remix-run/react";
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { LogoIcon } from "~/components/LogoIcon";
import { Header1 } from "~/components/primitives/Headers";
import { Paragraph } from "~/components/primitives/Paragraph";
import { getTimezones } from "~/utils/timezones.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
return typedjson({
timezones: getTimezones(),
});
};
export default function Page() {
const { timezones } = useTypedLoaderData<typeof loader>();
return (
<div className="grid grid-rows-[2.5rem,1fr]">
<div className="flex items-center border-b border-b-grid-dimmed px-3">
<Link to="/">
<LogoIcon className="relative -top-px mr-2 h-4 w-4 min-w-[1rem]" />
</Link>
</div>
<div className="overflow-y-auto p-8 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
<Header1 spacing>Supported timezones</Header1>
<Paragraph spacing>We support these timezones when creating a schedule.</Paragraph>
<ul className="">
{timezones.map((timezone) => (
<li key={timezone}>{timezone}</li>
))}
</ul>
</div>
</div>
);
}
@@ -79,9 +79,9 @@ export async function action({ request, params }: ActionFunctionArgs) {
friendlyId: parsedParams.data.scheduleId, friendlyId: parsedParams.data.scheduleId,
taskIdentifier: body.data.task, taskIdentifier: body.data.task,
cron: body.data.cron, cron: body.data.cron,
timezone: body.data.timezone,
environments: [authenticationResult.environment.id], environments: [authenticationResult.environment.id],
externalId: body.data.externalId, externalId: body.data.externalId,
deduplicationKey: body.data.deduplicationKey,
}; };
const schedule = await service.call(authenticationResult.environment.projectId, options); const schedule = await service.call(authenticationResult.environment.projectId, options);
@@ -95,6 +95,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
expression: schedule.cron, expression: schedule.cron,
description: schedule.cronDescription, description: schedule.cronDescription,
}, },
timezone: schedule.timezone,
externalId: schedule.externalId ?? undefined, externalId: schedule.externalId ?? undefined,
deduplicationKey: schedule.deduplicationKey, deduplicationKey: schedule.deduplicationKey,
environments: schedule.environments, environments: schedule.environments,
@@ -43,6 +43,7 @@ export async function action({ request }: ActionFunctionArgs) {
environments: [authenticationResult.environment.id], environments: [authenticationResult.environment.id],
externalId: body.data.externalId, externalId: body.data.externalId,
deduplicationKey: body.data.deduplicationKey, deduplicationKey: body.data.deduplicationKey,
timezone: body.data.timezone,
}; };
const schedule = await service.call(authenticationResult.environment.projectId, options); const schedule = await service.call(authenticationResult.environment.projectId, options);
@@ -56,6 +57,7 @@ export async function action({ request }: ActionFunctionArgs) {
expression: schedule.cron, expression: schedule.cron,
description: schedule.cronDescription, description: schedule.cronDescription,
}, },
timezone: schedule.timezone,
externalId: schedule.externalId ?? undefined, externalId: schedule.externalId ?? undefined,
deduplicationKey: schedule.deduplicationKey, deduplicationKey: schedule.deduplicationKey,
environments: schedule.environments, environments: schedule.environments,
@@ -111,6 +113,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
expression: schedule.cron, expression: schedule.cron,
description: schedule.cronDescription, description: schedule.cronDescription,
}, },
timezone: schedule.timezone,
deduplicationKey: schedule.userProvidedDeduplicationKey deduplicationKey: schedule.userProvidedDeduplicationKey
? schedule.deduplicationKey ? schedule.deduplicationKey
: undefined, : undefined,
@@ -0,0 +1,28 @@
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { z } from "zod";
import { apiCors } from "~/utils/apiCors";
import { getTimezones } from "~/utils/timezones.server";
const SearchParamsSchema = z.object({
excludeUtc: z.preprocess((value) => value === "true", z.boolean()).default(false),
});
export async function loader({ request }: LoaderFunctionArgs) {
if (request.method.toUpperCase() === "OPTIONS") {
return apiCors(request, json({}));
}
const rawSearchParams = new URL(request.url).searchParams;
const params = SearchParamsSchema.safeParse(Object.fromEntries(rawSearchParams.entries()));
if (!params.success) {
return apiCors(
request,
json({ error: "Invalid request parameters", issues: params.error.issues }, { status: 400 })
);
}
const timezones = getTimezones(!params.data.excludeUtc);
return apiCors(request, json({ timezones }));
}
@@ -3,9 +3,10 @@ import { parse } from "@conform-to/zod";
import { CheckIcon, XMarkIcon } from "@heroicons/react/20/solid"; import { CheckIcon, XMarkIcon } from "@heroicons/react/20/solid";
import { Form, useActionData, useLocation, useNavigation } from "@remix-run/react"; import { Form, useActionData, useLocation, useNavigation } from "@remix-run/react";
import { ActionFunctionArgs, json } from "@remix-run/server-runtime"; import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
import { useVirtualizer } from "@tanstack/react-virtual";
import { parseExpression } from "cron-parser"; import { parseExpression } from "cron-parser";
import cronstrue from "cronstrue"; import cronstrue from "cronstrue";
import { useState } from "react"; import { useRef, useState } from "react";
import { import {
environmentTextClassName, environmentTextClassName,
environmentTitle, environmentTitle,
@@ -42,6 +43,7 @@ import { ProjectParamSchema, docsPath, v3SchedulesPath } from "~/utils/pathBuild
import { CronPattern, UpsertSchedule } from "~/v3/schedules"; import { CronPattern, UpsertSchedule } from "~/v3/schedules";
import { UpsertTaskScheduleService } from "~/v3/services/upsertTaskSchedule.server"; import { UpsertTaskScheduleService } from "~/v3/services/upsertTaskSchedule.server";
import { AIGeneratedCronField } from "../resources.orgs.$organizationSlug.projects.$projectParam.schedules.new.natural-language"; import { AIGeneratedCronField } from "../resources.orgs.$organizationSlug.projects.$projectParam.schedules.new.natural-language";
import { TimezoneList } from "~/components/scheduled/timezones";
const cronFormat = `* * * * * const cronFormat = `* * * * *
┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬
@@ -117,9 +119,12 @@ export function UpsertScheduleForm({
schedule, schedule,
possibleTasks, possibleTasks,
possibleEnvironments, possibleEnvironments,
possibleTimezones,
showGenerateField, showGenerateField,
}: EditableScheduleElements & { showGenerateField: boolean }) { }: EditableScheduleElements & { showGenerateField: boolean }) {
const lastSubmission = useActionData(); const lastSubmission = useActionData();
const [selectedTimezone, setSelectedTimezone] = useState<string>(schedule?.timezone ?? "UTC");
const isUtc = selectedTimezone === "UTC";
const [cronPattern, setCronPattern] = useState<string>(schedule?.cron ?? ""); const [cronPattern, setCronPattern] = useState<string>(schedule?.cron ?? "");
const navigation = useNavigation(); const navigation = useNavigation();
const isLoading = navigation.state !== "idle"; const isLoading = navigation.state !== "idle";
@@ -127,18 +132,20 @@ export function UpsertScheduleForm({
const project = useProject(); const project = useProject();
const location = useLocation(); const location = useLocation();
const [form, { taskIdentifier, cron, externalId, environments, deduplicationKey }] = useForm({ const [form, { taskIdentifier, cron, timezone, externalId, environments, deduplicationKey }] =
id: "create-schedule", useForm({
// TODO: type this id: "create-schedule",
lastSubmission: lastSubmission as any, // TODO: type this
shouldRevalidate: "onSubmit", lastSubmission: lastSubmission as any,
onValidate({ formData }) { shouldRevalidate: "onSubmit",
return parse(formData, { schema: UpsertSchedule }); onValidate({ formData }) {
}, return parse(formData, { schema: UpsertSchedule });
}); },
});
let cronPatternResult: CronPatternResult | undefined = undefined; let cronPatternResult: CronPatternResult | undefined = undefined;
let nextRuns: Date[] | undefined = undefined; let nextRuns: Date[] | undefined = undefined;
if (cronPattern !== "") { if (cronPattern !== "") {
const result = CronPattern.safeParse(cronPattern); const result = CronPattern.safeParse(cronPattern);
@@ -149,7 +156,10 @@ export function UpsertScheduleForm({
}; };
} else { } else {
try { try {
const expression = parseExpression(cronPattern, { utc: true }); const expression = parseExpression(
cronPattern,
isUtc ? { utc: true } : { tz: selectedTimezone }
);
cronPatternResult = { cronPatternResult = {
isValid: true, isValid: true,
description: cronstrue.toString(cronPattern), description: cronstrue.toString(cronPattern),
@@ -195,6 +205,7 @@ export function UpsertScheduleForm({
items={possibleTasks} items={possibleTasks}
filter={(task, search) => task.toLowerCase().includes(search.toLowerCase())} filter={(task, search) => task.toLowerCase().includes(search.toLowerCase())}
dropdownIcon dropdownIcon
variant="tertiary/medium"
> >
{(matches) => ( {(matches) => (
<> <>
@@ -241,25 +252,52 @@ export function UpsertScheduleForm({
<ValidCronMessage isValid={false} message={cronPatternResult.error} /> <ValidCronMessage isValid={false} message={cronPatternResult.error} />
)} )}
</InputGroup> </InputGroup>
<InputGroup>
<Label htmlFor={timezone.id}>Timezone</Label>
<Select
{...conform.select(timezone)}
placeholder="Select a timezone"
defaultValue={selectedTimezone}
value={selectedTimezone}
setValue={(e) => {
if (Array.isArray(e)) return;
setSelectedTimezone(e);
}}
items={possibleTimezones}
filter={{ keys: [(item) => item.replace(/\//g, " ").replace(/_/g, " ")] }}
dropdownIcon
variant="tertiary/medium"
>
{(matches) => <TimezoneList timezones={matches} />}
</Select>
<Hint>
{isUtc
? "UTC will not change with daylight savings time."
: "This will automatically adjust for daylight savings time."}
</Hint>
<FormError id={timezone.errorId}>{timezone.error}</FormError>
</InputGroup>
{nextRuns !== undefined && ( {nextRuns !== undefined && (
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<Header3>Next 5 runs</Header3> <Header3>Next 5 runs</Header3>
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
{!isUtc && <TableHeaderCell>{selectedTimezone}</TableHeaderCell>}
<TableHeaderCell>UTC</TableHeaderCell> <TableHeaderCell>UTC</TableHeaderCell>
<TableHeaderCell>Local time</TableHeaderCell>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{nextRuns.map((run, index) => ( {nextRuns.map((run, index) => (
<TableRow key={index}> <TableRow key={index}>
{!isUtc && (
<TableCell>
<DateTime date={run} timeZone={selectedTimezone} />
</TableCell>
)}
<TableCell> <TableCell>
<DateTime date={run} timeZone="UTC" /> <DateTime date={run} timeZone="UTC" />
</TableCell> </TableCell>
<TableCell>
<DateTime date={run} />
</TableCell>
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
@@ -150,6 +150,7 @@ export const apiRateLimiter = authorizationRateLimitMiddleware({
/^\/api\/v1\/http-endpoints\/[^\/]+\/env\/[^\/]+\/[^\/]+$/, // /api/v1/http-endpoints/$httpEndpointId/env/$envType/$shortcode /^\/api\/v1\/http-endpoints\/[^\/]+\/env\/[^\/]+\/[^\/]+$/, // /api/v1/http-endpoints/$httpEndpointId/env/$envType/$shortcode
/^\/api\/v1\/sources\/http\/[^\/]+$/, // /api/v1/sources/http/$id /^\/api\/v1\/sources\/http\/[^\/]+$/, // /api/v1/sources/http/$id
/^\/api\/v1\/endpoints\/[^\/]+\/[^\/]+\/index\/[^\/]+$/, // /api/v1/endpoints/$environmentId/$endpointSlug/index/$indexHookIdentifier /^\/api\/v1\/endpoints\/[^\/]+\/[^\/]+\/index\/[^\/]+$/, // /api/v1/endpoints/$environmentId/$endpointSlug/index/$indexHookIdentifier
"/api/v1/timezones",
], ],
log: { log: {
rejections: env.API_RATE_LIMIT_REJECTION_LOGS_ENABLED === "1", rejections: env.API_RATE_LIMIT_REJECTION_LOGS_ENABLED === "1",
@@ -0,0 +1,7 @@
export function getTimezones(includeUtc = true) {
const possibleTimezones = Intl.supportedValuesOf("timeZone").sort();
if (includeUtc) {
possibleTimezones.unshift("UTC");
}
return possibleTimezones;
}
+1
View File
@@ -55,6 +55,7 @@ export const UpsertSchedule = z.object({
), ),
externalId: z.string().optional(), externalId: z.string().optional(),
deduplicationKey: z.string().optional(), deduplicationKey: z.string().optional(),
timezone: z.string().optional(),
}); });
export type UpsertSchedule = z.infer<typeof UpsertSchedule>; export type UpsertSchedule = z.infer<typeof UpsertSchedule>;
@@ -20,6 +20,7 @@ export class RegisterNextTaskScheduleInstanceService extends BaseService {
const nextScheduledTimestamp = calculateNextScheduledTimestamp( const nextScheduledTimestamp = calculateNextScheduledTimestamp(
instance.taskSchedule.generatorExpression, instance.taskSchedule.generatorExpression,
instance.taskSchedule.timezone,
instance.lastScheduledTimestamp ?? new Date() instance.lastScheduledTimestamp ?? new Date()
); );
@@ -27,6 +27,7 @@ export class TestTaskService extends BaseService {
scheduleId: "sched_1234", scheduleId: "sched_1234",
timestamp: data.timestamp, timestamp: data.timestamp,
lastTimestamp: data.lastTimestamp, lastTimestamp: data.lastTimestamp,
timezone: data.timezone,
externalId: data.externalId, externalId: data.externalId,
upcoming: [], upcoming: [],
}; };
@@ -96,8 +96,10 @@ export class TriggerScheduledTaskService extends BaseService {
timestamp: instance.nextScheduledTimestamp, timestamp: instance.nextScheduledTimestamp,
lastTimestamp: instance.lastScheduledTimestamp ?? undefined, lastTimestamp: instance.lastScheduledTimestamp ?? undefined,
externalId: instance.taskSchedule.externalId ?? undefined, externalId: instance.taskSchedule.externalId ?? undefined,
timezone: instance.taskSchedule.timezone,
upcoming: nextScheduledTimestamps( upcoming: nextScheduledTimestamps(
instance.taskSchedule.generatorExpression, instance.taskSchedule.generatorExpression,
instance.taskSchedule.timezone,
instance.nextScheduledTimestamp!, instance.nextScheduledTimestamp!,
10 10
), ),
@@ -8,6 +8,8 @@ import { BaseService, ServiceValidationError } from "./baseService.server";
import { RegisterNextTaskScheduleInstanceService } from "./registerNextTaskScheduleInstance.server"; import { RegisterNextTaskScheduleInstanceService } from "./registerNextTaskScheduleInstance.server";
import cronstrue from "cronstrue"; import cronstrue from "cronstrue";
import { calculateNextScheduledTimestamp } from "../utils/calculateNextSchedule.server"; import { calculateNextScheduledTimestamp } from "../utils/calculateNextSchedule.server";
import { getTimezones } from "~/utils/timezones.server";
import { env } from "~/env.server";
export type UpsertTaskScheduleServiceOptions = UpsertSchedule; export type UpsertTaskScheduleServiceOptions = UpsertSchedule;
@@ -62,6 +64,48 @@ export class UpsertTaskScheduleService extends BaseService {
); );
} }
//if creating a schedule, check they're under the limits
if (!schedule.friendlyId) {
//check they're within their limit
const limits = await this._prisma.organization.findFirst({
select: {
maximumSchedulesLimit: true,
},
where: {
projects: {
some: {
id: projectId,
},
},
},
});
if (!limits) {
throw new ServiceValidationError("Organization not found");
}
const schedulesCount = await this._prisma.taskSchedule.count({
where: {
projectId,
},
});
if (schedulesCount >= limits.maximumSchedulesLimit) {
throw new ServiceValidationError(
`You have created ${schedulesCount}/${limits.maximumSchedulesLimit} schedules so you'll need to increase your limits or delete some schedules. Increase your limits by contacting support.`
);
}
}
if (schedule.timezone) {
const possibleTimezones = getTimezones();
if (!possibleTimezones.includes(schedule.timezone)) {
throw new ServiceValidationError(
`Invalid IANA timezone: "${schedule.timezone}". View the list of valid timezones at ${env.APP_ORIGIN}/timezones`
);
}
}
const result = await $transaction(this._prisma, async (tx) => { const result = await $transaction(this._prisma, async (tx) => {
const deduplicationKey = const deduplicationKey =
typeof schedule.deduplicationKey === "string" && schedule.deduplicationKey !== "" typeof schedule.deduplicationKey === "string" && schedule.deduplicationKey !== ""
@@ -115,6 +159,7 @@ export class UpsertTaskScheduleService extends BaseService {
options.deduplicationKey !== undefined && options.deduplicationKey !== "", options.deduplicationKey !== undefined && options.deduplicationKey !== "",
generatorExpression: options.cron, generatorExpression: options.cron,
generatorDescription: cronstrue.toString(options.cron), generatorDescription: cronstrue.toString(options.cron),
timezone: options.timezone ?? "UTC",
externalId: options.externalId ? options.externalId : undefined, externalId: options.externalId ? options.externalId : undefined,
}, },
}); });
@@ -164,6 +209,7 @@ export class UpsertTaskScheduleService extends BaseService {
data: { data: {
generatorExpression: options.cron, generatorExpression: options.cron,
generatorDescription: cronstrue.toString(options.cron), generatorDescription: cronstrue.toString(options.cron),
timezone: options.timezone ?? "UTC",
externalId: options.externalId ? options.externalId : null, externalId: options.externalId ? options.externalId : null,
}, },
}); });
@@ -280,7 +326,11 @@ export class UpsertTaskScheduleService extends BaseService {
: undefined, : undefined,
cron: taskSchedule.generatorExpression, cron: taskSchedule.generatorExpression,
cronDescription: taskSchedule.generatorDescription, cronDescription: taskSchedule.generatorDescription,
nextRun: calculateNextScheduledTimestamp(taskSchedule.generatorExpression), timezone: taskSchedule.timezone,
nextRun: calculateNextScheduledTimestamp(
taskSchedule.generatorExpression,
taskSchedule.timezone
),
environments: instances.map((instance) => ({ environments: instances.map((instance) => ({
id: instance.environment.id, id: instance.environment.id,
shortcode: instance.environment.shortcode, shortcode: instance.environment.shortcode,
+1
View File
@@ -32,6 +32,7 @@ export const TestTaskData = z
(val) => (val === "" ? undefined : val), (val) => (val === "" ? undefined : val),
z.coerce.date().optional() z.coerce.date().optional()
), ),
timezone: z.string(),
externalId: z.preprocess((val) => (val === "" ? undefined : val), z.string().optional()), externalId: z.preprocess((val) => (val === "" ? undefined : val), z.string().optional()),
}), }),
]) ])
@@ -2,21 +2,23 @@ import { parseExpression } from "cron-parser";
export function calculateNextScheduledTimestamp( export function calculateNextScheduledTimestamp(
schedule: string, schedule: string,
timezone: string | null,
lastScheduledTimestamp: Date = new Date() lastScheduledTimestamp: Date = new Date()
) { ) {
let nextStep = calculateNextStep(schedule, lastScheduledTimestamp); let nextStep = calculateNextStep(schedule, timezone, lastScheduledTimestamp);
while (nextStep.getTime() < Date.now()) { while (nextStep.getTime() < Date.now()) {
nextStep = calculateNextStep(schedule, nextStep); nextStep = calculateNextStep(schedule, timezone, nextStep);
} }
return nextStep; return nextStep;
} }
function calculateNextStep(schedule: string, currentDate: Date) { function calculateNextStep(schedule: string, timezone: string | null, currentDate: Date) {
return parseExpression(schedule, { return parseExpression(schedule, {
currentDate, currentDate,
utc: true, utc: timezone === null,
tz: timezone ?? undefined,
}) })
.next() .next()
.toDate(); .toDate();
@@ -24,6 +26,7 @@ function calculateNextStep(schedule: string, currentDate: Date) {
export function nextScheduledTimestamps( export function nextScheduledTimestamps(
cron: string, cron: string,
timezone: string | null,
lastScheduledTimestamp: Date, lastScheduledTimestamp: Date,
count: number = 1 count: number = 1
) { ) {
@@ -31,7 +34,11 @@ export function nextScheduledTimestamps(
let nextScheduledTimestamp = lastScheduledTimestamp; let nextScheduledTimestamp = lastScheduledTimestamp;
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
nextScheduledTimestamp = calculateNextScheduledTimestamp(cron, nextScheduledTimestamp); nextScheduledTimestamp = calculateNextScheduledTimestamp(
cron,
timezone,
nextScheduledTimestamp
);
result.push(nextScheduledTimestamp); result.push(nextScheduledTimestamp);
} }
+5
View File
@@ -0,0 +1,5 @@
declare namespace Intl {
type Key = "calendar" | "collation" | "currency" | "numberingSystem" | "timeZone" | "unit";
function supportedValuesOf(input: Key): string[];
}
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"exclude": ["./cypress", "./cypress.config.ts"], "exclude": ["./cypress", "./cypress.config.ts"],
"include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], "include": ["remix.env.d.ts", "global.d.ts", "**/*.ts", "**/*.tsx"],
"compilerOptions": { "compilerOptions": {
"types": ["vitest/globals"], "types": ["vitest/globals"],
"lib": ["DOM", "DOM.Iterable", "ES2019"], "lib": ["DOM", "DOM.Iterable", "ES2019"],
+7
View File
@@ -0,0 +1,7 @@
<Accordion title="How to increase these limits?">
These are soft-limits and can be increased. Before we introduce paid plans in July you can request
more [on Discord](https://trigger.dev/discord) or by [contacting us](https://trigger.dev/contact).
If you increase these defaults you may have to subscribe to a paid plan when we introduce them.
For more details on the v3 Cloud pricing see the [pricing
details](https://trigger.dev/blog/v3-developer-preview-launch#cloud-pricing).
</Accordion>
+2 -1
View File
@@ -190,7 +190,8 @@
"v3/management/schedules/update", "v3/management/schedules/update",
"v3/management/schedules/delete", "v3/management/schedules/delete",
"v3/management/schedules/deactivate", "v3/management/schedules/deactivate",
"v3/management/schedules/activate" "v3/management/schedules/activate",
"v3/management/schedules/timezones"
] ]
}, },
{ {
+506 -16
View File
@@ -5,7 +5,7 @@ info:
description: "The REST API lets you trigger and manage runs on Trigger.dev. You description: "The REST API lets you trigger and manage runs on Trigger.dev. You
can trigger a run, get the status of a run, and get the results of a run. " can trigger a run, get the status of a run, and get the results of a run. "
version: 2024-04 version: 2024-04
license: license:
name: Apache 2.0 name: Apache 2.0
url: https://www.apache.org/licenses/LICENSE-2.0.html url: https://www.apache.org/licenses/LICENSE-2.0.html
servers: servers:
@@ -48,6 +48,8 @@ paths:
const schedule = await schedules.create({ const schedule = await schedules.create({
task: 'my-task', task: 'my-task',
cron: '0 0 * * *' cron: '0 0 * * *'
deduplicationKey: 'my-schedule',
timezone: 'America/New_York'
}); });
get: get:
@@ -138,7 +140,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
"$ref": "#/components/schemas/CreateScheduleOptions" "$ref": "#/components/schemas/UpdateScheduleOptions"
responses: responses:
"200": "200":
description: Schedule updated successfully description: Schedule updated successfully
@@ -265,6 +267,34 @@ paths:
const schedule = await schedules.activate(scheduleId); const schedule = await schedules.activate(scheduleId);
"/api/v1/timezones":
get:
operationId: get_timezones_v1
summary: Get all supported timezones
description: Get all supported timezones that schedule tasks support.
parameters:
- in: query
name: excludeUtc
schema:
type: boolean
required: false
description: Defaults to false. Whether to include UTC in the results or not.
responses:
"200":
description: Successful request
content:
application/json:
schema:
"$ref": "#/components/schemas/GetTimezonesResult"
tags:
- schedules
x-codeSamples:
- lang: typescript
source: |-
import { schedules } from "@trigger.dev/sdk/v3";
const { timezones } = await schedules.timezones();
"/api/v1/runs/{runId}/replay": "/api/v1/runs/{runId}/replay":
parameters: parameters:
- $ref: "#/components/parameters/runId" - $ref: "#/components/parameters/runId"
@@ -401,7 +431,7 @@ paths:
operationId: retrieve_run_v1 operationId: retrieve_run_v1
summary: Retrieve a run summary: Retrieve a run
description: | description: |
Retrieve information about a run, including its status, payload, output, and attempts. If you authenticate with a Public API key, we will omit the payload and output fields for security reasons. Retrieve information about a run, including its status, payload, output, and attempts. If you authenticate with a Public API key, we will omit the payload and output fields for security reasons.
responses: responses:
"200": "200":
description: Successful request description: Successful request
@@ -619,8 +649,6 @@ paths:
for (const run of response.data) { for (const run of response.data) {
console.log(`Run ID: ${run.id}, Status: ${run.status}`); console.log(`Run ID: ${run.id}, Status: ${run.status}`);
} }
"/api/v1/projects/{projectRef}/envvars/{env}": "/api/v1/projects/{projectRef}/envvars/{env}":
parameters: parameters:
@@ -792,7 +820,7 @@ paths:
format: binary format: binary
override: override:
type: boolean type: boolean
required: required:
- variables - variables
responses: responses:
"200": "200":
@@ -851,7 +879,7 @@ paths:
source: |- source: |-
import { envvars } from "@trigger.dev/sdk/v3"; import { envvars } from "@trigger.dev/sdk/v3";
import { createReadStream } from "node:fs"; import { createReadStream } from "node:fs";
// Import variables in dotenv format from a file // Import variables in dotenv format from a file
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", { await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
variables: createReadStream(".env"), variables: createReadStream(".env"),
@@ -861,7 +889,7 @@ paths:
label: Import variables from a response label: Import variables from a response
source: |- source: |-
import { envvars } from "@trigger.dev/sdk/v3"; import { envvars } from "@trigger.dev/sdk/v3";
// Import variables in dotenv format from a response // Import variables in dotenv format from a response
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", { await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
variables: await fetch("https://example.com/.env"), variables: await fetch("https://example.com/.env"),
@@ -871,7 +899,7 @@ paths:
label: Import variables from a Buffer label: Import variables from a Buffer
source: |- source: |-
import { envvars } from "@trigger.dev/sdk/v3"; import { envvars } from "@trigger.dev/sdk/v3";
// Import variables in dotenv format from a buffer // Import variables in dotenv format from a buffer
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", { await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
variables: Buffer.from("SLACK_API_KEY=slack_1234"), variables: Buffer.from("SLACK_API_KEY=slack_1234"),
@@ -881,14 +909,13 @@ paths:
label: Import variables from a File label: Import variables from a File
source: |- source: |-
import { envvars } from "@trigger.dev/sdk/v3"; import { envvars } from "@trigger.dev/sdk/v3";
// Import variables in dotenv format from a file // Import variables in dotenv format from a file
await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", { await envvars.upload("proj_yubjwjsfkxnylobaqvqz", "dev", {
variables: new File(["SLACK_API_KEY=slack_1234"], ".env"), variables: new File(["SLACK_API_KEY=slack_1234"], ".env"),
override: false override: false
}); });
"/api/v1/projects/{projectRef}/envvars/{env}/{name}": "/api/v1/projects/{projectRef}/envvars/{env}/{name}":
parameters: parameters:
- $ref: "#/components/parameters/projectRef" - $ref: "#/components/parameters/projectRef"
@@ -951,7 +978,7 @@ paths:
console.log(`Value: ${variable.value}`); console.log(`Value: ${variable.value}`);
} }
}) })
delete: delete:
operationId: delete_project_envvar_v1 operationId: delete_project_envvar_v1
summary: Delete environment variable summary: Delete environment variable
@@ -1068,7 +1095,6 @@ paths:
}); });
} }
}) })
components: components:
parameters: parameters:
@@ -1471,11 +1497,11 @@ components:
payload: payload:
type: object type: object
description: The payload that was sent to the task. Will be omitted if the request was made with a Public API key description: The payload that was sent to the task. Will be omitted if the request was made with a Public API key
example: {"foo": "bar"} example: { "foo": "bar" }
output: output:
type: object type: object
description: The output of the run. Will be omitted if the request was made with a Public API key description: The output of the run. Will be omitted if the request was made with a Public API key
example: {"foo": "bar"} example: { "foo": "bar" }
idempotencyKey: idempotencyKey:
type: string type: string
description: The idempotency key used to prevent creating duplicate runs, if provided description: The idempotency key used to prevent creating duplicate runs, if provided
@@ -1580,6 +1606,27 @@ components:
type: string type: string
externalId: externalId:
type: string type: string
timezone:
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.
required:
- task
- cron
- deduplicationKey
UpdateScheduleOptions:
type: object
properties:
task:
type: string
cron:
type: string
externalId:
type: string
timezone:
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.
required: required:
- task - task
- cron - cron
@@ -1623,6 +1670,10 @@ components:
type: string type: string
description: The description of the generator in plain english description: The description of the generator in plain english
example: Every day at midnight example: Every day at midnight
timezone:
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.
nextRun: nextRun:
type: string type: string
format: date-time format: date-time
@@ -1648,6 +1699,445 @@ components:
type: integer type: integer
count: count:
type: integer type: integer
GetTimezonesResult:
type: object
properties:
timezones:
type: array
items:
type: string
example:
[
"UTC",
"Africa/Abidjan",
"Africa/Accra",
"Africa/Addis_Ababa",
"Africa/Algiers",
"Africa/Asmera",
"Africa/Bamako",
"Africa/Bangui",
"Africa/Banjul",
"Africa/Bissau",
"Africa/Blantyre",
"Africa/Brazzaville",
"Africa/Bujumbura",
"Africa/Cairo",
"Africa/Casablanca",
"Africa/Ceuta",
"Africa/Conakry",
"Africa/Dakar",
"Africa/Dar_es_Salaam",
"Africa/Djibouti",
"Africa/Douala",
"Africa/El_Aaiun",
"Africa/Freetown",
"Africa/Gaborone",
"Africa/Harare",
"Africa/Johannesburg",
"Africa/Juba",
"Africa/Kampala",
"Africa/Khartoum",
"Africa/Kigali",
"Africa/Kinshasa",
"Africa/Lagos",
"Africa/Libreville",
"Africa/Lome",
"Africa/Luanda",
"Africa/Lubumbashi",
"Africa/Lusaka",
"Africa/Malabo",
"Africa/Maputo",
"Africa/Maseru",
"Africa/Mbabane",
"Africa/Mogadishu",
"Africa/Monrovia",
"Africa/Nairobi",
"Africa/Ndjamena",
"Africa/Niamey",
"Africa/Nouakchott",
"Africa/Ouagadougou",
"Africa/Porto-Novo",
"Africa/Sao_Tome",
"Africa/Tripoli",
"Africa/Tunis",
"Africa/Windhoek",
"America/Adak",
"America/Anchorage",
"America/Anguilla",
"America/Antigua",
"America/Araguaina",
"America/Argentina/La_Rioja",
"America/Argentina/Rio_Gallegos",
"America/Argentina/Salta",
"America/Argentina/San_Juan",
"America/Argentina/San_Luis",
"America/Argentina/Tucuman",
"America/Argentina/Ushuaia",
"America/Aruba",
"America/Asuncion",
"America/Bahia",
"America/Bahia_Banderas",
"America/Barbados",
"America/Belem",
"America/Belize",
"America/Blanc-Sablon",
"America/Boa_Vista",
"America/Bogota",
"America/Boise",
"America/Buenos_Aires",
"America/Cambridge_Bay",
"America/Campo_Grande",
"America/Cancun",
"America/Caracas",
"America/Catamarca",
"America/Cayenne",
"America/Cayman",
"America/Chicago",
"America/Chihuahua",
"America/Ciudad_Juarez",
"America/Coral_Harbour",
"America/Cordoba",
"America/Costa_Rica",
"America/Creston",
"America/Cuiaba",
"America/Curacao",
"America/Danmarkshavn",
"America/Dawson",
"America/Dawson_Creek",
"America/Denver",
"America/Detroit",
"America/Dominica",
"America/Edmonton",
"America/Eirunepe",
"America/El_Salvador",
"America/Fort_Nelson",
"America/Fortaleza",
"America/Glace_Bay",
"America/Godthab",
"America/Goose_Bay",
"America/Grand_Turk",
"America/Grenada",
"America/Guadeloupe",
"America/Guatemala",
"America/Guayaquil",
"America/Guyana",
"America/Halifax",
"America/Havana",
"America/Hermosillo",
"America/Indiana/Knox",
"America/Indiana/Marengo",
"America/Indiana/Petersburg",
"America/Indiana/Tell_City",
"America/Indiana/Vevay",
"America/Indiana/Vincennes",
"America/Indiana/Winamac",
"America/Indianapolis",
"America/Inuvik",
"America/Iqaluit",
"America/Jamaica",
"America/Jujuy",
"America/Juneau",
"America/Kentucky/Monticello",
"America/Kralendijk",
"America/La_Paz",
"America/Lima",
"America/Los_Angeles",
"America/Louisville",
"America/Lower_Princes",
"America/Maceio",
"America/Managua",
"America/Manaus",
"America/Marigot",
"America/Martinique",
"America/Matamoros",
"America/Mazatlan",
"America/Mendoza",
"America/Menominee",
"America/Merida",
"America/Metlakatla",
"America/Mexico_City",
"America/Miquelon",
"America/Moncton",
"America/Monterrey",
"America/Montevideo",
"America/Montserrat",
"America/Nassau",
"America/New_York",
"America/Nipigon",
"America/Nome",
"America/Noronha",
"America/North_Dakota/Beulah",
"America/North_Dakota/Center",
"America/North_Dakota/New_Salem",
"America/Ojinaga",
"America/Panama",
"America/Pangnirtung",
"America/Paramaribo",
"America/Phoenix",
"America/Port-au-Prince",
"America/Port_of_Spain",
"America/Porto_Velho",
"America/Puerto_Rico",
"America/Punta_Arenas",
"America/Rainy_River",
"America/Rankin_Inlet",
"America/Recife",
"America/Regina",
"America/Resolute",
"America/Rio_Branco",
"America/Santa_Isabel",
"America/Santarem",
"America/Santiago",
"America/Santo_Domingo",
"America/Sao_Paulo",
"America/Scoresbysund",
"America/Sitka",
"America/St_Barthelemy",
"America/St_Johns",
"America/St_Kitts",
"America/St_Lucia",
"America/St_Thomas",
"America/St_Vincent",
"America/Swift_Current",
"America/Tegucigalpa",
"America/Thule",
"America/Thunder_Bay",
"America/Tijuana",
"America/Toronto",
"America/Tortola",
"America/Vancouver",
"America/Whitehorse",
"America/Winnipeg",
"America/Yakutat",
"America/Yellowknife",
"Antarctica/Casey",
"Antarctica/Davis",
"Antarctica/DumontDUrville",
"Antarctica/Macquarie",
"Antarctica/Mawson",
"Antarctica/McMurdo",
"Antarctica/Palmer",
"Antarctica/Rothera",
"Antarctica/Syowa",
"Antarctica/Troll",
"Antarctica/Vostok",
"Arctic/Longyearbyen",
"Asia/Aden",
"Asia/Almaty",
"Asia/Amman",
"Asia/Anadyr",
"Asia/Aqtau",
"Asia/Aqtobe",
"Asia/Ashgabat",
"Asia/Atyrau",
"Asia/Baghdad",
"Asia/Bahrain",
"Asia/Baku",
"Asia/Bangkok",
"Asia/Barnaul",
"Asia/Beirut",
"Asia/Bishkek",
"Asia/Brunei",
"Asia/Calcutta",
"Asia/Chita",
"Asia/Choibalsan",
"Asia/Colombo",
"Asia/Damascus",
"Asia/Dhaka",
"Asia/Dili",
"Asia/Dubai",
"Asia/Dushanbe",
"Asia/Famagusta",
"Asia/Gaza",
"Asia/Hebron",
"Asia/Hong_Kong",
"Asia/Hovd",
"Asia/Irkutsk",
"Asia/Jakarta",
"Asia/Jayapura",
"Asia/Jerusalem",
"Asia/Kabul",
"Asia/Kamchatka",
"Asia/Karachi",
"Asia/Katmandu",
"Asia/Khandyga",
"Asia/Krasnoyarsk",
"Asia/Kuala_Lumpur",
"Asia/Kuching",
"Asia/Kuwait",
"Asia/Macau",
"Asia/Magadan",
"Asia/Makassar",
"Asia/Manila",
"Asia/Muscat",
"Asia/Nicosia",
"Asia/Novokuznetsk",
"Asia/Novosibirsk",
"Asia/Omsk",
"Asia/Oral",
"Asia/Phnom_Penh",
"Asia/Pontianak",
"Asia/Pyongyang",
"Asia/Qatar",
"Asia/Qostanay",
"Asia/Qyzylorda",
"Asia/Rangoon",
"Asia/Riyadh",
"Asia/Saigon",
"Asia/Sakhalin",
"Asia/Samarkand",
"Asia/Seoul",
"Asia/Shanghai",
"Asia/Singapore",
"Asia/Srednekolymsk",
"Asia/Taipei",
"Asia/Tashkent",
"Asia/Tbilisi",
"Asia/Tehran",
"Asia/Thimphu",
"Asia/Tokyo",
"Asia/Tomsk",
"Asia/Ulaanbaatar",
"Asia/Urumqi",
"Asia/Ust-Nera",
"Asia/Vientiane",
"Asia/Vladivostok",
"Asia/Yakutsk",
"Asia/Yekaterinburg",
"Asia/Yerevan",
"Atlantic/Azores",
"Atlantic/Bermuda",
"Atlantic/Canary",
"Atlantic/Cape_Verde",
"Atlantic/Faeroe",
"Atlantic/Madeira",
"Atlantic/Reykjavik",
"Atlantic/South_Georgia",
"Atlantic/St_Helena",
"Atlantic/Stanley",
"Australia/Adelaide",
"Australia/Brisbane",
"Australia/Broken_Hill",
"Australia/Currie",
"Australia/Darwin",
"Australia/Eucla",
"Australia/Hobart",
"Australia/Lindeman",
"Australia/Lord_Howe",
"Australia/Melbourne",
"Australia/Perth",
"Australia/Sydney",
"Europe/Amsterdam",
"Europe/Andorra",
"Europe/Astrakhan",
"Europe/Athens",
"Europe/Belgrade",
"Europe/Berlin",
"Europe/Bratislava",
"Europe/Brussels",
"Europe/Bucharest",
"Europe/Budapest",
"Europe/Busingen",
"Europe/Chisinau",
"Europe/Copenhagen",
"Europe/Dublin",
"Europe/Gibraltar",
"Europe/Guernsey",
"Europe/Helsinki",
"Europe/Isle_of_Man",
"Europe/Istanbul",
"Europe/Jersey",
"Europe/Kaliningrad",
"Europe/Kiev",
"Europe/Kirov",
"Europe/Lisbon",
"Europe/Ljubljana",
"Europe/London",
"Europe/Luxembourg",
"Europe/Madrid",
"Europe/Malta",
"Europe/Mariehamn",
"Europe/Minsk",
"Europe/Monaco",
"Europe/Moscow",
"Europe/Oslo",
"Europe/Paris",
"Europe/Podgorica",
"Europe/Prague",
"Europe/Riga",
"Europe/Rome",
"Europe/Samara",
"Europe/San_Marino",
"Europe/Sarajevo",
"Europe/Saratov",
"Europe/Simferopol",
"Europe/Skopje",
"Europe/Sofia",
"Europe/Stockholm",
"Europe/Tallinn",
"Europe/Tirane",
"Europe/Ulyanovsk",
"Europe/Uzhgorod",
"Europe/Vaduz",
"Europe/Vatican",
"Europe/Vienna",
"Europe/Vilnius",
"Europe/Volgograd",
"Europe/Warsaw",
"Europe/Zagreb",
"Europe/Zaporozhye",
"Europe/Zurich",
"Indian/Antananarivo",
"Indian/Chagos",
"Indian/Christmas",
"Indian/Cocos",
"Indian/Comoro",
"Indian/Kerguelen",
"Indian/Mahe",
"Indian/Maldives",
"Indian/Mauritius",
"Indian/Mayotte",
"Indian/Reunion",
"Pacific/Apia",
"Pacific/Auckland",
"Pacific/Bougainville",
"Pacific/Chatham",
"Pacific/Easter",
"Pacific/Efate",
"Pacific/Enderbury",
"Pacific/Fakaofo",
"Pacific/Fiji",
"Pacific/Funafuti",
"Pacific/Galapagos",
"Pacific/Gambier",
"Pacific/Guadalcanal",
"Pacific/Guam",
"Pacific/Honolulu",
"Pacific/Johnston",
"Pacific/Kiritimati",
"Pacific/Kosrae",
"Pacific/Kwajalein",
"Pacific/Majuro",
"Pacific/Marquesas",
"Pacific/Midway",
"Pacific/Nauru",
"Pacific/Niue",
"Pacific/Norfolk",
"Pacific/Noumea",
"Pacific/Pago_Pago",
"Pacific/Palau",
"Pacific/Pitcairn",
"Pacific/Ponape",
"Pacific/Port_Moresby",
"Pacific/Rarotonga",
"Pacific/Saipan",
"Pacific/Tahiti",
"Pacific/Tarawa",
"Pacific/Tongatapu",
"Pacific/Truk",
"Pacific/Wake",
"Pacific/Wallis",
]
ScheduleEnvironment: ScheduleEnvironment:
type: object type: object
properties: properties:
@@ -1659,7 +2149,7 @@ components:
type: string type: string
SerializedError: SerializedError:
type: object type: object
required: required:
- message - message
properties: properties:
message: message:
+16 -2
View File
@@ -7,13 +7,13 @@ description: "There are some hard and soft limits in v3 that you might hit."
These are the default limits on a free account. These are the default limits on a free account.
Before we introduce paid plans in July you can request more [on Discord](https://trigger.dev/discord) or by [contacting us](https://trigger.dev/contact). If you increase these defaults you may have to subscribe to a paid plan when we introduce them. For more details on the v3 Cloud pricing see the [pricing page](https://trigger.dev/blog/v3-developer-preview-launch#cloud-pricing).
| Limit | Details | | Limit | Details |
| ------------ | ------------------ | | ------------ | ------------------ |
| Organization | 10 concurrent runs | | Organization | 10 concurrent runs |
| Environment | 5 concurrent runs | | Environment | 5 concurrent runs |
<Snippet file="v3/soft-limit.mdx" />
## Rate limits ## Rate limits
| Limit | Details | | Limit | Details |
@@ -23,3 +23,17 @@ Before we introduce paid plans in July you can request more [on Discord](https:/
Generally speaking each SDK call is an API call. Generally speaking each SDK call is an API call.
The most common cause of hitting the API rate limit is if you're calling `trigger()` on a task in a loop, instead of doing this use `batchTrigger()` which will trigger multiple tasks in a single API call. You can have up to 100 tasks in a single batch trigger call. The most common cause of hitting the API rate limit is if you're calling `trigger()` on a task in a loop, instead of doing this use `batchTrigger()` which will trigger multiple tasks in a single API call. You can have up to 100 tasks in a single batch trigger call.
## Schedules
| Limit | Details |
| --------- | ------------- |
| Schedules | 5 per project |
When attaching schedules to tasks we strongly recommend you add them in our dashboard if they're "static". That way you can control them easily per environment.
If you add them dynamically using code make sure you add a `deduplicationKey` so you don't add the same schedule to a task multiple times. If you don't your task will get triggered multiple times, it will cost you more, and you will hit the limit.
If you're creating schedules for your user you will definitely need to request more schedules from us.
<Snippet file="v3/soft-limit.mdx" />
@@ -0,0 +1,4 @@
---
title: "Get timezones"
openapi: "v3-openapi GET /api/v1/timezones"
---
+42 -11
View File
@@ -29,6 +29,16 @@ export const firstScheduledTask = schedules.task({
//this can be undefined if it's never been run //this can be undefined if it's never been run
console.log(payload.lastTimestamp); //is a Date object or undefined console.log(payload.lastTimestamp); //is a Date object or undefined
//the timezone the schedule was registered with, defaults to "UTC"
//this is in IANA format, e.g. "America/New_York"
//See the full list here: https://cloud.trigger.dev/timezones
console.log(payload.timezone); //is a string
//If you want to output the time in the user's timezone do this:
const formatted = payload.timestamp.toLocaleString("en-US", {
timeZone: payload.timezone,
});
//the schedule id (you can have many schedules for the same task) //the schedule id (you can have many schedules for the same task)
//using this you can remove the schedule, update it, etc //using this you can remove the schedule, update it, etc
console.log(payload.scheduleId); //is a string console.log(payload.scheduleId); //is a string
@@ -46,8 +56,9 @@ export const firstScheduledTask = schedules.task({
You can see from the comments that the payload has several useful properties: You can see from the comments that the payload has several useful properties:
- `timestamp` - the time the task was scheduled to run - `timestamp` - the time the task was scheduled to run, as a UTC date.
- `lastTimestamp` - the time the task was last run - `lastTimestamp` - the time the task was last run, as a UTC date.
- `timezone` - the timezone the schedule was registered with, defaults to "UTC". In IANA format, e.g. "America/New_York".
- `scheduleId` - the id of the schedule that triggered the task - `scheduleId` - the id of the schedule that triggered the task
- `externalId` - the external id you (optionally) provided when creating the schedule - `externalId` - the external id you (optionally) provided when creating the schedule
- `upcoming` - the next 5 times the task is scheduled to run - `upcoming` - the next 5 times the task is scheduled to run
@@ -103,6 +114,7 @@ These are the options when creating a schedule:
| ----------------- | --------------------------------------------------------------------------------------------- | | ----------------- | --------------------------------------------------------------------------------------------- |
| Task | The id of the task you want to attach to. | | Task | The id of the task you want to attach to. |
| CRON pattern | The schedule in CRON format. | | CRON pattern | The schedule in CRON format. |
| Timezone | The timezone the schedule will run in. Defaults to "UTC" |
| External id | An optional external id, usually you'd use a userId. | | 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. | | Deduplication key | An optional deduplication key. If you pass the same value, it will update rather than create. |
| Environments | The environments this schedule will run in. | | Environments | The environments this schedule will run in. |
@@ -121,6 +133,8 @@ const createdSchedule = await schedules.create({
task: firstScheduledTask.id, task: firstScheduledTask.id,
//The schedule in CRON format. //The schedule in CRON format.
cron: "0 0 * * *", cron: "0 0 * * *",
//this is required, it prevents you from creating duplicate schedules. It will update the schedule if it already exists.
deduplicationKey: "my-deduplication-key",
}); });
``` ```
@@ -136,17 +150,21 @@ const createdSchedule = await schedules.create({
task: firstScheduledTask.id, task: firstScheduledTask.id,
//The schedule in CRON format. //The schedule in CRON format.
cron: "0 0 * * *", cron: "0 0 * * *",
// Optional, it defaults to "UTC". In IANA format, e.g. "America/New_York".
// In this case, the task will run at midnight every day in New York time.
// If you specify a timezone it will automatically work with daylight saving time.
timezone: "America/New_York",
//Optionally, you can specify your own IDs (like a user ID) and then use it inside the run function of your task. //Optionally, you can specify your own IDs (like a user ID) and then use it inside the run function of your task.
//This allows you to have per-user CRON tasks. //This allows you to have per-user CRON tasks.
externalId: "user_123456", externalId: "user_123456",
//(Optional) You can only create one schedule with this key. //You can only create one schedule with this key.
//If you use it twice, the second call will update the schedule. //If you use it twice, the second call will update the schedule.
//This is useful if you don't want to create duplicate schedules for a user. //This is useful because you don't want to create duplicate schedules for a user.
deduplicationKey: "user_123456-todo_reminder", deduplicationKey: "user_123456-todo_reminder",
}); });
``` ```
See [the SDK reference](/v3/management-create-schedule) for full details. See [the SDK reference](/v3/management/schedules/create) for full details.
### Dynamic schedules (or multi-tenant schedules) ### Dynamic schedules (or multi-tenant schedules)
@@ -189,6 +207,8 @@ export async function POST(request: Request) {
task: reminderTask.id, task: reminderTask.id,
//8am every day //8am every day
cron: "0 8 * * *", cron: "0 8 * * *",
//the user's timezone
timezone: data.timezone,
//the user id //the user id
externalId: data.userId, externalId: data.userId,
//this makes it impossible to have two reminder schedules for the same user //this makes it impossible to have two reminder schedules for the same user
@@ -228,7 +248,7 @@ You can test a scheduled task in the dashboard. Note that the `scheduleId` will
const retrievedSchedule = await schedules.retrieve(scheduleId); const retrievedSchedule = await schedules.retrieve(scheduleId);
``` ```
See [the SDK reference](/v3/management-retrieve-schedule) for full details. See [the SDK reference](/v3/management/schedules/retrieve) for full details.
### Listing schedules ### Listing schedules
@@ -236,7 +256,7 @@ See [the SDK reference](/v3/management-retrieve-schedule) for full details.
const allSchedules = await schedules.list(); const allSchedules = await schedules.list();
``` ```
See [the SDK reference](/v3/management-list-schedules) for full details. See [the SDK reference](/v3/management/schedules/list) for full details.
### Updating a schedule ### Updating a schedule
@@ -245,10 +265,11 @@ const updatedSchedule = await schedules.update(scheduleId, {
task: firstScheduledTask.id, task: firstScheduledTask.id,
cron: "0 0 1 * *", cron: "0 0 1 * *",
externalId: "ext_1234444", externalId: "ext_1234444",
deduplicationKey: "my-deduplication-key",
}); });
``` ```
See [the SDK reference](/v3/management-update-schedule) for full details. See [the SDK reference](/v3/management/schedules/update) for full details.
### Deactivating a schedule ### Deactivating a schedule
@@ -256,7 +277,7 @@ See [the SDK reference](/v3/management-update-schedule) for full details.
const deactivatedSchedule = await schedules.deactivate(scheduleId); const deactivatedSchedule = await schedules.deactivate(scheduleId);
``` ```
See [the SDK reference](/v3/management-deactivate-schedule) for full details. See [the SDK reference](/v3/management/schedules/deactivate) for full details.
### Activating a schedule ### Activating a schedule
@@ -264,7 +285,7 @@ See [the SDK reference](/v3/management-deactivate-schedule) for full details.
const activatedSchedule = await schedules.activate(scheduleId); const activatedSchedule = await schedules.activate(scheduleId);
``` ```
See [the SDK reference](/v3/management-activate-schedule) for full details. See [the SDK reference](/v3/management/schedules/activate) for full details.
### Deleting a schedule ### Deleting a schedule
@@ -272,4 +293,14 @@ See [the SDK reference](/v3/management-activate-schedule) for full details.
const deletedSchedule = await schedules.del(scheduleId); const deletedSchedule = await schedules.del(scheduleId);
``` ```
See [the SDK reference](/v3/management-delete-schedule) for full details. See [the SDK reference](/v3/management/schedules/delete) for full details.
### Getting possible timezones
You might want to show a dropdown menu in your UI so your users can select their timezone. You can get a list of all possible timezones using the SDK:
```ts
const timezones = await schedules.timezones();
```
See [the SDK reference](/v3/management/schedules/timezones) for full details.
+1 -1
View File
@@ -1,7 +1,7 @@
export * from "./apiClient"; export * from "./apiClient";
export * from "./apiClient/types"; export * from "./apiClient/types";
export * from "./apiClient/pagination"; export * from "./apiClient/pagination";
export type { ApiPromise } from "./apiClient/core"; export type { ApiPromise, OffsetLimitPagePromise, CursorPagePromise } from "./apiClient/core";
export * from "./apiClient/errors"; export * from "./apiClient/errors";
export * from "./clock-api"; export * from "./clock-api";
export * from "./errors"; export * from "./errors";
+35 -6
View File
@@ -222,15 +222,26 @@ export const ScheduledTaskPayload = z.object({
You can use this to remove the schedule, update it, etc */ You can use this to remove the schedule, update it, etc */
scheduleId: z.string(), scheduleId: z.string(),
/** When the task was scheduled to run. /** When the task was scheduled to run.
* Note this will be slightly different from `new Date()` because it takes a few ms to run the task. */ * Note this will be slightly different from `new Date()` because it takes a few ms to run the task.
*
* This date is UTC. To output it as a string with a timezone you would do this:
* ```ts
* const formatted = payload.timestamp.toLocaleString("en-US", {
timeZone: payload.timezone,
});
``` */
timestamp: z.date(), timestamp: z.date(),
/** When the task was last run (it has been). /** When the task was last run (it has been).
This can be undefined if it's never been run */ This can be undefined if it's never been run. This date is UTC. */
lastTimestamp: z.date().optional(), lastTimestamp: z.date().optional(),
/** You can optionally provide an external id when creating the schedule. /** You can optionally provide an external id when creating the schedule.
Usually you would use a userId or some other unique identifier. Usually you would use a userId or some other unique identifier.
This defaults to undefined if you didn't provide one. */ This defaults to undefined if you didn't provide one. */
externalId: z.string().optional(), externalId: z.string().optional(),
/** The IANA timezone the schedule is set to. The default is UTC.
* You can see the full list of supported timezones here: https://cloud.trigger.dev/timezones
*/
timezone: z.string(),
/** The next 5 dates this task is scheduled to run */ /** The next 5 dates this task is scheduled to run */
upcoming: z.array(z.date()), upcoming: z.array(z.date()),
}); });
@@ -257,20 +268,31 @@ export const CreateScheduleOptions = z.object({
*/ */
cron: z.string(), cron: z.string(),
/** (Optional) You can only create one schedule with this key. If you use it twice, the second call will update the schedule. /** You can only create one schedule with this key. If you use it twice, the second call will update the schedule.
* *
* This is useful if you don't want to create duplicate schedules for a user. */ * This is required to prevent you from creating duplicate schedules. */
deduplicationKey: z.string().optional(), deduplicationKey: z.string(),
/** Optionally, you can specify your own IDs (like a user ID) and then use it inside the run function of your task. /** Optionally, you can specify your own IDs (like a user ID) and then use it inside the run function of your task.
* *
* This allows you to have per-user CRON tasks. * This allows you to have per-user CRON tasks.
*/ */
externalId: z.string().optional(), externalId: z.string().optional(),
/** Optionally, you can specify a timezone in the IANA format. If unset it will use UTC.
* If specified then the CRON will be evaluated in that timezone and will respect daylight savings.
*
* If you set the CRON to `0 0 * * *` and the timezone to `America/New_York` then the task will run at midnight in New York time, no matter whether it's daylight savings or not.
*
* You can see the full list of supported timezones here: https://cloud.trigger.dev/timezones
*
* @example "America/New_York", "Europe/London", "Asia/Tokyo", "Africa/Cairo"
*
*/
timezone: z.string().optional(),
}); });
export type CreateScheduleOptions = z.infer<typeof CreateScheduleOptions>; export type CreateScheduleOptions = z.infer<typeof CreateScheduleOptions>;
export const UpdateScheduleOptions = CreateScheduleOptions; export const UpdateScheduleOptions = CreateScheduleOptions.omit({ deduplicationKey: true });
export type UpdateScheduleOptions = z.infer<typeof UpdateScheduleOptions>; export type UpdateScheduleOptions = z.infer<typeof UpdateScheduleOptions>;
@@ -289,6 +311,7 @@ export const ScheduleObject = z.object({
deduplicationKey: z.string().nullish(), deduplicationKey: z.string().nullish(),
externalId: z.string().nullish(), externalId: z.string().nullish(),
generator: ScheduleGenerator, generator: ScheduleGenerator,
timezone: z.string(),
nextRun: z.coerce.date().nullish(), nextRun: z.coerce.date().nullish(),
environments: z.array( environments: z.array(
z.object({ z.object({
@@ -325,6 +348,12 @@ export const ListScheduleOptions = z.object({
export type ListScheduleOptions = z.infer<typeof ListScheduleOptions>; export type ListScheduleOptions = z.infer<typeof ListScheduleOptions>;
export const TimezonesResult = z.object({
timezones: z.array(z.string()),
});
export type TimezonesResult = z.infer<typeof TimezonesResult>;
export const RunStatus = z.enum([ export const RunStatus = z.enum([
/// Task hasn't been deployed yet but is waiting to be executed /// Task hasn't been deployed yet but is waiting to be executed
"WAITING_FOR_DEPLOY", "WAITING_FOR_DEPLOY",
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Organization" ADD COLUMN "maximumScheduleInstancesLimit" INTEGER NOT NULL DEFAULT 20;
@@ -0,0 +1,9 @@
/*
Warnings:
- You are about to drop the column `maximumScheduleInstancesLimit` on the `Organization` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Organization" DROP COLUMN "maximumScheduleInstancesLimit",
ADD COLUMN "maximumSchedulesLimit" INTEGER NOT NULL DEFAULT 5;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "TaskSchedule" ADD COLUMN "timezone" TEXT;
@@ -0,0 +1,8 @@
/*
Warnings:
- You are about to drop the column `timezone` on the `TaskSchedule` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "TaskSchedule" DROP COLUMN "timezone";
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "TaskSchedule" ADD COLUMN "timezone" TEXT NOT NULL DEFAULT 'UTC';
+6 -1
View File
@@ -111,6 +111,7 @@ model Organization {
maximumExecutionTimePerRunInMs Int @default(900000) // 15 minutes maximumExecutionTimePerRunInMs Int @default(900000) // 15 minutes
maximumConcurrencyLimit Int @default(10) maximumConcurrencyLimit Int @default(10)
maximumSchedulesLimit Int @default(5)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@ -2208,8 +2209,12 @@ model TaskSchedule {
generatorExpression String generatorExpression String
generatorDescription String @default("") generatorDescription String @default("")
generatorType ScheduleGeneratorType @default(CRON) generatorType ScheduleGeneratorType @default(CRON)
/// These are IANA format string, or the default "UTC". E.g. "America/New_York"
timezone String @default("UTC")
///Can be provided by the user then accessed inside a run ///Can be provided by the user then accessed inside a run
externalId String? externalId String?
///Instances of the schedule that are active ///Instances of the schedule that are active
instances TaskScheduleInstance[] instances TaskScheduleInstance[]
+1 -1
View File
@@ -5,10 +5,10 @@ import {
ReplayRunResponse, ReplayRunResponse,
RetrieveRunResponse, RetrieveRunResponse,
apiClientManager, apiClientManager,
CursorPagePromise,
} from "@trigger.dev/core/v3"; } from "@trigger.dev/core/v3";
import type { ListProjectRunsQueryParams, ListRunsQueryParams } from "@trigger.dev/core/v3"; import type { ListProjectRunsQueryParams, ListRunsQueryParams } from "@trigger.dev/core/v3";
import { apiClientMissingError } from "./shared"; import { apiClientMissingError } from "./shared";
import { CursorPagePromise } from "@trigger.dev/core/v3/apiClient/core";
export type RetrieveRunResult = RetrieveRunResponse; export type RetrieveRunResult = RetrieveRunResponse;
+28 -1
View File
@@ -2,13 +2,15 @@ import {
ApiPromise, ApiPromise,
DeletedScheduleObject, DeletedScheduleObject,
InitOutput, InitOutput,
OffsetLimitPagePromise,
ScheduleObject, ScheduleObject,
TimezonesResult,
apiClientManager, apiClientManager,
taskCatalog, taskCatalog,
} from "@trigger.dev/core/v3"; } from "@trigger.dev/core/v3";
import { zodfetch } from "@trigger.dev/core/v3/zodfetch";
import { Task, TaskOptions, apiClientMissingError, createTask } from "../shared"; import { Task, TaskOptions, apiClientMissingError, createTask } from "../shared";
import * as SchedulesAPI from "./api"; import * as SchedulesAPI from "./api";
import { OffsetLimitPagePromise } from "@trigger.dev/core/v3/apiClient/core";
export function task<TOutput, TInitOutput extends InitOutput>( export function task<TOutput, TInitOutput extends InitOutput>(
params: TaskOptions<SchedulesAPI.ScheduledTaskPayload, TOutput, TInitOutput> params: TaskOptions<SchedulesAPI.ScheduledTaskPayload, TOutput, TInitOutput>
@@ -27,6 +29,7 @@ export function task<TOutput, TInitOutput extends InitOutput>(
* @param options * @param options
* @param options.task - The identifier of the task to be scheduled (Must already exist and be a scheduled task) * @param options.task - The identifier of the task to be scheduled (Must already exist and be a scheduled task)
* @param options.cron - The cron expression for the schedule (e.g. `0 0 * * *`) * @param options.cron - The cron expression for the schedule (e.g. `0 0 * * *`)
* @param options.timezone - An optional timezone for the schedule in the IANA format (e.g. `America/Los_Angeles`). Defaults to "UTC".
* @param options.externalId - An optional external identifier for the schedule * @param options.externalId - An optional external identifier for the schedule
* @param options.deduplicationKey - An optional deduplication key for the schedule * @param options.deduplicationKey - An optional deduplication key for the schedule
* @returns The created schedule * @returns The created schedule
@@ -62,6 +65,7 @@ export function retrieve(scheduleId: string): ApiPromise<ScheduleObject> {
* @param options - The updated schedule options * @param options - The updated schedule options
* @param options.task - The identifier of the task to be scheduled (Must already exist and be a scheduled task) * @param options.task - The identifier of the task to be scheduled (Must already exist and be a scheduled task)
* @param options.cron - The cron expression for the schedule (e.g. `0 0 * * *`) * @param options.cron - The cron expression for the schedule (e.g. `0 0 * * *`)
* @param options.timezone - An optional timezone for the schedule in the IANA format (e.g. `America/Los_Angeles`). Defaults to "UTC".
* @param options.externalId - An optional external identifier for the schedule * @param options.externalId - An optional external identifier for the schedule
* @returns The updated schedule * @returns The updated schedule
*/ */
@@ -138,3 +142,26 @@ export function list(
return apiClient.listSchedules(options); return apiClient.listSchedules(options);
} }
/**
* Lists the possible timezones we support
* @param excludeUtc - By default "UTC" is included and is first. If true, "UTC" will be excluded.
*/
export function timezones(options?: { excludeUtc?: boolean }) {
const baseUrl = apiClientManager.baseURL;
if (!baseUrl) {
throw apiClientMissingError();
}
return zodfetch(
TimezonesResult,
`${baseUrl}/api/v1/timezones${options?.excludeUtc === true ? "?excludeUtc=true" : ""}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
}
+2 -2
View File
@@ -273,8 +273,8 @@ async function doTriggerUnfriendlyTaskId() {
} }
// doRuns().catch(console.error); // doRuns().catch(console.error);
// doListRuns().catch(console.error); doListRuns().catch(console.error);
// doScheduleLists().catch(console.error); // doScheduleLists().catch(console.error);
// doSchedules().catch(console.error); // doSchedules().catch(console.error);
// doEnvVars().catch(console.error); // doEnvVars().catch(console.error);
doTriggerUnfriendlyTaskId().catch(console.error); // doTriggerUnfriendlyTaskId().catch(console.error);
+45 -1
View File
@@ -1,4 +1,4 @@
import { logger, schedules } from "@trigger.dev/sdk/v3"; import { logger, schedules, task } from "@trigger.dev/sdk/v3";
export const firstScheduledTask = schedules.task({ export const firstScheduledTask = schedules.task({
id: "first-scheduled-task", id: "first-scheduled-task",
@@ -6,6 +6,50 @@ export const firstScheduledTask = schedules.task({
const distanceInMs = const distanceInMs =
payload.timestamp.getTime() - (payload.lastTimestamp ?? new Date()).getTime(); payload.timestamp.getTime() - (payload.lastTimestamp ?? new Date()).getTime();
logger.log(payload.timezone);
logger.log("First scheduled tasks", { payload, distanceInMs }); logger.log("First scheduled tasks", { payload, distanceInMs });
const formatted = payload.timestamp.toLocaleString("en-US", {
timeZone: payload.timezone,
});
logger.log(formatted);
},
});
export const manageSchedules = task({
id: "manage-schedules",
run: async (payload) => {
const createdSchedule = await schedules.create({
//The id of the scheduled task you want to attach to.
task: firstScheduledTask.id,
//The schedule in CRON format.
cron: "* * * * *",
deduplicationKey: `create-schedule-1718277290717`,
timezone: "Asia/Tokyo",
});
logger.log("Created schedule", createdSchedule);
const editedSchedule = await schedules.update(createdSchedule.id, {
//The id of the scheduled task you want to attach to.
task: firstScheduledTask.id,
//The schedule in CRON format.
cron: "* * * * *",
timezone: "Europe/Athens",
});
logger.log("Edited schedule", editedSchedule);
const sched = await schedules.retrieve(createdSchedule.id);
logger.log("Retrieved schedule", sched);
const allSchedules = await schedules.list();
logger.log("All schedules", { allSchedules });
const { timezones } = await schedules.timezones();
logger.log("Timezones", { timezones });
const withoutUtc = await schedules.timezones({ excludeUtc: true });
logger.log("Timezones without UTC", { withoutUtc });
}, },
}); });