Declarative schedules (#1226)

* Added type (STATIC or DYNAMIC) to TaskSchedule. Defaults to dynamic

* WIP with dev indexing of static schedules

* Added a code comment

* First stab at deleting unused static schedules

* Dashboard changes for the static schedules

* Generate the description. Upsert the instances when editing. Fix for the friendlyId

* Don’t allow deleting of static schedules

* Don’t allow enabling/disabling of static schedules

* Added filtering for schedule types

* Syncing of schedule for deployed tasks

* Static schedules are now created for each environment

* Added a second static schedule for testing

* Add the type to the schedule task run payload and the object you get back from the SDK

* Changed static/dynamic to declarative/imperative

* Timezone example

* Changeset

* Updated scheduled docs to include declarative

* When you test a schedule it set the type to “IMPERATIVE”

* Improved the tooltip

* Fix for queue time continuing to rise when a run is canceled/expired etc

* Update the info panel on a selected declarative schedule

* Check if there are no instances. This should never happen but log an error if it does

* Throw errors and push them through to the CLI dev command

* Fail deployments if creating the background tasks or schedules fails

* Format the deployment error so it gets displayed

* Changed the maxed out schedules error message to remove bit about support
This commit is contained in:
Matt Aitken
2024-07-18 20:24:54 +01:00
committed by GitHub
parent 7056ce53f2
commit 8ba9987944
30 changed files with 790 additions and 189 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"@trigger.dev/sdk": patch
"@trigger.dev/core": patch
---
Added declarative cron schedules
@@ -1,6 +1,6 @@
import { XMarkIcon } from "@heroicons/react/20/solid";
import { useNavigate } from "@remix-run/react";
import { RuntimeEnvironment } from "@trigger.dev/database";
import { type RuntimeEnvironment } from "@trigger.dev/database";
import { useCallback } from "react";
import { z } from "zod";
import { Input } from "~/components/primitives/Input";
@@ -17,6 +17,7 @@ import {
SelectTrigger,
SelectValue,
} from "../../primitives/SimpleSelect";
import { ScheduleTypeCombo } from "./ScheduleType";
export const ScheduleListFilters = z.object({
page: z.coerce.number().default(1),
@@ -28,6 +29,7 @@ export const ScheduleListFilters = z.object({
.string()
.optional()
.transform((value) => (value ? value.split(",") : undefined)),
type: z.union([z.literal("declarative"), z.literal("imperative")]).optional(),
search: z.string().optional(),
});
@@ -48,7 +50,7 @@ export function ScheduleFilters({ possibleEnvironments, possibleTasks }: Schedul
const navigate = useNavigate();
const location = useOptimisticLocation();
const searchParams = new URLSearchParams(location.search);
const { environments, tasks, page, search } = ScheduleListFilters.parse(
const { environments, tasks, page, search, type } = ScheduleListFilters.parse(
Object.fromEntries(searchParams.entries())
);
@@ -73,6 +75,10 @@ export function ScheduleFilters({ possibleEnvironments, possibleTasks }: Schedul
handleFilterChange("environments", value === "ALL" ? undefined : value);
}, []);
const handleTypeChange = useCallback((value: string | typeof All) => {
handleFilterChange("type", value === "ALL" ? undefined : value);
}, []);
const handleSearchChange = useThrottle((value: string) => {
handleFilterChange("search", value.length === 0 ? undefined : value);
}, 300);
@@ -97,6 +103,30 @@ export function ScheduleFilters({ possibleEnvironments, possibleTasks }: Schedul
defaultValue={search}
onChange={(e) => handleSearchChange(e.target.value)}
/>
<SelectGroup>
<Select name="type" value={type ?? "ALL"} onValueChange={handleTypeChange}>
<SelectTrigger size="minimal" width="full">
<SelectValue placeholder={"Select type"} className="ml-2 whitespace-nowrap p-0" />
</SelectTrigger>
<SelectContent>
<SelectItem value={"ALL"}>
<Paragraph
variant="extra-small"
className="whitespace-nowrap pl-0.5 transition group-hover:text-text-bright"
>
All types
</Paragraph>
</SelectItem>
<SelectItem value={"declarative"}>
<ScheduleTypeCombo type="DECLARATIVE" className="text-xs text-text-dimmed" />
</SelectItem>
<SelectItem value={"imperative"}>
<ScheduleTypeCombo type="IMPERATIVE" className="text-xs text-text-dimmed" />
</SelectItem>
</SelectContent>
</Select>
</SelectGroup>
<SelectGroup>
<Select
name="environment"
@@ -0,0 +1,29 @@
import { ArchiveBoxIcon, ArrowsRightLeftIcon } from "@heroicons/react/20/solid";
import type { ScheduleType } from "@trigger.dev/database";
import { cn } from "~/utils/cn";
export function ScheduleTypeCombo({ type, className }: { type: ScheduleType; className?: string }) {
return (
<div className={cn("flex items-center space-x-1", className)}>
<ScheduleTypeIcon type={type} />
<span>{scheduleTypeName(type)}</span>
</div>
);
}
export function ScheduleTypeIcon({ type, className }: { type: ScheduleType; className?: string }) {
switch (type) {
case "IMPERATIVE":
return <ArrowsRightLeftIcon className={cn("size-4", className)} />;
case "DECLARATIVE":
return <ArchiveBoxIcon className={cn("size-4", className)} />;
}
}
export function scheduleTypeName(type: ScheduleType) {
switch (type) {
case "IMPERATIVE":
return "Imperative";
case "DECLARATIVE":
return "Declarative";
}
}
@@ -230,8 +230,12 @@ export function TaskRunsTable({
formatDuration(new Date(run.createdAt), new Date(run.startedAt), {
style: "short",
})
) : (
) : run.isCancellable ? (
<LiveTimer startTime={new Date(run.createdAt)} />
) : (
formatDuration(new Date(run.createdAt), new Date(run.updatedAt), {
style: "short",
})
)}
</div>
</TableCell>
@@ -88,6 +88,7 @@ export class EditSchedulePresenter {
const schedule = await this.#prismaClient.taskSchedule.findFirst({
select: {
id: true,
type: true,
friendlyId: true,
generatorExpression: true,
externalId: true,
@@ -1,4 +1,4 @@
import { Prisma, RuntimeEnvironmentType } from "@trigger.dev/database";
import { Prisma, RuntimeEnvironmentType, ScheduleType } from "@trigger.dev/database";
import { ScheduleListFilters } from "~/components/runs/v3/ScheduleFilters";
import { sqlDatabaseSchema } from "~/db.server";
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
@@ -16,6 +16,7 @@ const DEFAULT_PAGE_SIZE = 20;
export type ScheduleListItem = {
id: string;
type: ScheduleType;
friendlyId: string;
taskIdentifier: string;
deduplicationKey: string | null;
@@ -44,10 +45,17 @@ export class ScheduleListPresenter extends BasePresenter {
environments,
search,
page,
type,
pageSize = DEFAULT_PAGE_SIZE,
}: ScheduleListOptions) {
const hasFilters =
tasks !== undefined || environments !== undefined || (search !== undefined && search !== "");
type !== undefined ||
tasks !== undefined ||
environments !== undefined ||
(search !== undefined && search !== "");
const filterType =
type === "declarative" ? "DECLARATIVE" : type === "imperative" ? "IMPERATIVE" : undefined;
// Find the project scoped to the organization
const project = await this._replica.project.findFirstOrThrow({
@@ -105,6 +113,7 @@ export class ScheduleListPresenter extends BasePresenter {
environmentId: environments ? { in: environments } : undefined,
},
},
type: filterType,
AND: search
? {
OR: [
@@ -141,6 +150,7 @@ export class ScheduleListPresenter extends BasePresenter {
const rawSchedules = await this._replica.taskSchedule.findMany({
select: {
id: true,
type: true,
friendlyId: true,
taskIdentifier: true,
deduplicationKey: true,
@@ -166,6 +176,7 @@ export class ScheduleListPresenter extends BasePresenter {
},
}
: undefined,
type: filterType,
AND: search
? {
OR: [
@@ -215,11 +226,12 @@ export class ScheduleListPresenter extends BasePresenter {
ON t."scheduleId" = r."scheduleId" AND t."createdAt" = r."LatestRun";`
: [];
const schedules = rawSchedules.map((schedule) => {
const schedules: ScheduleListItem[] = rawSchedules.map((schedule) => {
const latestRun = latestRuns.find((r) => r.scheduleId === schedule.id);
return {
id: schedule.id,
type: schedule.type,
friendlyId: schedule.friendlyId,
taskIdentifier: schedule.taskIdentifier,
deduplicationKey: schedule.deduplicationKey,
@@ -21,6 +21,7 @@ export class ViewSchedulePresenter {
const schedule = await this.#prismaClient.taskSchedule.findFirst({
select: {
id: true,
type: true,
friendlyId: true,
generatorExpression: true,
generatorDescription: true,
@@ -99,6 +100,7 @@ export class ViewSchedulePresenter {
public toJSONResponse(result: NonNullable<Awaited<ReturnType<ViewSchedulePresenter["call"]>>>) {
const response: ScheduleObject = {
id: result.schedule.friendlyId,
type: result.schedule.type,
task: result.schedule.taskIdentifier,
active: result.schedule.active,
nextRun: result.schedule.nextRuns[0],
@@ -1,15 +1,21 @@
import { parse } from "@conform-to/zod";
import { BoltIcon, BoltSlashIcon, PencilSquareIcon, TrashIcon } from "@heroicons/react/20/solid";
import {
BoltIcon,
BoltSlashIcon,
BookOpenIcon,
PencilSquareIcon,
TrashIcon,
} from "@heroicons/react/20/solid";
import { DialogDescription } from "@radix-ui/react-dialog";
import { Form, useLocation } from "@remix-run/react";
import { ActionFunctionArgs, LoaderFunctionArgs, json } from "@remix-run/server-runtime";
import { token } from "morgan";
import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { z } from "zod";
import { ExitIcon } from "~/assets/icons/ExitIcon";
import { InlineCode } from "~/components/code/InlineCode";
import { EnvironmentLabel, EnvironmentLabels } from "~/components/environments/EnvironmentLabel";
import { EnvironmentLabels } from "~/components/environments/EnvironmentLabel";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { Callout, variantClasses } from "~/components/primitives/Callout";
import { DateTime } from "~/components/primitives/DateTime";
import {
Dialog,
@@ -19,6 +25,7 @@ import {
DialogTrigger,
} from "~/components/primitives/Dialog";
import { Header2, Header3 } from "~/components/primitives/Headers";
import { InfoPanel } from "~/components/primitives/InfoPanel";
import { Paragraph } from "~/components/primitives/Paragraph";
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
import {
@@ -31,11 +38,11 @@ import {
TableRow,
} from "~/components/primitives/Table";
import { EnabledStatus } from "~/components/runs/v3/EnabledStatus";
import { ScheduleTypeCombo } from "~/components/runs/v3/ScheduleType";
import { TaskRunsTable } from "~/components/runs/v3/TaskRunsTable";
import { prisma } from "~/db.server";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useUser } from "~/hooks/useUser";
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
import { findProjectBySlug } from "~/models/project.server";
import { ViewSchedulePresenter } from "~/presenters/v3/ViewSchedulePresenter.server";
@@ -197,8 +204,15 @@ export default function Page() {
const isUtc = schedule.timezone === "UTC";
const isImperative = schedule.type === "IMPERATIVE";
return (
<div className="grid h-full max-h-full grid-rows-[2.5rem_1fr_3.25rem] overflow-hidden bg-background-bright">
<div
className={cn(
"grid h-full max-h-full overflow-hidden bg-background-bright",
isImperative ? "grid-rows-[2.5rem_1fr_3.25rem]" : "grid-rows-[2.5rem_1fr]"
)}
>
<div className="mx-3 flex items-center justify-between gap-2 border-b border-grid-dimmed">
<Header2 className={cn("whitespace-nowrap")}>{schedule.friendlyId}</Header2>
<LinkButton
@@ -214,6 +228,9 @@ export default function Page() {
<PropertyTable>
<Property label="Schedule ID">{schedule.friendlyId}</Property>
<Property label="Task ID">{schedule.taskIdentifier}</Property>
<Property label="Type">
<ScheduleTypeCombo type={schedule.type} className="text-sm" />
</Property>
<Property label="CRON (UTC)" labelClassName="self-start">
<div className="space-y-2">
<InlineCode variant="extra-small">{schedule.cron}</InlineCode>
@@ -224,15 +241,19 @@ export default function Page() {
<Property label="Environments">
<EnvironmentLabels size="small" environments={schedule.environments} />
</Property>
<Property label="External ID">
{schedule.externalId ? schedule.externalId : ""}
</Property>
<Property label="Deduplication key">
{schedule.userProvidedDeduplicationKey ? schedule.deduplicationKey : ""}
</Property>
<Property label="Status">
<EnabledStatus enabled={schedule.active} />
</Property>
{isImperative && (
<>
<Property label="External ID">
{schedule.externalId ? schedule.externalId : ""}
</Property>
<Property label="Deduplication key">
{schedule.userProvidedDeduplicationKey ? schedule.deduplicationKey : ""}
</Property>
<Property label="Status">
<EnabledStatus enabled={schedule.active} />
</Property>
</>
)}
</PropertyTable>
<div className="flex flex-col gap-1">
<Header3>Last 5 runs</Header3>
@@ -288,68 +309,84 @@ export default function Page() {
</TableBody>
</Table>
</div>
{!isImperative && (
<InfoPanel
title="Editing declarative schedules"
icon={BookOpenIcon}
iconClassName="text-indigo-500"
variant="info"
buttonLabel="Docs"
to="https://trigger.dev/docs/v3/tasks-scheduled"
panelClassName="max-w-full"
>
You can only edit a declarative schedule by updating your schedules.task and then
running the CLI dev and deploy commands.
</InfoPanel>
)}
</div>
</div>
</div>
<div className="flex items-center justify-between gap-2 border-t border-grid-dimmed px-2">
<div className="flex items-center gap-4">
<Form method="post">
<Button
type="submit"
variant="minimal/medium"
LeadingIcon={schedule.active ? BoltSlashIcon : BoltIcon}
leadingIconClassName={schedule.active ? "text-dimmed" : "text-success"}
name="action"
value={schedule.active ? "disable" : "enable"}
>
{schedule.active ? "Disable" : "Enable"}
</Button>
</Form>
<Dialog>
<DialogTrigger asChild>
{isImperative && (
<div className="flex items-center justify-between gap-2 border-t border-grid-dimmed px-2">
<div className="flex items-center gap-4">
<Form method="post">
<Button
type="submit"
variant="minimal/medium"
LeadingIcon={TrashIcon}
leadingIconClassName="text-error"
className="text-error"
LeadingIcon={schedule.active ? BoltSlashIcon : BoltIcon}
leadingIconClassName={schedule.active ? "text-dimmed" : "text-success"}
name="action"
value="delete"
value={schedule.active ? "disable" : "enable"}
>
Delete
{schedule.active ? "Disable" : "Enable"}
</Button>
</DialogTrigger>
<DialogContent className="max-w-md">
<DialogHeader>Delete schedule</DialogHeader>
<DialogDescription>
Are you sure you want to delete this schedule? This can't be reversed.
</DialogDescription>
<DialogFooter>
<Form method="post">
<Button
type="submit"
variant="danger/small"
LeadingIcon={TrashIcon}
name="action"
value="delete"
>
Delete
</Button>
</Form>
</DialogFooter>
</DialogContent>
</Dialog>
</Form>
<Dialog>
<DialogTrigger asChild>
<Button
type="submit"
variant="minimal/medium"
LeadingIcon={TrashIcon}
leadingIconClassName="text-error"
className="text-error"
name="action"
value="delete"
>
Delete
</Button>
</DialogTrigger>
<DialogContent className="max-w-md">
<DialogHeader>Delete schedule</DialogHeader>
<DialogDescription>
Are you sure you want to delete this schedule? This can't be reversed.
</DialogDescription>
<DialogFooter>
<Form method="post">
<Button
type="submit"
variant="danger/small"
LeadingIcon={TrashIcon}
name="action"
value="delete"
>
Delete
</Button>
</Form>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
<div className="flex items-center gap-4">
<LinkButton
variant="tertiary/medium"
to={`${v3EditSchedulePath(organization, project, schedule)}${location.search}`}
LeadingIcon={PencilSquareIcon}
>
Edit schedule
</LinkButton>
</div>
</div>
<div className="flex items-center gap-4">
<LinkButton
variant="tertiary/medium"
to={`${v3EditSchedulePath(organization, project, schedule)}${location.search}`}
LeadingIcon={PencilSquareIcon}
>
Edit schedule
</LinkButton>
</div>
</div>
)}
</div>
);
}
@@ -1,8 +1,8 @@
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { EditSchedulePresenter } from "~/presenters/v3/EditSchedulePresenter.server";
import { requireUserId } from "~/services/session.server";
import { ProjectParamSchema, v3ScheduleParams } from "~/utils/pathBuilder";
import { ProjectParamSchema, v3ScheduleParams, v3SchedulesPath } from "~/utils/pathBuilder";
import { humanToCronSupported } from "~/v3/humanToCron.server";
import { UpsertScheduleForm } from "../resources.orgs.$organizationSlug.projects.$projectParam.schedules.new/route";
@@ -17,6 +17,10 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
friendlyId: scheduleParam,
});
if (result.schedule?.type === "DECLARATIVE") {
throw redirect(v3SchedulesPath({ slug: organizationSlug }, { slug: projectParam }));
}
return typedjson({ ...result, showGenerateField: humanToCronSupported });
};
@@ -10,6 +10,11 @@ import { EnvironmentLabels } from "~/components/environments/EnvironmentLabel";
import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { DateTime } from "~/components/primitives/DateTime";
import {
ScheduleTypeCombo,
ScheduleTypeIcon,
scheduleTypeName,
} from "~/components/runs/v3/ScheduleType";
import {
Dialog,
DialogContent,
@@ -334,6 +339,51 @@ function SchedulesTable({
<TableRow>
<TableHeaderCell>ID</TableHeaderCell>
<TableHeaderCell>Task ID</TableHeaderCell>
<TableHeaderCell
tooltip={
<div className="flex max-w-xs flex-col gap-4 p-1">
<div>
<div className="mb-0.5 flex items-center gap-1.5 text-sm">
<div className={"flex items-center space-x-1"}>
<ScheduleTypeIcon type={"DECLARATIVE"} className="text-sky-500" />
<span className="font-medium">{scheduleTypeName("DECLARATIVE")}</span>
</div>
</div>
<Paragraph variant="small" className="!text-wrap text-text-dimmed">
Declarative schedules are defined in a{" "}
<InlineCode variant="extra-small">schedules.task</InlineCode> with the{" "}
<InlineCode variant="extra-small">cron</InlineCode>
property. They sync when you update your{" "}
<InlineCode variant="extra-small">schedules.task</InlineCode> definition and run
the CLI dev or deploy commands.
</Paragraph>
</div>
<div>
<div className="mb-0.5 flex items-center gap-1.5 text-sm">
<div className={"flex items-center space-x-1"}>
<ScheduleTypeIcon type={"IMPERATIVE"} className="text-teal-500" />
<span className="font-medium">{scheduleTypeName("IMPERATIVE")}</span>
</div>
</div>
<Paragraph variant="small" className="!text-wrap text-text-dimmed">
Imperative schedules are defined here in the dashboard or by using the SDK
functions to create or delete them. They can be created, updated, disabled, and
deleted from the dashboard or using the SDK.
</Paragraph>
</div>
<div>
<LinkButton
variant="tertiary/medium"
to="https://trigger.dev/docs/v3/tasks-scheduled"
>
View the docs
</LinkButton>
</div>
</div>
}
>
Type
</TableHeaderCell>
<TableHeaderCell>External ID</TableHeaderCell>
<TableHeaderCell>CRON</TableHeaderCell>
<TableHeaderCell hiddenLabel>CRON description</TableHeaderCell>
@@ -362,7 +412,14 @@ function SchedulesTable({
{schedule.taskIdentifier}
</TableCell>
<TableCell to={path} className={cellClass}>
{schedule.externalId ? schedule.externalId : ""}
<ScheduleTypeCombo type={schedule.type} />
</TableCell>
<TableCell to={path} className={cellClass}>
{schedule.type === "IMPERATIVE"
? schedule.externalId
? schedule.externalId
: ""
: "N/A"}
</TableCell>
<TableCell to={path} className={cellClass}>
{schedule.cron}
@@ -384,13 +441,21 @@ function SchedulesTable({
)}
</TableCell>
<TableCell to={path} className={cellClass}>
{schedule.userProvidedDeduplicationKey ? schedule.deduplicationKey : ""}
{schedule.type === "IMPERATIVE"
? schedule.userProvidedDeduplicationKey
? schedule.deduplicationKey
: ""
: "N/A"}
</TableCell>
<TableCell to={path} className={cellClass}>
<EnvironmentLabels environments={schedule.environments} size="small" />
</TableCell>
<TableCell to={path}>
<EnabledStatus enabled={schedule.active} />
{schedule.type === "IMPERATIVE" ? (
<EnabledStatus enabled={schedule.active} />
) : (
"N/A"
)}
</TableCell>
</TableRow>
);
@@ -3,7 +3,11 @@ import { CreateBackgroundWorkerRequestBody } from "@trigger.dev/core/v3";
import { z } from "zod";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { CreateBackgroundWorkerService } from "~/v3/services/createBackgroundWorker.server";
import { ServiceValidationError } from "~/v3/services/baseService.server";
import {
CreateBackgroundWorkerService,
CreateDeclarativeScheduleError,
} from "~/v3/services/createBackgroundWorker.server";
const ParamsSchema = z.object({
projectRef: z.string(),
@@ -42,14 +46,26 @@ export async function action({ request, params }: ActionFunctionArgs) {
const service = new CreateBackgroundWorkerService();
const backgroundWorker = await service.call(projectRef, authenticatedEnv, body.data);
try {
const backgroundWorker = await service.call(projectRef, authenticatedEnv, body.data);
return json(
{
id: backgroundWorker.friendlyId,
version: backgroundWorker.version,
contentHash: backgroundWorker.contentHash,
},
{ status: 200 }
);
return json(
{
id: backgroundWorker.friendlyId,
version: backgroundWorker.version,
contentHash: backgroundWorker.contentHash,
},
{ status: 200 }
);
} catch (e) {
logger.error("Failed to create background worker", { error: e });
if (e instanceof ServiceValidationError) {
return json({ error: e.message }, { status: 400 });
} else if (e instanceof CreateDeclarativeScheduleError) {
return json({ error: e.message }, { status: 400 });
}
return json({ error: "Failed to create background worker" }, { status: 500 });
}
}
@@ -88,6 +88,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
const responseObject: ScheduleObject = {
id: schedule.id,
type: schedule.type,
task: schedule.task,
active: schedule.active,
generator: {
@@ -50,6 +50,7 @@ export async function action({ request }: ActionFunctionArgs) {
const responseObject: ScheduleObject = {
id: schedule.id,
type: schedule.type,
task: schedule.task,
active: schedule.active,
generator: {
@@ -107,6 +108,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
return {
data: result.schedules.map((schedule) => ({
id: schedule.friendlyId,
type: schedule.type,
task: schedule.taskIdentifier,
generator: {
type: "CRON",
@@ -0,0 +1,93 @@
import { ZodError } from "zod";
import { CronPattern } from "../schedules";
import { BaseService, ServiceValidationError } from "./baseService.server";
import { getLimit } from "~/services/platform.v3.server";
import { getTimezones } from "~/utils/timezones.server";
import { env } from "~/env.server";
type Schedule = {
cron: string;
timezone?: string;
taskIdentifier: string;
friendlyId?: string;
};
export class CheckScheduleService extends BaseService {
public async call(projectId: string, schedule: Schedule) {
//validate the cron expression
try {
CronPattern.parse(schedule.cron);
} catch (e) {
if (e instanceof ZodError) {
throw new ServiceValidationError(`Invalid cron expression: ${e.issues[0].message}`);
}
throw new ServiceValidationError(
`Invalid cron expression: ${e instanceof Error ? e.message : JSON.stringify(e)}`
);
}
//chek it's a valid timezone
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`
);
}
}
//check the task exists
const task = await this._prisma.backgroundWorkerTask.findFirst({
where: {
slug: schedule.taskIdentifier,
projectId: projectId,
},
orderBy: {
createdAt: "desc",
},
});
if (!task) {
throw new ServiceValidationError(
`Task with identifier ${schedule.taskIdentifier} not found in project.`
);
}
if (task.triggerSource !== "SCHEDULED") {
throw new ServiceValidationError(
`Task with identifier ${schedule.taskIdentifier} is not a scheduled task.`
);
}
//if creating a schedule, check they're under the limits
if (!schedule.friendlyId) {
//check they're within their limit
const project = await this._prisma.project.findFirst({
where: {
id: projectId,
},
select: {
organizationId: true,
},
});
if (!project) {
throw new ServiceValidationError("Project not found");
}
const limit = await getLimit(project.organizationId, "schedules", 500);
const schedulesCount = await this._prisma.taskSchedule.count({
where: {
projectId,
},
});
if (schedulesCount >= limit) {
throw new ServiceValidationError(
`You have created ${schedulesCount}/${limit} schedules so you'll need to increase your limits or delete some schedules.`
);
}
}
}
}
@@ -8,6 +8,9 @@ import { generateFriendlyId } from "../friendlyIdentifiers";
import { calculateNextBuildVersion } from "../utils/calculateNextBuildVersion";
import { BaseService } from "./baseService.server";
import { projectPubSub } from "./projectPubSub.server";
import { RegisterNextTaskScheduleInstanceService } from "./registerNextTaskScheduleInstance.server";
import cronstrue from "cronstrue";
import { CheckScheduleService } from "./checkSchedule.server";
export class CreateBackgroundWorkerService extends BaseService {
public async call(
@@ -68,6 +71,12 @@ export class CreateBackgroundWorkerService extends BaseService {
});
await createBackgroundTasks(body.metadata.tasks, backgroundWorker, environment, this._prisma);
await syncDeclarativeSchedules(
body.metadata.tasks,
backgroundWorker,
environment,
this._prisma
);
try {
//send a notification that a new worker has been created
@@ -220,3 +229,155 @@ export async function createBackgroundTasks(
}
}
}
//CreateDeclarativeScheduleError with a message
export class CreateDeclarativeScheduleError extends Error {
constructor(message: string) {
super(message);
this.name = "CreateDeclarativeScheduleError";
}
}
export async function syncDeclarativeSchedules(
tasks: TaskResource[],
worker: BackgroundWorker,
environment: AuthenticatedEnvironment,
prisma: PrismaClientOrTransaction
) {
const tasksWithDeclarativeSchedules = tasks.filter((task) => task.schedule);
logger.info("Syncing declarative schedules", {
tasksWithDeclarativeSchedules,
environment,
});
const existingDeclarativeSchedules = await prisma.taskSchedule.findMany({
where: {
type: "DECLARATIVE",
projectId: environment.projectId,
},
include: {
instances: true,
},
});
const checkSchedule = new CheckScheduleService(prisma);
const registerNextService = new RegisterNextTaskScheduleInstanceService(prisma);
//start out by assuming they're all missing
const missingSchedules = new Set<string>(
existingDeclarativeSchedules.map((schedule) => schedule.id)
);
//create/update schedules (+ instances)
for (const task of tasksWithDeclarativeSchedules) {
if (task.schedule === undefined) continue;
const existingSchedule = existingDeclarativeSchedules.find(
(schedule) =>
schedule.taskIdentifier === task.id &&
schedule.instances.some((instance) => instance.environmentId === environment.id)
);
//this throws errors if the schedule is invalid
await checkSchedule.call(environment.projectId, {
cron: task.schedule.cron,
timezone: task.schedule.timezone,
taskIdentifier: task.id,
friendlyId: existingSchedule?.friendlyId,
});
if (existingSchedule) {
const schedule = await prisma.taskSchedule.update({
where: {
id: existingSchedule.id,
},
data: {
generatorExpression: task.schedule.cron,
generatorDescription: cronstrue.toString(task.schedule.cron),
timezone: task.schedule.timezone,
},
include: {
instances: true,
},
});
missingSchedules.delete(existingSchedule.id);
const instance = schedule.instances.at(0);
if (instance) {
await registerNextService.call(instance.id);
} else {
throw new CreateDeclarativeScheduleError(
`Missing instance for declarative schedule ${schedule.id}`
);
}
} else {
const newSchedule = await prisma.taskSchedule.create({
data: {
friendlyId: generateFriendlyId("sched"),
projectId: environment.projectId,
taskIdentifier: task.id,
generatorExpression: task.schedule.cron,
generatorDescription: cronstrue.toString(task.schedule.cron),
timezone: task.schedule.timezone,
type: "DECLARATIVE",
instances: {
create: [
{
environmentId: environment.id,
},
],
},
},
include: {
instances: true,
},
});
const instance = newSchedule.instances.at(0);
if (instance) {
await registerNextService.call(instance.id);
} else {
throw new CreateDeclarativeScheduleError(
`Missing instance for declarative schedule ${newSchedule.id}`
);
}
}
}
//Delete instances for this environment
//Delete schedules that have no instances left
const potentiallyDeletableSchedules = await prisma.taskSchedule.findMany({
where: {
id: {
in: Array.from(missingSchedules),
},
},
include: {
instances: true,
},
});
for (const schedule of potentiallyDeletableSchedules) {
const canDeleteSchedule =
schedule.instances.length === 0 ||
schedule.instances.every((instance) => instance.environmentId === environment.id);
if (canDeleteSchedule) {
//we can delete schedules with no instances other than ones for the current environment
await prisma.taskSchedule.delete({
where: {
id: schedule.id,
},
});
} else {
//otherwise we delete the instance (other environments remain untouched)
await prisma.taskScheduleInstance.deleteMany({
where: {
taskScheduleId: schedule.id,
environmentId: environment.id,
},
});
}
}
}
@@ -3,7 +3,7 @@ import type { BackgroundWorker } from "@trigger.dev/database";
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { generateFriendlyId } from "../friendlyIdentifiers";
import { BaseService } from "./baseService.server";
import { createBackgroundTasks } from "./createBackgroundWorker.server";
import { createBackgroundTasks, syncDeclarativeSchedules } from "./createBackgroundWorker.server";
import { CURRENT_DEPLOYMENT_LABEL } from "~/consts";
import { projectPubSub } from "./projectPubSub.server";
import { marqs } from "~/v3/marqs/index.server";
@@ -50,7 +50,39 @@ export class CreateDeployedBackgroundWorkerService extends BaseService {
},
});
await createBackgroundTasks(body.metadata.tasks, backgroundWorker, environment, this._prisma);
try {
await createBackgroundTasks(
body.metadata.tasks,
backgroundWorker,
environment,
this._prisma
);
await syncDeclarativeSchedules(
body.metadata.tasks,
backgroundWorker,
environment,
this._prisma
);
} catch (error) {
const name = error instanceof Error ? error.name : "UnknownError";
const message = error instanceof Error ? error.message : JSON.stringify(error);
await this._prisma.workerDeployment.update({
where: {
id: deployment.id,
},
data: {
status: "FAILED",
failedAt: new Date(),
errorData: {
name,
message,
},
},
});
throw error;
}
// Link the deployment with the background worker
await this._prisma.workerDeployment.update({
@@ -27,6 +27,20 @@ export class DeleteTaskScheduleService extends BaseService {
}
try {
const schedule = await this._prisma.taskSchedule.findFirst({
where: {
friendlyId,
},
});
if (!schedule) {
throw new Error("Schedule not found");
}
if (schedule.type === "DECLARATIVE") {
throw new Error("Cannot delete declarative schedules");
}
await this._prisma.taskSchedule.delete({
where: {
friendlyId,
@@ -28,6 +28,20 @@ export class SetActiveOnTaskScheduleService extends BaseService {
}
try {
const schedule = await this._prisma.taskSchedule.findFirst({
where: {
friendlyId,
},
});
if (!schedule) {
throw new Error("Schedule not found");
}
if (schedule.type === "DECLARATIVE") {
throw new Error("Cannot enable/disable declarative schedules");
}
await this._prisma.taskSchedule.update({
where: {
friendlyId,
@@ -24,6 +24,7 @@ export class TestTaskService extends BaseService {
case "SCHEDULED": {
const payload = {
scheduleId: "sched_1234",
type: "IMPERATIVE",
timestamp: data.timestamp,
lastTimestamp: data.lastTimestamp,
timezone: data.timezone,
@@ -94,6 +94,7 @@ export class TriggerScheduledTaskService extends BaseService {
const payload = {
scheduleId: instance.taskSchedule.friendlyId,
type: instance.taskSchedule.type,
timestamp: instance.nextScheduledTimestamp,
lastTimestamp: instance.lastScheduledTimestamp ?? undefined,
externalId: instance.taskSchedule.externalId ?? undefined,
@@ -1,16 +1,13 @@
import { Prisma, TaskSchedule } from "@trigger.dev/database";
import cronstrue from "cronstrue";
import { nanoid } from "nanoid";
import { ZodError } from "zod";
import { $transaction, PrismaClientOrTransaction } from "~/db.server";
import { generateFriendlyId } from "../friendlyIdentifiers";
import { CronPattern, UpsertSchedule } from "../schedules";
import { BaseService, ServiceValidationError } from "./baseService.server";
import { RegisterNextTaskScheduleInstanceService } from "./registerNextTaskScheduleInstance.server";
import cronstrue from "cronstrue";
import { UpsertSchedule } from "../schedules";
import { calculateNextScheduledTimestamp } from "../utils/calculateNextSchedule.server";
import { getTimezones } from "~/utils/timezones.server";
import { env } from "~/env.server";
import { getLimit } from "~/services/platform.v3.server";
import { BaseService, ServiceValidationError } from "./baseService.server";
import { CheckScheduleService } from "./checkSchedule.server";
import { RegisterNextTaskScheduleInstanceService } from "./registerNextTaskScheduleInstance.server";
export type UpsertTaskScheduleServiceOptions = UpsertSchedule;
@@ -30,79 +27,9 @@ type InstanceWithEnvironment = Prisma.TaskScheduleInstanceGetPayload<{
export class UpsertTaskScheduleService extends BaseService {
public async call(projectId: string, schedule: UpsertTaskScheduleServiceOptions) {
//validate the cron expression
try {
CronPattern.parse(schedule.cron);
} catch (e) {
if (e instanceof ZodError) {
throw new ServiceValidationError(`Invalid cron expression: ${e.issues[0].message}`);
}
throw new ServiceValidationError(
`Invalid cron expression: ${e instanceof Error ? e.message : JSON.stringify(e)}`
);
}
const task = await this._prisma.backgroundWorkerTask.findFirst({
where: {
slug: schedule.taskIdentifier,
projectId: projectId,
},
orderBy: {
createdAt: "desc",
},
});
if (!task) {
throw new ServiceValidationError(
`Task with identifier ${schedule.taskIdentifier} not found in project.`
);
}
if (task.triggerSource !== "SCHEDULED") {
throw new ServiceValidationError(
`Task with identifier ${schedule.taskIdentifier} is not a scheduled task.`
);
}
//if creating a schedule, check they're under the limits
if (!schedule.friendlyId) {
//check they're within their limit
const project = await this._prisma.project.findFirst({
where: {
id: projectId,
},
select: {
organizationId: true,
},
});
if (!project) {
throw new ServiceValidationError("Project not found");
}
const limit = await getLimit(project.organizationId, "schedules", 500);
const schedulesCount = await this._prisma.taskSchedule.count({
where: {
projectId,
},
});
if (schedulesCount >= limit) {
throw new ServiceValidationError(
`You have created ${schedulesCount}/${limit} 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`
);
}
}
//this throws errors if the schedule is invalid
const checkSchedule = new CheckScheduleService(this._prisma);
await checkSchedule.call(projectId, schedule);
const result = await $transaction(this._prisma, async (tx) => {
const deduplicationKey =
@@ -126,6 +53,10 @@ export class UpsertTaskScheduleService extends BaseService {
});
if (existingSchedule) {
if (existingSchedule.type === "DECLARATIVE") {
throw new ServiceValidationError("Cannot update a declarative schedule");
}
return await this.#updateExistingSchedule(tx, existingSchedule, schedule, projectId);
} else {
return await this.#createNewSchedule(tx, schedule, projectId, deduplicationKey);
@@ -317,6 +248,7 @@ export class UpsertTaskScheduleService extends BaseService {
#createReturnObject(taskSchedule: TaskSchedule, instances: InstanceWithEnvironment[]) {
return {
id: taskSchedule.friendlyId,
type: taskSchedule.type,
task: taskSchedule.taskIdentifier,
active: taskSchedule.active,
externalId: taskSchedule.externalId,
+9 -5
View File
@@ -16,7 +16,7 @@ paths:
post:
operationId: create_schedule_v1
summary: Create a schedule
description: Create a new schedule based on the specified options.
description: Create a new `IMPERATIVE` schedule based on the specified options.
requestBody:
required: true
content:
@@ -127,7 +127,7 @@ paths:
put:
operationId: update_schedule_v1
summary: Update Schedule
description: Update a schedule by its ID.
description: Update a schedule by its ID. This will only work on `IMPERATIVE` schedules that were created in the dashboard or using the imperative SDK functions like `schedules.create()`.
parameters:
- in: path
name: schedule_id
@@ -173,7 +173,7 @@ paths:
delete:
operationId: delete_schedule_v1
summary: Delete Schedule
description: Delete a schedule by its ID.
description: Delete a schedule by its ID. This will only work on `IMPERATIVE` schedules that were created in the dashboard or using the imperative SDK functions like `schedules.create()`.
parameters:
- in: path
name: schedule_id
@@ -203,7 +203,7 @@ paths:
post:
operationId: deactivate_schedule_v1
summary: Deactivate Schedule.
description: Deactivate a schedule by its ID.
description: Deactivate a schedule by its ID. This will only work on `IMPERATIVE` schedules that were created in the dashboard or using the imperative SDK functions like `schedules.create()`.
parameters:
- in: path
name: schedule_id
@@ -237,7 +237,7 @@ paths:
post:
operationId: activate_schedule_v1
summary: Activate Schedule
description: Activate a schedule by its ID.
description: Activate a schedule by its ID. This will only work on `IMPERATIVE` schedules that were created in the dashboard or using the imperative SDK functions like `schedules.create()`.
parameters:
- in: path
name: schedule_id
@@ -1971,6 +1971,10 @@ components:
example: my-scheduled-task
description: The id of the scheduled task that will be triggered by this
schedule
"type":
type: string
example: IMPERATIVE
description: The type of schedule, `DECLARATIVE` or `IMPERATIVE`. Declarative schedules are declared in your code by setting the `cron` property on a `schedules.task`. Imperative schedules are created in the dashboard or by using the imperative SDK functions like `schedules.create()`.
active:
type: boolean
example: true
+62 -10
View File
@@ -3,21 +3,13 @@ title: "Scheduled tasks"
description: "A task that is triggered on a recurring schedule using CRON syntax."
---
To use scheduled tasks you need to do two things:
1. Define a task in your code using `schedules.task()`.
2. Attach a schedule to the task either using the dashboard or the SDK.
<Info>A task can have multiple schedules attached to it.</Info>
Like all tasks they don't have timeouts, they should be placed inside a [/trigger folder](/v3/trigger-folder), and you [can configure them](/v3/tasks-overview#defining-a-task).
## Defining a scheduled task
This task will run when any of the attached schedules trigger. They have a predefined payload with some useful properties:
```ts
import { schedules } from "@trigger.dev/sdk/v3";
//this task will run when any of the attached schedules trigger
export const firstScheduledTask = schedules.task({
id: "first-scheduled-task",
run: async (payload) => {
@@ -68,6 +60,66 @@ You can see from the comments that the payload has several useful properties:
to do that.
</Note>
Like all tasks they don't have timeouts, they should be placed inside a [/trigger folder](/v3/trigger-folder), and you [can configure them](/v3/tasks-overview#defining-a-task).
## How to attach a schedule
Now that we've defined a scheduled task, we need to define when it will actually run. To do this we need to attach one or more schedules.
There are two ways of doing this:
- **Declarative:** defined on your `schedules.task`. They sync when you run the dev command or deploy.
- **Imperative:** created from the dashboard or by using the imperative SDK functions like `schedules.create()`.
<Info>
A scheduled task can have multiple schedules attached to it, including a declarative schedule
and/or many imperative schedules.
</Info>
### Declarative schedules
These sync when you run the [dev](/v3/cli-dev) or [deploy](/v3/cli-deploy) commands.
To create them you add the `cron` property to your `schedules.task()`. This property is optional and is only used if you want to add a declarative schedule to your task:
```ts
export const firstScheduledTask = schedules.task({
id: "first-scheduled-task",
//every two hours (UTC timezone)
cron: "0 */2 * * *",
run: async (payload, { ctx }) => {
//do something
},
});
```
If you use a string it will be in UTC. Alternatively, you can specify a timezone like this:
```ts
export const secondScheduledTask = schedules.task({
id: "second-scheduled-task",
cron: {
//5am every day Tokyo time
pattern: "0 5 * * *",
timezone: "Asia/Tokyo",
},
run: async (payload) => {},
});
```
When you run the [dev](/v3/cli-dev) or [deploy](/v3/cli-deploy) commands, declarative schedules will be synced. If you add, delete or edit the `cron` property it will be updated when you run these commands. You can view your schedules on the Schedules page in the dashboard.
### Imperative schedules
Alternatively you can explicitly attach schedules to a `schedules.task`. You can do this in the Schedules page in the dashboard by just pressing the "New schedule" button, or you can use the SDK to create schedules.
The advantage of imperative schedules is that they can be created dynamically, for example, you could create a schedule for each user in your database. They can also be activated, disabled, edited, and deleted without deploying new code by using the SDK or dashboard.
To use imperative schedules you need to do two things:
1. Define a task in your code using `schedules.task()`.
2. Attach 1+ schedules to the task either using the dashboard or the SDK.
## Supported CRON syntax
```
+10
View File
@@ -226,10 +226,19 @@ export const CanceledRunResponse = z.object({
export type CanceledRunResponse = z.infer<typeof CanceledRunResponse>;
export const ScheduleType = z.union([z.literal("DECLARATIVE"), z.literal("IMPERATIVE")]);
export const ScheduledTaskPayload = z.object({
/** The schedule id associated with this run (you can have many schedules for the same task).
You can use this to remove the schedule, update it, etc */
scheduleId: z.string(),
/** The type of schedule `"DECLARATIVE"` or `"IMPERATIVE"`.
*
* **DECLARATIVE** defined inline on your `schedules.task` using the `cron` property. They can only be created, updated or deleted by modifying the `cron` property on your task.
*
* **IMPERATIVE** created using the `schedules.create` functions or in the dashboard.
*/
type: ScheduleType,
/** 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.
*
@@ -315,6 +324,7 @@ export type ScheduleGenerator = z.infer<typeof ScheduleGenerator>;
export const ScheduleObject = z.object({
id: z.string(),
type: ScheduleType,
task: z.string(),
active: z.boolean(),
deduplicationKey: z.string().nullish(),
+2 -1
View File
@@ -1,5 +1,5 @@
import { z } from "zod";
import { QueueOptions, RetryOptions } from "./schemas";
import { QueueOptions, RetryOptions, ScheduleMetadata } from "./schemas";
import { MachineConfig } from "./common";
export const TaskResource = z.object({
@@ -10,6 +10,7 @@ export const TaskResource = z.object({
retry: RetryOptions.optional(),
machine: MachineConfig.optional(),
triggerSource: z.string().optional(),
schedule: ScheduleMetadata.optional(),
});
export type TaskResource = z.infer<typeof TaskResource>;
+7
View File
@@ -142,6 +142,11 @@ export const QueueOptions = z.object({
export type QueueOptions = z.infer<typeof QueueOptions>;
export const ScheduleMetadata = z.object({
cron: z.string(),
timezone: z.string(),
});
export const TaskMetadata = z.object({
id: z.string(),
packageVersion: z.string(),
@@ -149,6 +154,7 @@ export const TaskMetadata = z.object({
retry: RetryOptions.optional(),
machine: MachineConfig.optional(),
triggerSource: z.string().optional(),
schedule: ScheduleMetadata.optional(),
});
export type TaskMetadata = z.infer<typeof TaskMetadata>;
@@ -167,6 +173,7 @@ export const TaskMetadataWithFilePath = z.object({
retry: RetryOptions.optional(),
machine: MachineConfig.optional(),
triggerSource: z.string().optional(),
schedule: ScheduleMetadata.optional(),
filePath: z.string(),
exportName: z.string(),
});
@@ -0,0 +1,6 @@
-- CreateEnum
CREATE TYPE "ScheduleType" AS ENUM ('DECLARATIVE', 'IMPERATIVE');
-- AlterTable
ALTER TABLE "TaskSchedule"
ADD COLUMN "type" "ScheduleType" NOT NULL DEFAULT 'IMPERATIVE';
+9
View File
@@ -2231,6 +2231,8 @@ model WorkerDeploymentPromotion {
model TaskSchedule {
id String @id @default(cuid())
type ScheduleType @default(IMPERATIVE)
///users see this as `id`. They start with schedule_
friendlyId String @unique
///a reference to a task (not a foreign key because it's across versions)
@@ -2267,6 +2269,13 @@ model TaskSchedule {
@@unique([projectId, deduplicationKey])
}
enum ScheduleType {
/// defined on your task using the `cron` property
DECLARATIVE
/// explicit calls to the SDK are used to create, or using the dashboard
IMPERATIVE
}
enum ScheduleGeneratorType {
CRON
}
+45 -1
View File
@@ -16,13 +16,57 @@ import { Task, TaskOptions, apiClientMissingError, createTask } from "../shared"
import * as SchedulesAPI from "./api";
import { tracer } from "../tracer";
export type ScheduleOptions<
TIdentifier extends string,
TOutput,
TInitOutput extends InitOutput,
> = TaskOptions<TIdentifier, SchedulesAPI.ScheduledTaskPayload, TOutput, TInitOutput> & {
/** You can optionally specify a CRON schedule on your task. You can also dynamically add a schedule in the dashboard or using the SDK functions.
*
* 1. Pass a CRON pattern string
* ```ts
* "0 0 * * *"
* ```
*
* 2. Or an object with a pattern and an optional timezone (default is "UTC")
* ```ts
* {
* pattern: "0 0 * * *",
* timezone: "America/Los_Angeles"
* }
* ```
*
* @link https://trigger.dev/docs/v3/tasks-scheduled
*/
cron?:
| string
| {
pattern: string;
timezone?: string;
};
};
export function task<TIdentifier extends string, TOutput, TInitOutput extends InitOutput>(
params: TaskOptions<TIdentifier, SchedulesAPI.ScheduledTaskPayload, TOutput, TInitOutput>
params: ScheduleOptions<TIdentifier, TOutput, TInitOutput>
): Task<TIdentifier, SchedulesAPI.ScheduledTaskPayload, TOutput> {
const task = createTask(params);
const cron = params.cron
? typeof params.cron === "string"
? params.cron
: params.cron.pattern
: undefined;
const timezone =
(params.cron && typeof params.cron !== "string" ? params.cron.timezone : "UTC") ?? "UTC";
taskCatalog.updateTaskMetadata(task.id, {
triggerSource: "schedule",
schedule: cron
? {
cron: cron,
timezone,
}
: undefined,
});
return task;
+12 -1
View File
@@ -2,7 +2,9 @@ import { logger, schedules, task } from "@trigger.dev/sdk/v3";
export const firstScheduledTask = schedules.task({
id: "first-scheduled-task",
run: async (payload) => {
//every other minute
cron: "0 */2 * * *",
run: async (payload, { ctx }) => {
const distanceInMs =
payload.timestamp.getTime() - (payload.lastTimestamp ?? new Date()).getTime();
@@ -18,6 +20,15 @@ export const firstScheduledTask = schedules.task({
},
});
export const secondScheduledTask = schedules.task({
id: "second-scheduled-task",
cron: {
pattern: "0 5 * * *",
timezone: "Asia/Tokyo",
},
run: async (payload) => {},
});
export const manageSchedules = task({
id: "manage-schedules",
run: async (payload) => {