diff --git a/packages/frontend/editor-ui/src/components/ParameterInput.vue b/packages/frontend/editor-ui/src/components/ParameterInput.vue
index f84ba68134..083002ff85 100644
--- a/packages/frontend/editor-ui/src/components/ParameterInput.vue
+++ b/packages/frontend/editor-ui/src/components/ParameterInput.vue
@@ -23,7 +23,7 @@ import type {
IParameterLabel,
NodeParameterValueType,
} from 'n8n-workflow';
-import { CREDENTIAL_EMPTY_VALUE, NodeHelpers } from 'n8n-workflow';
+import { CREDENTIAL_EMPTY_VALUE, isResourceLocatorValue, NodeHelpers } from 'n8n-workflow';
import CodeNodeEditor from '@/components/CodeNodeEditor/CodeNodeEditor.vue';
import CredentialsSelect from '@/components/CredentialsSelect.vue';
@@ -38,7 +38,6 @@ import SqlEditor from '@/components/SqlEditor/SqlEditor.vue';
import TextEdit from '@/components/TextEdit.vue';
import { hasExpressionMapping, isValueExpression } from '@/utils/nodeTypesUtils';
-import { isResourceLocatorValue } from '@/utils/typeGuards';
import {
AI_TRANSFORM_NODE_TYPE,
diff --git a/packages/frontend/editor-ui/src/components/ParameterInputFull.vue b/packages/frontend/editor-ui/src/components/ParameterInputFull.vue
index 62cb8b3f26..7644122481 100644
--- a/packages/frontend/editor-ui/src/components/ParameterInputFull.vue
+++ b/packages/frontend/editor-ui/src/components/ParameterInputFull.vue
@@ -12,9 +12,9 @@ import { useToast } from '@/composables/useToast';
import { useNDVStore } from '@/stores/ndv.store';
import { getMappedResult } from '@/utils/mappingUtils';
import { hasExpressionMapping, hasOnlyListMode, isValueExpression } from '@/utils/nodeTypesUtils';
-import { isResourceLocatorValue } from '@/utils/typeGuards';
import { createEventBus } from '@n8n/utils/event-bus';
import {
+ isResourceLocatorValue,
type INodeProperties,
type IParameterLabel,
type NodeParameterValueType,
diff --git a/packages/frontend/editor-ui/src/components/ParameterOptions.vue b/packages/frontend/editor-ui/src/components/ParameterOptions.vue
index d9ed1ff4c2..6dd5e34dd5 100644
--- a/packages/frontend/editor-ui/src/components/ParameterOptions.vue
+++ b/packages/frontend/editor-ui/src/components/ParameterOptions.vue
@@ -1,7 +1,10 @@
diff --git a/packages/frontend/editor-ui/src/composables/__snapshots__/useContextMenu.test.ts.snap b/packages/frontend/editor-ui/src/composables/__snapshots__/useContextMenu.test.ts.snap
index a0f49f740e..0ab19d109e 100644
--- a/packages/frontend/editor-ui/src/composables/__snapshots__/useContextMenu.test.ts.snap
+++ b/packages/frontend/editor-ui/src/composables/__snapshots__/useContextMenu.test.ts.snap
@@ -191,6 +191,128 @@ exports[`useContextMenu > Read-only mode > should return the correct actions whe
]
`;
+exports[`useContextMenu > should include "Open Sub-workflow" action when node is "Execute Workflow" with a set workflow 1`] = `
+[
+ {
+ "id": "open",
+ "label": "Open...",
+ "shortcut": {
+ "keys": [
+ "↵",
+ ],
+ },
+ },
+ {
+ "disabled": false,
+ "id": "execute",
+ "label": "Test step",
+ },
+ {
+ "disabled": false,
+ "id": "rename",
+ "label": "Rename",
+ "shortcut": {
+ "keys": [
+ "Space",
+ ],
+ },
+ },
+ {
+ "disabled": false,
+ "id": "open_sub_workflow",
+ "label": "Open Sub-workflow",
+ "shortcut": {
+ "keys": [
+ "O",
+ ],
+ "metaKey": true,
+ "shiftKey": true,
+ },
+ },
+ {
+ "disabled": false,
+ "id": "toggle_activation",
+ "label": "Deactivate",
+ "shortcut": {
+ "keys": [
+ "D",
+ ],
+ },
+ },
+ {
+ "disabled": true,
+ "id": "toggle_pin",
+ "label": "Pin",
+ "shortcut": {
+ "keys": [
+ "p",
+ ],
+ },
+ },
+ {
+ "id": "copy",
+ "label": "Copy",
+ "shortcut": {
+ "keys": [
+ "C",
+ ],
+ "metaKey": true,
+ },
+ },
+ {
+ "disabled": true,
+ "id": "duplicate",
+ "label": "Duplicate",
+ "shortcut": {
+ "keys": [
+ "D",
+ ],
+ "metaKey": true,
+ },
+ },
+ {
+ "divided": true,
+ "id": "tidy_up",
+ "label": "Tidy up workflow",
+ "shortcut": {
+ "altKey": true,
+ "keys": [
+ "T",
+ ],
+ "shiftKey": true,
+ },
+ },
+ {
+ "disabled": false,
+ "divided": true,
+ "id": "select_all",
+ "label": "Select all",
+ "shortcut": {
+ "keys": [
+ "A",
+ ],
+ "metaKey": true,
+ },
+ },
+ {
+ "disabled": false,
+ "id": "deselect_all",
+ "label": "Clear selection",
+ },
+ {
+ "disabled": false,
+ "divided": true,
+ "id": "delete",
+ "label": "Delete",
+ "shortcut": {
+ "keys": [
+ "Del",
+ ],
+ },
+ },
+]
+`;
+
exports[`useContextMenu > should return the correct actions opening the menu from the button 1`] = `
[
{
@@ -492,6 +614,250 @@ exports[`useContextMenu > should return the correct actions when right clicking
]
`;
+exports[`useContextMenu > should show "Open Sub-workflow" action (disabled) when node is "Execute Workflow" without a set workflow 1`] = `
+[
+ {
+ "id": "open",
+ "label": "Open...",
+ "shortcut": {
+ "keys": [
+ "↵",
+ ],
+ },
+ },
+ {
+ "disabled": false,
+ "id": "execute",
+ "label": "Test step",
+ },
+ {
+ "disabled": false,
+ "id": "rename",
+ "label": "Rename",
+ "shortcut": {
+ "keys": [
+ "Space",
+ ],
+ },
+ },
+ {
+ "disabled": true,
+ "id": "open_sub_workflow",
+ "label": "Open Sub-workflow",
+ "shortcut": {
+ "keys": [
+ "O",
+ ],
+ "metaKey": true,
+ "shiftKey": true,
+ },
+ },
+ {
+ "disabled": false,
+ "id": "toggle_activation",
+ "label": "Deactivate",
+ "shortcut": {
+ "keys": [
+ "D",
+ ],
+ },
+ },
+ {
+ "disabled": true,
+ "id": "toggle_pin",
+ "label": "Pin",
+ "shortcut": {
+ "keys": [
+ "p",
+ ],
+ },
+ },
+ {
+ "id": "copy",
+ "label": "Copy",
+ "shortcut": {
+ "keys": [
+ "C",
+ ],
+ "metaKey": true,
+ },
+ },
+ {
+ "disabled": true,
+ "id": "duplicate",
+ "label": "Duplicate",
+ "shortcut": {
+ "keys": [
+ "D",
+ ],
+ "metaKey": true,
+ },
+ },
+ {
+ "divided": true,
+ "id": "tidy_up",
+ "label": "Tidy up workflow",
+ "shortcut": {
+ "altKey": true,
+ "keys": [
+ "T",
+ ],
+ "shiftKey": true,
+ },
+ },
+ {
+ "disabled": false,
+ "divided": true,
+ "id": "select_all",
+ "label": "Select all",
+ "shortcut": {
+ "keys": [
+ "A",
+ ],
+ "metaKey": true,
+ },
+ },
+ {
+ "disabled": false,
+ "id": "deselect_all",
+ "label": "Clear selection",
+ },
+ {
+ "disabled": false,
+ "divided": true,
+ "id": "delete",
+ "label": "Delete",
+ "shortcut": {
+ "keys": [
+ "Del",
+ ],
+ },
+ },
+]
+`;
+
+exports[`useContextMenu > should show "Open Sub-workflow" action (enabled) when node is "Execute Workflow" with a set workflow 1`] = `
+[
+ {
+ "id": "open",
+ "label": "Open...",
+ "shortcut": {
+ "keys": [
+ "↵",
+ ],
+ },
+ },
+ {
+ "disabled": false,
+ "id": "execute",
+ "label": "Test step",
+ },
+ {
+ "disabled": false,
+ "id": "rename",
+ "label": "Rename",
+ "shortcut": {
+ "keys": [
+ "Space",
+ ],
+ },
+ },
+ {
+ "disabled": false,
+ "id": "open_sub_workflow",
+ "label": "Open Sub-workflow",
+ "shortcut": {
+ "keys": [
+ "O",
+ ],
+ "metaKey": true,
+ "shiftKey": true,
+ },
+ },
+ {
+ "disabled": false,
+ "id": "toggle_activation",
+ "label": "Deactivate",
+ "shortcut": {
+ "keys": [
+ "D",
+ ],
+ },
+ },
+ {
+ "disabled": true,
+ "id": "toggle_pin",
+ "label": "Pin",
+ "shortcut": {
+ "keys": [
+ "p",
+ ],
+ },
+ },
+ {
+ "id": "copy",
+ "label": "Copy",
+ "shortcut": {
+ "keys": [
+ "C",
+ ],
+ "metaKey": true,
+ },
+ },
+ {
+ "disabled": true,
+ "id": "duplicate",
+ "label": "Duplicate",
+ "shortcut": {
+ "keys": [
+ "D",
+ ],
+ "metaKey": true,
+ },
+ },
+ {
+ "divided": true,
+ "id": "tidy_up",
+ "label": "Tidy up workflow",
+ "shortcut": {
+ "altKey": true,
+ "keys": [
+ "T",
+ ],
+ "shiftKey": true,
+ },
+ },
+ {
+ "disabled": false,
+ "divided": true,
+ "id": "select_all",
+ "label": "Select all",
+ "shortcut": {
+ "keys": [
+ "A",
+ ],
+ "metaKey": true,
+ },
+ },
+ {
+ "disabled": false,
+ "id": "deselect_all",
+ "label": "Clear selection",
+ },
+ {
+ "disabled": false,
+ "divided": true,
+ "id": "delete",
+ "label": "Delete",
+ "shortcut": {
+ "keys": [
+ "Del",
+ ],
+ },
+ },
+]
+`;
+
exports[`useContextMenu > should support opening and closing (default = right click on canvas) 1`] = `
[
{
diff --git a/packages/frontend/editor-ui/src/composables/useCanvasOperations.ts b/packages/frontend/editor-ui/src/composables/useCanvasOperations.ts
index e20f93a3c2..aa9131312d 100644
--- a/packages/frontend/editor-ui/src/composables/useCanvasOperations.ts
+++ b/packages/frontend/editor-ui/src/composables/useCanvasOperations.ts
@@ -2008,6 +2008,15 @@ export function useCanvasOperations({ router }: { router: ReturnType = {}): INodeUi => ({
id: faker.string.uuid(),
@@ -92,6 +92,43 @@ describe('useContextMenu', () => {
expect(targetNodeIds.value).toEqual([sticky.id]);
});
+ it('should show "Open Sub-workflow" action (enabled) when node is "Execute Workflow" with a set workflow', () => {
+ const { open, isOpen, actions, targetNodeIds } = useContextMenu();
+ const executeWorkflow = nodeFactory({
+ type: EXECUTE_WORKFLOW_NODE_TYPE,
+ parameters: {
+ workflowId: {
+ __rl: true,
+ value: 'qseYRPbw6joqU7RC',
+ mode: 'list',
+ cachedResultName: '',
+ },
+ },
+ });
+ vi.spyOn(workflowsStore, 'getNodeById').mockReturnValue(executeWorkflow);
+ open(mockEvent, { source: 'node-right-click', nodeId: executeWorkflow.id });
+
+ expect(isOpen.value).toBe(true);
+ expect(actions.value).toMatchSnapshot();
+ expect(targetNodeIds.value).toEqual([executeWorkflow.id]);
+ });
+
+ it('should show "Open Sub-workflow" action (disabled) when node is "Execute Workflow" without a set workflow', () => {
+ const { open, isOpen, actions, targetNodeIds } = useContextMenu();
+ const executeWorkflow = nodeFactory({
+ type: EXECUTE_WORKFLOW_NODE_TYPE,
+ parameters: {
+ workflowId: {},
+ },
+ });
+ vi.spyOn(workflowsStore, 'getNodeById').mockReturnValue(executeWorkflow);
+ open(mockEvent, { source: 'node-right-click', nodeId: executeWorkflow.id });
+
+ expect(isOpen.value).toBe(true);
+ expect(actions.value).toMatchSnapshot();
+ expect(targetNodeIds.value).toEqual([executeWorkflow.id]);
+ });
+
it('should disable pinning for node that has other inputs then "main"', () => {
const { open, isOpen, actions, targetNodeIds } = useContextMenu();
const basicChain = nodeFactory({ type: BASIC_CHAIN_NODE_TYPE });
diff --git a/packages/frontend/editor-ui/src/composables/useContextMenu.ts b/packages/frontend/editor-ui/src/composables/useContextMenu.ts
index 850813cb65..5ccfc9a5c9 100644
--- a/packages/frontend/editor-ui/src/composables/useContextMenu.ts
+++ b/packages/frontend/editor-ui/src/composables/useContextMenu.ts
@@ -1,5 +1,9 @@
import type { ActionDropdownItem, XYPosition, INodeUi } from '@/Interface';
-import { NOT_DUPLICATABLE_NODE_TYPES, STICKY_NODE_TYPE } from '@/constants';
+import {
+ NOT_DUPLICATABLE_NODE_TYPES,
+ STICKY_NODE_TYPE,
+ EXECUTE_WORKFLOW_NODE_TYPE,
+} from '@/constants';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useSourceControlStore } from '@/stores/sourceControl.store';
import { useUIStore } from '@/stores/ui.store';
@@ -33,6 +37,7 @@ export type ContextMenuAction =
| 'add_node'
| 'add_sticky'
| 'change_color'
+ | 'open_sub_workflow'
| 'tidy_up';
const position = ref([0, 0]);
@@ -61,6 +66,15 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) =
workflowsStore.workflow.isArchived,
);
+ const canOpenSubworkflow = computed(() => {
+ if (targetNodes.value.length !== 1) return false;
+
+ const node = targetNodes.value[0];
+ if (node.type !== EXECUTE_WORKFLOW_NODE_TYPE) return false;
+
+ return NodeHelpers.getSubworkflowId(node);
+ });
+
const targetNodeIds = computed(() => {
if (!isOpen.value || !target.value) return [];
@@ -128,6 +142,7 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) =
const nodes = targetNodes.value;
const onlyStickies = nodes.every((node) => node.type === STICKY_NODE_TYPE);
+
const i18nOptions = {
adjustToNumber: nodes.length,
interpolate: {
@@ -221,7 +236,8 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) =
].filter(Boolean) as ActionDropdownItem[];
if (nodes.length === 1) {
- const singleNodeActions = onlyStickies
+ const isExecuteWorkflowNode = nodes[0].type === EXECUTE_WORKFLOW_NODE_TYPE;
+ const singleNodeActions: ActionDropdownItem[] = onlyStickies
? [
{
id: 'open',
@@ -253,6 +269,15 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) =
disabled: isReadOnly.value,
},
];
+
+ if (isExecuteWorkflowNode) {
+ singleNodeActions.push({
+ id: 'open_sub_workflow',
+ label: i18n.baseText('contextMenu.openSubworkflow'),
+ shortcut: { shiftKey: true, metaKey: true, keys: ['O'] },
+ disabled: !canOpenSubworkflow.value,
+ });
+ }
// Add actions only available for a single node
menuActions.unshift(...singleNodeActions);
}
diff --git a/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json b/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json
index dfa4cffef8..37aeccf184 100644
--- a/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json
+++ b/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json
@@ -1516,6 +1516,7 @@
"contextMenu.open": "Open...",
"contextMenu.test": "Test step",
"contextMenu.rename": "Rename",
+ "contextMenu.openSubworkflow": "Open Sub-workflow",
"contextMenu.copy": "Copy | Copy {count} {subject}",
"contextMenu.deactivate": "Deactivate | Deactivate {count} {subject}",
"contextMenu.activate": "Activate | Activate {count} nodes",
diff --git a/packages/frontend/editor-ui/src/utils/mappingUtils.ts b/packages/frontend/editor-ui/src/utils/mappingUtils.ts
index 9587483d73..63721d4997 100644
--- a/packages/frontend/editor-ui/src/utils/mappingUtils.ts
+++ b/packages/frontend/editor-ui/src/utils/mappingUtils.ts
@@ -1,5 +1,5 @@
-import type { INodeProperties, NodeParameterValueType } from 'n8n-workflow';
import { isResourceLocatorValue } from 'n8n-workflow';
+import type { INodeProperties, NodeParameterValueType } from 'n8n-workflow';
import { isExpression } from './expressions';
const validJsIdNameRegex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
diff --git a/packages/frontend/editor-ui/src/utils/nodeTypesUtils.ts b/packages/frontend/editor-ui/src/utils/nodeTypesUtils.ts
index 8ced5b0242..1ea433540a 100644
--- a/packages/frontend/editor-ui/src/utils/nodeTypesUtils.ts
+++ b/packages/frontend/editor-ui/src/utils/nodeTypesUtils.ts
@@ -16,17 +16,17 @@ import { i18n as locale } from '@/plugins/i18n';
import { useCredentialsStore } from '@/stores/credentials.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
-import { isResourceLocatorValue } from '@/utils/typeGuards';
import { isJsonKeyObject } from '@/utils/typesUtils';
-import type {
- IDataObject,
- INodeCredentialDescription,
- INodeExecutionData,
- INodeProperties,
- INodeTypeDescription,
- NodeParameterValueType,
- ResourceMapperField,
- Themed,
+import {
+ isResourceLocatorValue,
+ type IDataObject,
+ type INodeCredentialDescription,
+ type INodeExecutionData,
+ type INodeProperties,
+ type INodeTypeDescription,
+ type NodeParameterValueType,
+ type ResourceMapperField,
+ type Themed,
} from 'n8n-workflow';
/*
diff --git a/packages/frontend/editor-ui/src/utils/typeGuards.ts b/packages/frontend/editor-ui/src/utils/typeGuards.ts
index 4e050b2f87..7856ce4d57 100644
--- a/packages/frontend/editor-ui/src/utils/typeGuards.ts
+++ b/packages/frontend/editor-ui/src/utils/typeGuards.ts
@@ -1,5 +1,4 @@
import type {
- INodeParameterResourceLocator,
INodeTypeDescription,
NodeConnectionType,
TriggerPanelDefinition,
@@ -27,10 +26,6 @@ export const checkExhaustive = (value: never): never => {
throw new Error(`Unhandled value: ${value}`);
};
-export function isResourceLocatorValue(value: unknown): value is INodeParameterResourceLocator {
- return Boolean(typeof value === 'object' && value && 'mode' in value && 'value' in value);
-}
-
export function isNotNull(value: T | null): value is T {
return value !== null;
}
diff --git a/packages/frontend/editor-ui/src/views/NodeView.vue b/packages/frontend/editor-ui/src/views/NodeView.vue
index e8a2c5c1fd..e87a4ed02f 100644
--- a/packages/frontend/editor-ui/src/views/NodeView.vue
+++ b/packages/frontend/editor-ui/src/views/NodeView.vue
@@ -213,6 +213,7 @@ const {
setNodeActiveByName,
clearNodeActive,
addConnections,
+ tryToOpenSubworkflowInNewTab,
importWorkflowData,
fetchWorkflowDataFromUrl,
resetWorkspace,
@@ -665,10 +666,22 @@ function onClickNode() {
closeNodeCreator();
}
-function onSetNodeActivated(id: string) {
+function onSetNodeActivated(id: string, event?: MouseEvent) {
+ // Handle Ctrl/Cmd + Double Click case
+ if (event?.metaKey || event?.ctrlKey) {
+ const didOpen = tryToOpenSubworkflowInNewTab(id);
+ if (didOpen) {
+ return;
+ }
+ }
+
setNodeActive(id);
}
+function onOpenSubWorkflow(id: string) {
+ tryToOpenSubworkflowInNewTab(id);
+}
+
function onSetNodeDeactivated() {
clearNodeActive();
}
@@ -1888,6 +1901,7 @@ onBeforeUnmount(() => {
@update:node:parameters="onUpdateNodeParameters"
@update:node:inputs="onUpdateNodeInputs"
@update:node:outputs="onUpdateNodeOutputs"
+ @open:sub-workflow="onOpenSubWorkflow"
@click:node="onClickNode"
@click:node:add="onClickNodeAdd"
@run:node="onRunWorkflowToNode"
diff --git a/packages/workflow/src/NodeHelpers.ts b/packages/workflow/src/NodeHelpers.ts
index 4b3f7e7858..8b640313e1 100644
--- a/packages/workflow/src/NodeHelpers.ts
+++ b/packages/workflow/src/NodeHelpers.ts
@@ -6,6 +6,7 @@
import get from 'lodash/get';
import isEqual from 'lodash/isEqual';
+import { EXECUTE_WORKFLOW_NODE_TYPE } from './Constants';
import { ApplicationError } from './errors/application.error';
import { NodeConnectionTypes } from './Interfaces';
import type {
@@ -39,6 +40,7 @@ import type {
import { validateFilterParameter } from './NodeParameters/FilterParameter';
import {
isFilterValue,
+ isResourceLocatorValue,
isResourceMapperValue,
isValidResourceLocatorParameterValue,
} from './type-guards';
@@ -1562,3 +1564,17 @@ export function isExecutable(workflow: Workflow, node: INode, nodeTypeData: INod
isTriggerNode(nodeTypeData)
);
}
+
+/**
+ * Attempts to retrieve the ID of a subworkflow from a execute workflow node.
+ */
+export function getSubworkflowId(node: INode): string | undefined {
+ if (
+ node &&
+ node.type === EXECUTE_WORKFLOW_NODE_TYPE &&
+ isResourceLocatorValue(node.parameters.workflowId)
+ ) {
+ return node.parameters.workflowId.value as string;
+ }
+ return;
+}
diff --git a/packages/workflow/src/WorkflowDataProxy.ts b/packages/workflow/src/WorkflowDataProxy.ts
index dd25093246..5710f03fbe 100644
--- a/packages/workflow/src/WorkflowDataProxy.ts
+++ b/packages/workflow/src/WorkflowDataProxy.ts
@@ -22,25 +22,19 @@ import type {
ITaskData,
IWorkflowDataProxyAdditionalKeys,
IWorkflowDataProxyData,
- INodeParameterResourceLocator,
NodeParameterValueType,
WorkflowExecuteMode,
ProxyInput,
INode,
} from './Interfaces';
import * as NodeHelpers from './NodeHelpers';
+import { isResourceLocatorValue } from './type-guards';
import { deepCopy, isObjectEmpty } from './utils';
import type { Workflow } from './Workflow';
import type { EnvProviderState } from './WorkflowDataProxyEnvProvider';
import { createEnvProvider, createEnvProviderState } from './WorkflowDataProxyEnvProvider';
import { getPinDataIfManualExecution } from './WorkflowDataProxyHelpers';
-export function isResourceLocatorValue(value: unknown): value is INodeParameterResourceLocator {
- return Boolean(
- typeof value === 'object' && value && 'mode' in value && 'value' in value && '__rl' in value,
- );
-}
-
const isScriptingNode = (nodeName: string, workflow: Workflow) => {
const node = workflow.getNode(nodeName);
diff --git a/packages/workflow/src/index.ts b/packages/workflow/src/index.ts
index 7e01f7200f..4e13469fa9 100644
--- a/packages/workflow/src/index.ts
+++ b/packages/workflow/src/index.ts
@@ -45,6 +45,7 @@ export {
isINodePropertyCollectionList,
isINodePropertyOptionsList,
isResourceMapperValue,
+ isResourceLocatorValue,
isFilterValue,
} from './type-guards';
diff --git a/packages/workflow/src/type-guards.ts b/packages/workflow/src/type-guards.ts
index 2e3bc7fb3d..bce8370f73 100644
--- a/packages/workflow/src/type-guards.ts
+++ b/packages/workflow/src/type-guards.ts
@@ -7,6 +7,12 @@ import type {
FilterValue,
} from './Interfaces';
+export function isResourceLocatorValue(value: unknown): value is INodeParameterResourceLocator {
+ return Boolean(
+ typeof value === 'object' && value && 'mode' in value && 'value' in value && '__rl' in value,
+ );
+}
+
export const isINodeProperties = (
item: INodePropertyOptions | INodeProperties | INodePropertyCollection,
): item is INodeProperties => 'name' in item && 'type' in item && !('value' in item);