Files
2026-08-18 13:23:29 +01:00

450 lines
18 KiB
Plaintext

---
title: "Scheduled tasks (cron)"
description: "A task that is triggered on a recurring schedule using cron syntax."
---
<Note>
Scheduled tasks are only for recurring tasks. If you want to trigger a one-off task at a future
time, you should [use the delay option](/triggering#delay).
</Note>
## 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";
export const firstScheduledTask = schedules.task({
id: "first-scheduled-task",
run: async (payload) => {
//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
console.log(payload.timestamp); //is a Date object
//when the task was last run
//this can be undefined if it's never been run
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)
//using this you can remove the schedule, update it, etc
console.log(payload.scheduleId); //is a string
//you can optionally provide an external id when creating the schedule
//usually you would set this to a userId or some other unique identifier
//this can be undefined if you didn't provide one
console.log(payload.externalId); //is a string or undefined
//the next 5 dates this task is scheduled to run
console.log(payload.upcoming); //is an array of Date objects
},
});
```
You can see from the comments that the payload has several useful properties:
- `timestamp` - the time the task was scheduled to run, as a UTC date.
- `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
- `externalId` - the external id you (optionally) provided when creating the schedule
- `upcoming` - the next 5 times the task is scheduled to run
<Note>
This task will NOT get triggered on a schedule until you attach a schedule to it. Read on for how
to do that.
</Note>
Like all tasks they don't have timeouts, they should be placed inside a [/trigger folder](/config/config-file), and you [can configure them](/tasks/overview#defining-a-task).
You can set a [TTL](/runs#time-to-live-ttl) on a scheduled task to automatically expire runs that aren't dequeued in time. This is useful when a schedule fires while the previous run is still executing - rather than queueing up stale runs, they'll expire:
```ts
export const frequentTask = schedules.task({
id: "frequent-task",
ttl: "5m",
run: async (payload) => {
//...
},
});
```
## 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](/cli-dev-commands) or [deploy](/cli-deploy-commands) 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",
//optional, defaults to all environments
//possible values are "PRODUCTION", "STAGING", "PREVIEW" and "DEVELOPMENT"
environments: ["PRODUCTION", "STAGING"],
},
run: async (payload) => {},
});
```
When you run the [dev](/cli-dev-commands) or [deploy](/cli-deploy-commands) 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 synced schedules in the dashboard: open the task on the Tasks page and check its "Schedules" tab.
### Imperative schedules
Alternatively you can explicitly attach schedules to a `schedules.task`. You can do this in the dashboard from the scheduled task's page by pressing the "Create 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.
## Spreading runs with windows
By default a schedule runs at its exact cron time. When many schedules share the same cron pattern, such as a daily `0 9 * * *`, they all fire at the same time and load your downstream systems as well as ours. A **window** spreads those runs out: each schedule is assigned a stable time within the window after its cron time, so the load is smoothed while each schedule keeps firing at a predictable, repeatable moment.
The assigned time is deterministic. A given schedule always lands at the same offset for a given interval, so runs don't jump around between occurrences. And you can always see exactly when the next run will start.
Set a `window` as either:
- **An absolute duration** in whole minutes or hours, up to 24 hours: `"30m"`, `"2h"`, `"24h"`. Absolute windows are capped at the next cron time, so a run is never delayed past its following occurrence.
- **A percentage** of the interval between runs: `"30%"`, `"100%"`. A `"50%"` window on an hourly schedule spreads runs across the first 30 minutes of each hour.
Set `"0m"` (or `"0%"`) for no spreading — the run fires at its exact cron time.
Declarative schedules set the window on the `cron` object:
```ts
export const dailyReport = schedules.task({
id: "daily-report",
cron: {
pattern: "0 0 * * *",
// spread this run across the 30 minutes after midnight
window: "30m",
},
run: async (payload) => {},
});
```
Imperative schedules set it when creating or updating a schedule, either in the dashboard form or through the SDK:
```ts
const createdSchedule = await schedules.create({
task: dailyReport.id,
cron: "0 0 * * *",
window: "30m",
deduplicationKey: "user_123456-daily-report",
});
```
<Note>
The payload `timestamp` and `upcoming` values are always the nominal cron times, not the assigned
times. When you retrieve a schedule, `nextRun` is the nominal cron time and `nextRunEffectiveAt`
is the assigned time the run will actually start.
</Note>
## Supported cron syntax
```
* * * * *
┬ ┬ ┬ ┬ ┬
│ │ │ │ |
│ │ │ │ └ day of week (0 - 7, 1L - 7L) (0 or 7 is Sun)
│ │ │ └───── month (1 - 12)
│ │ └────────── day of month (1 - 31, L)
│ └─────────────── hour (0 - 23)
└──────────────────── minute (0 - 59)
```
"L" means the last. In the "day of week" field, 1L means the last Monday of the month. In the "day of month" field, L means the last day of the month.
We do not support seconds in the cron syntax.
## When schedules won't trigger
There are two situations when a scheduled task won't trigger:
- For Dev environments scheduled tasks will only trigger if you're running the dev CLI.
- For Staging/Production environments scheduled tasks will only trigger if the task is in the current deployment (latest version). We won't trigger tasks from previous deployments.
## Attaching schedules in the dashboard
You need to attach a schedule to a task before it will run on a schedule. You can attach imperative schedules in the dashboard:
<Note>
**The Schedules page has moved.** There is no longer a standalone "Schedules" page in the
sidebar. Schedules now live on the Tasks page: open a scheduled task to create, view, edit,
enable/disable, and delete its schedules. The old `/schedules` URL redirects to the Tasks page.
The scheduled task must already exist first — define it in your code with `schedules.task()` and
sync it to the environment by running the [dev](/cli-dev-commands) or
[deploy](/cli-deploy-commands) command so it appears on the Tasks page. A project with no tasks
yet will only show the deploy onboarding.
</Note>
<Steps>
<Step title="Open the scheduled task">
In the sidebar select the "Tasks" page, then select the scheduled task you want to attach a
schedule to (scheduled tasks have a clock icon, and you can filter the list to Scheduled).
![Scheduled task page](/images/schedules-blank.png)
</Step>
<Step title="Create your schedule">
Press the "Create schedule" button, fill in the form, and press "Create schedule" when you're
done. ![Create schedule form](/images/schedules-create.png)
These are the options when creating a schedule:
| Name | Description |
| ----------------- | --------------------------------------------------------------------------------------------- |
| Task | The id of the task you want to attach to. |
| Cron pattern | The schedule in cron format. You can also describe it in natural language and press "Generate" to fill this in. |
| Timezone | The timezone the schedule will run in. Defaults to "UTC" |
| Window | An optional [window](#spreading-runs-with-windows) to spread runs after their cron time, e.g. `30m`, `2h`, or `50%`. |
| 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. Scoped per project, not per environment. |
| Environments | The environments this schedule will run in. |
</Step>
</Steps>
## Managing schedules in the dashboard
Open the scheduled task and switch to the "Schedules" tab to see every schedule attached to it — both declarative and imperative — with its type, cron pattern, external id, next and last run, and status.
Click a schedule to open the inspector, where you can **enable/disable**, **edit**, or **delete** imperative schedules without deploying new code. Declarative schedules are managed in your code, so they can't be edited or deleted from here.
## Attaching schedules with the SDK
You call `schedules.create()` to create a schedule from your code. Here's the simplest possible example:
```ts
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: "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",
});
```
<Note>The `task` id must be a task that you defined using `schedules.task()`.</Note>
You can create many schedules with the same `task`, `cron`, and `externalId` but only one with the same `deduplicationKey`.
<Note>
The deduplication key is **per project**, not per environment. Using the same key in Production and Staging creates a single schedule; the last create/update decides which environment it appears in. For fixed schedules, prefer **declarative** (cron on the task). If using imperative across environments, use a different deduplication key per environment (e.g. include the env name in the key).
</Note>
This means you can have thousands of schedules attached to a single task, but only one schedule per `deduplicationKey`. Here's an example with all the options:
```ts
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: "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.
//This allows you to have per-user cron tasks.
externalId: "user_123456",
//You can only create one schedule with this key.
//If you use it twice, the second call will update the schedule.
//This is useful because you don't want to create duplicate schedules for a user.
deduplicationKey: "user_123456-todo_reminder",
});
```
See [the SDK reference](/management/schedules/create) for full details.
### Dynamic schedules (or multi-tenant schedules)
By using the `externalId` you can have schedules for your users. This is useful for things like reminders, where you want to have a schedule for each user.
A reminder task:
```ts /trigger/reminder.ts
import { schedules } from "@trigger.dev/sdk";
//this task will run when any of the attached schedules trigger
export const reminderTask = schedules.task({
id: "todo-reminder",
run: async (payload) => {
if (!payload.externalId) {
throw new Error("externalId is required");
}
//get user using the externalId you used when creating the schedule
const user = await db.getUser(payload.externalId);
//send a reminder email
await sendReminderEmail(user);
},
});
```
Then in your backend code, you can create a schedule for each user:
```ts Next.js API route
import { reminderTask } from "~/trigger/reminder";
//app/reminders/route.ts
export async function POST(request: Request) {
//get the JSON from the request
const data = await request.json();
//create a schedule for the user
const createdSchedule = await schedules.create({
task: reminderTask.id,
//8am every day
cron: "0 8 * * *",
//the user's timezone
timezone: data.timezone,
//the user id
externalId: data.userId,
//this makes it impossible to have two reminder schedules for the same user
deduplicationKey: `${data.userId}-reminder`,
});
//return a success response with the schedule
return Response.json(createdSchedule);
}
```
You can also retrieve, list, delete, deactivate and re-activate schedules using the SDK. More on that later.
## Testing schedules
You can test a scheduled task in the dashboard. Note that the `scheduleId` will always come through as `sched_1234` to the run.
<Note>
There is no longer a standalone "Test" page in the sidebar. You test a task from the task itself —
open it on the Tasks page and press the "Test schedule" button.
</Note>
<Steps>
<Step title="Open the test page for your task">
On the "Tasks" page, open your scheduled task and press the "Test schedule" button.
![Scheduled task page](/images/schedules-test.png)
</Step>
<Step title="Run the test">
Fill in the form [1]. You can select from a recent run [2] to pre-populate the fields. Press "Run
test" when you're ready ![Schedule test form](/images/schedules-test-form.png)
</Step>
</Steps>
## Managing schedules with the SDK
### Retrieving an existing schedule
```ts
const retrievedSchedule = await schedules.retrieve(scheduleId);
```
See [the SDK reference](/management/schedules/retrieve) for full details.
### Listing schedules
```ts
const allSchedules = await schedules.list();
```
See [the SDK reference](/management/schedules/list) for full details.
### Updating a schedule
```ts
const updatedSchedule = await schedules.update(scheduleId, {
task: firstScheduledTask.id,
cron: "0 0 1 * *",
externalId: "ext_1234444",
deduplicationKey: "my-deduplication-key",
});
```
See [the SDK reference](/management/schedules/update) for full details.
### Deactivating a schedule
```ts
const deactivatedSchedule = await schedules.deactivate(scheduleId);
```
See [the SDK reference](/management/schedules/deactivate) for full details.
### Activating a schedule
```ts
const activatedSchedule = await schedules.activate(scheduleId);
```
See [the SDK reference](/management/schedules/activate) for full details.
### Deleting a schedule
```ts
const deletedSchedule = await schedules.del(scheduleId);
```
See [the SDK reference](/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](/management/schedules/timezones) for full details.