Skip to content
LastPing
GUIDE · TRIGGER.DEV · SCHEDULED TASKS · FREE

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

On this page
  1. Setup: onSuccess and onFailure hooks
  2. What this catches
  3. Questions people ask

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: onFailure pings /fail with the error; you're paged immediately.
  • A run that hangs: it ends as Timed out at maxDuration, or with a /start ping 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 onFailure hook 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 onSuccess and onFailure to the schedules.task() definition. In onSuccess, GET https://ping.lastping.dev/<id>; in onFailure, POST the error to the /fail path. 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 onFailure or onSuccess are 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 maxDuration ends as Timed out, and a run whose TTL passes before it starts ends as Expired. In both cases onSuccess never runs, so LastPing sees no success ping and opens the incident when the grace window ends. An optional /start ping from onStartAttempt also records how long each run took.

  • Is this free?

    Yes. LastPing is free for individuals. See vs Healthchecks.io and vs Cronitor.

FREE FOR INDIVIDUALS · FULLY HOSTED

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.