diff --git a/packages/nodes-base/nodes/Cron/Cron.node.json b/packages/nodes-base/nodes/Cron/Cron.node.json
index f6ef185226..fc761179ee 100644
--- a/packages/nodes-base/nodes/Cron/Cron.node.json
+++ b/packages/nodes-base/nodes/Cron/Cron.node.json
@@ -128,7 +128,7 @@
}
]
},
- "alias": ["Time", "Scheduler", "Polling"],
+ "alias": ["Time", "Scheduler", "Polling", "Cron", "Interval"],
"subcategories": {
"Core Nodes": ["Flow"]
}
diff --git a/packages/nodes-base/nodes/Cron/Cron.node.ts b/packages/nodes-base/nodes/Cron/Cron.node.ts
index 718d15f8f0..4607b1b3f6 100644
--- a/packages/nodes-base/nodes/Cron/Cron.node.ts
+++ b/packages/nodes-base/nodes/Cron/Cron.node.ts
@@ -17,6 +17,7 @@ export class Cron implements INodeType {
icon: 'fa:calendar',
group: ['trigger', 'schedule'],
version: 1,
+ hidden: true,
description: 'Triggers the workflow at a specific time',
eventTriggerDescription: '',
activationMessage:
@@ -69,7 +70,9 @@ export class Cron implements INodeType {
const timezone = this.getTimezone();
// Start the cron-jobs
- const cronJobs = cronTimes.map(cronTime => new CronJob(cronTime, executeTrigger, undefined, true, timezone));
+ const cronJobs = cronTimes.map(
+ (cronTime) => new CronJob(cronTime, executeTrigger, undefined, true, timezone),
+ );
// Stop the cron-jobs
async function closeFunction() {
diff --git a/packages/nodes-base/nodes/Interval/Interval.node.ts b/packages/nodes-base/nodes/Interval/Interval.node.ts
index cf36eb15af..57693b38fc 100644
--- a/packages/nodes-base/nodes/Interval/Interval.node.ts
+++ b/packages/nodes-base/nodes/Interval/Interval.node.ts
@@ -13,6 +13,7 @@ export class Interval implements INodeType {
icon: 'fa:hourglass',
group: ['trigger', 'schedule'],
version: 1,
+ hidden: true,
description: 'Triggers the workflow in a given interval',
eventTriggerDescription: '',
activationMessage:
diff --git a/packages/nodes-base/nodes/Schedule/CronInterface.ts b/packages/nodes-base/nodes/Schedule/CronInterface.ts
new file mode 100644
index 0000000000..22b6473cd1
--- /dev/null
+++ b/packages/nodes-base/nodes/Schedule/CronInterface.ts
@@ -0,0 +1,9 @@
+import { IDataObject } from 'n8n-workflow';
+
+export type ICronExpression = [
+ string | Date,
+ string | Date,
+ string | Date,
+ string | Date,
+ string | Date,
+];
diff --git a/packages/nodes-base/nodes/Schedule/ScheduleTrigger.node.json b/packages/nodes-base/nodes/Schedule/ScheduleTrigger.node.json
new file mode 100644
index 0000000000..fcaab357f9
--- /dev/null
+++ b/packages/nodes-base/nodes/Schedule/ScheduleTrigger.node.json
@@ -0,0 +1,18 @@
+{
+ "node": "n8n-nodes-base.scheduleTrigger",
+ "nodeVersion": "1.0",
+ "codexVersion": "1.0",
+ "categories": ["Core Nodes"],
+ "resources": {
+ "primaryDocumentation": [
+ {
+ "url": "https://docs.n8n.io/nodes/n8n-nodes-base.scheduleTrigger/"
+ }
+ ],
+ "generic": []
+ },
+ "alias": ["Time", "Scheduler", "Polling", "Cron", "Interval"],
+ "subcategories": {
+ "Core Nodes": ["Flow"]
+ }
+}
diff --git a/packages/nodes-base/nodes/Schedule/ScheduleTrigger.node.ts b/packages/nodes-base/nodes/Schedule/ScheduleTrigger.node.ts
new file mode 100644
index 0000000000..34833f2e8b
--- /dev/null
+++ b/packages/nodes-base/nodes/Schedule/ScheduleTrigger.node.ts
@@ -0,0 +1,520 @@
+import { ITriggerFunctions } from 'n8n-core';
+import {
+ IDataObject,
+ INodeType,
+ INodeTypeDescription,
+ ITriggerResponse,
+ NodeOperationError,
+} from 'n8n-workflow';
+
+import { CronJob } from 'cron';
+import { ICronExpression } from './CronInterface';
+import moment from 'moment';
+
+export class ScheduleTrigger implements INodeType {
+ description: INodeTypeDescription = {
+ displayName: 'Schedule Trigger',
+ name: 'scheduleTrigger',
+ icon: 'file:schedule.svg',
+ group: ['trigger', 'schedule'],
+ version: 1,
+ description: 'Triggers the workflow on a given schedule',
+ eventTriggerDescription: '',
+ activationMessage:
+ 'Your schedule trigger will now trigger executions on the schedule you have defined.',
+ defaults: {
+ name: 'Schedule Trigger',
+ color: '#00FF00',
+ },
+ // eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node
+ inputs: [],
+ outputs: ['main'],
+ properties: [
+ {
+ displayName:
+ 'This workflow will run on the schedule you define here once you activate it.
For testing, you can also trigger it manually: by going back to the canvas and clicking ‘execute workflow’',
+ name: 'notice',
+ type: 'notice',
+ default: '',
+ },
+ {
+ displayName: 'Trigger Rules',
+ name: 'rule',
+ placeholder: 'Add Rule',
+ type: 'fixedCollection',
+ typeOptions: {
+ multipleValues: true,
+ },
+ default: {
+ interval: [
+ {
+ field: 'days',
+ },
+ ],
+ },
+ options: [
+ {
+ name: 'interval',
+ displayName: 'Trigger Interval',
+ values: [
+ {
+ displayName: 'Trigger Interval',
+ name: 'field',
+ type: 'options',
+ default: 'days',
+ options: [
+ {
+ name: 'Custom (Cron)',
+ value: 'cronExpression',
+ },
+ {
+ name: 'Days',
+ value: 'days',
+ },
+ {
+ name: 'Hours',
+ value: 'hours',
+ },
+ {
+ name: 'Minutes',
+ value: 'minutes',
+ },
+ {
+ name: 'Months',
+ value: 'months',
+ },
+ {
+ name: 'Seconds',
+ value: 'seconds',
+ },
+ {
+ name: 'Weeks',
+ value: 'weeks',
+ },
+ ],
+ },
+ {
+ displayName: 'Seconds Between Triggers',
+ name: 'secondsInterval',
+ type: 'number',
+ default: 30,
+ displayOptions: {
+ show: {
+ field: ['seconds'],
+ },
+ },
+ description: 'Number of seconds between each workflow trigger',
+ },
+ {
+ displayName: 'Minutes Between Triggers',
+ name: 'minutesInterval',
+ type: 'number',
+ default: 5,
+ displayOptions: {
+ show: {
+ field: ['minutes'],
+ },
+ },
+ description: 'Number of minutes between each workflow trigger',
+ },
+ {
+ displayName: 'Hours Between Triggers',
+ name: 'hoursInterval',
+ type: 'number',
+ displayOptions: {
+ show: {
+ field: ['hours'],
+ },
+ },
+ default: 1,
+ description: 'Number of hours between each workflow trigger',
+ },
+ {
+ displayName: 'Days Between Triggers',
+ name: 'daysInterval',
+ type: 'number',
+ displayOptions: {
+ show: {
+ field: ['days'],
+ },
+ },
+ default: 1,
+ description: 'Number of days between each workflow trigger',
+ },
+ {
+ displayName: 'Weeks Between Triggers',
+ name: 'weeksInterval',
+ type: 'number',
+ displayOptions: {
+ show: {
+ field: ['weeks'],
+ },
+ },
+ default: 1,
+ description: 'Would run every week unless specified otherwise',
+ },
+ {
+ displayName: 'Months Between Triggers',
+ name: 'monthsInterval',
+ type: 'number',
+ displayOptions: {
+ show: {
+ field: ['months'],
+ },
+ },
+ default: 1,
+ description: 'Would run every month unless specified otherwise',
+ },
+ {
+ displayName: 'Trigger at Day of Month',
+ name: 'triggerAtDayOfMonth',
+ type: 'number',
+ displayOptions: {
+ show: {
+ field: ['months'],
+ },
+ },
+ typeOptions: {
+ minValue: 1,
+ maxValue: 31,
+ },
+ default: 1,
+ description: 'The day of the month to trigger (1-31)',
+ hint: 'If a month doesn’t have this day, the node won’t trigger',
+ },
+ {
+ displayName: 'Trigger on Weekdays',
+ name: 'triggerAtDay',
+ type: 'multiOptions',
+ displayOptions: {
+ show: {
+ field: ['weeks'],
+ },
+ },
+ typeOptions: {
+ maxValue: 7,
+ },
+ options: [
+ {
+ name: 'Monday',
+ value: 1,
+ },
+ {
+ name: 'Tuesday',
+ value: 2,
+ },
+ {
+ name: 'Wednesday',
+ value: 3,
+ },
+ {
+ name: 'Thursday',
+ value: 4,
+ },
+ {
+ name: 'Friday',
+ value: 5,
+ },
+
+ {
+ name: 'Saturday',
+ value: 6,
+ },
+ {
+ name: 'Sunday',
+ value: 7,
+ },
+ ],
+ default: [7],
+ },
+ {
+ displayName: 'Trigger at Hour',
+ name: 'triggerAtHour',
+ type: 'options',
+ default: 0,
+ displayOptions: {
+ show: {
+ field: ['days', 'weeks', 'months'],
+ },
+ },
+ options: [
+ {
+ name: 'Midnight',
+ displayName: 'Midnight',
+ value: 0,
+ },
+ {
+ name: '1am',
+ displayName: '1am',
+ value: 1,
+ },
+ {
+ name: '2am',
+ displayName: '2am',
+ value: 2,
+ },
+ {
+ name: '3am',
+ displayName: '3am',
+ value: 3,
+ },
+ {
+ name: '4am',
+ displayName: '4am',
+ value: 4,
+ },
+ {
+ name: '5am',
+ displayName: '5am',
+ value: 5,
+ },
+ {
+ name: '6am',
+ displayName: '6am',
+ value: 6,
+ },
+ {
+ name: '7am',
+ displayName: '7am',
+ value: 7,
+ },
+ {
+ name: '8am',
+ displayName: '8am',
+ value: 8,
+ },
+ {
+ name: '9am',
+ displayName: '9am',
+ value: 9,
+ },
+ {
+ name: '10am',
+ displayName: '10am',
+ value: 10,
+ },
+ {
+ name: '11am',
+ displayName: '11am',
+ value: 11,
+ },
+ {
+ name: 'Noon',
+ displayName: 'Noon',
+ value: 12,
+ },
+ {
+ name: '1pm',
+ displayName: '1pm',
+ value: 13,
+ },
+ {
+ name: '2pm',
+ displayName: '2pm',
+ value: 14,
+ },
+ {
+ name: '3pm',
+ displayName: '3pm',
+ value: 15,
+ },
+ {
+ name: '4pm',
+ displayName: '4pm',
+ value: 16,
+ },
+ {
+ name: '5pm',
+ displayName: '5pm',
+ value: 17,
+ },
+ {
+ name: '6pm',
+ displayName: '6pm',
+ value: 18,
+ },
+ {
+ name: '7pm',
+ displayName: '7pm',
+ value: 19,
+ },
+ {
+ name: '8pm',
+ displayName: '8pm',
+ value: 20,
+ },
+ {
+ name: '9pm',
+ displayName: '9pm',
+ value: 21,
+ },
+ {
+ name: '10pm',
+ displayName: '10pm',
+ value: 22,
+ },
+ {
+ name: '11pm',
+ displayName: '11pm',
+ value: 23,
+ },
+ ],
+ description: 'The hour of the day to trigger',
+ },
+ {
+ displayName: 'Trigger at Minute',
+ name: 'triggerAtMinute',
+ type: 'number',
+ default: 0,
+ displayOptions: {
+ show: {
+ field: ['hours', 'days', 'weeks', 'months'],
+ },
+ },
+ typeOptions: {
+ minValue: 0,
+ maxValue: 59,
+ },
+ description: 'The minute past the hour to trigger (0-59)',
+ },
+ {
+ displayName:
+ 'You can find help generating your cron expression here',
+ name: 'notice',
+ type: 'notice',
+ displayOptions: {
+ show: {
+ field: ['cronExpression'],
+ },
+ },
+ default: '',
+ },
+ {
+ displayName: 'Expression',
+ name: 'expression',
+ type: 'string',
+ default: '',
+ placeholder: 'eg. 0 15 * 1 sun',
+ displayOptions: {
+ show: {
+ field: ['cronExpression'],
+ },
+ },
+ hint: 'Format: [Minute] [Hour] [Day of Month] [Month] [Day of Week]',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+
+ async trigger(this: ITriggerFunctions): Promise {
+ const rule = this.getNodeParameter('rule', []) as IDataObject;
+ const interval = rule.interval as IDataObject[];
+ const timezone = this.getTimezone();
+ const date = moment.tz(timezone).week();
+ const cronJobs: CronJob[] = [];
+ let intervalObj: NodeJS.Timeout;
+ const executeTrigger = () => {
+ const resultData = {
+ timestamp: moment.tz(timezone).toISOString(true),
+ 'Readable date': moment.tz(timezone).format('MMMM Do YYYY, h:mm:ss a'),
+ 'Readable time': moment.tz(timezone).format('h:mm:ss a'),
+ 'Day of week': moment.tz(timezone).format('dddd'),
+ Year: moment.tz(timezone).format('YYYY'),
+ Month: moment.tz(timezone).format('MMMM'),
+ 'Day of month': moment.tz(timezone).format('DD'),
+ Hour: moment.tz(timezone).format('HH'),
+ Minute: moment.tz(timezone).format('mm'),
+ Second: moment.tz(timezone).format('ss'),
+ Timezone: moment.tz(timezone).format('z Z'),
+ };
+ this.emit([this.helpers.returnJsonArray([resultData])]);
+ };
+ for (let i = 0; i < interval.length; i++) {
+ let intervalValue = 1000;
+ if (interval[i].field === 'cronExpression') {
+ const cronExpression = interval[i].expression as string;
+ try {
+ const cronJob = new CronJob(cronExpression, executeTrigger, undefined, true, timezone);
+ cronJobs.push(cronJob);
+ } catch (error) {
+ throw new NodeOperationError(this.getNode(), 'Invalid cron expression', {
+ description: 'More information on how to build them at http://www.cronmaker.com',
+ });
+ }
+ }
+
+ if (interval[i].field === 'seconds') {
+ const seconds = interval[i].secondsInterval as number;
+ intervalValue *= seconds;
+ intervalObj = setInterval(executeTrigger, intervalValue);
+ }
+
+ if (interval[i].field === 'minutes') {
+ const minutes = interval[i].minutesInterval as number;
+ intervalValue *= 60 * minutes;
+ intervalObj = setInterval(executeTrigger, intervalValue);
+ }
+
+ if (interval[i].field === 'hours') {
+ const hour = interval[i].triggerAtHour?.toString() as string;
+ const minute = interval[i].triggerAtMinute?.toString() as string;
+ const week = interval[i].triggerAtWeek as number;
+ const cronTimes: ICronExpression = [minute, hour, `*/${week * 7}`, '*', '*'];
+ const cronExpression = cronTimes.join(' ');
+ const cronJob = new CronJob(cronExpression, executeTrigger, undefined, true, timezone);
+ cronJobs.push(cronJob);
+ }
+
+ if (interval[i].field === 'days') {
+ const day = interval[i].daysInterval?.toString() as string;
+ const hour = interval[i].triggerAtHour?.toString() as string;
+ const minute = interval[i].triggerAtMinute?.toString() as string;
+ const cronTimes: ICronExpression = [minute, hour, `*/${day}`, '*', '*'];
+ const cronExpression: string = cronTimes.join(' ');
+ const cronJob = new CronJob(cronExpression, executeTrigger, undefined, true, timezone);
+ cronJobs.push(cronJob);
+ }
+
+ if (interval[i].field === 'weeks') {
+ const days = interval[i].triggerAtDay as IDataObject[];
+ const day = days.join(',') as string;
+ const hour = interval[i].triggerAtHour?.toString() as string;
+ const minute = interval[i].triggerAtMinute?.toString() as string;
+ const cronTimes: ICronExpression = [minute, hour, '*', '*', day];
+ const cronExpression: string = cronTimes.join(' ');
+ const cronJob = new CronJob(cronExpression, executeTrigger, undefined, true, timezone);
+ cronJobs.push(cronJob);
+ }
+
+ if (interval[i].field === 'months') {
+ const month = interval[i].monthsInterval?.toString() as string;
+ const day = interval[i].triggerAtDayOfMonth?.toString() as string;
+ const hour = interval[i].triggerAtHour?.toString() as string;
+ const minute = interval[i].triggerAtMinute?.toString() as string;
+ const cronTimes: ICronExpression = [minute, hour, day, `*/${month}`, '*'];
+ const cronExpression: string = cronTimes.join(' ');
+ const cronJob = new CronJob(cronExpression, executeTrigger, undefined, true, timezone);
+ cronJobs.push(cronJob);
+ }
+ }
+
+ async function closeFunction() {
+ for (const cronJob of cronJobs) {
+ cronJob.stop();
+ }
+ clearInterval(intervalObj);
+ }
+
+ async function manualTriggerFunction() {
+ executeTrigger();
+ }
+
+ return {
+ closeFunction,
+ manualTriggerFunction,
+ };
+ }
+}
diff --git a/packages/nodes-base/nodes/Schedule/schedule.svg b/packages/nodes-base/nodes/Schedule/schedule.svg
new file mode 100644
index 0000000000..b496e09d6b
--- /dev/null
+++ b/packages/nodes-base/nodes/Schedule/schedule.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json
index 9dfc6ac9ce..dc91d5d6c7 100644
--- a/packages/nodes-base/package.json
+++ b/packages/nodes-base/package.json
@@ -630,6 +630,7 @@
"dist/nodes/S3/S3.node.js",
"dist/nodes/Salesforce/Salesforce.node.js",
"dist/nodes/Salesmate/Salesmate.node.js",
+ "dist/nodes/Schedule/ScheduleTrigger.node.js",
"dist/nodes/SeaTable/SeaTable.node.js",
"dist/nodes/SeaTable/SeaTableTrigger.node.js",
"dist/nodes/SecurityScorecard/SecurityScorecard.node.js",