n8n Cron Jobs: Scheduled Automation for Developers 2026

Set up n8n cron triggers for scheduled workflows. Covers cron syntax, timezone config, error handling, and production patterns for self-hosted n8n.

Problem: Your n8n Workflows Only Run When You Click Them

Manually triggering workflows defeats the purpose of automation. If you want n8n to scrape data every morning, sync a database hourly, or send a report every Friday — you need a working cron trigger with the right timezone, error handling, and retry logic.

This guide covers everything between clicking "Add Trigger" and having a workflow that reliably runs in production.

You'll learn:

  • How to configure the Schedule Trigger node with exact cron expressions
  • How to handle timezone offsets so jobs run when you actually mean them to
  • How to catch failures silently and alert yourself without breaking the chain

Time: 20 min | Difficulty: Intermediate


Why n8n's Schedule Trigger Trips Developers Up

n8n uses two overlapping systems for scheduled runs: a visual interval picker and raw cron expressions. The visual picker looks friendly but hides what's actually stored. Swapping between modes resets your config. And the timezone defaults to UTC — which breaks anyone not actively thinking about offsets.

Symptoms:

  • Workflow runs 5 hours later than expected
  • Cron expression works in testing but silently skips runs in production
  • Self-hosted n8n runs missed because the container restarted and no queue persisted the job

Solution

Step 1: Add a Schedule Trigger Node

Open your workflow and click Add first step (or the + button if adding to an existing flow).

Search for Schedule Trigger — not the legacy "Cron" node. The Schedule Trigger replaced the old Cron node in n8n 0.214 and is the only one maintained in 1.x.

Node panel → Search: "Schedule Trigger" → Select it

You'll see three interval modes:

ModeBest for
Every X minutes/hoursSimple polling intervals
Every day at timeDaily jobs with a fixed clock time
Custom (Cron)Everything else

For anything beyond "every N minutes", use Custom (Cron). It gives you exact control and is what actually gets stored under the hood anyway.


Step 2: Write the Cron Expression

n8n uses standard 5-field cron syntax:

┌─────────── minute (0–59)
│ ┌───────── hour (0–23)
│ │ ┌─────── day of month (1–31)
│ │ │ ┌───── month (1–12)
│ │ │ │ ┌─── day of week (0–6, Sunday = 0)
│ │ │ │ │
* * * * *

Common patterns you'll actually use:

# Every day at 8:00 AM
0 8 * * *

# Every Monday at 9:00 AM
0 9 * * 1

# Every 15 minutes
*/15 * * * *

# First day of every month at midnight
0 0 1 * *

# Every weekday at 6:30 PM
30 18 * * 1-5

# Every 6 hours
0 */6 * * *

Paste your expression into the Cron Expression field in the Schedule Trigger node. n8n shows a human-readable preview of the next 5 run times directly below the field — use this to confirm before saving.

If it fails:

  • Invalid cron expression → n8n does not support 6-field (seconds-level) cron. Remove the seconds field if you copied from a Node.js or AWS cron format.
  • Preview shows wrong times → Timezone not set yet. Fix in Step 3.

Step 3: Set the Correct Timezone

This is the most commonly skipped step and the most common source of broken schedules.

n8n evaluates cron expressions in the timezone configured at the instance level, not per-workflow. There is no per-workflow timezone override in the Schedule Trigger node itself.

For self-hosted n8n (Docker):

Set GENERIC_TIMEZONE in your environment:

# docker-compose.yml
services:
  n8n:
    image: n8nio/n8n
    environment:
      - GENERIC_TIMEZONE=Asia/Kuala_Lumpur   # or America/New_York, Europe/Berlin, etc.
      - TZ=Asia/Kuala_Lumpur                 # also set system TZ to match

Then restart the container:

docker compose down && docker compose up -d

For n8n Cloud:

Go to Settings → General → Timezone and select your timezone from the dropdown. This applies to all workflows in your instance.

Verify it's working:

# Check which timezone n8n is running under (self-hosted)
docker exec -it n8n date

The output should show the local time in your configured timezone.

Valid timezone strings follow the IANA database format: America/New_York, Europe/London, Asia/Tokyo. Do not use abbreviations like EST or GMT+8 — these are ambiguous and not reliably parsed.


Step 4: Handle Errors Without Killing the Schedule

A failing workflow node stops execution and marks the run as errored — but the next cron trigger still fires. The problem is silent failure: if you don't add error handling, failed runs just pile up in your execution history with no alert.

Add an Error Trigger workflow to catch these:

Workflow 1: Your scheduled workflow

Schedule Trigger → [your nodes] → (if any node errors, execution marked failed)

Workflow 2: Error handler (separate workflow)

  1. Create a new workflow
  2. Add an Error Trigger node as the first step — this fires whenever any workflow in your n8n instance errors
  3. Connect it to a notification node (Slack, email, Telegram, etc.)
Error Trigger → Slack node → Post to #alerts channel

Configure the Slack node message to include execution context:

// In the Slack message field (Expression mode)
Workflow "{{ $execution.workflowName }}" failed at {{ $now.toISO() }}
Error: {{ $execution.lastError.message }}
Execution ID: {{ $execution.id }}

This gives you one centralized error channel for all scheduled workflows without adding error branches to each one individually.


Step 5: Prevent Missed Runs After Downtime

By default, if your n8n instance is offline when a cron trigger was supposed to fire, the run is silently skipped. There is no built-in catch-up execution.

For critical scheduled jobs, add a manual check at the start of your workflow to detect and handle missed runs:

// Code node at the start of your workflow
// Checks if the workflow hasn't run successfully in the expected interval

const lastSuccessfulRun = $vars.lastRunTimestamp 
  ? new Date($vars.lastRunTimestamp) 
  : null;

const now = new Date();
const expectedInterval = 60 * 60 * 1000; // 1 hour in ms

if (lastSuccessfulRun && (now - lastSuccessfulRun) > expectedInterval * 1.5) {
  // We're running late — log it or send an alert
  return [{ json: { status: 'catch_up_run', late_by_ms: now - lastSuccessfulRun } }];
}

return [{ json: { status: 'on_schedule' } }];

At the end of a successful workflow, store the timestamp:

// Final Code node — saves run timestamp to workflow variables
await $vars.set('lastRunTimestamp', new Date().toISOString());
return items;

This pattern works for workflows where running twice is worse than running once — syncs, reports, and billing jobs.


Step 6: Test Without Waiting for the Schedule

Don't wait for the cron to fire to test your workflow logic. Use the Test workflow button to simulate a Schedule Trigger execution immediately.

The Schedule Trigger node passes a single item with this shape when it fires:

{
  "timestamp": "2026-03-09T08:00:00.000Z",
  "workflow": {
    "id": "abc123",
    "name": "Daily Report"
  }
}

If any downstream node uses $json.timestamp from the trigger, it will be populated correctly in test runs. You can also override it in a Code node for date-range testing:

// Simulate trigger at a specific past time for backfill testing
return [{ json: { timestamp: "2026-01-01T08:00:00.000Z" } }];

Verification

After deploying your workflow, check that executions are appearing on schedule:

# Self-hosted: check execution logs directly
docker exec -it n8n n8n list:workflow

In the n8n UI: go to Executions (left sidebar) → filter by your workflow name → confirm runs appear at the expected times with ✅ status.

For a 5-minute interval trigger, wait 10 minutes after activating — you should see at least one execution logged.

You should see: Green execution entries spaced at your configured interval, with the correct local time shown in the execution timestamp.


Production Checklist for Scheduled Workflows

Before marking a scheduled workflow as production-ready:

  • GENERIC_TIMEZONE and TZ both set to the same IANA timezone string
  • Error Trigger workflow connected to at least one notification channel
  • Workflow is activated (toggle in top-right is green) — inactive workflows ignore the schedule
  • Execution history retention set to a reasonable window: Settings → Execution Data → Keep for
  • For long-running jobs: Execution Timeout configured to prevent hung workflows from blocking the queue

What You Learned

  • The Schedule Trigger node (not the legacy Cron node) is what you should use in n8n 1.x
  • Timezone is set at the instance level via GENERIC_TIMEZONE — there's no per-workflow override
  • A separate Error Trigger workflow is the cleanest way to catch failures across all scheduled jobs
  • Missed runs from downtime are silent by default — use a timestamp-check pattern for critical jobs

Limitation: n8n's scheduler runs in-process, not as a separate job queue. On self-hosted instances under heavy load, schedule jitter of 1–2 minutes is possible. If sub-minute precision matters, use an external cron (crontab, GitHub Actions scheduled trigger) to hit your n8n webhook instead.

Tested on n8n 1.28.0, Docker 27.x, Ubuntu 24.04 and n8n Cloud