import type { IExecuteFunctions, INodeExecutionData, INodeTypeDescription, INodeProperties, IDisplayOptions, } from 'n8n-workflow'; import { WAIT_TIME_UNLIMITED } from 'n8n-workflow'; import { authenticationProperty, credentialsProperty, defaultWebhookDescription, httpMethodsProperty, optionsProperty, responseBinaryPropertyNameProperty, responseCodeProperty, responseDataProperty, responseModeProperty, } from '../Webhook/description'; import { Webhook } from '../Webhook/Webhook.node'; const displayOnWebhook: IDisplayOptions = { show: { resume: ['webhook'], }, }; export class Wait extends Webhook { authPropertyName = 'incomingAuthentication'; description: INodeTypeDescription = { displayName: 'Wait', name: 'wait', icon: 'fa:pause-circle', group: ['organization'], version: 1, description: 'Wait before continue with execution', defaults: { name: 'Wait', color: '#804050', }, inputs: ['main'], outputs: ['main'], credentials: credentialsProperty(this.authPropertyName), webhooks: [ { ...defaultWebhookDescription, responseData: '={{$parameter["responseData"]}}', path: '={{$parameter["options"]["webhookSuffix"] || ""}}', restartWebhook: true, }, ], properties: [ { displayName: 'Resume', name: 'resume', type: 'options', options: [ { name: 'After Time Interval', value: 'timeInterval', description: 'Waits for a certain amount of time', }, { name: 'At Specified Time', value: 'specificTime', description: 'Waits until a specific date and time to continue', }, { name: 'On Webhook Call', value: 'webhook', description: 'Waits for a webhook call before continuing', }, ], default: 'timeInterval', description: 'Determines the waiting mode to use before the workflow continues', }, { ...authenticationProperty(this.authPropertyName), description: 'If and how incoming resume-webhook-requests to $execution.resumeUrl should be authenticated for additional security', displayOptions: displayOnWebhook, }, // ---------------------------------- // resume:specificTime // ---------------------------------- { displayName: 'Date and Time', name: 'dateTime', type: 'dateTime', displayOptions: { show: { resume: ['specificTime'], }, }, default: '', description: 'The date and time to wait for before continuing', }, // ---------------------------------- // resume:timeInterval // ---------------------------------- { displayName: 'Wait Amount', name: 'amount', type: 'number', displayOptions: { show: { resume: ['timeInterval'], }, }, typeOptions: { minValue: 0, numberPrecision: 2, }, default: 1, description: 'The time to wait', }, { displayName: 'Wait Unit', name: 'unit', type: 'options', displayOptions: { show: { resume: ['timeInterval'], }, }, options: [ { name: 'Seconds', value: 'seconds', }, { name: 'Minutes', value: 'minutes', }, { name: 'Hours', value: 'hours', }, { name: 'Days', value: 'days', }, ], default: 'hours', description: 'The time unit of the Wait Amount value', }, // ---------------------------------- // resume:webhook // ---------------------------------- { displayName: 'The webhook URL will be generated at run time. It can be referenced with the $execution.resumeUrl variable. Send it somewhere before getting to this node. More info', name: 'webhookNotice', type: 'notice', displayOptions: displayOnWebhook, default: '', }, { ...httpMethodsProperty, displayOptions: displayOnWebhook, description: 'The HTTP method of the Webhook call', }, { ...responseCodeProperty, displayOptions: displayOnWebhook, }, { ...responseModeProperty, displayOptions: displayOnWebhook, }, { ...responseDataProperty, displayOptions: { show: { ...responseDataProperty.displayOptions?.show, ...displayOnWebhook.show, }, }, }, { ...responseBinaryPropertyNameProperty, displayOptions: { show: { ...responseBinaryPropertyNameProperty.displayOptions?.show, ...displayOnWebhook.show, }, }, }, { displayName: 'Limit Wait Time', name: 'limitWaitTime', type: 'boolean', default: false, // eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether description: 'If no webhook call is received, the workflow will automatically resume execution after the specified limit type', displayOptions: displayOnWebhook, }, { displayName: 'Limit Type', name: 'limitType', type: 'options', default: 'afterTimeInterval', description: 'Sets the condition for the execution to resume. Can be a specified date or after some time.', displayOptions: { show: { limitWaitTime: [true], ...displayOnWebhook.show, }, }, options: [ { name: 'After Time Interval', description: 'Waits for a certain amount of time', value: 'afterTimeInterval', }, { name: 'At Specified Time', description: 'Waits until the set date and time to continue', value: 'atSpecifiedTime', }, ], }, { displayName: 'Amount', name: 'resumeAmount', type: 'number', displayOptions: { show: { limitType: ['afterTimeInterval'], limitWaitTime: [true], ...displayOnWebhook.show, }, }, typeOptions: { minValue: 0, numberPrecision: 2, }, default: 1, description: 'The time to wait', }, { displayName: 'Unit', name: 'resumeUnit', type: 'options', displayOptions: { show: { limitType: ['afterTimeInterval'], limitWaitTime: [true], ...displayOnWebhook.show, }, }, options: [ { name: 'Seconds', value: 'seconds', }, { name: 'Minutes', value: 'minutes', }, { name: 'Hours', value: 'hours', }, { name: 'Days', value: 'days', }, ], default: 'hours', description: 'Unit of the interval value', }, { displayName: 'Max Date and Time', name: 'maxDateAndTime', type: 'dateTime', displayOptions: { show: { limitType: ['atSpecifiedTime'], limitWaitTime: [true], ...displayOnWebhook.show, }, }, default: '', description: 'Continue execution after the specified date and time', }, { ...optionsProperty, displayOptions: displayOnWebhook, options: [ ...(optionsProperty.options as INodeProperties[]), { displayName: 'Webhook Suffix', name: 'webhookSuffix', type: 'string', default: '', placeholder: 'webhook', description: 'This suffix path will be appended to the restart URL. Helpful when using multiple wait nodes. Note: Does not support expressions.', }, ], }, ], }; async execute(context: IExecuteFunctions): Promise { const resume = context.getNodeParameter('resume', 0) as string; if (resume === 'webhook') { return this.handleWebhookResume(context); } let waitTill: Date; if (resume === 'timeInterval') { const unit = context.getNodeParameter('unit', 0) as string; let waitAmount = context.getNodeParameter('amount', 0) as number; if (unit === 'minutes') { waitAmount *= 60; } if (unit === 'hours') { waitAmount *= 60 * 60; } if (unit === 'days') { waitAmount *= 60 * 60 * 24; } waitAmount *= 1000; waitTill = new Date(new Date().getTime() + waitAmount); } else { // resume: dateTime const dateTime = context.getNodeParameter('dateTime', 0) as string; waitTill = new Date(dateTime); } const waitValue = Math.max(waitTill.getTime() - new Date().getTime(), 0); if (waitValue < 65000) { // If wait time is shorter than 65 seconds leave execution active because // we just check the database every 60 seconds. return new Promise((resolve, _reject) => { setTimeout(() => { resolve([context.getInputData()]); }, waitValue); }); } // If longer than 65 seconds put execution to wait return this.putToWait(context, waitTill); } private async handleWebhookResume(context: IExecuteFunctions) { let waitTill = new Date(WAIT_TIME_UNLIMITED); const limitWaitTime = context.getNodeParameter('limitWaitTime', 0); if (limitWaitTime === true) { const limitType = context.getNodeParameter('limitType', 0); if (limitType === 'afterTimeInterval') { let waitAmount = context.getNodeParameter('resumeAmount', 0) as number; const resumeUnit = context.getNodeParameter('resumeUnit', 0); if (resumeUnit === 'minutes') { waitAmount *= 60; } if (resumeUnit === 'hours') { waitAmount *= 60 * 60; } if (resumeUnit === 'days') { waitAmount *= 60 * 60 * 24; } waitAmount *= 1000; waitTill = new Date(new Date().getTime() + waitAmount); } else { waitTill = new Date(context.getNodeParameter('maxDateAndTime', 0) as string); } } return this.putToWait(context, waitTill); } private async putToWait(context: IExecuteFunctions, waitTill: Date) { await context.putExecutionToWait(waitTill); return [context.getInputData()]; } }