first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,28 @@
{
"node": "n8n-nodes-base.wait",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Core Nodes"],
"resources": {
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.wait/"
}
],
"generic": [
{
"label": "How to get started with CRM automation (with 3 no-code workflow ideas",
"icon": "👥",
"url": "https://n8n.io/blog/how-to-get-started-with-crm-automation-and-no-code-workflow-ideas/"
},
{
"label": "7 no-code workflow automations for Amazon Web Services",
"url": "https://n8n.io/blog/aws-workflow-automation/"
}
]
},
"alias": ["pause", "sleep", "delay", "timeout"],
"subcategories": {
"Core Nodes": ["Helpers", "Flow"]
}
}
+625
View File
@@ -0,0 +1,625 @@
import type {
IExecuteFunctions,
INodeExecutionData,
INodeTypeDescription,
INodeProperties,
IDisplayOptions,
IWebhookFunctions,
} from 'n8n-workflow';
import {
NodeConnectionTypes,
WAIT_INDEFINITELY,
FORM_TRIGGER_NODE_TYPE,
tryToParseDateTime,
NodeOperationError,
} from 'n8n-workflow';
import { validateWaitAmount, validateWaitUnit } from './validation';
import { updateDisplayOptions } from '../../utils/utilities';
import {
formDescription,
formFields,
respondWithOptions,
formRespondMode,
formTitle,
appendAttributionToForm,
} from '../Form/common.descriptions';
import { formWebhook } from '../Form/utils/utils';
import {
authenticationProperty,
credentialsProperty,
defaultWebhookDescription,
httpMethodsProperty,
optionsProperty,
responseBinaryPropertyNameProperty,
responseCodeProperty,
responseDataProperty,
responseModeProperty,
} from '../Webhook/description';
import { Webhook } from '../Webhook/Webhook.node';
const toWaitAmount: INodeProperties = {
displayName: 'Wait Amount',
name: 'amount',
type: 'number',
typeOptions: {
minValue: 0,
numberPrecision: 2,
},
default: 1,
description: 'The time to wait',
validateType: 'number',
};
const unitSelector: INodeProperties = {
displayName: 'Wait Unit',
name: 'unit',
type: 'options',
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',
};
const waitTimeProperties: INodeProperties[] = [
{
displayName: 'Limit Wait Time',
name: 'limitWaitTime',
type: 'boolean',
default: false,
description:
'Whether to limit the time this node should wait for a user response before execution resumes',
displayOptions: {
show: {
resume: ['webhook', 'form'],
},
},
},
{
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],
resume: ['webhook', 'form'],
},
},
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],
resume: ['webhook', 'form'],
},
},
typeOptions: {
minValue: 0,
numberPrecision: 2,
},
default: 1,
description: 'The time to wait',
},
{
displayName: 'Unit',
name: 'resumeUnit',
type: 'options',
displayOptions: {
show: {
limitType: ['afterTimeInterval'],
limitWaitTime: [true],
resume: ['webhook', 'form'],
},
},
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],
resume: ['webhook', 'form'],
},
},
default: '',
description: 'Continue execution after the specified date and time',
},
];
const webhookSuffix: INodeProperties = {
displayName: 'Webhook Suffix',
name: 'webhookSuffix',
type: 'string',
default: '',
placeholder: 'webhook',
noDataExpression: true,
description:
'This suffix path will be appended to the restart URL. Helpful when using multiple wait nodes.',
};
const displayOnWebhook: IDisplayOptions = {
show: {
resume: ['webhook'],
},
};
const displayOnFormSubmission = {
show: {
resume: ['form'],
},
};
const onFormSubmitProperties = updateDisplayOptions(displayOnFormSubmission, [
formTitle,
formDescription,
formFields,
formRespondMode,
]);
const onWebhookCallProperties = updateDisplayOptions(displayOnWebhook, [
{
...httpMethodsProperty,
description: 'The HTTP method of the Webhook call',
},
responseCodeProperty,
responseModeProperty,
responseDataProperty,
responseBinaryPropertyNameProperty,
]);
const webhookPath = '={{$parameter["options"]["webhookSuffix"] || ""}}';
const waitingTooltip = (
parameters: { resume: string; options?: Record<string, string> },
resumeUrl: string,
formResumeUrl: string,
) => {
const resume = parameters.resume;
if (['webhook', 'form'].includes(resume as string)) {
const { webhookSuffix } = (parameters.options ?? {}) as { webhookSuffix: string };
const suffix = webhookSuffix && typeof webhookSuffix !== 'object' ? `/${webhookSuffix}` : '';
let message = '';
const url = `${resume === 'form' ? formResumeUrl : resumeUrl}${suffix}`;
if (resume === 'form') {
message = 'Execution will continue when form is submitted on ';
}
if (resume === 'webhook') {
message = 'Execution will continue when webhook is received on ';
}
return `${message}<a href="${url}" target="_blank">${url}</a>`;
}
return 'Execution will continue when wait time is over';
};
export class Wait extends Webhook {
authPropertyName = 'incomingAuthentication';
description: INodeTypeDescription = {
displayName: 'Wait',
name: 'wait',
icon: 'fa:pause-circle',
iconColor: 'crimson',
group: ['organization'],
version: [1, 1.1],
description: 'Wait before continue with execution',
defaults: {
name: 'Wait',
color: '#804050',
},
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: credentialsProperty(this.authPropertyName),
waitingNodeTooltip: `={{ (${waitingTooltip})($parameter, $execution.resumeUrl, $execution.resumeFormUrl) }}`,
webhooks: [
{
...defaultWebhookDescription,
responseData: '={{$parameter["responseData"]}}',
path: webhookPath,
restartWebhook: true,
},
{
name: 'default',
httpMethod: 'GET',
responseMode: 'onReceived',
path: webhookPath,
restartWebhook: true,
isFullPath: true,
nodeType: 'form',
},
{
name: 'default',
httpMethod: 'POST',
responseMode: '={{$parameter["responseMode"]}}',
responseData: '={{$parameter["responseMode"] === "lastNode" ? "noData" : undefined}}',
path: webhookPath,
restartWebhook: true,
isFullPath: true,
nodeType: 'form',
},
],
properties: [
{
displayName: 'Resume',
name: 'resume',
type: 'options',
builderHint: {
message:
'For user approval workflows, consider using nodes with operation: "sendAndWait" (e.g., email, Slack) instead of Wait node. If using "webhook", the URL will be generated at runtime and can be referenced with {{ $execution.resumeUrl }}.',
},
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',
},
{
name: 'On Form Submitted',
value: 'form',
description: 'Waits for a form submission before continuing',
},
],
default: 'timeInterval',
description: 'Determines the waiting mode to use before the workflow continues',
},
{
displayName: 'Authentication',
name: 'incomingAuthentication',
type: 'options',
options: [
{
name: 'Basic Auth',
value: 'basicAuth',
},
{
name: 'None',
value: 'none',
},
],
default: 'none',
description:
'If and how incoming resume-webhook-requests to $execution.resumeFormUrl should be authenticated for additional security',
displayOptions: {
show: {
resume: ['form'],
},
},
},
{
...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',
required: true,
},
// ----------------------------------
// resume:timeInterval
// ----------------------------------
{
...toWaitAmount,
displayOptions: {
show: {
resume: ['timeInterval'],
'@version': [1],
},
},
},
{
...toWaitAmount,
default: 5,
displayOptions: {
show: {
resume: ['timeInterval'],
},
hide: {
'@version': [1],
},
},
},
{
...unitSelector,
displayOptions: {
show: {
resume: ['timeInterval'],
'@version': [1],
},
},
},
{
...unitSelector,
default: 'seconds',
displayOptions: {
show: {
resume: ['timeInterval'],
},
hide: {
'@version': [1],
},
},
},
// ----------------------------------
// resume:webhook & form
// ----------------------------------
{
displayName:
'The webhook URL will be generated at run time. It can be referenced with the <strong>$execution.resumeUrl</strong> variable. Send it somewhere before getting to this node. <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.wait/?utm_source=n8n_app&utm_medium=node_settings_modal-credential_link&utm_campaign=n8n-nodes-base.wait" target="_blank">More info</a>',
name: 'webhookNotice',
type: 'notice',
displayOptions: displayOnWebhook,
default: '',
},
{
displayName:
'The form url will be generated at run time. It can be referenced with the <strong>$execution.resumeFormUrl</strong> variable. Send it somewhere before getting to this node. <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.wait/?utm_source=n8n_app&utm_medium=node_settings_modal-credential_link&utm_campaign=n8n-nodes-base.wait" target="_blank">More info</a>',
name: 'formNotice',
type: 'notice',
displayOptions: displayOnFormSubmission,
default: '',
},
...onFormSubmitProperties,
...onWebhookCallProperties,
...waitTimeProperties,
{
...optionsProperty,
displayOptions: displayOnWebhook,
options: [...(optionsProperty.options as INodeProperties[]), webhookSuffix],
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
displayOptions: {
show: {
resume: ['form'],
},
hide: {
responseMode: ['responseNode'],
},
},
options: [appendAttributionToForm, respondWithOptions, webhookSuffix],
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
displayOptions: {
show: {
resume: ['form'],
},
hide: {
responseMode: ['onReceived', 'lastNode'],
},
},
options: [appendAttributionToForm, webhookSuffix],
},
],
};
async webhook(context: IWebhookFunctions) {
const resume = context.getNodeParameter('resume', 0) as string;
if (resume === 'form') return await formWebhook(context, this.authPropertyName);
return await super.webhook(context);
}
async execute(context: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const resume = context.getNodeParameter('resume', 0) as string;
if (['webhook', 'form'].includes(resume)) {
let hasFormTrigger = false;
if (resume === 'form') {
const parentNodes = context.getParentNodes(context.getNode().name);
hasFormTrigger = parentNodes.some((node) => node.type === FORM_TRIGGER_NODE_TYPE);
}
const returnData = await this.configureAndPutToWait(context);
if (resume === 'form' && hasFormTrigger) {
context.sendResponse({
headers: {
location: context.evaluateExpression('{{ $execution.resumeFormUrl }}', 0),
},
statusCode: 307,
});
}
return returnData;
}
let waitTill: Date;
if (resume === 'timeInterval') {
const unit = context.getNodeParameter('unit', 0);
if (!validateWaitUnit(unit)) {
throw new NodeOperationError(
context.getNode(),
"Invalid wait unit. Valid units are 'seconds', 'minutes', 'hours', or 'days'.",
);
}
let waitAmount = context.getNodeParameter('amount', 0);
if (!validateWaitAmount(waitAmount)) {
throw new NodeOperationError(
context.getNode(),
'Invalid wait amount. Please enter a number that is 0 or greater.',
);
}
if (unit === 'minutes') {
waitAmount *= 60;
}
if (unit === 'hours') {
waitAmount *= 60 * 60;
}
if (unit === 'days') {
waitAmount *= 60 * 60 * 24;
}
waitAmount *= 1000;
// Timezone does not change relative dates, since they are just
// a number of seconds added to the current timestamp
waitTill = new Date(new Date().getTime() + waitAmount);
} else {
try {
const dateTimeStrRaw = context.getNodeParameter('dateTime', 0);
const parsedDateTime = tryToParseDateTime(dateTimeStrRaw, context.getTimezone());
waitTill = parsedDateTime.toUTC().toJSDate();
} catch (e) {
throw new NodeOperationError(
context.getNode(),
'Cannot put execution to wait because `dateTime` parameter is not a valid date. Please pick a specific date and time to wait until.',
);
}
}
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 await new Promise((resolve, _reject) => {
const timer = setTimeout(() => resolve([context.getInputData()]), waitValue);
context.onExecutionCancellation(() => {
clearTimeout(timer);
resolve([context.getInputData()]);
});
});
}
// If longer than 65 seconds put execution to wait
return await this.putToWait(context, waitTill);
}
private async configureAndPutToWait(context: IExecuteFunctions) {
let waitTill = WAIT_INDEFINITELY;
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 await this.putToWait(context, waitTill);
}
private async putToWait(context: IExecuteFunctions, waitTill: Date) {
await context.putExecutionToWait(waitTill);
return [context.getInputData()];
}
}
@@ -0,0 +1,188 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import { mock } from 'jest-mock-extended';
import { DateTime } from 'luxon';
import { NodeOperationError, type IExecuteFunctions } from 'n8n-workflow';
import { Wait } from '../Wait.node';
describe('Execute Wait Node', () => {
let timer: NodeJS.Timeout;
const { clearInterval, setInterval } = global;
const nextDay = DateTime.now().startOf('day').plus({ days: 1 });
beforeAll(() => {
timer = setInterval(() => jest.advanceTimersByTime(1000), 10);
jest.useFakeTimers().setSystemTime(new Date('2025-01-01'));
});
afterAll(() => {
clearInterval(timer);
jest.useRealTimers();
});
test.each([
{ value: 'invalid_date', isValid: false },
{
value: nextDay.toISO(),
isValid: true,
expectedWaitTill: nextDay.toJSDate(),
},
{
value: nextDay.toISO({ includeOffset: true }),
isValid: true,
expectedWaitTill: nextDay.toUTC().toJSDate(),
},
{
value: nextDay.toJSDate(),
isValid: true,
expectedWaitTill: nextDay.toJSDate(),
},
{
value: nextDay,
isValid: true,
expectedWaitTill: nextDay.toJSDate(),
},
])(
'Test Wait Node with specificTime $value and isValid $isValid',
async ({ value, isValid, expectedWaitTill }) => {
const putExecutionToWaitSpy = jest.fn();
const waitNode = new Wait();
const executeFunctionsMock = mock<IExecuteFunctions>({
getNodeParameter: jest.fn().mockImplementation((paramName: string) => {
if (paramName === 'resume') return 'specificTime';
if (paramName === 'dateTime') return value;
}),
getTimezone: jest.fn().mockReturnValue('UTC'),
putExecutionToWait: putExecutionToWaitSpy,
getInputData: jest.fn(),
getNode: jest.fn(),
});
if (isValid) {
await expect(waitNode.execute(executeFunctionsMock)).resolves.not.toThrow();
expect(putExecutionToWaitSpy).toHaveBeenCalledWith(expectedWaitTill);
} else {
await expect(waitNode.execute(executeFunctionsMock)).rejects.toThrow(NodeOperationError);
}
},
);
test('should resolve with input data if canceled', async () => {
const putExecutionToWaitSpy = jest.fn();
const waitNode = new Wait();
let cancelSignal: (() => void) | null = null;
const inputData = [{ json: { test: 'data' } }];
const executeFunctionsMock = mock<IExecuteFunctions>({
getNodeParameter: jest.fn().mockImplementation((paramName: string) => {
if (paramName === 'resume') return 'timeInterval';
if (paramName === 'unit') return 'seconds';
if (paramName === 'amount') return 60;
}),
getTimezone: jest.fn().mockReturnValue('UTC'),
putExecutionToWait: putExecutionToWaitSpy,
getInputData: jest.fn(() => inputData),
getNode: jest.fn(),
onExecutionCancellation: (handler) => {
cancelSignal = handler;
},
});
const waitPromise = waitNode.execute(executeFunctionsMock);
for (let index = 0; index < 20; index++) {
await new Promise((r) => setTimeout(r, 10));
if (cancelSignal !== null) break;
}
expect(cancelSignal).not.toBeNull();
cancelSignal!();
await expect(waitPromise).resolves.toEqual([inputData]);
});
describe('Validation', () => {
describe('Time interval', () => {
it.each([
{
unit: 'seconds',
amount: 300,
expectedWaitTill: () => DateTime.now().plus({ seconds: 300 }).toJSDate(),
},
{
unit: 'minutes',
amount: 2,
expectedWaitTill: () => DateTime.now().plus({ minutes: 2 }).toJSDate(),
},
{
unit: 'hours',
amount: 1,
expectedWaitTill: () => DateTime.now().plus({ hours: 1 }).toJSDate(),
},
{
unit: 'days',
amount: 10,
expectedWaitTill: () => DateTime.now().plus({ days: 10 }).toJSDate(),
},
{
unit: 'seconds',
amount: 0,
mode: 'timeout',
expectedWaitTill: () => DateTime.now().toJSDate(),
},
{
unit: 'seconds',
amount: -10,
error: 'Invalid wait amount. Please enter a number that is 0 or greater.',
},
{
unit: 'years',
amount: 10,
error: "Invalid wait unit. Valid units are 'seconds', 'minutes', 'hours', or 'days'.",
},
{
unit: 'minutes',
amount: 'test',
error: 'Invalid wait amount. Please enter a number that is 0 or greater.',
},
])(
'Validate wait unit: $unit, amount: $amount',
async ({ unit, amount, expectedWaitTill, error, mode }) => {
const putExecutionToWaitSpy = jest.fn();
const waitNode = new Wait();
const inputData = [{ json: { inputData: true } }];
const executeFunctionsMock = mock<IExecuteFunctions>({
getNodeParameter: jest.fn().mockImplementation((paramName: string) => {
if (paramName === 'resume') return 'timeInterval';
if (paramName === 'amount') return amount;
if (paramName === 'unit') return unit;
}),
getTimezone: jest.fn().mockReturnValue('UTC'),
putExecutionToWait: putExecutionToWaitSpy,
getInputData: jest.fn(() => inputData),
getNode: jest.fn(),
});
if (!error) {
if (mode === 'timeout') {
// for short wait times (<65s) a simple timeout is used
const resultPromise = waitNode.execute(executeFunctionsMock);
jest.runAllTimers();
await expect(resultPromise).resolves.toEqual([inputData]);
} else {
// for longer wait times (>=65s) the execution is put to wait
await expect(waitNode.execute(executeFunctionsMock)).resolves.not.toThrow();
expect(putExecutionToWaitSpy).toHaveBeenCalledWith(expectedWaitTill?.());
}
} else {
await expect(waitNode.execute(executeFunctionsMock)).rejects.toThrowError(error);
}
},
);
});
});
new NodeTestHarness().setupTests();
});
@@ -0,0 +1,162 @@
{
"name": "[Unit Test] Wait Node",
"nodes": [
{
"parameters": {},
"id": "76e5dcfd-fdc7-472f-8832-bccc0eb122c0",
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [120, 420]
},
{
"parameters": {
"amount": 42,
"unit": "seconds"
},
"id": "37f2c758-6fb2-43ce-86ae-ca11ec957cbd",
"name": "Wait",
"type": "n8n-nodes-base.wait",
"typeVersion": 1,
"position": [560, 420],
"webhookId": "35edc971-c3e4-48cf-835d-4d73a4fd1fd8"
},
{
"parameters": {
"conditions": {
"number": [
{
"value1": "={{ parseInt($json.afterTimestamp) }}",
"operation": "largerEqual",
"value2": "={{ parseInt($json.startTimestamp) + 42 }}"
}
]
}
},
"id": "c5c53934-2677-4adf-a4df-b32f3b0642a2",
"name": "IF",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [960, 420]
},
{
"parameters": {
"keepOnlySet": true,
"values": {
"boolean": [
{
"name": "success",
"value": true
}
]
},
"options": {}
},
"id": "a78417b6-d3f5-4bbc-916a-d4b9d46961cc",
"name": "Set1",
"type": "n8n-nodes-base.set",
"typeVersion": 1,
"position": [1180, 400]
},
{
"parameters": {
"value": "={{ $now }}",
"dataPropertyName": "afterTimestamp",
"toFormat": "X",
"options": {}
},
"id": "94f042ea-49d5-44ea-9ccf-93dac8d27d4a",
"name": "After",
"type": "n8n-nodes-base.dateTime",
"typeVersion": 1,
"position": [760, 420]
},
{
"parameters": {
"value": "={{ $now }}",
"dataPropertyName": "startTimestamp",
"toFormat": "X",
"options": {}
},
"id": "43f8a396-1bf7-484e-962c-120f677dfa51",
"name": "Before",
"type": "n8n-nodes-base.dateTime",
"typeVersion": 1,
"position": [360, 420]
}
],
"pinData": {
"Set1": [
{
"json": {
"success": true
}
}
]
},
"connections": {
"When clicking \"Execute Workflow\"": {
"main": [
[
{
"node": "Before",
"type": "main",
"index": 0
}
]
]
},
"Wait": {
"main": [
[
{
"node": "After",
"type": "main",
"index": 0
}
]
]
},
"IF": {
"main": [
[
{
"node": "Set1",
"type": "main",
"index": 0
}
]
]
},
"After": {
"main": [
[
{
"node": "IF",
"type": "main",
"index": 0
}
]
]
},
"Before": {
"main": [
[
{
"node": "Wait",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {},
"versionId": "8ed794a0-5c04-4b8a-9a49-02c2c7f8003f",
"id": "500",
"meta": {
"instanceId": "8c8c5237b8e37b006a7adce87f4369350c58e41f3ca9de16196d3197f69eabcd"
},
"tags": []
}
@@ -0,0 +1,8 @@
export function validateWaitAmount(amount: unknown): amount is number {
return typeof amount === 'number' && amount >= 0;
}
export type WaitUnit = 'seconds' | 'minutes' | 'hours' | 'days';
export function validateWaitUnit(unit: unknown): unit is WaitUnit {
return typeof unit === 'string' && ['seconds', 'minutes', 'hours', 'days'].includes(unit);
}