Repository: n8n-io/n8n
Pull request #14349: feat(editor): Include NodeDetailsView in URL
Author: login: CharlieKolb
Labels: name: ui
description: Enhancement in /editor-ui or /design-system
name: n8n team
description: Authored by the n8n team
name: Released
Created: 2025-04-02T10:22:57Z | Merged: 2025-04-25T08:57:22Z

## Summary

Introduce a new optional `/nodeId` route to the `workflow/` route leading to the Canvas. This enables linking and browser navigation from/to the NDV.

Mutasem raised a valid point that the NDV is technically a modal so this being a separate route isn't really accurate, but the optional syntax makes this change a lot easier than introducing a separate route to support `/workflowId#nodeId` or `/<workflowId>/ndv/<nodeId>` syntax.

Open to change this of course.

One outstanding bug is that if the NDV is closed via the browser back button on a touchpad, the canvas may scroll left due to the remains of the scroll event.
Fixing this without degrading the UX by locking the canvas in the first second after route change seems non-trivial.

This issue also exists when going back to a workflow from the projects page (though the page load hides this unless you swipe really aggressively).
Will file a follow up ticket for both of these once this is merged.


## Related Linear tickets, Github issues, and Community forum posts

https://linear.app/n8n/issue/ADO-2969/feature-expand-workflow-url-to-support-opened-ndv-by-node-id


## Review / Merge checklist

- [ ] PR title and summary are descriptive. ([conventions](../blob/master/.github/pull_request_title_conventions.md)) <!--
   **Remember, the title automatically goes into the changelog.
   Use `(no-changelog)` otherwise.**
-->
- [ ] [Docs updated](https://github.com/n8n-io/n8n-docs) or follow-up ticket created.
- [ ] Tests included. <!--
   A bug is not considered fixed, unless a test is added to prevent it from happening again.
   A feature is not complete without tests.
-->
- [ ] PR Labeled with `release/backport` (if the PR is an urgent fix that needs to be backported)



Diff:
diff --git a/cypress/e2e/39-projects.cy.ts b/cypress/e2e/39-projects.cy.ts
index af3542154cf7e..0f484771c6c43 100644
--- a/cypress/e2e/39-projects.cy.ts
+++ b/cypress/e2e/39-projects.cy.ts
@@ -1,6 +1,10 @@
 import { createResource } from '../composables/create';
 import { setCredentialValues } from '../composables/modals/credential-modal';
-import { clickCreateNewCredential, selectResourceLocatorItem } from '../composables/ndv';
+import {
+	clickCreateNewCredential,
+	getNdvContainer,
+	selectResourceLocatorItem,
+} from '../composables/ndv';
 import * as projects from '../composables/projects';
 import {
 	EDIT_FIELDS_SET_NODE_NAME,
@@ -302,7 +306,8 @@ describe('Projects', { disableAutoLogin: true }, () => {
 			});
 
 			selectResourceLocatorItem('workflowId', 0, 'Create a');
-
+			// Need to wait for the trigger node to auto-open after a delay
+			getNdvContainer().should('be.visible');
 			cy.get('body').type('{esc}');
 			workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
 			clickCreateNewCredential();
diff --git a/cypress/e2e/44-routing.cy.ts b/cypress/e2e/44-routing.cy.ts
index c3129c4e9d5d0..1b2d77ff0eb69 100644
--- a/cypress/e2e/44-routing.cy.ts
+++ b/cypress/e2e/44-routing.cy.ts
@@ -3,8 +3,9 @@ import {
 	getCloseSaveChangesButton,
 	getSaveChangesModal,
 } from '../composables/modals/save-changes-modal';
+import { getNdvContainer } from '../composables/ndv';
 import { getHomeButton } from '../composables/projects';
-import { addNodeToCanvas } from '../composables/workflow';
+import { addNodeToCanvas, saveWorkflowOnButtonClick } from '../composables/workflow';
 import {
 	getCreateWorkflowButton,
 	getNewWorkflowCardButton,
@@ -12,6 +13,7 @@ import {
 	visitWorkflowsPage,
 } from '../composables/workflowsPage';
 import { EDIT_FIELDS_SET_NODE_NAME } from '../constants';
+import { warningToast } from '../pages/notifications';
 
 describe('Workflows', () => {
 	beforeEach(() => {
@@ -42,11 +44,11 @@ describe('Workflows', () => {
 		getCreateWorkflowButton().click();
 
 		cy.createFixtureWorkflow('Test_workflow_1.json', 'Empty State Card Workflow');
-		addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
+		addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true, true);
 
 		// Here we go back via browser rather than the home button
 		// As this already updates the route
-		cy.go(-1);
+		cy.go(-2);
 
 		cy.url().should('include', getWorkflowsPageUrl());
 
@@ -56,4 +58,79 @@ describe('Workflows', () => {
 		// Confirm the url is back to the workflow
 		cy.url().should('include', '/workflow/');
 	});
+
+	it('should correct route when opening and closing NDV via browser button', () => {
+		getCreateWorkflowButton().click();
+		saveWorkflowOnButtonClick();
+		cy.url().then((startUrl) => {
+			cy.createFixtureWorkflow('Test_workflow_1.json', 'Empty State Card Workflow');
+			cy.url().should('equal', startUrl);
+
+			addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true, true);
+
+			// Getting the generated nodeId is awkward, so we just ensure the URL changed
+			cy.url().should('not.equal', startUrl);
+
+			// Here we go back via browser rather than the home button
+			// As this already updates the route
+			cy.go(-1);
+
+			cy.url().should('equal', startUrl);
+		});
+	});
+
+	it('should correct route when opening and closing NDV via browser button', () => {
+		getCreateWorkflowButton().click();
+		saveWorkflowOnButtonClick();
+		cy.url().then((startUrl) => {
+			cy.createFixtureWorkflow('Test_workflow_1.json', 'Empty State Card Workflow');
+			cy.url().should('equal', startUrl);
+
+			addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true, true);
+
+			// Getting the generated nodeId is awkward, so we just ensure the URL changed
+			cy.url().should('not.equal', startUrl);
+
+			cy.get('body').type('{esc}');
+
+			cy.url().should('equal', startUrl);
+		});
+	});
+
+	it('should open ndv via URL', () => {
+		getCreateWorkflowButton().click();
+		saveWorkflowOnButtonClick();
+		cy.createFixtureWorkflow('Test_workflow_1.json', 'Empty State Card Workflow');
+
+		addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true, true);
+		cy.url().then((ndvUrl) => {
+			cy.get('body').type('{esc}');
+			saveWorkflowOnButtonClick();
+
+			getNdvContainer().should('not.be.visible');
+
+			cy.visit(ndvUrl);
+
+			getNdvContainer().should('be.visible');
+		});
+	});
+
+	it('should open show warning and drop nodeId from URL if it contained an unknown nodeId', () => {
+		getCreateWorkflowButton().click();
+		saveWorkflowOnButtonClick();
+		cy.createFixtureWorkflow('Test_workflow_1.json', 'Empty State Card Workflow');
+
+		addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true, true);
+		cy.url().then((ndvUrl) => {
+			cy.get('body').type('{esc}');
+			saveWorkflowOnButtonClick();
+
+			getNdvContainer().should('not.be.visible');
+
+			cy.visit(ndvUrl + 'thisMessesUpTheNodeId');
+
+			warningToast().should('be.visible');
+			cy.url().should('equal', ndvUrl.split('/').slice(0, -1).join('/'));
+		});
+	});
 });
diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/methods/localResourceMapping.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/methods/localResourceMapping.ts
index 74de9baae2f18..47f06b570afb2 100644
--- a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/methods/localResourceMapping.ts
+++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/methods/localResourceMapping.ts
@@ -7,8 +7,10 @@ export async function loadSubWorkflowInputs(
 	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-workflow’s trigger</a>`
+		const { triggerId, workflowId } = subworkflowInfo ?? {};
+		const path = (workflowId ?? '') + (triggerId ? `/${triggerId.slice(0, 6)}` : '');
+		const subworkflowLink = workflowId
+			? `<a href="/workflow/${path}" target="_blank">sub-workflow’s trigger</a>`
 			: 'sub-workflow’s trigger';
 
 		switch (dataMode) {
diff --git a/packages/frontend/editor-ui/src/components/WorkflowSelectorParameterInput/WorkflowSelectorParameterInput.vue b/packages/frontend/editor-ui/src/components/WorkflowSelectorParameterInput/WorkflowSelectorParameterInput.vue
index 6104a3a9e79ee..905263f71ec16 100644
--- a/packages/frontend/editor-ui/src/components/WorkflowSelectorParameterInput/WorkflowSelectorParameterInput.vue
+++ b/packages/frontend/editor-ui/src/components/WorkflowSelectorParameterInput/WorkflowSelectorParameterInput.vue
@@ -21,7 +21,7 @@ import { useWorkflowResourcesLocator } from './useWorkflowResourcesLocator';
 import { useProjectsStore } from '@/stores/projects.store';
 import { useTelemetry } from '@/composables/useTelemetry';
 import { VIEWS } from '@/constants';
-import { SAMPLE_SUBWORKFLOW_WORKFLOW } from '@/constants.workflows';
+import { SAMPLE_SUBWORKFLOW_TRIGGER_ID, SAMPLE_SUBWORKFLOW_WORKFLOW } from '@/constants.workflows';
 import type { IWorkflowDataCreate } from '@/Interface';
 import { useDocumentVisibility } from '@/composables/useDocumentVisibility';
 
@@ -260,7 +260,10 @@ const onAddResourceClicked = async () => {
 	telemetry.track('User clicked create new sub-workflow button', {}, { withPostHog: true });
 
 	const newWorkflow = await workflowsStore.createNewWorkflow(workflow);
-	const { href } = router.resolve({ name: VIEWS.WORKFLOW, params: { name: newWorkflow.id } });
+	const { href } = router.resolve({
+		name: VIEWS.WORKFLOW,
+		params: { name: newWorkflow.id, nodeId: SAMPLE_SUBWORKFLOW_TRIGGER_ID },
+	});
 	await reloadWorkflows();
 	onInputChange(newWorkflow.id);
 	hideDropdown();
diff --git a/packages/frontend/editor-ui/src/constants.workflows.ts b/packages/frontend/editor-ui/src/constants.workflows.ts
index 4f5fa8659686e..7c28a179b7aec 100644
--- a/packages/frontend/editor-ui/src/constants.workflows.ts
+++ b/packages/frontend/editor-ui/src/constants.workflows.ts
@@ -1,11 +1,12 @@
 import { NodeConnectionTypes } from 'n8n-workflow';
 import type { INodeUi, IWorkflowDataCreate } from './Interface';
 
+export const SAMPLE_SUBWORKFLOW_TRIGGER_ID = 'c055762a-8fe7-4141-a639-df2372f30060';
 export const SAMPLE_SUBWORKFLOW_WORKFLOW: IWorkflowDataCreate = {
 	name: 'My Sub-Workflow',
 	nodes: [
 		{
-			id: 'c055762a-8fe7-4141-a639-df2372f30060',
+			id: SAMPLE_SUBWORKFLOW_TRIGGER_ID,
 			typeVersion: 1.1,
 			name: 'When Executed by Another Workflow',
 			type: 'n8n-nodes-base.executeWorkflowTrigger',
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 1fc90bcfa6cee..bf51f8ab61c9e 100644
--- a/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json
+++ b/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json
@@ -1463,6 +1463,8 @@
 	"nodeView.showMessage.debug.content": "You can make edits and re-execute. Once you're done, unpin the the first node.",
 	"nodeView.showMessage.debug.missingNodes.title": "Some execution data wasn't imported",
 	"nodeView.showMessage.debug.missingNodes.content": "Some nodes have been deleted or renamed or added to the workflow since the execution ran.",
+	"nodeView.showMessage.ndvUrl.missingNodes.title": "Node not found",
+	"nodeView.showMessage.ndvUrl.missingNodes.content": "URL contained a reference to an unknown node. Maybe the node was deleted?",
 	"nodeView.stopCurrentExecution": "Stop current execution",
 	"nodeView.stopWaitingForWebhookCall": "Stop waiting for webhook call",
 	"nodeView.stoppingCurrentExecution": "Stopping current execution",
diff --git a/packages/frontend/editor-ui/src/router.test.ts b/packages/frontend/editor-ui/src/router.test.ts
index 8af15d9da5dca..ca577e9ece465 100644
--- a/packages/frontend/editor-ui/src/router.test.ts
+++ b/packages/frontend/editor-ui/src/router.test.ts
@@ -45,6 +45,8 @@ describe('router', () => {
 		['/workflow', VIEWS.NEW_WORKFLOW],
 		['/workflow/new', VIEWS.NEW_WORKFLOW],
 		['/workflow/R9JFXwkUCL1jZBuw', VIEWS.WORKFLOW],
+		['/workflow/R9JFXwkUCL1jZBuw/myNodeId', VIEWS.WORKFLOW],
+		['/workflow/R9JFXwkUCL1jZBuw/398-1ewq213', VIEWS.WORKFLOW],
 		['/workflow/R9JFXwkUCL1jZBuw/executions/29021', VIEWS.EXECUTION_PREVIEW],
 		['/workflows/templates/R9JFXwkUCL1jZBuw', VIEWS.TEMPLATE_IMPORT],
 		['/workflows/demo', VIEWS.DEMO],
diff --git a/packages/frontend/editor-ui/src/router.ts b/packages/frontend/editor-ui/src/router.ts
index 1b11163e81c06..4c525d1389dbe 100644
--- a/packages/frontend/editor-ui/src/router.ts
+++ b/packages/frontend/editor-ui/src/router.ts
@@ -389,7 +389,7 @@ export const routes: RouteRecordRaw[] = [
 		},
 	},
 	{
-		path: '/workflow/:name',
+		path: '/workflow/:name/:nodeId?',
 		name: VIEWS.WORKFLOW,
 		components: {
 			default: NodeView,
diff --git a/packages/frontend/editor-ui/src/stores/workflows.store.test.ts b/packages/frontend/editor-ui/src/stores/workflows.store.test.ts
index f57047cba6ce8..04ac8e7f90d69 100644
--- a/packages/frontend/editor-ui/src/stores/workflows.store.test.ts
+++ b/packages/frontend/editor-ui/src/stores/workflows.store.test.ts
@@ -815,6 +815,44 @@ describe('useWorkflowsStore', () => {
 			);
 		},
 	);
+
+	describe('findNodeByPartialId', () => {
+		test.each([
+			[[], 'D', undefined],
+			[['A', 'B', 'C'], 'D', undefined],
+			[['A', 'B', 'C'], 'B', 1],
+			[['AA', 'BB', 'CC'], 'B', 1],
+			[['AA', 'BB', 'BC'], 'B', 1],
+			[['AA', 'BB', 'BC'], 'BC', 2],
+		] as Array<[string[], string, number | undefined]>)(
+			'with input %s , %s returns node with index %s',
+			(ids, id, expectedIndex) => {
+				workflowsStore.workflow.nodes = ids.map((x) => ({ id: x }) as never);
+
+				expect(workflowsStore.findNodeByPartialId(id)).toBe(
+					workflowsStore.workflow.nodes[expectedIndex ?? -1],
+				);
+			},
+		);
+	});
+
+	describe('getPartialIdForNode', () => {
+		test.each([
+			[[], 'Alphabet', 'Alphabet'],
+			[['Alphabet'], 'Alphabet', 'Alphab'],
+			[['Alphabet', 'Alphabeta'], 'Alphabeta', 'Alphabeta'],
+			[['Alphabet', 'Alphabeta', 'Alphabetagamma'], 'Alphabet', 'Alphabet'],
+			[['Alphabet', 'Alphabeta', 'Alphabetagamma'], 'Alphabeta', 'Alphabeta'],
+			[['Alphabet', 'Alphabeta', 'Alphabetagamma'], 'Alphabetagamma', 'Alphabetag'],
+		] as Array<[string[], string, string]>)(
+			'with input %s , %s returns %s',
+			(ids, id, expected) => {
+				workflowsStore.workflow.nodes = ids.map((x) => ({ id: x }) as never);
+
+				expect(workflowsStore.getPartialIdForNode(id)).toBe(expected);
+			},
+		);
+	});
 });
 
 function getMockEditFieldsNode() {
diff --git a/packages/frontend/editor-ui/src/stores/workflows.store.ts b/packages/frontend/editor-ui/src/stores/workflows.store.ts
index aad599961fede..4fb3892515c4d 100644
--- a/packages/frontend/editor-ui/src/stores/workflows.store.ts
+++ b/packages/frontend/editor-ui/src/stores/workflows.store.ts
@@ -343,6 +343,22 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
 		return workflow.value.nodes.find((node) => node.id === nodeId);
 	}
 
+	// Finds the full id for a given partial id for a node, relying on order for uniqueness in edge cases
+	function findNodeByPartialId(partialId: string): INodeUi | undefined {
+		return workflow.value.nodes.find((node) => node.id.startsWith(partialId));
+	}
+
+	// Finds a uniquely identifying partial id for a node, relying on order for uniqueness in edge cases
+	function getPartialIdForNode(fullId: string): string {
+		for (let length = 6; length < fullId.length; ++length) {
+			const partialId = fullId.slice(0, length);
+			if (workflow.value.nodes.filter((x) => x.id.startsWith(partialId)).length === 1) {
+				return partialId;
+			}
+		}
+		return fullId;
+	}
+
 	function getNodesByIds(nodeIds: string[]): INodeUi[] {
 		return nodeIds.map(getNodeById).filter(isPresent);
 	}
@@ -1875,6 +1891,8 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
 		setNodes,
 		setConnections,
 		markExecutionAsStopped,
+		findNodeByPartialId,
+		getPartialIdForNode,
 		totalWorkflowCount,
 	};
 });
diff --git a/packages/frontend/editor-ui/src/views/NodeView.vue b/packages/frontend/editor-ui/src/views/NodeView.vue
index 86cd2f07ea4a4..f48ec1a7e651b 100644
--- a/packages/frontend/editor-ui/src/views/NodeView.vue
+++ b/packages/frontend/editor-ui/src/views/NodeView.vue
@@ -234,6 +234,7 @@ const workflowId = computed(() => {
 		? undefined
 		: workflowIdParam;
 });
+const routeNodeId = computed(() => route.params.nodeId as string | undefined);
 
 const isNewWorkflowRoute = computed(() => route.name === VIEWS.NEW_WORKFLOW || !workflowId.value);
 const isWorkflowRoute = computed(() => !!route?.meta?.nodeView || isDemoRoute.value);
@@ -1606,6 +1607,23 @@ function showAddFirstStepIfEnabled() {
  * Routing
  */
 
+function updateNodeRoute(nodeId: string) {
+	const nodeUi = workflowsStore.findNodeByPartialId(nodeId);
+	if (nodeUi) {
+		setNodeActive(nodeUi.id);
+	} else {
+		toast.showToast({
+			title: i18n.baseText('nodeView.showMessage.ndvUrl.missingNodes.title'),
+			message: i18n.baseText('nodeView.showMessage.ndvUrl.missingNodes.content'),
+			type: 'warning',
+		});
+		void router.replace({
+			name: route.name,
+			params: { name: workflowId.value },
+		});
+	}
+}
+
 watch(
 	() => route.name,
 	async (newRouteName, oldRouteName) => {
@@ -1617,6 +1635,35 @@ watch(
 	},
 );
 
+// This keeps the selected node in sync if the URL is updated
+watch(
+	() => route.params.nodeId,
+	async (newId) => {
+		if (typeof newId !== 'string' || newId === '') ndvStore.activeNodeName = null;
+		else {
+			updateNodeRoute(newId);
+		}
+	},
+);
+
+// This keeps URL in sync if the activeNode is changed
+watch(
+	() => ndvStore.activeNode,
+	async (val) => {
+		// This is just out of caution
+		if (!([VIEWS.WORKFLOW] as string[]).includes(String(route.name))) return;
+
+		// Route params default to '' instead of undefined if not present
+		const nodeId = val?.id ? workflowsStore.getPartialIdForNode(val?.id) : '';
+
+		if (nodeId !== route.params.nodeId) {
+			await router.push({
+				name: route.name,
+				params: { name: workflowId.value, nodeId },
+			});
+		}
+	},
+);
 onBeforeRouteLeave(async (to, from, next) => {
 	const toNodeViewTab = getNodeViewTab(to);
 
@@ -1689,6 +1736,13 @@ onMounted(() => {
 
 				void externalHooks.run('nodeView.mount').catch(() => {});
 
+				// A delay here makes opening the NDV a bit less jarring
+				setTimeout(() => {
+					if (routeNodeId.value) {
+						updateNodeRoute(routeNodeId.value);
+					}
+				}, 500);
+
 				emitPostMessageReady();
 			});
 
diff --git a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/methods/localResourceMapping.ts b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/methods/localResourceMapping.ts
index e6770dde6f83f..dd323ad5e4ab1 100644
--- a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/methods/localResourceMapping.ts
+++ b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/methods/localResourceMapping.ts
@@ -8,8 +8,10 @@ export async function loadSubWorkflowInputs(
 	const { fields, dataMode, subworkflowInfo } = await loadWorkflowInputMappings.bind(this)();
 	let emptyFieldsNotice: string | undefined;
 	if (fields.length === 0) {
-		const subworkflowLink = subworkflowInfo?.id
-			? `<a href="/workflow/${subworkflowInfo?.id}" target="_blank">sub-workflow’s trigger</a>`
+		const { triggerId, workflowId } = subworkflowInfo ?? {};
+		const path = (workflowId ?? '') + (triggerId ? `/${triggerId.slice(0, 6)}` : '');
+		const subworkflowLink = workflowId
+			? `<a href="/workflow/${path}" target="_blank">sub-workflow’s trigger</a>`
 			: 'sub-workflow’s trigger';
 
 		switch (dataMode) {
diff --git a/packages/nodes-base/utils/workflowInputsResourceMapping/GenericFunctions.ts b/packages/nodes-base/utils/workflowInputsResourceMapping/GenericFunctions.ts
index 1ca9e7034e8a3..d05e3bb7694b6 100644
--- a/packages/nodes-base/utils/workflowInputsResourceMapping/GenericFunctions.ts
+++ b/packages/nodes-base/utils/workflowInputsResourceMapping/GenericFunctions.ts
@@ -96,7 +96,12 @@ export function getFieldEntries(context: IWorkflowNodeContext): {
 	if (Array.isArray(result)) {
 		const dataMode = String(inputSource);
 		const workflow = context.getWorkflow();
-		return { fields: result, dataMode, subworkflowInfo: { id: workflow.id } };
+		const node = context.getNode();
+		return {
+			fields: result,
+			dataMode,
+			subworkflowInfo: { workflowId: workflow.id, triggerId: node.id },
+		};
 	}
 	throw new NodeOperationError(context.getNode(), result);
 }
@@ -153,7 +158,7 @@ export async function loadWorkflowInputMappings(
 	const nodeLoadContext = await this.getWorkflowNodeContext(EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE);
 	let fields: ResourceMapperField[] = [];
 	let dataMode: string = PASSTHROUGH;
-	let subworkflowInfo: { id?: string } | undefined;
+	let subworkflowInfo: { workflowId?: string; triggerId?: string } | undefined;
 
 	if (nodeLoadContext) {
 		const fieldValues = getFieldEntries(nodeLoadContext);
diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts
index dc9cc5530e71e..8c6ee6917e827 100644
--- a/packages/workflow/src/Interfaces.ts
+++ b/packages/workflow/src/Interfaces.ts
@@ -2658,7 +2658,7 @@ export interface ResourceMapperFields {
 export interface WorkflowInputsData {
 	fields: ResourceMapperField[];
 	dataMode: string;
-	subworkflowInfo?: { id?: string };
+	subworkflowInfo?: { workflowId?: string; triggerId?: string };
 }
 
 export interface ResourceMapperField {


Reviews:
user: login: alexgrozav
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-04-17T10:22:17Z
user: login: MiloradFilipovic
body: Overall looks good to me. Left a few comments
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T12:47:58Z
user: login: CharlieKolb
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T12:57:54Z
user: login: CharlieKolb
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T13:04:13Z
user: login: MiloradFilipovic
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T07:34:51Z
user: login: MiloradFilipovic
body: Looks good, thanks for addressing the feedback.
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T07:37:11Z
user: login: MiloradFilipovic
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T08:26:04Z

Review comments:
diff_hunk: @@ -1457,6 +1457,8 @@
 	"nodeView.showMessage.debug.content": "You can make edits and re-execute. Once you're done, unpin the the first node.",
 	"nodeView.showMessage.debug.missingNodes.title": "Some execution data wasn't imported",
 	"nodeView.showMessage.debug.missingNodes.content": "Some nodes have been deleted or renamed or added to the workflow since the execution ran.",
+	"nodeView.showMessage.ndvUrl.missingNodes.title": "Node not found",
+	"nodeView.showMessage.ndvUrl.missingNodes.content": "URL contained a reference to an unknown nodeId. Maybe the node was deleted?",
path: packages/frontend/editor-ui/src/plugins/i18n/locales/en.json
user: login: MiloradFilipovic
body: Since this is a user-facing message can we go with `node id` instead of `nodeId`?
created_at: 2025-04-24T12:31:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -347,6 +347,22 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
 		return workflow.value.nodes.find((node) => node.id === nodeId);
 	}
 
+	// Finds the full id for a given partial id for a node, relying on order for uniqueness in edge cases
+	function findNodeByPartialId(partialId: string): INodeUi | undefined {
+		return workflow.value.nodes.find((node) => node.id.startsWith(partialId));
+	}
+
+	// Finds a uniquely identifying partial id for a node, relying on order for uniqueness in edge cases
+	function getPartialIdForNode(fullId: string): string {
path: packages/frontend/editor-ui/src/stores/workflows.store.ts
user: login: MiloradFilipovic
body: I like how partial ids look in urls but have you considered performance implication of these calculations?
created_at: 2025-04-24T12:40:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1613,6 +1614,34 @@ watch(
 	},
 );
 
+watch(
path: packages/frontend/editor-ui/src/views/NodeView.vue
user: login: MiloradFilipovic
body: I don't think I follow why do we need two watchers here. Can you explain a bit? Just curious if we need both
created_at: 2025-04-24T12:43:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -347,6 +347,22 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
 		return workflow.value.nodes.find((node) => node.id === nodeId);
 	}
 
+	// Finds the full id for a given partial id for a node, relying on order for uniqueness in edge cases
+	function findNodeByPartialId(partialId: string): INodeUi | undefined {
+		return workflow.value.nodes.find((node) => node.id.startsWith(partialId));
+	}
+
+	// Finds a uniquely identifying partial id for a node, relying on order for uniqueness in edge cases
+	function getPartialIdForNode(fullId: string): string {
path: packages/frontend/editor-ui/src/stores/workflows.store.ts
user: login: CharlieKolb
body: Yes, this was on my mind. O(nodeCount * 6 [characters in the id]) seems pretty reasonable for the usual case, and the case where we have an actual clash is mostly theoretical.

Could keep a list of remaining candidates to improve this a bit, but this seems like 1 in a billion that we'll do more than one iteration.
created_at: 2025-04-24T12:52:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1613,6 +1614,34 @@ watch(
 	},
 );
 
+watch(
path: packages/frontend/editor-ui/src/views/NodeView.vue
user: login: CharlieKolb
body: The first one is for if we change the route internally without an actual reload, to ensure the selected node stays in sync with the URL.

The second one is the inverse, where if a node is selected we ensure the URL stays in sync.

Ideally we'd exclusively have one source of truth and only update the other - but I don't think this reflects reality today.

Will add a comment tho, had to think about it again just now too
created_at: 2025-04-24T12:57:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1457,6 +1457,8 @@
 	"nodeView.showMessage.debug.content": "You can make edits and re-execute. Once you're done, unpin the the first node.",
 	"nodeView.showMessage.debug.missingNodes.title": "Some execution data wasn't imported",
 	"nodeView.showMessage.debug.missingNodes.content": "Some nodes have been deleted or renamed or added to the workflow since the execution ran.",
+	"nodeView.showMessage.ndvUrl.missingNodes.title": "Node not found",
+	"nodeView.showMessage.ndvUrl.missingNodes.content": "URL contained a reference to an unknown nodeId. Maybe the node was deleted?",
path: packages/frontend/editor-ui/src/plugins/i18n/locales/en.json
user: login: CharlieKolb
body: Maybe even just `node` instead of `node id`?
created_at: 2025-04-24T13:04:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -347,6 +347,22 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
 		return workflow.value.nodes.find((node) => node.id === nodeId);
 	}
 
+	// Finds the full id for a given partial id for a node, relying on order for uniqueness in edge cases
+	function findNodeByPartialId(partialId: string): INodeUi | undefined {
+		return workflow.value.nodes.find((node) => node.id.startsWith(partialId));
+	}
+
+	// Finds a uniquely identifying partial id for a node, relying on order for uniqueness in edge cases
+	function getPartialIdForNode(fullId: string): string {
path: packages/frontend/editor-ui/src/stores/workflows.store.ts
user: login: MiloradFilipovic
body: Yeah,makes sense. Also makes me wonder if we should use something like nanoid for these ids.
created_at: 2025-04-25T07:34:51Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-04-03T09:36:35Z
body: ## [Codecov](https://app.codecov.io/gh/n8n-io/n8n/pull/14349?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io) Report
Attention: Patch coverage is `87.32394%` with `9 lines` in your changes missing coverage. Please review.
| [Files with missing lines](https://app.codecov.io/gh/n8n-io/n8n/pull/14349?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io) | Patch % | Lines |
|---|---|---|
| [...rParameterInput/WorkflowSelectorParameterInput.vue](https://app.codecov.io/gh/n8n-io/n8n/pull/14349?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io#diff-cGFja2FnZXMvZnJvbnRlbmQvZWRpdG9yLXVpL3NyYy9jb21wb25lbnRzL1dvcmtmbG93U2VsZWN0b3JQYXJhbWV0ZXJJbnB1dC9Xb3JrZmxvd1NlbGVjdG9yUGFyYW1ldGVySW5wdXQudnVl) | 20.00% | [4 Missing :warning: ](https://app.codecov.io/gh/n8n-io/n8n/pull/14349?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io) |
| [...ow/ExecuteWorkflow/methods/localResourceMapping.ts](https://app.codecov.io/gh/n8n-io/n8n/pull/14349?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io#diff-cGFja2FnZXMvbm9kZXMtYmFzZS9ub2Rlcy9FeGVjdXRlV29ya2Zsb3cvRXhlY3V0ZVdvcmtmbG93L21ldGhvZHMvbG9jYWxSZXNvdXJjZU1hcHBpbmcudHM=) | 0.00% | [3 Missing :warning: ](https://app.codecov.io/gh/n8n-io/n8n/pull/14349?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io) |
| [.../workflowInputsResourceMapping/GenericFunctions.ts](https://app.codecov.io/gh/n8n-io/n8n/pull/14349?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io#diff-cGFja2FnZXMvbm9kZXMtYmFzZS91dGlscy93b3JrZmxvd0lucHV0c1Jlc291cmNlTWFwcGluZy9HZW5lcmljRnVuY3Rpb25zLnRz) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/n8n-io/n8n/pull/14349?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=n8n-io) |

:loudspeaker: Thoughts on this report? [Let us know!](https://github.com/codecov/feedback/issues/255)
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-17T10:30:28Z
body: :warning: Some Cypress E2E specs are failing, please fix them before merging
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-17T11:15:56Z
body: :warning: Some Cypress E2E specs are failing, please fix them before merging
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-17T11:22:51Z
body: :warning: Some Cypress E2E specs are failing, please fix them before merging
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-25T07:46:42Z
body: :warning: Some Cypress E2E specs are failing, please fix them before merging
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-25T07:52:57Z
body: :warning: Some Cypress E2E specs are failing, please fix them before merging
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-25T08:10:28Z
body: :warning: Some Cypress E2E specs are failing, please fix them before merging
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-25T08:35:39Z
body: :white_check_mark: All Cypress E2E specs passed
author_association: CONTRIBUTOR
user: login: janober
created_at: 2025-04-28T15:53:11Z
body: Got released with [`n8n@1.91.0`](https://github.com/n8n-io/n8n/releases/tag/n8n%401.91.0)
author_association: MEMBER