Tutorials n8n

n8n Schedule Trigger Guide: Cron Syntax and Time Zones

Run n8n workflows on a schedule: set up Schedule Trigger intervals, write cron expressions for weekday and monthly jobs, fix 12-hour time zone offsets with GENERIC_TIMEZONE, and publish so the schedule actually runs. Includes an importable 8 a.m. email workflow and a troubleshooting checklist.

The RoamerHost Team 20 min read
Ready to start? Managed n8n hosting, set up in 60 seconds Workflow automation platform. From avg. NT$499/mo (billed yearly). Subscribe to n8n
On this page

This tutorial shows you how to run n8n workflows on a schedule with the Schedule Trigger node: checking orders every 15 minutes, sending a summary email at 8 a.m. every day, posting a reminder at 9 a.m. on weekdays, or running a month-end job on the 1st. All you need is an n8n instance you can log in to, self-hosted or managed, and enough familiarity to drag nodes onto the canvas and connect them. If you haven't built a workflow yet, start with building your first workflow. By the end you'll know how each interval works, how to write cron expressions, why so many schedules fire 12 hours late, and why in n8n 2.x nothing runs until you click Publish.

Verified as of October 2026 against the official n8n documentation and hands-on testing in n8n 2.43.

Add a Schedule Trigger to your workflow

Create a new workflow and click the plus button in the middle of the canvas to open the trigger menu. The schedule option is listed as "On a schedule", described as "Runs the flow every day, hour, or custom interval". Typing schedule or cron into the search box finds it too. Once it's on the canvas, the node is called Schedule Trigger. Open it and the first thing you'll see is a notice explaining that the workflow will run on this schedule once you publish it, and that you can trigger it manually from the canvas with Execute workflow while testing. Keep that notice in mind; a whole section below is about it.

The main setting is Trigger Rules. Each rule combines a Trigger Interval with a few time fields, and the default is Days, meaning once a day. Click Add Rule to stack several rules on the same node, for example one for 9 a.m. on weekdays and another for noon on Saturdays. Whenever any rule comes due, the workflow runs. When you need the same job at different times, multiple rules are much easier to maintain than several copies of the workflow.

What each interval asks for

Trigger Interval offers seven options: Seconds, Minutes, Hours, Days, Weeks, Months, and Custom (Cron). The fields underneath change depending on which one you pick. Here is what you'll see in n8n 2.43, with an example for each:

Trigger IntervalFieldsExample
SecondsSeconds Between Triggers (1-59)10: every 10 seconds
MinutesMinutes Between Triggers (1-59)15: every 15 minutes
HoursHours Between Triggers (1-23), Trigger at Minute6 and 30: every 6 hours at half past
DaysDays Between Triggers, Trigger at Hour, Trigger at Minute1, 8am, 0: daily at 8:00 a.m.
WeeksWeeks Between Triggers, Trigger on Weekdays, Trigger at Hour, Trigger at Minute1, Monday, 9am, 0: Mondays at 9:00 a.m.
MonthsMonths Between Triggers, Trigger at Day of Month (1-31), Trigger at Hour, Trigger at Minute1, 1, 10am, 0: the 1st of each month at 10:00 a.m.
Custom (Cron)Expression0 9 * * 1-5: weekdays at 9:00 a.m.

A few details trip people up the first time. Trigger at Hour is a dropdown in 12-hour format, with options like 8am, Noon, and 3pm, so 3 p.m. is 3pm, not 3am. In Months, if Trigger at Day of Month is the 29th, 30th, or 31st, any month without that day is skipped entirely. The official docs use the 30th as their example: it never fires in February. For "last day of the month", use the cron workaround in the next section. Seconds and Minutes both max out at 59, so for something like every two hours, switch to Hours.

One change in n8n 2.45 is worth knowing about up front. The older scheduler aligned "every N seconds" and "every N minutes" to the clock, so every 15 minutes meant :00, :15, :30, and :45 past the hour. From 2.45.0 the new durable scheduler is on by default, and these two intervals are measured from the moment you publish the workflow: publish at 10:07 and runs land at 10:22, 10:37, and so on. Cron expressions keep their timing. If you care about clock-aligned runs, use the cron expression */15 * * * *, which behaves the same on every version.

Writing cron expressions

Choosing Custom (Cron) leaves a single Expression field, and the hint below it spells out the format: ([Second]) [Minute] [Hour] [Day of Month] [Month] [Day of Week]. Five fields is the norm: minute, hour, day of month, month, and day of week. You can add a sixth field at the front for seconds, but it's optional. For day of week, 0 is Sunday, 1 is Monday, and 6 is Saturday; abbreviations such as MON-FRI work too.

┌───────── minute (0-59)
│ ┌─────── hour (0-23)
│ │ ┌───── day of month (1-31)
│ │ │ ┌─── month (1-12)
│ │ │ │ ┌─ day of week (0-6, 0 is Sunday)
│ │ │ │ │
0 9 * * 1-5

Four symbols cover almost everything. An asterisk * means every value. A comma lists values, so 1,15 is the 1st and the 15th. A hyphen is a range, so 9-17 is 9 a.m. through 5 p.m. A slash is a step, so */15 is every 15 units. Hours use the 24-hour clock, so 6 p.m. is 18.

Here are the schedules office teams ask for most often. We ran each one through the cron parser bundled with n8n 2.43 and checked the next three run times:

ScheduleCron expression
Weekdays (Monday to Friday) at 9 a.m.0 9 * * 1-5
The 1st of every month at 9 a.m.0 9 1 * *
Every 15 minutes, aligned to :00, :15, :30, :45*/15 * * * *
Every 30 minutes during office hours on weekdays (9:00 to 17:30)*/30 9-17 * * 1-5
Mondays at 8 a.m. (weekly report)0 8 * * 1
Weekdays at noon0 12 * * 1-5
Every two hours on the hour0 */2 * * *
Midnight on the first day of each quarter0 0 1 1,4,7,10 *

To double-check an expression, paste it into crontab.guru, which explains it in plain English. It only accepts five fields, so drop the seconds field first if you used one.

"Last day of the month" is a common request, and many cron tutorials suggest L. The parser n8n uses doesn't support it: in our test on 2.43, 0 9 L * * was rejected and the node reported Invalid cron expression. The workaround is to run on the 28th through the 31st and add an IF node that only continues when tomorrow is the 1st:

Schedule Trigger -> Custom (Cron)
  Expression: 0 18 28-31 * *

IF node -> condition type Number -> is equal to
  Left:  {{ $now.plus({ days: 1 }).day }}
  Right: 1

That check relies on $now, and $now is only right if your time zone is right. Otherwise "tomorrow" is tomorrow somewhere else. Which brings us to the most common scheduling problem.

Time zones: why your schedule runs 12 hours late

The Schedule Trigger has no time zone field of its own. It follows n8n's time zone setting in this order: the workflow's Timezone setting if you set one; otherwise the instance time zone from the GENERIC_TIMEZONE environment variable; and if neither is set, self-hosted n8n defaults to America/New_York.

So if you run n8n in Docker without GENERIC_TIMEZONE, a "daily at 8 a.m." schedule fires at 8 a.m. New York time. In Taiwan that's 8 p.m., or 9 p.m. once US daylight saving time ends and the gap grows to 13 hours. It looks like the scheduler is broken when it's only the time zone. n8n Cloud tries to detect your time zone at sign-up, falls back to GMT, and lets you change it in the Cloud admin dashboard.

There are two quick ways to see which time zone you're on. Open the workflow menu (the three dots in the top right), choose Settings, and look at the default entry in the Timezone dropdown: "Default - America/New_York" means the instance time zone was never set. Or run the Schedule Trigger manually and look at its output, which includes a Timezone field such as "Asia/Taipei (UTC+08:00)".

On a self-hosted instance, fix it with environment variables. The official Docker install guide sets two of them: GENERIC_TIMEZONE for schedule-based nodes, and TZ for the container's system clock, which is what commands like date and scripts see. With Docker Compose, add them under the n8n service:

services:
  n8n:
    # keep your existing settings and add these two lines under environment
    environment:
      - GENERIC_TIMEZONE=Asia/Taipei
      - TZ=Asia/Taipei

Run docker compose up -d to recreate the container; refreshing the browser does nothing. The next day, check the run times in the execution list. If anything still looks off, unpublish and republish the scheduled workflows so their schedules are registered again with the new time zone.

If one workflow needs to follow another country's clock, say an email that goes out before the US market opens, set Timezone in that workflow's settings and click Save. It only affects that workflow; everything else keeps the instance time zone.

A related trap lives in the Code node. JavaScript's native new Date() ignores the workflow time zone, which the n8n docs call out explicitly. It uses the system clock instead, and in a Docker container without TZ that is usually UTC, eight hours behind Taiwan. Use n8n's built-in $now and $today in expressions and Code nodes. They are Luxon date objects that follow n8n's time zone, so {{ $now.toFormat('yyyy/MM/dd HH:mm') }} gives you the local date and time.

n8n instances on RoamerHost ship with GENERIC_TIMEZONE=Asia/Taipei, so new schedules use Taiwan time out of the box and the Timezone dropdown shows "Default - Asia/Taipei".

In n8n 2.x, nothing runs until you publish

This catches almost everyone coming from 1.x. In n8n 1.x, an Active toggle in the top right put a workflow live, and every save after that went straight to production. Version 2.0, released in December 2025, split this into two steps: your edits are saved automatically as a draft, and production runs whichever version you publish. The Active toggle is gone, replaced by a Publish button in the canvas header.

Once the workflow is tested, click Publish (or press Shift + P). The Publish workflow dialog lets you name the version and describe what changed. After you confirm, the button briefly shows "Publishing…" while triggers are registered, then switches to "Published". Only from that point does the Schedule Trigger fire on time.

Two misunderstandings come up constantly. A successful Execute workflow run in the editor only proves the workflow logic works; it doesn't put the schedule live. And if you edit the schedule on a published workflow, the change sits in the draft and production keeps the old timing until you publish again. The Publish button lights up whenever there are unpublished changes. The official docs also note that after you change the interval and republish, the new interval counts from the publish time: if a workflow ran every hour and was due at 12:00, switching it to every two hours and publishing at 11:30 makes the next run 13:30.

To pause a schedule, open the dropdown arrow next to Publish and choose Unpublish (Ctrl or Cmd + U), or do it from the workflow list. If a node has configuration issues, n8n asks you to fix them before publishing. If only some triggers activate, the workflow shows as partially published, and publishing again retries the ones that failed. On 1.x you still have the Active toggle, and switching it on is the equivalent of publishing.

Example: email exchange rates every morning at 8

Here is a real workflow that ties everything together: every morning at 8, fetch reference rates for the US dollar, Japanese yen, and euro against the New Taiwan dollar and email a short summary. The data comes from ExchangeRate-API's open endpoint, which needs no API key or sign-up, and the email goes out through the Send Email node using Gmail's SMTP server. It's four nodes and takes about 15 minutes.

  1. Schedule Trigger: set Trigger Interval to Days, Days Between Triggers to 1, Trigger at Hour to 8am, and Trigger at Minute to 0. To send on weekdays only, switch to Custom (Cron) and enter 0 8 * * 1-5.
  2. HTTP Request: leave Method as GET and set URL to https://open.er-api.com/v6/latest/USD. Click Execute step and you'll see a rates object listing how much of each currency one US dollar buys; the New Taiwan dollar is rates.TWD.
  3. Edit Fields: add four fields of type String, switch each value to Expression, and paste the expressions below. Yen is quoted per 100 units, which is easier to read.
  4. Send Email: under Credential choose Create new credential. Set User to your Gmail address, Password to an app password, Host to smtp.gmail.com, and Port to 465 with SSL/TLS on. Fill in From Email and To Email, set Email Format to HTML, and switch Subject and HTML to Expression before pasting the sample content, so the fields inside {{ }} get replaced with numbers.
  5. Test and publish: click Execute workflow at the bottom of the canvas, confirm the email arrives with sensible numbers, then click Publish. After 8 a.m. the next day, open the Executions tab above the canvas and look for a new execution.

The four Edit Fields entries (field name on the left, value on the right):

usd      {{ $json.rates.TWD.toFixed(2) }}
jpy100   {{ ($json.rates.TWD / $json.rates.JPY * 100).toFixed(2) }}
eur      {{ ($json.rates.TWD / $json.rates.EUR).toFixed(2) }}
updated  {{ DateTime.fromSeconds($json.time_last_update_unix).setZone('Asia/Taipei').toFormat('yyyy/MM/dd HH:mm') }}

The Send Email subject and body:

Subject:
Exchange rates for {{ $now.toFormat('yyyy-MM-dd') }}

HTML:
<p>Good morning. Here are today's reference rates (TWD per unit of foreign currency):</p>
<p>USD: {{ $json.usd }}<br>JPY (per 100): {{ $json.jpy100 }}<br>EUR: {{ $json.eur }}</p>
<p><small>Last updated: {{ $json.updated }} (Taipei time)<br>Rates By <a href="https://www.exchangerate-api.com">Exchange Rate API</a></small></p>

Gmail app passwords require 2-Step Verification on the account. Once that's on, create one on the App passwords page of your Google Account; you'll get a 16-character password that is separate from your normal Gmail password. If you use Google Workspace and the option is missing, your admin may have disabled it, so check with IT. Version 2.1 of the Send Email node appends "This email was sent automatically with n8n" by default; to remove it, add Append n8n Attribution under Options and turn it off.

The rates API updates once a day. When we checked, the update landed a few minutes after 8 a.m. Taiwan time, so a run at exactly 8:00 may pick up the previous day's figures. The example puts the update time in the email so you can see which day the data is from; if that matters to you, move the schedule to 8:30. The API's terms ask you to credit the source wherever the rates appear, which is why the email ends with "Rates By Exchange Rate API". These are reference rates; for actual currency exchange, go by your bank's posted rates.

If you'd rather not configure each node by hand, copy the JSON below and paste it onto the n8n canvas with Ctrl or Cmd + V. The four nodes appear already connected. You still need to pick an SMTP credential in Send Email, replace the sender and recipient addresses, and check the Timezone in workflow settings. We imported this JSON into n8n 2.43.8 and ran the first three nodes; Edit Fields returned ready-to-use figures such as 32.59, 20.70, and 36.46.

{
  "name": "Daily exchange rate email",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "triggerAtHour": 8,
              "triggerAtMinute": 0
            }
          ]
        }
      },
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "url": "https://open.er-api.com/v6/latest/USD",
        "options": {}
      },
      "name": "HTTP Request",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        220,
        0
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "f1",
              "name": "usd",
              "value": "={{ $json.rates.TWD.toFixed(2) }}",
              "type": "string"
            },
            {
              "id": "f2",
              "name": "jpy100",
              "value": "={{ ($json.rates.TWD / $json.rates.JPY * 100).toFixed(2) }}",
              "type": "string"
            },
            {
              "id": "f3",
              "name": "eur",
              "value": "={{ ($json.rates.TWD / $json.rates.EUR).toFixed(2) }}",
              "type": "string"
            },
            {
              "id": "f4",
              "name": "updated",
              "value": "={{ DateTime.fromSeconds($json.time_last_update_unix).setZone('Asia/Taipei').toFormat('yyyy/MM/dd HH:mm') }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "name": "Edit Fields",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        440,
        0
      ]
    },
    {
      "parameters": {
        "fromEmail": "[email protected]",
        "toEmail": "[email protected]",
        "subject": "=Exchange rates for {{ $now.toFormat('yyyy-MM-dd') }}",
        "html": "=<p>Good morning. Here are today's reference rates (TWD per unit of foreign currency):</p>\n<p>USD: {{ $json.usd }}<br>JPY (per 100): {{ $json.jpy100 }}<br>EUR: {{ $json.eur }}</p>\n<p><small>Last updated: {{ $json.updated }} (Taipei time)<br>Rates By <a href=\"https://www.exchangerate-api.com\">Exchange Rate API</a></small></p>",
        "options": {
          "appendAttribution": false
        }
      },
      "name": "Send Email",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        660,
        0
      ]
    }
  ],
  "connections": {
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "HTTP Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request": {
      "main": [
        [
          {
            "node": "Edit Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields": {
      "main": [
        [
          {
            "node": "Send Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Schedule didn't run? Check in this order

Most of the time the cause is one of the first three items. Working down the list usually finds it within minutes:

  1. Is it published? In the workflow list, check that the status is Published, then open the workflow and see whether the Publish button is lit, which means there are unpublished changes. If you edited the schedule without republishing, production is still on the old settings.
  2. Is there anything in Executions? Open the Executions tab above the canvas. A failed execution means the schedule fired and a later node broke, so open it and read which node failed and why. If there is nothing at all, keep going.
  3. Is it off by 12 or 13 hours? If executions exist at the wrong time, go back to the time zone section. The fastest check is the Timezone field in the Schedule Trigger output.
  4. Are successful runs being saved? If Save successful production executions is set to Do not save in workflow settings, or the server has EXECUTIONS_DATA_SAVE_ON_SUCCESS=none, successful runs leave no record and look like they never happened. Check the results instead, such as the email or the spreadsheet.
  5. Was n8n running at that moment? Before 2.45, the default scheduler didn't persist schedules, so anything due while the server was rebooting or the container was updating was skipped, not caught up. From 2.45.0, the durable scheduler writes upcoming runs to the database first, so a run that is slightly late (within 60 seconds by default) still fires. Beyond that, the If Execution Is Missed option in the Schedule Trigger's Settings tab decides whether to run one catch-up execution; the default is not to. Only Schedule Trigger nodes added in 2.36 or later have this option.
  6. Is the cron or date itself wrong? With Months set to the 29th through 31st, months without that day are skipped. A malformed cron shows Invalid cron expression. If you build the cron from n8n variables, their values are read only at publish time, so after changing a variable, unpublish and publish again.
  7. Are too many workflows firing at once? If a self-hosted instance sets N8N_CONCURRENCY_PRODUCTION_LIMIT, executions beyond the limit wait in a queue, and you'll see delays when many workflows fire on the hour. Stagger the less urgent ones by a few minutes.

Avoid duplicate runs and runaway execution logs

When a schedule runs twice, the cause is usually in the setup. The first is a workflow copied for testing, with both copies published, so each sends its own email. The second is a migration: the new server has imported and published the workflows while the old one is still running, so the same schedule fires on both. After a move, unpublish everything on the old server or shut it down. The third is overlapping Trigger Rules in the same Schedule Trigger, such as one for 9 a.m. daily and another for 9 a.m. on weekdays, which fires twice every weekday.

Another case is an execution that takes longer than the interval. The Schedule Trigger starts a new execution when the time comes and doesn't wait for the previous one to finish, so a workflow triggered every minute that takes three minutes to run ends up with several overlapping executions processing the same data. Keep the interval well above the workflow's normal run time. For workflows that process new records, add a Remove Duplicates node with the operation Remove Items Processed in Previous Executions, keyed on a unique value such as an order number, so nothing gets processed twice.

Frequency also decides how fast your execution log grows. Every 15 minutes is 96 executions a day, close to 2,900 a month. Every minute is 1,440 a day, more than 40,000 a month, and each one stores the input and output of every node. Self-hosted n8n deletes executions older than 336 hours (14 days) and keeps at most 10,000 by default, and high-frequency schedules hit that cap quickly while the database keeps growing. The most effective fix for scheduled workflows is setting Save successful production executions to Do not save in workflow settings while still saving failures, so you can investigate when something breaks. For a full cleanup guide, see n8n execution data growth and cleanup.

Finally, ask whether the job really needs a schedule. If you poll every minute for new orders and the other system can notify you when an order arrives, a webhook is faster and creates almost no wasted executions; see n8n webhooks and third-party integrations. Schedules are best for work that is naturally periodic: daily and weekly reports, month-end jobs, regular data backups, and reminders at fixed times. To have AI summarize the data you pull each morning, add an AI node after the schedule; the n8n AI Agent tutorial shows how.

If you'd rather not manage the server

Once a schedule is set up, the headaches usually come from the server side: a reboot in the middle of the night, a container update, a full disk, and the schedule quietly stops for days before anyone notices. Self-hosting means owning time zones, version updates, backups, and execution log cleanup yourself. If you'd rather focus on the workflows, RoamerHost's fully managed n8n runs in an isolated container preset to Taiwan time, with a dedicated HTTPS subdomain and webhook URL, no limits on executions or workflows, automatic updates and backups, and Taiwan-based support. It costs NT$5,988 billed yearly (avg. NT$499/mo). To see what self-hosting really costs, read the self-hosting cost breakdown.

FAQ

Q: Can the Schedule Trigger run every few seconds?

Yes. Set Trigger Interval to Seconds to run anywhere from every 1 to every 59 seconds. Cron also accepts a sixth field at the front for seconds, so */10 * * * * * runs every 10 seconds. Keep in mind that second-level schedules grow the execution log very quickly, so a webhook or a minute-level interval is usually the better choice.

Q: Can one workflow run at several different times?

Yes. In the Schedule Trigger, click Add Rule to add a second or third Trigger Rule, for example 9 a.m. on weekdays and noon on Saturdays. The workflow runs whenever any rule comes due. Just make sure the rules don't overlap, or the workflow will run twice at the same moment.

Q: Execute workflow worked, so why didn't it run on its own the next day?

Execute workflow is a manual test and has nothing to do with the schedule. Schedules only start after the workflow is published: in 2.x click Publish above the canvas and wait for Published; in 1.x switch on the Active toggle. If you change anything after publishing, publish again so production picks up the new settings.

Q: I set GENERIC_TIMEZONE, so why is the schedule still at the wrong time?

First check whether that workflow's settings specify a different Timezone, because the workflow setting overrides the instance. Then confirm you recreated the container after changing the environment variable, rather than just reloading the page. If the wrong time comes from a Code node, it is probably using new Date(); switch to $now, which follows n8n's time zone.

Sources

Ready to get started with n8n?

60 seconds after you subscribe, n8n is installed for you — an isolated container with hard resource limits you never share, and HTTPS out of the box.

Billed yearly · no cancellation fee · cancel renewal anytime

Hi, I'm Roamer! Tap me anytime with a question and I'll help you out.

Roamer

Roamer - AI assistant

Online
Roamer

Ask me anything, anytime — I'll do my best to help!

Powered by RoamerHost AI