mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-01 17:27:14 +02:00
191 lines
7.8 KiB
TypeScript
191 lines
7.8 KiB
TypeScript
import { Container } from '@n8n/di';
|
|
import {
|
|
type IWorkflowExecuteAdditionalData,
|
|
type WorkflowExecuteMode,
|
|
type IRunExecutionData,
|
|
type Workflow,
|
|
} from 'n8n-workflow';
|
|
|
|
import { assertExecutionDataExists } from '@/utils/assertions';
|
|
|
|
import { ExecutionContextService } from './execution-context.service';
|
|
|
|
/**
|
|
* Establishes the execution context for a workflow run.
|
|
*
|
|
* This function creates or inherits the execution context that persists throughout the workflow
|
|
* execution lifecycle. The context is stored in `runExecutionData.executionData.runtimeData`.
|
|
*
|
|
* @param workflow - The workflow instance being executed (reserved for future context extraction)
|
|
* @param runExecutionData - The execution data structure that will be mutated to include the execution context
|
|
* @param additionalData - Additional workflow execution data used for validation and future context extraction
|
|
* @param mode - The workflow execution mode (manual, trigger, webhook, error, etc.)
|
|
*
|
|
* @returns Promise that resolves when context has been established
|
|
*
|
|
* @throws {UnexpectedError} When `runExecutionData.executionData` is missing or invalid
|
|
*
|
|
* @remarks
|
|
* ## Context Establishment Strategy
|
|
*
|
|
* The function follows a priority-based approach to establish execution context:
|
|
*
|
|
* ### 1. Preserve Existing Context (Webhook Resume)
|
|
* If `executionData.runtimeData` already exists, the function returns immediately without
|
|
* modification. This preserves context when workflows resume from database (e.g., after
|
|
* waiting for a webhook or manual continuation).
|
|
*
|
|
* ### 2. Inherit from Parent Execution (Sub-workflows)
|
|
* If `runExecutionData.parentExecution` exists, creates a new context by inheriting all
|
|
* fields from the parent context while generating fresh values for:
|
|
* - `establishedAt`: Set to current timestamp
|
|
* - `source`: Set to current execution mode
|
|
* - `parentExecutionId`: Tracks the parent execution ID
|
|
*
|
|
* This applies to sub-workflows invoked via "Execute Workflow" node.
|
|
*
|
|
* ### 3. Inherit from Start Node Metadata (Error Workflows)
|
|
* If `startItem.metadata.parentExecution.executionContext` exists, creates a new context
|
|
* by inheriting from the parent context. This applies to error workflows that need to
|
|
* preserve the original workflow's context.
|
|
*
|
|
* ### 4. Create Fresh Context (New Executions)
|
|
* For new root executions, creates a fresh context with:
|
|
* - `version`: 1
|
|
* - `establishedAt`: Current timestamp
|
|
* - `source`: Current execution mode
|
|
*
|
|
* ## Mutation Behavior
|
|
* This function mutates `runExecutionData.executionData.runtimeData` with the execution context.
|
|
*
|
|
* ## Context Inheritance Pattern
|
|
* When inheriting context, the strategy is:
|
|
* 1. Spread all parent context fields (credentials, custom fields, etc.)
|
|
* 2. Override `establishedAt` with current timestamp
|
|
* 3. Override `source` with current execution mode
|
|
* 4. Add `parentExecutionId` to track lineage
|
|
*
|
|
* This ensures child executions reflect their own timing and mode while preserving
|
|
* contextual information like credentials and authentication state.
|
|
*
|
|
* ## Special Cases
|
|
*
|
|
* ### Chat Trigger Workflows
|
|
* Workflows containing only Chat Trigger nodes have an empty `nodeExecutionStack`.
|
|
* Basic context is still established with version and timestamp.
|
|
*
|
|
* ### Empty Execution Stack
|
|
* If no start item exists and no parent context is available, establishes minimal
|
|
* context (version, timestamp, source) without additional enrichment.
|
|
*
|
|
* ## Future Enhancements
|
|
* The function is designed to support extracting context information from:
|
|
* - Start node parameters (e.g., webhook authentication tokens)
|
|
* - Start node type (trigger, manual, webhook, etc.)
|
|
* - Input data from triggering events
|
|
* - User identification from various sources
|
|
*
|
|
* ## Example Usage
|
|
* ```typescript
|
|
* // New execution
|
|
* await establishExecutionContext(workflow, runExecutionData, additionalData, 'manual');
|
|
* // Context: { version: 1, establishedAt: 1234567890, source: 'manual' }
|
|
*
|
|
* // Sub-workflow execution (with parent context)
|
|
* await establishExecutionContext(workflow, runExecutionData, additionalData, 'trigger');
|
|
* // Context: { ...parentContext, establishedAt: 9876543210, source: 'trigger', parentExecutionId: 'parent-id' }
|
|
*
|
|
* // Resumed execution (webhook wait completed)
|
|
* await establishExecutionContext(workflow, runExecutionData, additionalData, 'webhook');
|
|
* // Context: <preserved from original execution>
|
|
* ```
|
|
*
|
|
* @see IExecutionContextV1 for context structure definition
|
|
* @see IRunExecutionData for execution data structure
|
|
* @see IWorkflowExecuteAdditionalData for additional execution data
|
|
* @see RelatedExecution for parent execution context propagation
|
|
*/
|
|
export const establishExecutionContext = async (
|
|
workflow: Workflow,
|
|
runExecutionData: IRunExecutionData,
|
|
additionalData: IWorkflowExecuteAdditionalData,
|
|
mode: WorkflowExecuteMode,
|
|
): Promise<void> => {
|
|
assertExecutionDataExists(runExecutionData.executionData, workflow, additionalData, mode);
|
|
|
|
const executionData = runExecutionData.executionData;
|
|
|
|
if (executionData.runtimeData) {
|
|
// Context is already established, no further action needed.
|
|
// This can happen, when a workflow is resumed from the database.
|
|
return;
|
|
}
|
|
|
|
// At this point we have established the basic execution context.
|
|
// If a context is already established we overwrite it.
|
|
// This might change depending on the propagation strategy we want to implement in the future.
|
|
executionData.runtimeData = {
|
|
version: 1,
|
|
establishedAt: Date.now(),
|
|
source: mode,
|
|
};
|
|
|
|
if (runExecutionData.parentExecution) {
|
|
// Create a new context by inheriting everything from the parent execution context,
|
|
// except for the establishedAt timestamp which we set to now and the source which we set to the current mode.
|
|
// This ensures that the child execution context reflects the time it was established
|
|
// and the mode in which it is running, while still retaining all other contextual information
|
|
// from the parent execution.
|
|
executionData.runtimeData = {
|
|
...(runExecutionData.parentExecution.executionContext ?? {}),
|
|
...executionData.runtimeData,
|
|
parentExecutionId: runExecutionData.parentExecution.executionId,
|
|
};
|
|
return;
|
|
}
|
|
|
|
// Next, we attempt to extract additional context from the start node of the execution stack.
|
|
const [startItem] = executionData.nodeExecutionStack;
|
|
|
|
// The nodeExecutionStack is typically initialized in one of three ways:
|
|
// 1. run() method: Creates stack with start node (workflow-execute.ts:143-157)
|
|
// 2. runPartialWorkflow2(): Recreates stack from existing runData via recreateNodeExecutionStack()
|
|
// 3. Constructor with executionData: Pre-populated from caller (resume scenarios)
|
|
//
|
|
// However, the stack CAN be legitimately empty for workflows containing only Chat Trigger nodes
|
|
// (see workflow-execute.ts:1368-1369). In such cases, we cannot extract context from a start
|
|
// node, but we should still establish basic execution context.
|
|
//
|
|
// We cannot extract user specific information from the initial item though. So we exit early.
|
|
if (!startItem) {
|
|
return;
|
|
}
|
|
|
|
// We were triggered from a parent execution
|
|
// and can inherit context from there
|
|
if (startItem.metadata?.parentExecution?.executionContext) {
|
|
executionData.runtimeData = {
|
|
...startItem.metadata.parentExecution.executionContext,
|
|
...executionData.runtimeData,
|
|
parentExecutionId: startItem.metadata.parentExecution.executionId,
|
|
};
|
|
return;
|
|
}
|
|
|
|
// Call the execution context service to augment the context with any hook-based data
|
|
const executionContextService = Container.get(ExecutionContextService);
|
|
|
|
const { context, triggerItems } = await executionContextService.augmentExecutionContextWithHooks(
|
|
workflow,
|
|
startItem,
|
|
executionData.runtimeData,
|
|
);
|
|
|
|
executionData.runtimeData = context;
|
|
|
|
// If the trigger items were modified by hooks, update the start item accordingly
|
|
if (triggerItems) {
|
|
startItem.data['main'][0] = triggerItems;
|
|
}
|
|
};
|