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",