Why did n8n remove expression-support in schedule-trigger? 😢

Describe the problem/error/question

Recently I filed a bug report ( Schedule Trigger: Random interval not executed randomly · Issue #35898 · n8n-io/n8n · GitHub ) about the expressions not working in the schedule trigger anymore. But this was closed as “This is partially expected as we don’t support this”. But actually n8n has supported this all the time until just recently.

Who else has been heavily relying on randomized scheduling via expressions here?
Have you found any feasable solution? Why did n8n remove this suuuuper handy feature to use expressions in the scheduled-trigger node?

Please share your workflow

Information on your n8n setup

  • n8n version: 2.31.6
  • Database (default: SQLite): sqlite
  • Running n8n via (Docker, npm, n8n cloud, desktop app): docker
  • Operating system: Linux

Hey @nm5182, while you wait for a response, here are some things that might help:

Suggested resources

Automatically matched to your question.

Docs:

Forum:

@fahmiiireza, @Anshul_Namdev, @Deepankar - you’ve helped with similar issues before, can you take a look?

Automatically suggested by n8n’s community bot. It’s a pilot - please share feedback here.

The issue stems from a change in how n8n handles the Schedule Trigger; it now requires static values for the internal cron-scheduler rather than evaluating expressions dynamically. Because expressions are only processed at the moment of workflow activation, they cannot create a truly “random” recurring trigger. This shift effectively removes the ability to use Math.random() directly within the trigger node to vary execution times.

For high-volume or long-term scheduling, an external database pattern is more robust. By storing target execution dates in a table (like PostgreSQL or Airtable) and using a frequent “checker” workflow to trigger pending tasks, you avoid bloating the n8n database with thousands of suspended executions. This approach is significantly more scalable than relying on internal wait states.

Since you are currently running n8n on SQLite via Docker, be cautious with long-term wait nodes. Large numbers of suspended executions can degrade SQLite performance and may be deleted if your execution TTL settings are too aggressive. For production-grade randomized scheduling over several months, migrating to PostgreSQL is highly recommended to ensure stability.

@kjooleng Thanks for your (AI-generated) response. I already thought about this. But actually I often need to have flows run at random execution dateTimes that are in a range of multiple weeks or months. You can’t let those executions run for weeks or months, waiting for the wait node to complete, since your approach just bloat up the running executions list the server has to handle, causing a lot of resources to get used unnecessarily or maybe even crash the server.

The workflow that you shared indicated otherwise

An answer that did not meet with your requirements does not mean it is

The OP should sometimes reflect if correct info or sufficient context is given

@nm5182 my understanding is that we never officially supported this due to how the triggers are scheduled to run. Did it actually work or did it just appear to work?

Hey @nm5182, good morning!
I hope you’re doing well.

the Schedule Trigger was designed to register a fixed schedule (by interval, time, or cron expression) and trigger an active workflow according to that rule. The official documentation describes it exactly like this: execution at defined intervals and times, similar to cron. Schedule Trigger | Nodes | n8n Docs

I noticed you put Math.Random inside the hour and minute fields, which requires the scheduler itself to re-evaluate a dynamic expression and change the next execution over time. This was never a supported/guaranteed behavior by the Schedule Trigger. (not according to what’s documented).

I think it’s a good idea for a feature enhancement.
There was already a good discussion on the subject in 2013 Schedule node and usage expressions - Questions - n8n Community

Hi @nm5182
Keep the trigger rule fixed and hold the randomness in a next-run date the workflow rolls for itself. Schedule Trigger once a day, then a Code node that decides whether today is the day and picks the next date before it exits:

const store = $getWorkflowStaticData('global');
const now = Date.now();
if (store.nextRun && now < store.nextRun) {
  return [];
}
const days = 1 + Math.floor(Math.random() * 60);
const next = new Date(now + days * 86400000);
next.setHours(9 + Math.floor(Math.random() * 4), Math.floor(Math.random() * 60), 0, 0);
store.nextRun = next.getTime();
return [{ json: { nextRun: next.toISOString() } }];

Returning an empty array ends the branch on the days it isn’t due, so the rest of the workflow runs only on the random date and nothing is held open in between. Change the 60 to widen or narrow the window in days. Static data saves only on a published workflow, so test it published rather than with Execute workflow.

Hello @nm5182,
I remember this used to work in the past, I even suggested one myself here:

Actually, there is a very simple workaround, since the new behaviour is that n8n evaluates the expression only once at publish time, and locks in whatever value it produced:

So you just need to unpublish the workflow then publish it again, so every republish re-evaluates the expression and locks in a fresh, genuinely random value..

And you don’t even need to do that manually of course, using n8n API by just adding the n8n node at the end of the workflow to Unpublish/Publish the workflow, so every run locks in a new random value for the next one,

I tested that and i think it works:

There’s also a workaround inside the cron expression itself (irregular, but not truly random): define multiple seconds (intervals) manually, e.g. 4,8,15,16,23,42 * * * * *, so it fires at those specific seconds every minute, this approach doesn’t need the unpublish/publish trick at all.

A note for anyone taking the static-data approach @Anshul_Namdev suggested above — this is where it bit us in production.

Inside a Code node, new Date() does not honor the workflow/instance timezone; it gives you UTC. The Schedule Trigger does honor the timezone setting. So a daily trigger firing at your local time, compared against a next-run date computed with new Date(), is off by your UTC offset — and it fails quietly, because nothing errors. Use $now, which is timezone-aware:

const staticData = $getWorkflowStaticData('global');
const today = $now.toISODate();               // timezone-aware
if (staticData.nextRun && staticData.nextRun > today) return [];
staticData.nextRun = $now.plus({ days: 7 + Math.floor(Math.random() * 50) }).toISODate();
return items;

One more silent failure worth guarding against if you end up building a weeks-based schedule instead: if weeksInterval is missing from the node parameters, the node validates, the workflow saves, and it simply never fires. There is no error anywhere and you cannot see it from the canvas.

Neither of these answers the “why was it removed” question — they are just the two potholes on the road the workaround takes.

Hi @Jon,
Yes, it actually worked for years. I’ve used it in dozens of flows. As a n8n user I might not know the exact technical details of how n8n works under the hood. I only see that it worked for such a long time. It was amazingly easy and intuitive to set up random schedules via expressions in the schedule-trigger and suddenly it stopped working :cry: I (and while I can’t speak for others I strongly assume there are some) would love to see it back working as intuitive as it was, because that’s at least what I love n8n for. :heart:

@mohamed3nan Thanks a lot for sharing your temporary workaround, that is simple and reliable. The only problem with workarounds is that they always come with trade-offs. In the case of your definately elegant approach it’s the fact that it’s “misusing” the publish functionality to get the desired random-execution-schedule-effect. But if I had e.g. another flow that pushes every flow to a git repo every time it was published, I would create a lot of commits in that repo for the flows that use your workaround.

I guess the point that’s I’m trying to make is: Really thanks a lot for sharing it. And at the same time I would love to see n8n actually bringing back the simple way of using expressions in a schedule-trigger.