n8n/packages/nodes-base/nodes/Schedule/ScheduleTrigger.node.ts
Mutasem Aldmour 72eca2f398
refactor: Rename node-level builderHint.message to searchHint and propertyHint (#30062)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 13:32:50 +00:00

540 lines
13 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ExecutionsConfig } from '@n8n/config';
import { Container } from '@n8n/di';
import { sendAt } from 'cron';
import moment from 'moment-timezone';
import type {
ITriggerFunctions,
INodeType,
INodeTypeDescription,
ITriggerResponse,
Cron,
} from 'n8n-workflow';
import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
import {
intervalToRecurrence,
recurrenceCheck,
toCronExpression,
validateInterval,
} from './GenericFunctions';
import type { IRecurrenceRule, Rule } from './SchedulerInterface';
export class ScheduleTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Schedule Trigger',
name: 'scheduleTrigger',
icon: 'node:schedule-trigger',
iconColor: 'black',
group: ['trigger', 'schedule'],
version: [1, 1.1, 1.2, 1.3],
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',
},
inputs: [],
outputs: [NodeConnectionTypes.Main],
properties: [
{
displayName:
"This workflow will run on the schedule you define here once you publish it.<br><br>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',
builderHint: {
propertyHint:
'You can add multiple intervals to trigger at different times. Use "Custom (Cron)" for more specific scheduling patterns.',
},
values: [
{
displayName: 'Trigger Interval',
name: 'field',
type: 'options',
default: 'days',
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
options: [
{
name: 'Seconds',
value: 'seconds',
},
{
name: 'Minutes',
value: 'minutes',
},
{
name: 'Hours',
value: 'hours',
},
{
name: 'Days',
value: 'days',
},
{
name: 'Weeks',
value: 'weeks',
},
{
name: 'Months',
value: 'months',
},
{
name: 'Custom (Cron)',
value: 'cronExpression',
},
],
},
{
displayName: 'Seconds Between Triggers',
name: 'secondsInterval',
type: 'number',
default: 30,
displayOptions: {
show: {
field: ['seconds'],
},
},
description: 'Number of seconds between each workflow trigger',
hint: 'Must be in range 1-59',
},
{
displayName: 'Minutes Between Triggers',
name: 'minutesInterval',
type: 'number',
default: 5,
displayOptions: {
show: {
field: ['minutes'],
},
},
description: 'Number of minutes between each workflow trigger',
hint: 'Must be in range 1-59',
},
{
displayName: 'Hours Between Triggers',
name: 'hoursInterval',
type: 'number',
displayOptions: {
show: {
field: ['hours'],
},
},
default: 1,
description: 'Number of hours between each workflow trigger',
hint: 'Must be in range 1-23',
},
{
displayName: 'Days Between Triggers',
name: 'daysInterval',
type: 'number',
displayOptions: {
show: {
field: ['days'],
},
},
default: 1,
description: 'Number of days between each workflow trigger',
hint: 'Must be in range 1-31',
},
{
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 doesnt have this day, the node wont 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: 0,
},
],
default: [0],
},
{
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 <a href="https://crontab.guru/examples.html" target="_blank">here</a>',
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: [Second] [Minute] [Hour] [Day of Month] [Month] [Day of Week]',
},
],
},
],
},
],
};
async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> {
const version = this.getNode().typeVersion;
const { interval: intervals } = this.getNodeParameter('rule', []) as Rule;
const timezone = this.getTimezone();
const staticData = this.getWorkflowStaticData('node') as {
recurrenceRules: number[];
};
if (!staticData.recurrenceRules) {
staticData.recurrenceRules = [];
}
if (version >= 1.3) {
for (let i = 0; i < intervals.length; i++) {
validateInterval(this.getNode(), i, intervals[i]);
}
}
const workflowId = this.getWorkflow().id;
const nodeId = this.getNode().id;
const configDedupEnabled =
Container.get(ExecutionsConfig).scheduledExecutionDeduplicationEnabled;
// The workflowId should always be defined, but if it isn't we skip
// the deduplication key.
const dedupEnabled = configDedupEnabled && Boolean(workflowId);
const executeTrigger = (
recurrence: IRecurrenceRule,
skipRecurrenceCheck = false,
scheduledTime?: Date,
) => {
if (!skipRecurrenceCheck) {
const shouldTrigger = recurrenceCheck(recurrence, staticData.recurrenceRules, timezone);
if (!shouldTrigger) return;
}
const momentTz = moment.tz(timezone);
const resultData = {
timestamp: momentTz.toISOString(true),
'Readable date': momentTz.format('MMMM Do YYYY, h:mm:ss a'),
'Readable time': momentTz.format('h:mm:ss a'),
'Day of week': momentTz.format('dddd'),
Year: momentTz.format('YYYY'),
Month: momentTz.format('MMMM'),
'Day of month': momentTz.format('DD'),
Hour: momentTz.format('HH'),
Minute: momentTz.format('mm'),
Second: momentTz.format('ss'),
Timezone: `${timezone} (UTC${momentTz.format('Z')})`,
};
const deduplicationKey =
dedupEnabled && scheduledTime
? `${workflowId}:${nodeId}:${scheduledTime.toISOString()}`
: undefined;
this.emit(
[this.helpers.returnJsonArray([resultData])],
/* responsePromise= */ undefined,
/* donePromise= */ undefined,
deduplicationKey,
);
};
const nodeKey = `${workflowId ?? ''}:${nodeId}`;
const rules = intervals.map((interval, i) => ({
interval,
cronExpression: toCronExpression(interval, nodeKey),
recurrence: intervalToRecurrence(interval, i),
}));
if (this.getMode() !== 'manual') {
for (const { interval, cronExpression, recurrence } of rules) {
try {
const cron: Cron = {
expression: cronExpression,
recurrence,
};
this.helpers.registerCron(cron, (scheduledTime: Date) =>
executeTrigger(recurrence, /* skipRecurrenceCheck= */ false, scheduledTime),
);
} catch (error) {
if (interval.field === 'cronExpression') {
throw new NodeOperationError(this.getNode(), 'Invalid cron expression', {
description: 'More information on how to build them at https://crontab.guru/',
});
} else {
throw error;
}
}
}
return {};
} else {
const manualTriggerFunction = async () => {
const { interval, cronExpression, recurrence } = rules[0];
if (interval.field === 'cronExpression') {
try {
sendAt(cronExpression);
} catch (error) {
throw new NodeOperationError(this.getNode(), 'Invalid cron expression', {
description: 'More information on how to build them at https://crontab.guru/',
});
}
}
executeTrigger(recurrence, true);
};
return { manualTriggerFunction };
}
}
}