diff --git a/packages/core/src/execution-engine/__tests__/execution-context.test.ts b/packages/core/src/execution-engine/__tests__/execution-context.test.ts new file mode 100644 index 00000000000..e18b6096cad --- /dev/null +++ b/packages/core/src/execution-engine/__tests__/execution-context.test.ts @@ -0,0 +1,345 @@ +import { mock } from 'jest-mock-extended'; +import { + UnexpectedError, + type INode, + type IRunExecutionData, + type IWorkflowExecuteAdditionalData, + type Workflow, + type WorkflowExecuteMode, +} from 'n8n-workflow'; + +import { establishExecutionContext } from '../execution-context'; + +describe('establishExecutionContext', () => { + const mockWorkflow = mock({ id: 'test-workflow-id' }); + const mockAdditionalData = mock(); + const mockMode: WorkflowExecuteMode = 'manual'; + + describe('successful context establishment', () => { + it('should establish context with version 1 and timestamp', async () => { + const startNode = mock({ name: 'Start', type: 'n8n-nodes-base.start' }); + const runExecutionData: IRunExecutionData = { + startData: {}, + resultData: { + runData: {}, + }, + executionData: { + contextData: {}, + nodeExecutionStack: [ + { + node: startNode, + data: { + main: [[{ json: {} }]], + }, + source: null, + }, + ], + metadata: {}, + waitingExecution: {}, + waitingExecutionSource: {}, + }, + }; + + const beforeTimestamp = Date.now(); + await establishExecutionContext(mockWorkflow, runExecutionData, mockAdditionalData, mockMode); + const afterTimestamp = Date.now(); + + expect(runExecutionData.executionData!.runtimeData).toBeDefined(); + expect(runExecutionData.executionData!.runtimeData).toHaveProperty('version', 1); + expect(runExecutionData.executionData!.runtimeData).toHaveProperty('establishedAt'); + expect(runExecutionData.executionData!.runtimeData).toHaveProperty('source', 'manual'); + + const establishedAt = runExecutionData.executionData!.runtimeData!.establishedAt; + expect(establishedAt).toBeGreaterThanOrEqual(beforeTimestamp); + expect(establishedAt).toBeLessThanOrEqual(afterTimestamp); + }); + + it('should mutate the provided runExecutionData object', async () => { + const startNode = mock({ name: 'Start', type: 'n8n-nodes-base.start' }); + const runExecutionData: IRunExecutionData = { + startData: {}, + resultData: { + runData: {}, + }, + executionData: { + contextData: {}, + nodeExecutionStack: [ + { + node: startNode, + data: { + main: [[{ json: {} }]], + }, + source: null, + }, + ], + metadata: {}, + waitingExecution: {}, + waitingExecutionSource: {}, + }, + }; + + const referenceBefore = runExecutionData.executionData; + await establishExecutionContext(mockWorkflow, runExecutionData, mockAdditionalData, mockMode); + const referenceAfter = runExecutionData.executionData; + + // Verify the same object reference is used (mutation, not replacement) + expect(referenceBefore).toBe(referenceAfter); + expect(runExecutionData.executionData!.runtimeData).toBeDefined(); + }); + + it('should establish context when execution stack has multiple nodes', async () => { + const startNode = mock({ name: 'Start', type: 'n8n-nodes-base.start' }); + const secondNode = mock({ name: 'Second', type: 'n8n-nodes-base.set' }); + const runExecutionData: IRunExecutionData = { + startData: {}, + resultData: { + runData: {}, + }, + executionData: { + contextData: {}, + nodeExecutionStack: [ + { + node: startNode, + data: { + main: [[{ json: {} }]], + }, + source: null, + }, + { + node: secondNode, + data: { + main: [[{ json: {} }]], + }, + source: null, + }, + ], + metadata: {}, + waitingExecution: {}, + waitingExecutionSource: {}, + }, + }; + + await establishExecutionContext(mockWorkflow, runExecutionData, mockAdditionalData, mockMode); + + expect(runExecutionData.executionData!.runtimeData).toBeDefined(); + expect(runExecutionData.executionData!.runtimeData!.version).toBe(1); + }); + }); + + describe('Chat Trigger workflow support', () => { + it('should establish basic context for empty execution stack (Chat Trigger)', async () => { + const runExecutionData: IRunExecutionData = { + startData: {}, + resultData: { + runData: {}, + }, + executionData: { + contextData: {}, + nodeExecutionStack: [], // Empty stack - Chat Trigger workflow + metadata: {}, + waitingExecution: {}, + waitingExecutionSource: {}, + }, + }; + + const beforeTimestamp = Date.now(); + await establishExecutionContext(mockWorkflow, runExecutionData, mockAdditionalData, mockMode); + const afterTimestamp = Date.now(); + + // Should establish basic context even with empty stack + expect(runExecutionData.executionData!.runtimeData).toBeDefined(); + expect(runExecutionData.executionData!.runtimeData!.version).toBe(1); + expect(runExecutionData.executionData!.runtimeData!.source).toBe('manual'); + expect(runExecutionData.executionData!.runtimeData!.establishedAt).toBeGreaterThanOrEqual( + beforeTimestamp, + ); + expect(runExecutionData.executionData!.runtimeData!.establishedAt).toBeLessThanOrEqual( + afterTimestamp, + ); + }); + + it('should establish basic context without start node extraction for Chat Trigger', async () => { + const runExecutionData: IRunExecutionData = { + startData: {}, + resultData: { + runData: {}, + }, + executionData: { + contextData: {}, + nodeExecutionStack: [], // Empty stack + metadata: {}, + waitingExecution: {}, + waitingExecutionSource: {}, + }, + }; + + await establishExecutionContext(mockWorkflow, runExecutionData, mockAdditionalData, mockMode); + + const context = runExecutionData.executionData!.runtimeData; + + // Verify context has only basic properties (no start-node-specific extraction) + expect(Object.keys(context!)).toEqual(['version', 'establishedAt', 'source']); + expect(typeof context!.version).toBe('number'); + expect(typeof context!.establishedAt).toBe('number'); + expect(context!.source).toBe('manual'); + }); + }); + + describe('error handling', () => { + it('should throw error when executionData is missing', async () => { + const runExecutionData: IRunExecutionData = { + startData: {}, + resultData: { + runData: {}, + }, + // executionData is missing + }; + + await expect( + establishExecutionContext(mockWorkflow, runExecutionData, mockAdditionalData, mockMode), + ).rejects.toThrow(UnexpectedError); + }); + + it('should throw error when executionData is undefined', async () => { + const runExecutionData: IRunExecutionData = { + startData: {}, + resultData: { + runData: {}, + }, + executionData: undefined, + }; + + await expect( + establishExecutionContext(mockWorkflow, runExecutionData, mockAdditionalData, mockMode), + ).rejects.toThrow(UnexpectedError); + }); + }); + + describe('context structure validation', () => { + it('should create context with correct structure', async () => { + const startNode = mock({ name: 'Webhook', type: 'n8n-nodes-base.webhook' }); + const runExecutionData: IRunExecutionData = { + startData: {}, + resultData: { + runData: {}, + }, + executionData: { + contextData: {}, + nodeExecutionStack: [ + { + node: startNode, + data: { + main: [[{ json: { test: 'data' } }]], + }, + source: null, + }, + ], + metadata: {}, + waitingExecution: {}, + waitingExecutionSource: {}, + }, + }; + + await establishExecutionContext(mockWorkflow, runExecutionData, mockAdditionalData, mockMode); + + const context = runExecutionData.executionData!.runtimeData; + + // Verify context has only expected properties + expect(Object.keys(context!)).toEqual(['version', 'establishedAt', 'source']); + expect(typeof context!.version).toBe('number'); + expect(typeof context!.establishedAt).toBe('number'); + expect(context!.source).toBe('manual'); + }); + + it('should create unique timestamps for different executions', async () => { + const startNode = mock({ name: 'Start', type: 'n8n-nodes-base.start' }); + + const runExecutionData1: IRunExecutionData = { + startData: {}, + resultData: { runData: {} }, + executionData: { + contextData: {}, + nodeExecutionStack: [ + { + node: startNode, + data: { main: [[{ json: {} }]] }, + source: null, + }, + ], + metadata: {}, + waitingExecution: {}, + waitingExecutionSource: {}, + }, + }; + + const runExecutionData2: IRunExecutionData = { + startData: {}, + resultData: { runData: {} }, + executionData: { + contextData: {}, + nodeExecutionStack: [ + { + node: startNode, + data: { main: [[{ json: {} }]] }, + source: null, + }, + ], + metadata: {}, + waitingExecution: {}, + waitingExecutionSource: {}, + }, + }; + + await establishExecutionContext( + mockWorkflow, + runExecutionData1, + mockAdditionalData, + mockMode, + ); + // Small delay to ensure different timestamps + await new Promise((resolve) => setTimeout(resolve, 5)); + await establishExecutionContext( + mockWorkflow, + runExecutionData2, + mockAdditionalData, + mockMode, + ); + + const timestamp1 = runExecutionData1.executionData!.runtimeData!.establishedAt; + const timestamp2 = runExecutionData2.executionData!.runtimeData!.establishedAt; + + expect(timestamp1).toBeLessThan(timestamp2); + }); + + it('should work with different execution modes', async () => { + const startNode = mock({ name: 'Trigger', type: 'n8n-nodes-base.cron' }); + const modes: WorkflowExecuteMode[] = ['manual', 'trigger', 'webhook']; + + for (const mode of modes) { + const runExecutionData: IRunExecutionData = { + startData: {}, + resultData: { runData: {} }, + executionData: { + contextData: {}, + nodeExecutionStack: [ + { + node: startNode, + data: { main: [[{ json: {} }]] }, + source: null, + }, + ], + metadata: {}, + waitingExecution: {}, + waitingExecutionSource: {}, + }, + }; + + await establishExecutionContext(mockWorkflow, runExecutionData, mockAdditionalData, mode); + + expect(runExecutionData.executionData!.runtimeData).toBeDefined(); + expect(runExecutionData.executionData!.runtimeData!.version).toBe(1); + expect(runExecutionData.executionData!.runtimeData!.source).toBe(mode); + } + }); + }); +}); diff --git a/packages/core/src/execution-engine/__tests__/workflow-execute.test.ts b/packages/core/src/execution-engine/__tests__/workflow-execute.test.ts index 74f37645a20..91ef4c0bdd4 100644 --- a/packages/core/src/execution-engine/__tests__/workflow-execute.test.ts +++ b/packages/core/src/execution-engine/__tests__/workflow-execute.test.ts @@ -122,6 +122,14 @@ describe('WorkflowExecute', () => { expect(result.finished).toEqual(true); expect(result.data.executionData!.contextData).toEqual({}); expect(result.data.executionData!.nodeExecutionStack).toEqual([]); + // Check if execution context was established + expect(result.data.executionData!.runtimeData).toBeDefined(); + expect(result.data.executionData!.runtimeData).toHaveProperty('version', 1); + expect(result.data.executionData!.runtimeData).toHaveProperty('establishedAt'); + expect(result.data.executionData!.runtimeData).toHaveProperty('source'); + expect(result.data.executionData!.runtimeData!.source).toEqual('manual'); + expect(typeof result.data.executionData!.runtimeData!.establishedAt).toBe('number'); + expect(result.data.executionData!.runtimeData!.establishedAt).toBeGreaterThan(0); }); } }); @@ -191,6 +199,15 @@ describe('WorkflowExecute', () => { expect(result.finished).toEqual(true); expect(result.data.executionData!.contextData).toEqual({}); expect(result.data.executionData!.nodeExecutionStack).toEqual([]); + + // Check if execution context was established + expect(result.data.executionData!.runtimeData).toBeDefined(); + expect(result.data.executionData!.runtimeData).toHaveProperty('version', 1); + expect(result.data.executionData!.runtimeData).toHaveProperty('establishedAt'); + expect(result.data.executionData!.runtimeData).toHaveProperty('source'); + expect(result.data.executionData!.runtimeData!.source).toEqual('manual'); + expect(typeof result.data.executionData!.runtimeData!.establishedAt).toBe('number'); + expect(result.data.executionData!.runtimeData!.establishedAt).toBeGreaterThan(0); }); } }); @@ -405,6 +422,15 @@ describe('WorkflowExecute', () => { expect(result.finished).toEqual(true); // expect(result.data.executionData!.contextData).toEqual({}); //Fails when test workflow Includes splitInbatches expect(result.data.executionData!.nodeExecutionStack).toEqual([]); + + // Check if execution context was established + expect(result.data.executionData!.runtimeData).toBeDefined(); + expect(result.data.executionData!.runtimeData).toHaveProperty('version', 1); + expect(result.data.executionData!.runtimeData).toHaveProperty('establishedAt'); + expect(result.data.executionData!.runtimeData).toHaveProperty('source'); + expect(result.data.executionData!.runtimeData!.source).toEqual('manual'); + expect(typeof result.data.executionData!.runtimeData!.establishedAt).toBe('number'); + expect(result.data.executionData!.runtimeData!.establishedAt).toBeGreaterThan(0); }); } }); diff --git a/packages/core/src/execution-engine/execution-context.ts b/packages/core/src/execution-engine/execution-context.ts new file mode 100644 index 00000000000..32d6c941159 --- /dev/null +++ b/packages/core/src/execution-engine/execution-context.ts @@ -0,0 +1,119 @@ +import { + type IWorkflowExecuteAdditionalData, + type WorkflowExecuteMode, + type IRunExecutionData, + type Workflow, +} from 'n8n-workflow'; + +import { assertExecutionDataExists } from '@/utils/assertions'; + +/** + * Establishes the execution context for a workflow run. + * + * This function creates and initializes the execution context that persists throughout + * the workflow execution lifecycle. The context is stored directly in the provided + * `runExecutionData.executionData.runtimeData` object. + * + * @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, etc.) + * + * @returns Promise that resolves when context has been established + * + * @throws {UnexpectedError} When `runExecutionData.executionData` is missing or invalid + * + * @remarks + * ## Mutation Behavior + * This function mutates the provided `runExecutionData` object by setting: + * - `runExecutionData.executionData.runtimeData` with the newly created execution context + * + * ## Context Creation + * - Creates a new context with version 1 schema and current timestamp + * - Establishes basic context for all workflows (including Chat Trigger workflows) + * - Supports future extraction of context information from start node (when available) + * - Validates execution data structure before context creation + * + * ## Chat Trigger Support + * Workflows containing only Chat Trigger nodes have an empty `nodeExecutionStack`. + * In such cases, basic context (version + timestamp) is still established, but no + * start-node-specific context extraction is performed. + * + * ## 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 + * await establishExecutionContext(workflow, runExecutionData, additionalData, mode); + * // Context is now available in: runExecutionData.executionData.runtimeData + * ``` + * + * @see IExecutionContextV1 for context structure definition + * @see IRunExecutionData for execution data structure + * @see IWorkflowExecuteAdditionalData for additional execution data + */ +export const establishExecutionContext = async ( + workflow: Workflow, + runExecutionData: IRunExecutionData, + additionalData: IWorkflowExecuteAdditionalData, + mode: WorkflowExecuteMode, +): Promise => { + assertExecutionDataExists(runExecutionData.executionData, workflow, additionalData, mode); + + const executionData = runExecutionData.executionData; + + // 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, + }; + + // 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; + } + + // TODO: the following comments will be implemented in future iterations + // to extract more context information based on the start node + // and the data that triggered the workflow execution. + // the comments are left here for now to provide more context on how + // this can be achieved. + + // startNodeParameters will hold the parameters of the start node + // this can be the settings for the different hooks to be executed + // for example to extract the bearer token from the start node data. + + // const startNodeParameters = startItem.node.parameters; + + // startNodeType holds the type of the start node + + // const startNodeType = startItem.node.type; + + // Main input data is an array of items, each item represents an event that triggers the workflow execution + // The 'main' selector selects the input name of the nodes, and the 0 index represents the runIndex, + // 0 being the first run of this node in the workflow. + + // const mainInput = startItem.data["main"][0]; + + // based on startNodeParameters, startNodeType and mainInput we can now + // iterate over the different hooks to extract specific data for the runtime context +}; diff --git a/packages/core/src/execution-engine/workflow-execute.ts b/packages/core/src/execution-engine/workflow-execute.ts index 750f3e080a4..1f4a7706d5d 100644 --- a/packages/core/src/execution-engine/workflow-execute.ts +++ b/packages/core/src/execution-engine/workflow-execute.ts @@ -59,8 +59,10 @@ import PCancelable from 'p-cancelable'; import { ErrorReporter } from '@/errors/error-reporter'; import { WorkflowHasIssuesError } from '@/errors/workflow-has-issues.error'; import * as NodeExecuteFunctions from '@/node-execute-functions'; +import { assertExecutionDataExists } from '@/utils/assertions'; import { isJsonCompatible } from '@/utils/is-json-compatible'; +import { establishExecutionContext } from './execution-context'; import type { ExecutionLifecycleHooks } from './execution-lifecycle-hooks'; import { ExecuteContext, PollContext } from './node-execution-context'; import { @@ -1325,22 +1327,6 @@ export class WorkflowExecute { ); } - private assertExecutionDataExists( - this: WorkflowExecute, - executionData: IRunExecutionData['executionData'], - workflow: Workflow, - ): asserts executionData is NonNullable { - if (!executionData) { - throw new UnexpectedError('Failed to run workflow due to missing execution data', { - extra: { - workflowId: workflow.id, - executionId: this.additionalData.executionId, - mode: this.mode, - }, - }); - } - } - /** * Handles executions that have been waiting by * 1. unsetting the `waitTill` @@ -1354,7 +1340,12 @@ export class WorkflowExecute { if (this.runExecutionData.waitTill) { this.runExecutionData.waitTill = undefined; - this.assertExecutionDataExists(this.runExecutionData.executionData, workflow); + assertExecutionDataExists( + this.runExecutionData.executionData, + workflow, + this.additionalData, + this.mode, + ); this.runExecutionData.executionData.nodeExecutionStack[0].node.disabled = true; const lastNodeExecuted = this.runExecutionData.resultData.lastNodeExecuted as string; @@ -1363,7 +1354,12 @@ export class WorkflowExecute { } private checkForWorkflowIssues(workflow: Workflow): void { - this.assertExecutionDataExists(this.runExecutionData.executionData, workflow); + assertExecutionDataExists( + this.runExecutionData.executionData, + workflow, + this.additionalData, + this.mode, + ); // Node execution stack will be empty for an execution containing only Chat // Trigger. const startNode = this.runExecutionData.executionData.nodeExecutionStack.at(0)?.node.name; @@ -1487,6 +1483,14 @@ export class WorkflowExecute { // eslint-disable-next-line complexity const returnPromise = (async () => { try { + // Establish the execution context + await establishExecutionContext( + workflow, + this.runExecutionData, + this.additionalData, + this.mode, + ); + if (!this.additionalData.restartExecutionId) { await hooks.runHook('workflowExecuteBefore', [workflow, this.runExecutionData]); } diff --git a/packages/core/src/utils/__tests__/assertions.test.ts b/packages/core/src/utils/__tests__/assertions.test.ts new file mode 100644 index 00000000000..7164dceac26 --- /dev/null +++ b/packages/core/src/utils/__tests__/assertions.test.ts @@ -0,0 +1,109 @@ +import { mock } from 'jest-mock-extended'; +import type { IRunExecutionData, IWorkflowExecuteAdditionalData, Workflow } from 'n8n-workflow'; +import { UnexpectedError } from 'n8n-workflow'; + +import { assertExecutionDataExists } from '../assertions'; + +describe('assertExecutionDataExists', () => { + const mockWorkflow = mock({ id: 'test-workflow-123' }); + const mockAdditionalData = mock({ + executionId: 'test-execution-456', + }); + const mode = 'manual'; + + describe('when executionData is valid', () => { + it('should not throw when executionData exists', () => { + const validExecutionData = { + contextData: {}, + nodeExecutionStack: [], + metadata: {}, + waitingExecution: {}, + waitingExecutionSource: {}, + }; + + expect(() => + assertExecutionDataExists(validExecutionData, mockWorkflow, mockAdditionalData, mode), + ).not.toThrow(); + }); + + it('should not throw when executionData has minimal properties', () => { + const minimalExecutionData = { + nodeExecutionStack: [], + }; + + expect(() => + assertExecutionDataExists( + minimalExecutionData as unknown as IRunExecutionData['executionData'], + mockWorkflow, + mockAdditionalData, + mode, + ), + ).not.toThrow(); + }); + }); + + describe('when executionData is missing', () => { + it('should throw UnexpectedError when executionData is undefined', () => { + expect(() => + assertExecutionDataExists(undefined, mockWorkflow, mockAdditionalData, mode), + ).toThrow(UnexpectedError); + }); + + it('should throw UnexpectedError when executionData is null', () => { + expect(() => + assertExecutionDataExists( + null as unknown as IRunExecutionData['executionData'], + mockWorkflow, + mockAdditionalData, + mode, + ), + ).toThrow(UnexpectedError); + }); + + it('should throw error with correct message', () => { + expect(() => + assertExecutionDataExists(undefined, mockWorkflow, mockAdditionalData, mode), + ).toThrow(/missing execution data/i); + }); + + it('should include workflow metadata in error', () => { + try { + assertExecutionDataExists(undefined, mockWorkflow, mockAdditionalData, mode); + fail('Expected error to be thrown'); + } catch (error) { + expect(error).toBeInstanceOf(UnexpectedError); + const unexpectedError = error as UnexpectedError; + expect(unexpectedError.extra).toEqual({ + workflowId: 'test-workflow-123', + executionId: 'test-execution-456', + mode: 'manual', + }); + } + }); + }); + + describe('with different execution modes', () => { + it('should work with trigger mode', () => { + const validExecutionData = { nodeExecutionStack: [] }; + + expect(() => + assertExecutionDataExists( + validExecutionData as unknown as IRunExecutionData['executionData'], + mockWorkflow, + mockAdditionalData, + 'trigger', + ), + ).not.toThrow(); + }); + + it('should include mode in error metadata', () => { + try { + assertExecutionDataExists(undefined, mockWorkflow, mockAdditionalData, 'webhook'); + fail('Expected error to be thrown'); + } catch (error) { + const unexpectedError = error as UnexpectedError; + expect(unexpectedError.extra?.mode).toBe('webhook'); + } + }); + }); +}); diff --git a/packages/core/src/utils/assertions.ts b/packages/core/src/utils/assertions.ts new file mode 100644 index 00000000000..9110d0f797e --- /dev/null +++ b/packages/core/src/utils/assertions.ts @@ -0,0 +1,24 @@ +import { + type IRunExecutionData, + type IWorkflowExecuteAdditionalData, + UnexpectedError, + type Workflow, + type WorkflowExecuteMode, +} from 'n8n-workflow'; + +export function assertExecutionDataExists( + executionData: IRunExecutionData['executionData'], + workflow: Workflow, + additionalData: IWorkflowExecuteAdditionalData, + mode: WorkflowExecuteMode, +): asserts executionData is NonNullable { + if (!executionData) { + throw new UnexpectedError('Failed to run workflow due to missing execution data', { + extra: { + workflowId: workflow.id, + executionId: additionalData.executionId, + mode, + }, + }); + } +} diff --git a/packages/workflow/src/execution-context.ts b/packages/workflow/src/execution-context.ts index 862d6852b9f..ee9585ea7df 100644 --- a/packages/workflow/src/execution-context.ts +++ b/packages/workflow/src/execution-context.ts @@ -30,12 +30,33 @@ export const CredentialContextSchema = z */ export type ICredentialContext = z.output; +const WorkflowExecuteModeSchema = z.union([ + z.literal('cli'), + z.literal('error'), + z.literal('integrated'), + z.literal('internal'), + z.literal('manual'), + z.literal('retry'), + z.literal('trigger'), + z.literal('webhook'), + z.literal('evaluation'), + z.literal('chat'), +]); + +export type WorkflowExecuteModeValues = z.infer; + const ExecutionContextSchemaV1 = z.object({ version: z.literal(1), /** * When the context was established (Unix timestamp in milliseconds) */ establishedAt: z.number(), + + /** + * The mode in which the workflow is being executed + */ + source: WorkflowExecuteModeSchema, + /** * Encrypted credential context for dynamic credential resolution * Always encrypted when stored, decrypted on-demand by credential resolver diff --git a/packages/workflow/src/interfaces.ts b/packages/workflow/src/interfaces.ts index 10ef7018a54..f0798f1ec2e 100644 --- a/packages/workflow/src/interfaces.ts +++ b/packages/workflow/src/interfaces.ts @@ -24,11 +24,16 @@ import type { NodeApiError } from './errors/node-api.error'; import type { NodeOperationError } from './errors/node-operation.error'; import type { WorkflowActivationError } from './errors/workflow-activation.error'; import type { WorkflowOperationError } from './errors/workflow-operation.error'; +import type { + IExecutionContext, + WorkflowExecuteModeValues as WorkflowExecuteMode, +} from './execution-context'; import type { ExecutionStatus } from './execution-status'; import type { Result } from './result'; import type { Workflow } from './workflow'; import type { EnvProviderState } from './workflow-data-proxy-env-provider'; -import type { IExecutionContext } from './execution-context'; + +export type { WorkflowExecuteModeValues as WorkflowExecuteMode } from './execution-context'; export interface IAdditionalCredentialOptions { oauth2?: IOAuth2Options; @@ -2723,18 +2728,6 @@ export interface IWorkflowExecuteAdditionalData { ): Promise>; } -export type WorkflowExecuteMode = - | 'cli' - | 'error' - | 'integrated' - | 'internal' - | 'manual' - | 'retry' - | 'trigger' - | 'webhook' - | 'evaluation' - | 'chat'; - export type WorkflowActivateMode = | 'init' | 'create' // unused