Files
n8n/packages/cli/src/workflows/workflow-execution.service.ts
T

911 lines
28 KiB
TypeScript
Raw 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 { Logger } from '@n8n/backend-common';
import { EventService, InstanceWriteAccessService } from '@n8n/backend-services';
import { GlobalConfig, WorkflowsConfig } from '@n8n/config';
import type {
Project,
User,
CreateExecutionPayload,
WorkflowEntity,
PollLeaseFence,
} from '@n8n/db';
import { WorkflowRepository } from '@n8n/db';
import { Service } from '@n8n/di';
import { ensureError } from '@n8n/utils/errors/ensure-error';
import type { IDeferredPromise } from '@n8n/utils/promise/deferred-promise';
import type { Response } from 'express';
import {
DirectedGraph,
ErrorReporter,
ExecutionContextService,
anyReachableRootHasRunData,
} from 'n8n-core';
import type {
IExecuteData,
IExecuteResponsePromiseData,
INode,
INodeExecutionData,
IPinData,
IRunExecutionData,
IWorkflowExecuteAdditionalData,
WorkflowExecuteMode,
IWorkflowExecutionDataProcess,
IWorkflowBase,
PollCursor,
} from 'n8n-workflow';
import {
OperationalError,
SubworkflowOperationError,
UnexpectedError,
Workflow,
createRunExecutionData,
} from 'n8n-workflow';
import { ExecutionAlreadyResumingError } from '@/errors/execution-already-resuming.error';
import { PreExecuteBlockedError } from '@/errors/pre-execute-blocked.error';
import { ForbiddenError } from '@n8n/errors';
import { ExecutionCrashService } from '@/executions/execution-crash.service';
import { ExecutionPersistence } from '@/executions/execution-persistence';
import { FailedRunFactory } from '@/executions/failed-run-factory';
import { SubworkflowPolicyChecker } from '@/executions/pre-execution-checks';
import type { IWorkflowErrorData } from '@/interfaces';
import { NodeTypes } from '@/node-types';
import { OwnershipService } from '@/services/ownership.service';
import { TestWebhooks } from '@/webhooks/test-webhooks';
import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data';
import { WorkflowRunner } from '@/workflow-runner';
import { PollCursorService } from '@/workflows/triggers/poll-cursor.service';
import { getWorkflowProjectDetailsSafe } from '@/workflows/utils';
import { WorkflowPublishedDataService } from '@/workflows/workflow-published-data.service';
import type { WorkflowRequest } from '@/workflows/workflow.request';
@Service()
export class WorkflowExecutionService {
constructor(
private readonly logger: Logger,
private readonly errorReporter: ErrorReporter,
private readonly executionPersistence: ExecutionPersistence,
private readonly workflowRepository: WorkflowRepository,
private readonly nodeTypes: NodeTypes,
private readonly testWebhooks: TestWebhooks,
private readonly workflowRunner: WorkflowRunner,
private readonly globalConfig: GlobalConfig,
private readonly subworkflowPolicyChecker: SubworkflowPolicyChecker,
private readonly failedRunFactory: FailedRunFactory,
private readonly eventService: EventService,
private readonly ownershipService: OwnershipService,
private readonly executionContextService: ExecutionContextService,
private readonly workflowsConfig: WorkflowsConfig,
private readonly workflowPublishedDataService: WorkflowPublishedDataService,
private readonly pollCursorService: PollCursorService,
private readonly executionCrashService: ExecutionCrashService,
private readonly instanceWriteAccess: InstanceWriteAccessService,
) {}
async runWorkflow(
workflowData: IWorkflowBase,
node: INode,
data: INodeExecutionData[][],
additionalData: IWorkflowExecuteAdditionalData,
mode: WorkflowExecuteMode,
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
deduplicationKey?: string,
callerAwaitsOutcome: IWorkflowExecutionDataProcess['callerAwaitsOutcome'] = 'none',
) {
const nodeExecutionStack: IExecuteData[] = [
{
node,
data: {
main: data,
},
source: null,
},
];
const executionData = createRunExecutionData({
executionData: {
nodeExecutionStack,
},
});
const { projectId, projectName } = await getWorkflowProjectDetailsSafe(
this.ownershipService,
workflowData.id,
);
// Start the workflow
const runData: IWorkflowExecutionDataProcess = {
userId: additionalData.userId,
executionMode: mode,
executionData,
workflowData,
deduplicationKey,
projectId,
projectName,
callerAwaitsOutcome,
};
return await this.workflowRunner.run(runData, true, undefined, undefined, responsePromise);
}
/**
* Starts an execution for polled items, committing its row in the same transaction
* as the poll's cursor advance so neither can exist without the other.
*/
async runPolledWorkflow(
workflowData: IWorkflowBase,
node: INode,
data: INodeExecutionData[][],
additionalData: IWorkflowExecuteAdditionalData,
mode: WorkflowExecuteMode,
cursor: PollCursor,
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
fence?: PollLeaseFence,
callerAwaitsOutcome: IWorkflowExecutionDataProcess['callerAwaitsOutcome'] = 'none',
): Promise<string | undefined> {
const nodeExecutionStack: IExecuteData[] = [
{
node,
data: {
main: data,
},
source: null,
},
];
const executionData = createRunExecutionData({
executionData: {
nodeExecutionStack,
},
});
const { projectId, projectName } = await getWorkflowProjectDetailsSafe(
this.ownershipService,
workflowData.id,
);
const runData: IWorkflowExecutionDataProcess = {
userId: additionalData.userId,
executionMode: mode,
executionData,
workflowData,
projectId,
projectName,
callerAwaitsOutcome,
};
// Mask the trigger items before the payload is committed, so the persisted row
// never holds raw header data. `run` below establishes the context again, which
// early-exits once it is in place.
const establishContextError = await this.workflowRunner.establishContextForPersistence(runData);
if (establishContextError) {
this.errorReporter.error(establishContextError, { shouldBeLogged: false });
this.logger.error('Failed to prepare a polled execution, so its cursor was not committed', {
workflowId: workflowData.id,
nodeName: node.name,
error: establishContextError,
});
responsePromise?.reject(ensureError(establishContextError));
return undefined;
}
try {
await this.workflowRunner.prepareNewExecution(runData, true);
} catch (error) {
if (error instanceof PreExecuteBlockedError) {
this.logger.error('Blocked a polled execution before its row was committed', {
workflowId: workflowData.id,
nodeName: node.name,
error: error.cause,
});
responsePromise?.reject(error.cause);
return undefined;
}
throw error;
}
const payload: CreateExecutionPayload = {
data: executionData,
mode,
finished: false,
workflowData,
status: 'new',
workflowId: workflowData.id,
retryOf: runData.retryOf ?? undefined,
tracingContext: runData.tracingContext ?? null,
};
const commitResult = await this.pollCursorService.commitWithExecution({
workflowId: workflowData.id,
nodeId: node.id,
cursor,
payload,
fence,
});
if (commitResult === null) {
this.logger.debug('Poll cursor commit skipped: the poll no longer holds its lease', {
workflowId: workflowData.id,
nodeId: node.id,
nodeName: node.name,
});
responsePromise?.reject(
new OperationalError('Poll cursor commit skipped: the poll no longer holds its lease'),
);
return undefined;
}
const { executionId } = commitResult;
// The row was committed at `new`; `expectedStatus` claims it and moves it to
// running, so a concurrent starter cannot run it a second time.
try {
await this.workflowRunner.run(
runData,
false,
undefined,
{ executionId, expectedStatus: 'new' },
responsePromise,
);
} catch (error) {
if (error instanceof ExecutionAlreadyResumingError) {
this.logger.debug('Polled execution was already claimed, leaving it to its owner', {
executionId,
});
} else {
await this.crashFailedPolledExecution(executionId, error, responsePromise);
}
}
return executionId;
}
/**
* Starts a polled execution on engine v2, then advances the cursor.
*
* The v2 path keeps no control-plane execution row, so the cursor cannot
* commit in the same transaction as the run the way {@link runPolledWorkflow}
* does. It commits after the data plane confirms the run started instead: a
* crash between the two calls can duplicate a poll, never lose one.
* TODO(CAT-4078): add a dedup key once `StartExecutionRequest` supports one.
*/
async runPolledWorkflowV2(
workflowData: IWorkflowBase,
node: INode,
data: INodeExecutionData[][],
additionalData: IWorkflowExecuteAdditionalData,
mode: WorkflowExecuteMode,
cursor: PollCursor,
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
fence?: PollLeaseFence,
): Promise<string> {
const executionId = await this.runWorkflow(
workflowData,
node,
data,
additionalData,
mode,
responsePromise,
);
const committed = await this.pollCursorService.commitCursorOnly({
workflowId: workflowData.id,
nodeId: node.id,
cursor,
fence,
});
if (!committed) {
// The run already started; the lease loss only means a concurrent
// poller may repeat the same window, not that anything was lost.
this.logger.warn(
'Poll cursor commit skipped after its execution already started: the poll no longer holds its lease',
{ workflowId: workflowData.id, nodeId: node.id, nodeName: node.name, executionId },
);
}
return executionId;
}
/**
* Marks a committed row that failed to start as crashed, so it does not sit at
* `new` indefinitely.
*/
private async crashFailedPolledExecution(
executionId: string,
error: unknown,
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
): Promise<void> {
this.errorReporter.error(error, { executionId, shouldBeLogged: false });
this.logger.error('Failed to start the execution committed for a poll', {
executionId,
error,
});
responsePromise?.reject(ensureError(error));
await this.executionCrashService.markAsCrashed(executionId, 'start-failure');
}
private isDestinationNodeATrigger(destinationNode: string, workflow: IWorkflowBase) {
const node = workflow.nodes.find((n) => n.name === destinationNode);
if (node === undefined) {
return false;
}
const nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
return nodeType.description.group.includes('trigger');
}
async executeManually(
workflowData: IWorkflowBase,
payload: WorkflowRequest.ManualRunPayload,
user: User,
pushRef?: string,
n8nAuthCookie?: string,
): Promise<{ executionId: string } | { waitingForWebhook: boolean }> {
this.assertManualExecutionAllowed();
// Check whether this workflow is active.
const workflowIsActive = await this.workflowRepository.isActive(workflowData.id);
// For manual testing always set to not active
workflowData.active = false;
workflowData.activeVersionId = null;
// The UI can send runData alongside triggerToStartFrom, but a trigger
// means a full run, so stale runData must not demote it to a partial
// execution. ManualRunDto already strips it for endpoint traffic; this
// keeps the same precedence for direct callers.
// TODO: Remove once the FE stops sending it (CAT-1808)
if ('triggerToStartFrom' in payload && payload.triggerToStartFrom !== undefined) {
Reflect.deleteProperty(payload, 'runData');
}
let data: IWorkflowExecutionDataProcess | undefined;
// Case 1: Partial execution to a destination node, and we have enough runData to start the execution.
if (isPartialExecution(payload)) {
if (this.partialExecutionFulfilsPreconditions(workflowData, payload)) {
data = {
destinationNode: payload.destinationNode,
executionMode: 'manual',
runData: payload.runData,
pinData: workflowData.pinData,
pushRef,
workflowData,
userId: user.id,
dirtyNodeNames: payload.dirtyNodeNames,
agentRequest: payload.agentRequest,
};
} else {
payload = upgradeToFullManualExecutionFromUnknownTrigger(payload);
}
}
// Case 2: Full execution from a known trigger.
if (isFullExecutionFromKnownTrigger(payload)) {
// We must always register the webhook - even when the Chat Trigger has
// pinned data – because the chat SDK will POST the message to it.
if (
(payload.chatSessionId || triggerHasNoPinnedData(workflowData, payload)) &&
(await this.testWebhooks.needsWebhook({
userId: user.id,
workflowEntity: workflowData,
additionalData: await WorkflowExecuteAdditionalData.getBase({
userId: user.id,
workflowId: workflowData.id,
}),
pushRef,
triggerToStartFrom: payload.triggerToStartFrom,
destinationNode: payload.destinationNode,
chatSessionId: payload.chatSessionId,
workflowIsActive,
n8nAuthCookie,
}))
) {
return { waitingForWebhook: true };
}
data = {
executionMode: 'manual',
pinData: workflowData.pinData,
pushRef,
workflowData,
userId: user.id,
triggerToStartFrom: payload.triggerToStartFrom,
agentRequest: payload.agentRequest,
destinationNode: payload.destinationNode,
};
}
// Case 3: Full execution from an unknown trigger.
if (isFullExecutionFromUnknownTrigger(payload)) {
const pinnedTrigger = this.selectPinnedTrigger(
workflowData,
payload.destinationNode.nodeName,
workflowData.pinData ?? {},
);
if (
pinnedTrigger === undefined &&
(await this.testWebhooks.needsWebhook({
userId: user.id,
workflowEntity: workflowData,
additionalData: await WorkflowExecuteAdditionalData.getBase({
userId: user.id,
workflowId: workflowData.id,
}),
pushRef,
destinationNode: payload.destinationNode,
workflowIsActive,
n8nAuthCookie,
}))
) {
return { waitingForWebhook: true };
}
data = {
executionMode: 'manual',
pinData: workflowData.pinData,
pushRef,
workflowData,
userId: user.id,
agentRequest: payload.agentRequest,
destinationNode: payload.destinationNode,
triggerToStartFrom: pinnedTrigger ? { name: pinnedTrigger.name } : undefined,
};
}
if (data) {
const { projectId, projectName } = await getWorkflowProjectDetailsSafe(
this.ownershipService,
workflowData.id,
);
data.projectId = projectId;
data.projectName = projectName;
data.encryptedRunnerIdentity = n8nAuthCookie
? await this.executionContextService.buildManualExecutionCredentials(n8nAuthCookie)
: undefined;
const offloadingManualExecutionsInQueueMode =
this.globalConfig.executions.mode === 'queue' &&
process.env.OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS === 'true';
/**
* Historically, manual executions in scaling mode ran in the main process,
* so some execution details were never persisted in the database.
*
* Currently, manual executions in scaling mode are offloaded to workers,
* so we persist all details to give workers full access to them.
*/
if (data.executionMode === 'manual' && offloadingManualExecutionsInQueueMode) {
data.executionData = createRunExecutionData({
startData: {
startNodes: data.startNodes,
destinationNode: data.destinationNode,
},
resultData: {
pinData: data.pinData,
// Set this to null so `createRunExecutionData` doesn't initialize it.
// Otherwise this would be treated as a partial execution.
runData: data.runData ?? null,
},
manualData: {
userId: data.userId,
dirtyNodeNames: data.dirtyNodeNames,
triggerToStartFrom: data.triggerToStartFrom,
// A run of a single tool node needs this to reach the worker, or the
// tool runs on empty arguments.
agentRequest: data.agentRequest,
source: data.source,
},
// Set this to null so `createRunExecutionData` doesn't initialize it.
// Otherwise this would be treated as a resumed execution after waiting.
executionData: null,
});
}
const executionId = await this.workflowRunner.run(data);
return { executionId };
}
throw new UnexpectedError('`executeManually` was called with an unexpected payload', {
extra: { payload },
});
}
async executeChatWorkflow(
user: User,
workflowData: IWorkflowBase,
executionData: IRunExecutionData,
httpResponse?: Response,
streamingEnabled?: boolean,
executionMode: WorkflowExecuteMode = 'chat',
pushRef?: string,
) {
const { projectId, projectName } = await getWorkflowProjectDetailsSafe(
this.ownershipService,
workflowData.id,
);
const data: IWorkflowExecutionDataProcess = {
userId: user.id,
executionMode,
workflowData,
executionData,
streamingEnabled,
httpResponse,
pushRef,
projectId,
projectName,
};
const executionId = await this.workflowRunner.run(data, undefined, true);
this.eventService.emit('workflow-executed', {
user: { id: user.id },
workflowId: workflowData.id,
workflowName: workflowData.name,
executionId,
projectId,
projectName,
source: 'chat',
});
return {
executionId,
};
}
/**
* Loads the workflow to run as an error workflow, with its production
* nodes/connections applied. Returns `null` (after logging) when the workflow
* cannot be found or has no published/active version.
*/
private async loadErrorWorkflowData(
workflowId: string,
workflowErrorData: IWorkflowErrorData,
): Promise<WorkflowEntity | null> {
const notActiveError = `Calling Error Workflow for "${workflowErrorData.workflow.id}". Workflow "${workflowId}" is not active and cannot be executed`;
// Load the workflow with its production version in a single query, using
// the published nodes/connections for execution rather than the draft.
if (this.workflowsConfig.useWorkflowPublicationService) {
// Behind the flag, the published version comes from the
// workflow_published_version mapping. A null result means the workflow
// has no published version (not found or not published) — not active.
const publishedData =
await this.workflowPublishedDataService.getPublishedWorkflowData(workflowId);
if (publishedData === null) {
this.logger.error(notActiveError, { workflowId });
return null;
}
const workflowData = publishedData.workflow;
workflowData.nodes = publishedData.publishedVersion.nodes;
workflowData.connections = publishedData.publishedVersion.connections;
return workflowData;
}
const loaded = await this.workflowRepository.get(
{ id: workflowId },
{ relations: ['activeVersion'] },
);
if (loaded === null) {
this.logger.error(
`Calling Error Workflow for "${workflowErrorData.workflow.id}". Could not find workflow "${workflowId}"`,
{ workflowId },
);
return null;
}
if (loaded.activeVersion === null) {
this.logger.error(notActiveError, { workflowId });
return null;
}
loaded.nodes = loaded.activeVersion.nodes;
loaded.connections = loaded.activeVersion.connections;
return loaded;
}
/** Executes an error workflow */
async executeErrorWorkflow(
workflowId: string,
workflowErrorData: IWorkflowErrorData,
runningProject: Project,
): Promise<void> {
// Wrap everything in try/catch to make sure that no errors bubble up and all get caught here
try {
const executionMode = 'error';
const workflowData = await this.loadErrorWorkflowData(workflowId, workflowErrorData);
if (workflowData === null) return;
const workflowInstance = new Workflow({
id: workflowId,
name: workflowData.name,
nodeTypes: this.nodeTypes,
nodes: workflowData.nodes,
connections: workflowData.connections,
active: true,
staticData: workflowData.staticData,
settings: workflowData.settings,
});
try {
const failedNode = workflowErrorData.execution?.lastNodeExecuted
? workflowInstance.getNode(workflowErrorData.execution?.lastNodeExecuted)
: undefined;
await this.subworkflowPolicyChecker.check(
workflowInstance,
workflowErrorData.workflow.id!,
failedNode ?? undefined,
);
} catch (error) {
const initialNode = workflowInstance.getStartNode();
if (initialNode) {
const errorWorkflowPermissionError = new SubworkflowOperationError(
`Another workflow: (ID ${workflowErrorData.workflow.id}) tried to invoke this workflow to handle errors.`,
"Unfortunately current permissions do not allow this. Please check that this workflow's settings allow it to be called by others",
);
// Create a fake execution and save it to DB.
const fakeExecution = this.failedRunFactory.generateFailedExecutionFromError(
'error',
errorWorkflowPermissionError,
initialNode,
);
const fullExecutionData: CreateExecutionPayload = {
data: fakeExecution.data,
mode: fakeExecution.mode,
finished: false,
stoppedAt: new Date(),
workflowData,
waitTill: null,
status: fakeExecution.status,
workflowId: workflowData.id,
};
await this.executionPersistence.create(fullExecutionData);
}
this.logger.info('Error workflow execution blocked due to subworkflow settings', {
erroredWorkflowId: workflowErrorData.workflow.id,
errorWorkflowId: workflowId,
});
return;
}
let node: INode;
let workflowStartNode: INode | undefined;
const { errorTriggerType } = this.globalConfig.nodes;
for (const nodeName of Object.keys(workflowInstance.nodes)) {
node = workflowInstance.nodes[nodeName];
if (node.type === errorTriggerType) {
workflowStartNode = node;
}
}
if (workflowStartNode === undefined) {
this.logger.error(
`Calling Error Workflow for "${workflowErrorData.workflow.id}". Could not find "${errorTriggerType}" in workflow "${workflowId}"`,
);
return;
}
const parentExecution =
workflowErrorData.execution?.id && workflowErrorData.workflow?.id
? {
executionId: workflowErrorData.execution.id,
workflowId: workflowErrorData.workflow.id,
executionContext: workflowErrorData.execution.executionContext,
shouldResume: false, // Error workflows must not resume the failed parent workflow
}
: undefined;
// Can execute without webhook so go on
// Initialize the data of the webhook node
const nodeExecutionStack: IExecuteData[] = [];
nodeExecutionStack.push({
node: workflowStartNode,
data: {
main: [
[
{
json: workflowErrorData,
},
],
],
},
source: null,
...(parentExecution && {
metadata: {
parentExecution,
},
}),
});
const runExecutionData = createRunExecutionData({
executionData: {
nodeExecutionStack,
},
parentExecution,
});
const runData: IWorkflowExecutionDataProcess = {
executionMode,
executionData: runExecutionData,
workflowData,
projectId: runningProject.id,
projectName: runningProject.name,
};
const executionId = await this.workflowRunner.run(runData);
this.eventService.emit('workflow-executed', {
workflowId,
workflowName: workflowData.name,
executionId,
projectId: runningProject.id,
projectName: runningProject.name,
source: 'error',
});
} catch (error) {
this.errorReporter.error(error);
this.logger.error(
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
`Calling Error Workflow for "${workflowErrorData.workflow.id}": "${error.message}"`,
{ workflowId: workflowErrorData.workflow.id },
);
}
}
/**
* Select the pinned trigger node to use as starter for a manual execution.
*
* Finds all pinned trigger nodes in the workflow, then returns the first pinned trigger
* that is a parent of the destination node. Webhook triggers are prioritized over other
* trigger types in the sorting order.
*
* @param workflow The workflow containing the nodes and connections
* @param destinationNode The name of the node to find a pinned trigger for
* @param pinData Pin data mapping node names to their pinned data
* @returns The pinned trigger node if found, undefined otherwise
*/
selectPinnedTrigger(
workflow: IWorkflowBase,
destinationNode: string,
pinData: IPinData,
): INode | undefined {
const allPinnedTriggers = this.findAllPinnedTriggers(workflow, pinData);
if (allPinnedTriggers.length === 0) return undefined;
const destinationParents = new Set(
new Workflow({
nodes: workflow.nodes,
connections: workflow.connections,
active: workflow.activeVersionId !== null,
nodeTypes: this.nodeTypes,
}).getParentNodes(destinationNode),
);
const trigger = allPinnedTriggers.find((a) => destinationParents.has(a.name));
return trigger;
}
private findAllPinnedTriggers(workflow: IWorkflowBase, pinData?: IPinData) {
return workflow.nodes
.filter(
(node) =>
!node.disabled &&
pinData?.[node.name] &&
['trigger', 'webhook'].some((suffix) => node.type.toLowerCase().endsWith(suffix)) &&
node.type !== 'n8n-nodes-base.respondToWebhook',
)
.sort((a) => (a.type.endsWith('webhook') ? -1 : 1));
}
/**
* Checks if there is enough run data to run this as a partial execution and
* that we're not having the edge case that the destination node itself is a
* trigger.
*/
private partialExecutionFulfilsPreconditions(
workflowData: IWorkflowBase,
payload: WorkflowRequest.PartialManualExecutionToDestinationPayload,
): boolean {
// If the destination is a trigger node, we treat it as a full execution.
if (this.isDestinationNodeATrigger(payload.destinationNode.nodeName, workflowData)) {
return false;
}
// If we have enough run data to reach the destination from a trigger it's a partial execution.
// Otherwise it's a full execution.
return anyReachableRootHasRunData(
DirectedGraph.fromNodesAndConnections(workflowData.nodes, workflowData.connections),
payload.destinationNode.nodeName,
payload.runData,
);
}
/** Production triggers still run on a protected instance. Block only user-started manual runs. */
private assertManualExecutionAllowed() {
if (this.instanceWriteAccess.isReadOnly()) {
throw new ForbiddenError(
'Cannot run workflows manually on a protected instance. This instance is in read-only mode.',
);
}
}
}
/**
* Type guard to check if payload is a PartialManualExecutionToDestinationPayload.
*
* A partial execution payload has both `destinationNode` and `runData`.
* This indicates execution to a specific node using existing run data.
*/
function isPartialExecution(
payload: WorkflowRequest.ManualRunPayload,
): payload is WorkflowRequest.PartialManualExecutionToDestinationPayload {
return (
'runData' in payload && payload.runData !== undefined && payload.destinationNode !== undefined
);
}
/**
* Type guard to check if payload is a FullManualExecutionFromKnownTriggerPayload.
*
* A known trigger payload has `triggerToStartFrom` specified but no `runData`.
* This indicates the user has selected a specific trigger node to start the execution from.
*/
function isFullExecutionFromKnownTrigger(
payload: WorkflowRequest.ManualRunPayload,
): payload is WorkflowRequest.FullManualExecutionFromKnownTriggerPayload {
return 'triggerToStartFrom' in payload && payload.triggerToStartFrom !== undefined;
}
/**
* Type guard to check if payload is a FullManualExecutionFromUnknownTriggerPayload.
*
* An unknown trigger payload has a `destinationNode` to work back from,
* but neither `triggerToStartFrom` nor `runData`.
* The trigger will need to be determined.
*/
function isFullExecutionFromUnknownTrigger(
payload: WorkflowRequest.ManualRunPayload,
): payload is WorkflowRequest.FullManualExecutionFromUnknownTriggerPayload {
if ('triggerToStartFrom' in payload && payload.triggerToStartFrom !== undefined) {
return false;
}
if ('runData' in payload && payload.runData !== undefined) {
return false;
}
return payload.destinationNode !== undefined;
}
function triggerHasNoPinnedData(
workflowData: IWorkflowBase,
payload: WorkflowRequest.FullManualExecutionFromKnownTriggerPayload,
) {
return workflowData.pinData?.[payload.triggerToStartFrom.name] === undefined;
}
function upgradeToFullManualExecutionFromUnknownTrigger(
payload: WorkflowRequest.PartialManualExecutionToDestinationPayload,
): WorkflowRequest.FullManualExecutionFromUnknownTriggerPayload {
// If the payload has runData or executionData, remove them to convert to full execution.
return {
destinationNode: payload.destinationNode,
agentRequest: payload.agentRequest,
};
}