Check for cron expression

Describe the problem/error/question

I have a database with cron expressions (* * * * *) for every user and every channel (email, slack etc). What I want to achieve is, that a workflow runs every 5 minutes, load all users and all channels and check, if the cron expression is in the last 5 minute. If yes, go and do something. If not, do nothing.
Any idea how I can build this within n8n without external apis?

What is the error message (if any)?

Please share your workflow

No workflow at this moment

Share the output returned by the last node

Information on your n8n setup

  • n8n version: 2.12.2
  • Database (default: SQLite): postgresql
  • Running n8n via (Docker, npm, n8n cloud, desktop app): docker

The classic approach here is a Code node that handles the cron-matching logic. Load your users/channels from the DB, then for each row check if the cron expression would have triggered within the last 5 minutes — so between now - 5min and now. On self-hosted n8n you can require('cron-parser') inside a Code node since n8n uses it internally, parse the expression, call .prev() on the parsed interval starting from now, and verify whether that last trigger time falls within your 5-minute window.

Hi @schiker you try doing something like
1 > Schedule Trigger (*/5 * * * * )
2 > Postgres node to load your data of users
3 > Loop Over items with batch size 1 ofc so that it will work according to the data passed down

You code can look like this:

const parser = require('cron-parser');
const now = new Date();
const fiveMinutesAgo = new Date(now.getTime() - 5 * 60 * 1000);

const cronExpr = $json.cron_expression;
const interval = parser.parseExpression(cronExpr, { currentDate: fiveMinutesAgo });
const nextFire = interval.next().toDate();

return [{ json: { ...$json, shouldFire: nextFire <= now } }];

Not sure this would fit in your flow but this is how it should be.
and now after all that you can use an IF node that would check an expression {{ $json.shouldFire === true }} to route matching records to your action, that is what i think can be done although you can skip this IF node and do everything in code node also.

Thank you both. I tried cron-parser already and get this eror:
Module ‘cron-parser’ is disallowed [line 1]

@schiker that error means n8n’s sandbox is blocking external module requires — usually the case on n8n Cloud or when NODE_FUNCTION_ALLOW_EXTERNAL isn’t configured. You can work around it with a minimal pure JS cron checker, no imports needed:

function matchCron(expr, date) {
  const [min, hour, dom, mon, dow] = expr.trim().split(/\s+/);
  const m = (f, v) => f === '*' || (f.includes('/') ? v % +f.split('/')[1] === 0 : f.split(',').map(Number).includes(v));
  return m(min, date.getMinutes()) && m(hour, date.getHours()) && m(dom, date.getDate()) && m(mon, date.getMonth()+1) && m(dow, date.getDay());
}

const now = new Date();
const triggered = [0,1,2,3,4].some(i => {
  const t = new Date(now.getTime() - i * 60000);
  return matchCron($json.cron_expression, t);
});
return [{ json: { ...$json, shouldFire: triggered } }];

This checks each of the last 5 minutes against the expression and covers *, */n, and comma-separated values.

1 Like

The last code works somehow, but on the other side not correct.

My cron value is “0 21 * * *” and I tried to fire the job at 21pm, but nothing happen.

@schiker the most likely cause is timezone — new Date().getHours() in a Code node returns the server’s UTC hour, not your local time. if your n8n instance is running UTC and you expected 0 21 * * * to fire at 21:00 local, the hours won’t match. check your n8n GENERIC_TIMEZONE setting and either adjust the cron hour to the equivalent UTC value, or use new Date().toLocaleString('en-US', {timeZone: 'Europe/Berlin', hour: 'numeric', minute: 'numeric'}) (or whichever tz applies) to extract the local hour/minute instead of relying on getHours()/getMinutes().

The “Module disallowed” error happens because n8n’s Code node sandbox blocks require() calls for external packages by default, even ones that are installed internally — cron-parser included.

The good news: you don’t need it. You can match cron expressions against a time window using pure JS math. Here’s a self-contained approach:

const now = new Date();
const windowMs = 5 * 60 * 1000;
const windowStart = new Date(now.getTime() - windowMs);

const results = [];

for (const item of $input.all()) {
  const { cron_expression, user_id, channel } = item.json;
  
  // Parse the 5 cron fields
  const [minPart, hourPart, domPart, monPart, dowPart] = cron_expression.split(' ');
  
  // Check each minute in the last 5-minute window
  let triggered = false;
  for (let offset = 0; offset < 5; offset++) {
    const checkTime = new Date(windowStart.getTime() + offset * 60000);
    const m = checkTime.getUTCMinutes();
    const h = checkTime.getUTCHours();
    const dom = checkTime.getUTCDate();
    const mon = checkTime.getUTCMonth() + 1;
    const dow = checkTime.getUTCDay();

    const match = (part, val) => {
      if (part === '*') return true;
      if (part.includes('/')) {
        const [, step] = part.split('/');
        return val % parseInt(step) === 0;
      }
      return part.split(',').map(Number).includes(val);
    };

    if (match(minPart, m) && match(hourPart, h) && match(domPart, dom) &&
        match(monPart, mon) && match(dowPart, dow)) {
      triggered = true;
      break;
    }
  }

  if (triggered) {
    results.push({ json: { user_id, channel, cron_expression, triggered: true } });
  }
}

return results;

This handles *, */n (step), and comma-separated values. For most real-world cron expressions that’s enough. If you need ranges (5-10) you can extend the match() function to split on - as well.

One thing worth checking: make sure your DB timestamps and n8n server timezone align. Running everything in UTC (as above) avoids surprise mismatches.

1 Like

Thank you very much! The solution works now and make me very very happy!

Here the final code:

function matchCron(expr, date) {
  const now = new Date(date.toLocaleString("en-US", { timeZone: "Europe/Berlin" }));
  const [min, hour, dom, mon, dow] = expr.trim().split(/\s+/);
  
  const m = (f, v) => 
    f === '*' || 
    (f.includes('/') ? v % +f.split('/')[1] === 0 : f.split(',').map(Number).includes(v));

  return m(min, now.getMinutes()) && 
         m(hour, now.getHours()) && 
         m(dom, now.getDate()) && 
         m(mon, now.getMonth() + 1) && 
         m(dow, now.getDay());
}

const now = new Date();
const triggered = [0, 1, 2, 3, 4].some(i => {
  const t = new Date(now.getTime() - i * 60000);
  return matchCron($json.cron, t);
});

return [{ json: { ...$json, shouldFire: triggered } }];
1 Like

I am not sure if it makes a difference when I mark my comment as “solution” or yours. If it does, please re-paste my code and I will mark your comment as the solution.

@schiker doesn’t make a technical difference — Discourse marks the topic as solved regardless of which post gets the tick. glad the timezone fix worked out!

1 Like

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.