feat(editor): Relocate workflow ID expression notice (no-changelog) (#12942)

This commit is contained in:
Milorad FIlipović 2025-01-31 12:32:18 +01:00 committed by GitHub
parent 1ca6a9799a
commit 066908060f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 488 additions and 5 deletions

View file

@ -4,14 +4,21 @@ import type { ILocalLoadOptionsFunctions, ResourceMapperFields } from 'n8n-workf
export async function loadSubWorkflowInputs(
this: ILocalLoadOptionsFunctions,
): Promise<ResourceMapperFields> {
const { fields, subworkflowInfo } = await loadWorkflowInputMappings.bind(this)();
const { fields, subworkflowInfo, dataMode } = await loadWorkflowInputMappings.bind(this)();
let emptyFieldsNotice: string | undefined;
if (fields.length === 0) {
const subworkflowLink = subworkflowInfo?.id
? `<a href="/workflow/${subworkflowInfo?.id}" target="_blank">sub-workflows trigger</a>`
: 'sub-workflows trigger';
switch (dataMode) {
case 'passthrough':
emptyFieldsNotice = `This sub-workflow is set up to receive all input data, without specific inputs the Agent will not be able to pass data to this tool. You can define specific inputs in the ${subworkflowLink}.`;
break;
default:
emptyFieldsNotice = `This sub-workflow will not receive any input when called by your AI node. Define your expected input in the ${subworkflowLink}.`;
break;
}
}
return { fields, emptyFieldsNotice };
}

View file

@ -1471,6 +1471,7 @@ defineExpose({ enterEditMode });
:key="hint.message"
:class="$style.hintCallout"
:theme="hint.type || 'info'"
data-test-id="node-hint"
>
<N8nText v-n8n-html="hint.message" size="small"></N8nText>
</N8nCallout>

View file

@ -130,7 +130,6 @@ export class ExecuteWorkflow implements INodeType {
},
default: '',
required: true,
hint: "Note on using an expression here: if this node is set to run once with all items, they will all be sent to the <em>same</em> workflow. That workflow's ID will be calculated by evaluating the expression for the <strong>first input item</strong>.",
},
// ----------------------------------
// source:localFile
@ -270,6 +269,17 @@ export class ExecuteWorkflow implements INodeType {
],
},
],
hints: [
{
type: 'info',
message:
"Note on using an expression for workflow ID: Since this node is set to run once with all items, they will all be sent to the <em>same</em> workflow. That workflow's ID will be calculated by evaluating the expression for the <strong>first input item</strong>.",
displayCondition:
'={{ $rawParameter.workflowId.startsWith("=") && $parameter.mode === "once" && $nodeVersion >= 1.2 }}',
whenToDisplay: 'always',
location: 'outputPane',
},
],
};
methods = {

View file

@ -164,8 +164,9 @@ export class WorkflowDataProxy {
*
* @private
* @param {string} nodeName The name of the node to query data from
* @param {boolean} [resolveValue=true] If the expression value should get resolved
*/
private nodeParameterGetter(nodeName: string) {
private nodeParameterGetter(nodeName: string, resolveValue = true) {
const that = this;
const node = this.workflow.nodes[nodeName];
@ -223,7 +224,7 @@ export class WorkflowDataProxy {
}
}
if (typeof returnValue === 'string' && returnValue.charAt(0) === '=') {
if (resolveValue && typeof returnValue === 'string' && returnValue.charAt(0) === '=') {
// The found value is an expression so resolve it
return that.workflow.expression.getParameterValue(
returnValue,
@ -1359,6 +1360,7 @@ export class WorkflowDataProxy {
$node: this.nodeGetter(),
$self: this.selfGetter(),
$parameter: this.nodeParameterGetter(this.activeNodeName),
$rawParameter: this.nodeParameterGetter(this.activeNodeName, false),
$prevNode: this.prevNodeGetter(),
$runIndex: this.runIndex,
$mode: this.mode,

View file

@ -604,12 +604,237 @@ const manualTriggerNode: LoadedClass<INodeType> = {
},
};
const executeWorkflowNode: LoadedClass<INodeType> = {
type: {
description: {
name: 'n8n-nodes-base.executeWorkflow',
displayName: 'Execute Sub-workflow',
icon: 'fa:sign-in-alt',
iconColor: 'orange-red',
group: ['transform'],
version: [1, 1.1, 1.2],
subtitle: '={{"Workflow: " + $parameter["workflowId"]}}',
description: 'Execute another workflow',
defaults: { name: 'Execute Workflow', color: '#ff6d5a' },
inputs: [],
outputs: [],
properties: [
{
displayName: 'Operation',
name: 'operation',
type: 'hidden',
noDataExpression: true,
default: 'call_workflow',
options: [{ name: 'Execute a Sub-Workflow', value: 'call_workflow' }],
},
{
displayName:
'This node is out of date. Please upgrade by removing it and adding a new one',
name: 'outdatedVersionWarning',
type: 'notice',
displayOptions: { show: { '@version': [{ _cnd: { lte: 1.1 } }] } },
default: '',
},
{
displayName: 'Source',
name: 'source',
type: 'options',
options: [
{
name: 'Database',
value: 'database',
description: 'Load the workflow from the database by ID',
},
{
name: 'Local File',
value: 'localFile',
description: 'Load the workflow from a locally saved file',
},
{
name: 'Parameter',
value: 'parameter',
description: 'Load the workflow from a parameter',
},
{ name: 'URL', value: 'url', description: 'Load the workflow from an URL' },
],
default: 'database',
description: 'Where to get the workflow to execute from',
displayOptions: { show: { '@version': [{ _cnd: { lte: 1.1 } }] } },
},
{
displayName: 'Source',
name: 'source',
type: 'options',
options: [
{
name: 'Database',
value: 'database',
description: 'Load the workflow from the database by ID',
},
{
name: 'Define Below',
value: 'parameter',
description: 'Pass the JSON code of a workflow',
},
],
default: 'database',
description: 'Where to get the workflow to execute from',
displayOptions: { show: { '@version': [{ _cnd: { gte: 1.2 } }] } },
},
{
displayName: 'Workflow ID',
name: 'workflowId',
type: 'string',
displayOptions: { show: { source: ['database'], '@version': [1] } },
default: '',
required: true,
hint: 'Can be found in the URL of the workflow',
description:
"Note on using an expression here: if this node is set to run once with all items, they will all be sent to the <em>same</em> workflow. That workflow's ID will be calculated by evaluating the expression for the <strong>first input item</strong>.",
},
{
displayName: 'Workflow',
name: 'workflowId',
type: 'workflowSelector',
displayOptions: { show: { source: ['database'], '@version': [{ _cnd: { gte: 1.1 } }] } },
default: '',
required: true,
},
{
displayName: 'Workflow Path',
name: 'workflowPath',
type: 'string',
displayOptions: { show: { source: ['localFile'] } },
default: '',
placeholder: '/data/workflow.json',
required: true,
description: 'The path to local JSON workflow file to execute',
},
{
displayName: 'Workflow JSON',
name: 'workflowJson',
type: 'json',
typeOptions: { rows: 10 },
displayOptions: { show: { source: ['parameter'] } },
default: '\n\n\n',
required: true,
description: 'The workflow JSON code to execute',
},
{
displayName: 'Workflow URL',
name: 'workflowUrl',
type: 'string',
displayOptions: { show: { source: ['url'] } },
default: '',
placeholder: 'https://example.com/workflow.json',
required: true,
description: 'The URL from which to load the workflow from',
},
{
displayName:
'Any data you pass into this node will be output by the Execute Workflow Trigger. <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.executeworkflow/" target="_blank">More info</a>',
name: 'executeWorkflowNotice',
type: 'notice',
default: '',
displayOptions: { show: { '@version': [{ _cnd: { lte: 1.1 } }] } },
},
{
displayName: 'Workflow Inputs',
name: 'workflowInputs',
type: 'resourceMapper',
noDataExpression: true,
default: { mappingMode: 'defineBelow', value: null },
required: true,
typeOptions: {
loadOptionsDependsOn: ['workflowId.value'],
resourceMapper: {
localResourceMapperMethod: 'loadSubWorkflowInputs',
valuesLabel: 'Workflow Inputs',
mode: 'map',
fieldWords: { singular: 'input', plural: 'inputs' },
addAllFields: true,
multiKeyMatch: false,
supportAutoMap: false,
showTypeConversionOptions: true,
},
},
displayOptions: {
show: { source: ['database'], '@version': [{ _cnd: { gte: 1.2 } }] },
hide: { workflowId: [''] },
},
},
{
displayName: 'Mode',
name: 'mode',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Run once with all items',
value: 'once',
description: 'Pass all items into a single execution of the sub-workflow',
},
{
name: 'Run once for each item',
value: 'each',
description: 'Call the sub-workflow individually for each item',
},
],
default: 'once',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
default: {},
placeholder: 'Add option',
options: [
{
displayName: 'Wait For Sub-Workflow Completion',
name: 'waitForSubWorkflow',
type: 'boolean',
default: true,
description:
'Whether the main workflow should wait for the sub-workflow to complete its execution before proceeding',
},
],
},
],
hints: [
{
type: 'info',
message:
"Note on using an expression for workflow ID: if this node is set to run once with all items, they will all be sent to the <em>same</em> workflow. That workflow's ID will be calculated by evaluating the expression for the <strong>first input item</strong>.",
displayCondition:
'={{ $rawParameter.workflowId.startsWith("=") && $nodeVersion >= 1.2 }}',
whenToDisplay: 'always',
location: 'outputPane',
},
],
codex: {
categories: ['Core Nodes'],
subcategories: { 'Core Nodes': ['Helpers', 'Flow'] },
alias: ['n8n', 'call', 'sub', 'workflow', 'sub-workflow', 'subworkflow'],
resources: {
primaryDocumentation: [
{
url: 'https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.executeworkflow/',
},
],
},
},
},
},
sourcePath: '',
};
export class NodeTypes implements INodeTypes {
nodeTypes: INodeTypeData = {
'n8n-nodes-base.stickyNote': stickyNode,
'n8n-nodes-base.set': setNode,
'test.googleSheets': googleSheetsNode,
'test.set': setNode,
'n8n-nodes-base.executeWorkflow': executeWorkflowNode,
'test.setMulti': {
sourcePath: '',
type: {

View file

@ -549,4 +549,45 @@ describe('WorkflowDataProxy', () => {
expect(() => getFromAIProxy().$fromAI('invalid!')).toThrow(ExpressionError);
});
});
describe('$rawParameter', () => {
const fixture = loadFixture('rawParameter');
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'Execute Workflow', 'manual', {
connectionType: NodeConnectionType.Main,
throwOnMissingExecutionData: false,
runIndex: 0,
});
test('returns simple raw parameter value', () => {
expect(proxy.$rawParameter.options).toEqual({
waitForSubWorkflow: '={{ true }}',
});
});
test('returns raw parameter value for resource locator values', () => {
expect(proxy.$rawParameter.workflowId).toEqual('={{ $json.foo }}');
});
test('returns raw parameter value when there is no run data', () => {
const noRunDataProxy = getProxyFromFixture(
fixture.workflow,
{
data: { resultData: { runData: {} } },
mode: 'manual',
startedAt: new Date(),
status: 'success',
},
'Execute Workflow',
'manual',
{
connectionType: NodeConnectionType.Main,
throwOnMissingExecutionData: false,
runIndex: 0,
},
);
expect(noRunDataProxy.$rawParameter.options).toEqual({
waitForSubWorkflow: '={{ true }}',
});
});
});
});

View file

@ -0,0 +1,117 @@
{
"data": {
"startData": {},
"resultData": {
"runData": {
"_custom": {
"type": "reactive",
"stateTypeName": "Reactive",
"value": {
"Manual trigger": [
{
"_custom": {
"type": "reactive",
"stateTypeName": "Reactive",
"value": {
"hints": [],
"startTime": 1738314562475,
"executionTime": 1,
"source": [],
"executionStatus": "success",
"data": { "main": [[{ "json": {}, "pairedItem": { "item": 0 } }]] }
}
}
}
],
"Edit Fields": [
{
"_custom": {
"type": "reactive",
"stateTypeName": "Reactive",
"value": {
"hints": [],
"startTime": 1738314562477,
"executionTime": 0,
"source": [{ "previousNode": "Manual trigger" }],
"executionStatus": "success",
"data": {
"main": [[{ "json": { "foo": "test" }, "pairedItem": { "item": 0 } }]]
}
}
}
}
],
"Execute Workflow": [
{
"hints": [],
"startTime": 1738314562478,
"executionTime": 2,
"source": [{ "previousNode": "Edit Fields" }],
"executionStatus": "error",
"error": {
"level": "error",
"tags": { "packageName": "cli" },
"extra": { "workflowId": "1.2" },
"message": "Workflow does not exist.",
"stack": "Error: Workflow does not exist.\n at getWorkflowData (/Users/miloradfilipovic/workspace/n8n/packages/cli/src/workflow-execute-additional-data.ts:124:10)\n at Object.executeWorkflow (/Users/miloradfilipovic/workspace/n8n/packages/cli/src/workflow-execute-additional-data.ts:155:4)\n at ExecuteContext.executeWorkflow (/Users/miloradfilipovic/workspace/n8n/packages/core/src/execution-engine/node-execution-context/base-execute-context.ts:120:18)\n at ExecuteContext.execute (/Users/miloradfilipovic/workspace/n8n/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.ts:397:50)\n at WorkflowExecute.runNode (/Users/miloradfilipovic/workspace/n8n/packages/core/src/execution-engine/workflow-execute.ts:1097:8)\n at /Users/miloradfilipovic/workspace/n8n/packages/core/src/execution-engine/workflow-execute.ts:1503:27\n at /Users/miloradfilipovic/workspace/n8n/packages/core/src/execution-engine/workflow-execute.ts:2064:11"
}
}
]
}
}
},
"pinData": {},
"lastNodeExecuted": "Execute Workflow",
"error": {
"level": "error",
"tags": { "packageName": "cli" },
"extra": { "workflowId": "1.2" },
"message": "Workflow does not exist.",
"stack": "Error: Workflow does not exist.\n at getWorkflowData (/Users/miloradfilipovic/workspace/n8n/packages/cli/src/workflow-execute-additional-data.ts:124:10)\n at Object.executeWorkflow (/Users/miloradfilipovic/workspace/n8n/packages/cli/src/workflow-execute-additional-data.ts:155:4)\n at ExecuteContext.executeWorkflow (/Users/miloradfilipovic/workspace/n8n/packages/core/src/execution-engine/node-execution-context/base-execute-context.ts:120:18)\n at ExecuteContext.execute (/Users/miloradfilipovic/workspace/n8n/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.ts:397:50)\n at WorkflowExecute.runNode (/Users/miloradfilipovic/workspace/n8n/packages/core/src/execution-engine/workflow-execute.ts:1097:8)\n at /Users/miloradfilipovic/workspace/n8n/packages/core/src/execution-engine/workflow-execute.ts:1503:27\n at /Users/miloradfilipovic/workspace/n8n/packages/core/src/execution-engine/workflow-execute.ts:2064:11"
}
},
"executionData": {
"contextData": {},
"nodeExecutionStack": [
{
"node": {
"parameters": {
"operation": "call_workflow",
"source": "database",
"workflowId": {
"__rl": true,
"mode": "id",
"value": "=1.2",
"cachedResultName": "=1.2"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {},
"matchingColumns": [],
"schema": [],
"attemptToConvertTypes": false,
"convertFieldsToString": true
},
"mode": "once",
"options": {}
},
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [120, -100],
"id": "62717ac7-614d-4e3f-b2ec-1e28688068c4",
"name": "Execute Workflow"
},
"data": { "main": [[{ "json": { "foo": "test" }, "pairedItem": { "item": 0 } }]] },
"source": { "main": [{ "previousNode": "Edit Fields" }] }
}
],
"metadata": {},
"waitingExecution": {},
"waitingExecutionSource": {}
},
"mode": "manual",
"startedAt": "2024-02-08T15:45:18.848Z",
"stoppedAt": "2024-02-08T15:45:18.862Z",
"status": "success"
}
}

View file

@ -0,0 +1,80 @@
{
"nodes": [
{
"id": "804e5ba7-4b1d-48c2-abfa-a36717a9fa66",
"name": "Manual trigger",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [-320, -100],
"parameters": {}
},
{
"id": "f995b1a2-8a49-4f0c-ae0d-8fb4c600cdef",
"name": "Edit Fields",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [-100, -100],
"parameters": {
"assignments": {
"assignments": [
{
"id": "f4d80089-a3d7-470f-8c07-dec07e37f339",
"name": "foo",
"value": "={{ test }}",
"type": "string"
}
]
},
"options": {}
}
},
{
"id": "62717ac7-614d-4e3f-b2ec-1e28688068c4",
"name": "Execute Workflow",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [120, -100],
"parameters": {
"workflowId": {
"__rl": true,
"value": "={{ $json.foo }}",
"mode": "id"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {},
"matchingColumns": [],
"schema": [],
"attemptToConvertTypes": false,
"convertFieldsToString": true
},
"options": { "waitForSubWorkflow": "={{ true }}" }
}
}
],
"connections": {
"Manual trigger": {
"main": [
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
}
]
]
},
"Edit Fields": {
"main": [
[
{
"node": "Execute Workflow",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {}
}