Monitor Trigger.dev scheduled tasks: alerts for failed and missed runs
Trigger.dev can alert you when a run fails, and its lifecycle hooks run when a task succeeds
or fails. What neither sees is the run that never triggers: a scheduled task only fires in
Staging or Production if it is in the current deployment, and a deactivated schedule creates
no runs. Add onSuccess and onFailure hooks that ping LastPing, give
the monitor the same cron, and a missed run alerts as reliably as a failed one.
Your assistant can do this for you
Monitor my Trigger.dev scheduled tasks with LastPing
Setup: onSuccess and onFailure hooks
Ping on success, POST the error to /fail on failure. A matching monitor schedule catches the run that never triggered.
Create a heartbeat monitor in LastPing
In the console, create a monitor with the same cron as the task's schedule (a declarative cron string is UTC unless you give a timezone). Set the grace window to the task's expected runtime plus a margin. Copy the ping URL.
https://ping.lastping.dev/<your-monitor-id>
Add onSuccess and onFailure to the scheduled task
onSuccess runs when the run succeeds; onFailure runs once the run has exhausted all its retries and receives the error:
import { schedules } from "@trigger.dev/sdk";
const PING = "https://ping.lastping.dev/<your-monitor-id>";
export const nightlyEtl = schedules.task({
id: "nightly-etl",
cron: "0 3 * * *",
maxDuration: 1800, // seconds: a hung run ends as Timed out
onSuccess: async () => {
await fetch(PING);
},
onFailure: async ({ error }) => {
await fetch(`${PING}/fail`, {
method: "POST",
body: String(error).slice(0, 10000),
});
},
run: async (payload) => {
// your work; payload.timestamp is the scheduled time
},
});
Errors thrown inside onSuccess and onFailure are ignored by Trigger.dev, so a failed ping never fails the task. The POSTed body (up to 10,000 bytes) is stored with the fail ping.
Optional: a start ping and run duration
onStartAttempt (SDK v4.1.0 and later) runs before each attempt. Ping /start on the first attempt and pass the run id as rid on both pings, and LastPing arms the hung-job alarm and records how long each run took:
onStartAttempt: async ({ ctx }) => {
if (ctx.run.attempt.number === 1) {
await fetch(`${PING}/start?rid=${ctx.run.id}`);
}
},
onSuccess: async ({ ctx }) => {
await fetch(`${PING}?rid=${ctx.run.id}`);
},
Unlike onSuccess, an error thrown in onStartAttempt fails the attempt, so wrap the fetch in a try/catch if the ping should never block the task.
The run that never triggers needs no code
A scheduled task in Staging or Production triggers only if it is in the current deployment, so a deploy that drops or renames the task stops its schedule. A deactivated schedule creates no runs, and in Dev nothing triggers unless the dev CLI is running. In each case LastPing receives no ping, and the incident opens on its own when the grace window ends.
What this catches
- A run fails after its retries:
onFailurepings/failwith the error; you're paged immediately. - A run that hangs: it ends as Timed out at
maxDuration, or with a/startping the grace window fires first. - A run that never leaves the queue: it ends as Expired once its TTL passes; no success ping arrives, so the incident opens.
- A task missing from the current deployment, or a deactivated schedule: no run, no ping, incident.
- Recovery: the next successful run closes the incident automatically.
Comparing orchestrators? See Monitoring failed workflows across orchestrators. Using Inngest instead? See Inngest functions. Hatchet? See Hatchet workflows. For the base pattern, see Monitor Node.js scripts.
Questions people ask
Front-loaded answers: the most important fact first.
-
Doesn't Trigger.dev already alert on failed runs?
Yes. The Alerts page can notify you by email, Slack or webhook when a run fails or a deployment fails, and an
onFailurehook can send anything you like. Those cover runs that happened. Trigger.dev's docs list two cases where a scheduled task does not trigger at all: in Dev, when the dev CLI is not running, and in Staging or Production, when the task is not in the current deployment. A deactivated schedule also creates no runs. A dead-man's-switch expects a check-in on every scheduled run and opens an incident when one is missing. -
How do I add a dead-man's-switch to a Trigger.dev scheduled task?
Add
onSuccessandonFailureto theschedules.task()definition. InonSuccess, GEThttps://ping.lastping.dev/<id>; inonFailure, POST the error to the/failpath. Create a LastPing monitor with the same cron as the task's schedule so a run that never triggers also alerts. -
When does onFailure run?
Only once the run has exhausted all its retries, so a transient error that a retry fixes does not page you. Errors thrown inside
onFailureoronSuccessare ignored by Trigger.dev, so a failed ping never fails your task. -
What about a run that hangs or never leaves the queue?
A run that exceeds its
maxDurationends as Timed out, and a run whose TTL passes before it starts ends as Expired. In both casesonSuccessnever runs, so LastPing sees no success ping and opens the incident when the grace window ends. An optional/startping fromonStartAttemptalso records how long each run took. -
Is this free?
Yes. LastPing is free for individuals. See vs Healthchecks.io and vs Cronitor.
A schedule that stopped firing fails nothing. Catch it.
Two hooks and a matching schedule. First alert in under a minute, and LastPing monitors itself the same way.