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
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.
@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.
@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.
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!