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:
| Mode | Best for |
|---|---|
| Every X minutes/hours | Simple polling intervals |
| Every day at time | Daily 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)
- Create a new workflow
- Add an Error Trigger node as the first step — this fires whenever any workflow in your n8n instance errors
- 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_TIMEZONEandTZboth 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