import { Logger } from '@n8n/backend-common'; import { Container } from '@n8n/di'; import { type IRunExecutionData, type Workflow, type WorkflowExecuteMode } from 'n8n-workflow'; import { assertExecutionDataExists, type PreExecutionAdditionalData } 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: * ``` * * @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: PreExecutionAdditionalData | undefined, mode: WorkflowExecuteMode, ): Promise => { assertExecutionDataExists(runExecutionData.executionData, workflow, additionalData, mode); const executionData = runExecutionData.executionData; // Call the execution context service to augment the context with any hook-based data const executionContextService = Container.get(ExecutionContextService); if (executionData.runtimeData) { // Context is already established (e.g. established at webhook mint time, // before the real executionId existed, or resumed from the database). // Bind the now-known executionId to any sealed carrier so credential // resolution can gate on executionPath. No-op for legacy (subject-less) // carriers and when executionId is undefined; idempotent on resume. // A retry reloads the original run's data under a NEW executionId, so its // id must join the sealed carrier's path (allowInherit) or resolution would // reject it. Resume keeps the same id, so the bind stays a no-op regardless. executionData.runtimeData = await executionContextService.maybeBindExecutionId( executionData.runtimeData, additionalData?.executionId, { allowInherit: mode === 'retry' }, ); 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 (additionalData?.encryptedRunnerIdentity) { executionData.runtimeData.credentials = additionalData.encryptedRunnerIdentity; if (executionData.runtimeData.credentials) { executionData.runtimeData = await executionContextService.maybeBindExecutionId( executionData.runtimeData, additionalData.executionId, ); } } 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, }; executionData.runtimeData = await executionContextService.maybeBindExecutionId( executionData.runtimeData, additionalData?.executionId, { allowInherit: true }, ); // The child inherits the parent's context, but its OWN execution record must // still reflect context derived from the child workflow — most importantly // its redaction policy (a policy'd child called by a policy-less parent must // redact its own record). Re-run the global context hooks against the // inherited context so they can merge with it (redaction escalates // strictest-per-channel; the parent's top-down escalation is preserved). const [subExecutionStartItem] = executionData.nodeExecutionStack; if (subExecutionStartItem) { executionData.runtimeData = await Container.get( ExecutionContextService, ).augmentSubExecutionContext(workflow, subExecutionStartItem, executionData.runtimeData); } 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; } // Store basic trigger node info in the context for reference executionData.runtimeData.triggerNode = { name: startItem.node.name, type: startItem.node.type, }; // 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, }; // Bind this execution's id to any inherited sealed carrier so it stays // resolvable within its own execution (mirrors the parentExecution branch). executionData.runtimeData = await executionContextService.maybeBindExecutionId( executionData.runtimeData, additionalData?.executionId, { allowInherit: true }, ); // Re-run the global sub-execution hooks against the child workflow so its own // record reflects context derived from itself (redaction policy, private- // credential flag), merged with the inherited parent context. An error // workflow reaches inheritance through this branch, so without this a // policy'd or private-credential error workflow called by a policy-less // parent would not redact its own record. Mirrors the // `runExecutionData.parentExecution` branch above. executionData.runtimeData = await executionContextService.augmentSubExecutionContext( workflow, startItem, executionData.runtimeData, ); return; } try { 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; } } catch (error) { // Log the error Container.get(Logger).error('Failed to augment execution context with hooks.', { error }); throw error; } };