Skip to content

fix: Preserve node positions in layoutWorkflowJSON for update paths#28902

Open
dariacodes wants to merge 1 commit intomasterfrom
ado-5104-bug-mcp-update-workflow-tool-always-changes-sticky-note
Open

fix: Preserve node positions in layoutWorkflowJSON for update paths#28902
dariacodes wants to merge 1 commit intomasterfrom
ado-5104-bug-mcp-update-workflow-tool-always-changes-sticky-note

Conversation

@dariacodes
Copy link
Copy Markdown
Contributor

@dariacodes dariacodes commented Apr 22, 2026

Summary

When updating a workflow through MCP (or the instance-ai deep agent), all node positions get recalculated from scratch, even when every node already has correct positions. This is most visible with sticky notes, which jump to completely wrong locations after a simple content edit:
image

The problem comes from layoutWorkflowJSON(). When nodes are prepared for the layout, their existing positions are not kept. Because of this, the layout engine thinks every run is starting from scratch, so it moves all nodes again and can place things like sticky notes incorrectly.

The fix adds a preservePositions option to layoutWorkflowJSON(). When it’s turned on, existing node positions are kept so the layout engine doesn’t move them again. We enable this for updates, while keeping the default behavior for new workflows so they still get properly arranged.

To test, request an MCP client to update a workflow with a sticky note with a non-default position. E.g. "Update the sticky note in the workflow id=X to 'Hi', do not change its position".

Related Linear tickets, Github issues, and Community forum posts

Closes ADO-5104

Review / Merge checklist

  • I have seen this code, I have run this code, and I take responsibility for this code.
  • PR title and summary are descriptive. (conventions)
  • Docs updated or follow-up ticket created.
  • Tests included.
  • PR Labeled with Backport to Beta, Backport to Stable, or Backport to v1 (if the PR is an urgent fix that needs to be backported)

@n8n-assistant n8n-assistant Bot added core Enhancement outside /nodes-base and /editor-ui n8n team Authored by the n8n team labels Apr 22, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 22, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 5 files

Architecture diagram
sequenceDiagram
    participant AI as AI Agent / MCP Client
    participant Tool as Workflow Tool (Build/Submit/Update)
    participant SDK as Workflow SDK (layoutWorkflowJSON)
    participant Dagre as Dagre Layout Engine

    Note over AI,SDK: Workflow Update or Creation Request

    AI->>Tool: Request workflow update (with code/JSON)
    
    Tool->>Tool: Parse & validate workflow code
    
    alt NEW: Workflow Update (workflowId exists)
        Tool->>SDK: layoutWorkflowJSON(workflow, { preservePositions: true })
        SDK->>SDK: NEW: Map existing [x, y] to GraphNode config
        SDK-->>Tool: Return JSON (positions unchanged)
    else Workflow Creation (workflowId missing)
        Tool->>SDK: layoutWorkflowJSON(workflow, { preservePositions: false })
        SDK->>Dagre: Calculate optimal node placement
        Dagre-->>SDK: Generated coordinates
        SDK-->>Tool: Return JSON (new auto-layout positions)
    end

    Note over Tool: Finalize WorkflowEntity

    alt Success Path
        Tool-->>AI: 200 OK (Workflow with preserved/new layout)
    else Validation Error
        Tool-->>AI: Error (Invalid workflow structure)
    end
Loading

@github-actions
Copy link
Copy Markdown
Contributor

Performance Comparison

Comparing currentlatest master14-day baseline

Memory consumption baseline with starter plan resources

Metric Current Latest Master Baseline (avg) vs Master vs Baseline Status
memory-heap-used-baseline 115.56 MB 114.27 MB 114.47 MB (σ 0.24) +1.1% +1.0% 🔴
memory-rss-baseline 227.11 MB 348.40 MB 289.80 MB (σ 40.90) -34.8% -21.6% ⚠️

docker-stats

Metric Current Latest Master Baseline (avg) vs Master vs Baseline Status
docker-image-size-runners 388.00 MB 386.00 MB 392.13 MB (σ 11.18) +0.5% -1.1%
docker-image-size-n8n 1269.76 MB 1269.76 MB 1273.60 MB (σ 10.49) +0.0% -0.3%

Idle baseline with Instance AI module loaded

Metric Current Latest Master Baseline (avg) vs Master vs Baseline Status
instance-ai-heap-used-baseline 187.36 MB 186.50 MB 186.43 MB (σ 0.25) +0.5% +0.5% 🔴
instance-ai-rss-baseline 345.43 MB 392.12 MB 368.35 MB (σ 22.82) -11.9% -6.2% ⚠️
How to read this table
  • Current: This PR's value (or latest master if PR perf tests haven't run)
  • Latest Master: Most recent nightly master measurement
  • Baseline: Rolling 14-day average from master
  • vs Master: PR impact (current vs latest master)
  • vs Baseline: Drift from baseline (current vs rolling avg)
  • Status: ✅ within 1σ | ⚠️ 1-2σ | 🔴 >2σ regression

@dariacodes dariacodes requested a review from RicardoE105 April 22, 2026 13:51
@github-actions
Copy link
Copy Markdown
Contributor

Instance AI Workflow Eval Results

8/8 built | 5 run(s) | pass@5: 74% | pass^5: 15%

Workflow Build pass@5 pass^5
Create a workflow that handles contact form submissions via a webhook. 5/5 80% 19%
Get all the Linear issues created in the last 2 weeks. Filter them for 5/5 60% 8%
Every day, get the posts made in the past day on 3 different Slack cha 5/5 60% 8%
Create a form that collects: name, email, company, and interest level 5/5 100% 100%
Every day, fetch all open GitHub issues from repository 'acme-corp/bac 5/5 50% 0%
Create a workflow that receives webhook notifications with a JSON body 5/5 100% 1%
Fetch the latest posts from the JSONPlaceholder API (GET https://jsonp 5/5 100% 13%
Every hour, check the current weather for London, New York, and Tokyo 5/5 50% 0%
Failure details

Create a workflow that handles contact form submissions via a webhook. / happy-path — 4/5 passed

Run [builder_issue]: The workflow errored on the 'Log to Google Sheets' node with 'Could not get parameter'. The node uses 'appendOrUpdate' operation which requires a 'matchingColumns' parameter to know which column(s) to

Create a workflow that handles contact form submissions via a webhook. / missing-fields — 4/5 passed

Run [builder_issue]: The workflow crashed at the 'Log to Google Sheets' node with 'Could not get parameter', and that node produced no output. The error occurs because the 'appendOrUpdate' operation requires a column to b

Create a workflow that handles contact form submissions via a webhook. / partial-action-failure — 0/5 passed

Run [builder_issue]: The Telegram Notification node failed with 'Bad request - please check your parameters' because the chatId is set to a literal placeholder string '<__PLACEHOLDER_VALUE__Your Telegram Chat ID (e.g. -10
Run [builder_issue]: The workflow crashed entirely when the Telegram node failed. The connections show all three action nodes (Auto-Reply to Submitter, Notify Team via Telegram, Log to Google Sheets) are wired in parallel
Run [builder_issue]: The Telegram node errored with 'Bad request - please check your parameters' (triggered by the mock returning {ok: false, error_code: 400, description: 'Bad Request: chat not found'}), and this caused
Run [builder_issue]: The workflow crashed entirely when the Telegram node received an error response. The Telegram node has a placeholder chatId ('<PLACEHOLDER_VALUE__Your Telegram team chat ID (ask @get_id_bot)>') an
Run [builder_issue]: The workflow crashed when the Telegram node returned an error response (chat not found). The error propagated and halted the entire workflow execution. The Google Sheets node (Log to Google Sheets) ne

Create a workflow that handles contact form submissions via a webhook. / empty-message — 4/5 passed

Run [builder_issue]: The workflow errored on the 'Log to Google Sheets' node with 'Could not get parameter', causing the execution to fail. The Auto-Reply Email and Telegram Notification nodes both executed successfully a

Create a workflow that handles contact form submissions via a webhook. / invalid-email — 1/5 passed

Run [builder_issue]: The workflow crashed entirely when the Auto-Reply Email node encountered an invalid email address ('not-an-email'). The error 'Invalid email address (item 0)' was thrown and propagated as a workflow-l
Run [builder_issue]: The workflow crashed entirely when the Gmail node ('Send Auto-Reply Email') threw an 'Invalid email address (item 0)' error for the malformed email 'not-an-email'. Because this error was unhandled (no
Run [builder_issue]: The workflow crashed entirely when the Auto-Reply Email node encountered the invalid email address 'not-an-email'. The error 'Invalid email address (item 0)' caused the workflow to stop executing, mea
Run [builder_issue]: The workflow crashed at the 'Send Auto-Reply Email' node with the error 'Invalid email address (item 0)' because the Gmail node does not accept 'not-an-email' as a valid recipient. This error propagat

Get all the Linear issues created in the last 2 weeks. Filter them for / happy-path — 2/5 passed

Run [builder_issue]: The workflow executed without errors, but the Process Issues code node produced incorrect results. The node output shows crossTeamCreators: [], totalCrossTeamIssues: 0, and slackMessage: '_No cross-te
Run [builder_issue]: The workflow failed at the 'Fetch Linear Issues' node with 'Invalid Custom Auth JSON'. The pre-analysis flags a FRAMEWORK ISSUE where Phase 1 returned empty triggerContent, but the actual crash here i
Run [mock_issue]: The workflow executed without errors and all nodes ran successfully. The cross-team filter correctly identified Alice's 3 Backend issues (ISS-001, ISS-002, ISS-003) and Bob's 1 Frontend issue (ISS-005

Get all the Linear issues created in the last 2 weeks. Filter them for / multi-team-creator — 0/5 passed

Run [builder_issue]: The Filter Cross-Team Issues node uses a hardcoded teamMap that maps each creator to a single home team ('alice@company.com' → 'Frontend', 'bob@company.com' → 'Backend'). The scenario requires Alice t
Run [builder_issue]: The Process Issues code node produced 'No cross-team issues found.' with zero cross-team issues. This is wrong: Alice's 2 Backend issues (LIN-101, LIN-102) should be cross-team (Backend is not in her
Run [builder_issue]: The workflow failed before any meaningful execution. The 'Fetch Linear Issues' node threw 'Invalid Custom Auth JSON' and halted. None of the downstream nodes (Filter Cross-Team Issues, Count Per Creat
Run [builder_issue]: The checklist requires that Alice's AI issue is excluded because AI is one of her teams. However, the TEAM_MAPPING in 'Enrich and Filter Cross-Team Issues' maps alice@company.com to a single team 'Fro
Run [builder_issue]: The checklist requires that Alice's AI issue be excluded from the cross-team report (since AI is one of her teams), meaning Alice should have 2 cross-team issues (the two Backend issues) and Bob shoul

Get all the Linear issues created in the last 2 weeks. Filter them for / no-cross-team-issues — 4/5 passed

Run [framework_issue]: The workflow failed with 'Invalid Custom Auth JSON' error in the Fetch Linear Issues node. This prevented all downstream nodes (Filter Cross-Team Issues, Count Per Creator, Format Slack Message, Post

Get all the Linear issues created in the last 2 weeks. Filter them for / unknown-creator — 3/5 passed

Run [builder_issue]: The workflow executed without crashing, which satisfies the 'no crash' requirement. However, the checklist requires that Alice's cross-team issues are 'still correctly processed.' The Process Issues c
Run [framework_issue]: The workflow failed to execute due to a framework issue (empty trigger content) compounded by an 'Invalid Custom Auth JSON' error on the Fetch Linear Issues node. The workflow never reached the Filter

Get all the Linear issues created in the last 2 weeks. Filter them for / api-error — 0/5 passed

Run [builder_issue]: The workflow does not handle the API error gracefully. When the Linear API returns an authentication error (with data: null and an errors array), the Extract Issues code node silently swallows i
Run [builder_issue]: The workflow does not handle the API authentication error gracefully. When 'Fetch Linear Issues' receives a 401 authentication error, the workflow crashes with the error 'Invalid or missing Linear API
Run [builder_issue]: The workflow crashed with 'Invalid Custom Auth JSON' on the Fetch Linear Issues node. There is no error handling branch — no Try/Catch, no error workflow, and no alternative path to post an error mess
Run [builder_issue]: The workflow does not handle the API authentication error gracefully. When Fetch Linear Issues returns a GraphQL error response (with an errors array and no data field), the Extract Issues code no
Run [builder_issue]: The workflow does not handle the API error gracefully. When the Linear API returns an authentication error, the HTTP request node passes the error response as a normal output item (with an errors ar

Every day, get the posts made in the past day on 3 different Slack cha / happy-path — 4/5 passed

Run [builder_issue]: The workflow did not execute without errors in the sense that critical nodes did not run. Specifically, 'Aggregate Messages', 'OpenAI GPT-4o', and 'Post to #daily-digest' all DID NOT RUN. The root cau

Every day, get the posts made in the past day on 3 different Slack cha / empty-channel — 3/5 passed

Run [mock_issue]: The workflow did not complete end-to-end. The 'Summarize with GPT-4o' node errored with 'Cannot read properties of undefined (reading 'filter')'. The mock response for the OpenAI node returned a respo
Run [builder_issue]: The workflow did not complete end-to-end. While the empty channel (#product) was handled without errors and the Merge step completed with partial data (5 items from #general and #engineering), the wor

Every day, get the posts made in the past day on 3 different Slack cha / high-volume — 1/5 passed

Run [builder_issue]: The workflow executed without crashing and all nodes ran successfully. The merge step correctly combined all 13 messages (4 from #general, 5 from #engineering, 4 from #product) and the Aggregate by Ch
Run [builder_issue]: The workflow has multiple critical failures that prevent the summary from being posted. First, the Tag nodes (Set nodes) are configured with 'keepOnlySet' behavior — they output ONLY the 'channel' fie
Run: Error: The operation was aborted due to timeout
Run [builder_issue]: The workflow executed without crashing and all nodes ran, but there are two notable issues:

  1. Data loss in channel attribution (Aggregate Messages): The Aggregate Messages code node attempts to

Every day, get the posts made in the past day on 3 different Slack cha / channel-not-found — 0/5 passed

Run [builder_issue]: The workflow crashed entirely when 'Fetch #product' returned a channel_not_found error. No error handling exists on the Fetch #product node (e.g., no 'Continue on Error' setting, no error branch), so
Run [builder_issue]: The workflow crashed entirely when 'Get #product Messages' received a channel_not_found error. The error propagated and halted execution — no Tag product node ran, the Merge All Messages node never ra
Run [builder_issue]: The workflow has no error handling for the case where one Slack channel node fails. When 'Slack: #product' received a 404 (channel_not_found) response, the entire workflow crashed with 'Request failed
Run [builder_issue]: The workflow crashed entirely when 'Fetch #product' returned a channel_not_found error. The Slack node treated the error response as a fatal exception and propagated it, halting the entire workflow. N
Run [builder_issue]: The workflow crashed entirely when 'Get #product Messages' received a channel_not_found error. There is no error handling in the workflow — no Try/Catch node, no error branch, no IF node to check for

Every day, get the posts made in the past day on 3 different Slack cha / insufficient-permissions — 0/5 passed

Run [builder_issue]: The workflow crashed when Fetch #product returned a 403 error (triggered by the mock returning ok: false with not_in_channel). The workflow has no error handling on the Fetch #product node — no tr
Run [builder_issue]: The workflow crashed when 'Get #product Messages' received a 403 error (the mock returned a not_in_channel error response). The workflow has no error handling on this node — no 'Continue on Error' set
Run [builder_issue]: The workflow crashed entirely when Slack: #product returned a 403 error. The error propagated as 'Request failed with status code 403' and halted the entire workflow. The Merge Channels, Aggregate Mes
Run [builder_issue]: The workflow crashed entirely when 'Fetch #product' returned a 403 error. There is no error handling configured — no try/catch, no Continue on Error setting on the Fetch #product node, and no fallback
Run [builder_issue]: The workflow crashed with a 403 error when 'Get #product Messages' received a 'not_in_channel' error response. The workflow has no error handling (no try/catch, no error branch, no 'Continue on Error'

Every day, fetch all open GitHub issues from repository 'acme-corp/bac / happy-path — 0/5 passed

Run [builder_issue]: The workflow failed to execute due to a builder misconfiguration. The 'Split Issues' node (n8n-nodes-base.splitOut) has its required 'Fields To Split Out' parameter left empty (fieldToSplitOut: ''), c
Run [builder_issue]: The workflow has a critical issue: the Fetch GitHub Bug Issues node output is a binary Buffer instead of parsed JSON objects. The HTTP node's output shows {"type": "Buffer", "data": [...]} rather th
Run [builder_issue]: The workflow did not complete successfully. The 'Fetch GitHub Bug Issues' node returned the data as a binary Buffer instead of parsed JSON objects, because the HTTP Request node's response was not cor
Run [builder_issue]: The workflow failed to execute. The 'Split Issues' node has an empty 'fieldToSplitOut' parameter, which is required. This caused the execution to fail with 'The workflow has issues and cannot be execu
Run [builder_issue]: The workflow failed to execute due to a misconfigured 'Split Issues' node. The node's 'fieldToSplitOut' parameter is empty string, but it is a required field. This caused the execution to fail before

Every day, fetch all open GitHub issues from repository 'acme-corp/bac / no-bugs — 1/5 passed

Run [builder_issue]: The workflow failed to execute cleanly. The 'Split Issues' node has a builder misconfiguration — the required 'Fields To Split Out' parameter is empty. This caused the execution to fail with the error
Run [builder_issue]: The workflow errored with 'Cannot convert undefined or null to object' in the Create Notion Page node. The root cause is that the Fetch GitHub Bug Issues node returned a Buffer object ({type: 'Buffer'
Run [builder_issue]: The workflow failed to execute cleanly. The 'Split Issues' node has a misconfigured 'fieldToSplitOut' parameter (empty string), which caused the execution to fail entirely with the error: 'The workflo
Run [builder_issue]: The workflow failed to execute cleanly. The 'Split Issues' node has a misconfigured 'fieldToSplitOut' parameter (empty string), which caused the execution to fail with the error: 'The workflow has iss

Create a workflow that receives webhook notifications with a JSON body / high-priority — 2/5 passed

Run [builder_issue]: The Switch node ('Route by Level') correctly identified the 'high' level and routed the item to its first output (outputKey: 'high'). However, the connections JSON shows that 'Route by Level' has an e
Run [builder_issue]: The Switch node 'Route by Level' correctly identified the 'high' level and routed the item to its first output. However, the connections JSON shows that 'Route by Level' has an empty 'main' array — no
Run [builder_issue]: The Switch node 'Route by Level' correctly matched the 'high' level condition and routed the item to its first output. However, the connections JSON shows 'Route by Level' has an empty 'main' array —

Create a workflow that receives webhook notifications with a JSON body / medium-priority — 2/5 passed

Run [builder_issue]: The Switch node ('Route by Level') correctly identified the 'medium' level and routed the item to its 'medium' output. However, the connections JSON shows that 'Route by Level' has an empty 'main' arr
Run [builder_issue]: The Switch node (Route by Level) correctly identified the 'medium' level and routed output to its second output (index 1, labeled 'medium'). However, the Connections JSON shows that 'Route by Level' h
Run [builder_issue]: The Route by Level switch node correctly identified the 'medium' level and routed to its second output (index 1). However, the connections JSON shows that Route by Level has no downstream connections:

Create a workflow that receives webhook notifications with a JSON body / low-priority — 2/5 passed

Run [builder_issue]: The Send Gmail node did not run. The connections JSON shows 'Route by Level' has an empty 'main' array, meaning its outputs are not connected to any downstream nodes. Even though the Switch node corre
Run [builder_issue]: The Send Gmail node did not run. The Route by Level switch node correctly identified the 'low' level and produced output on its third output index, but the connections JSON shows 'Route by Level' has
Run [builder_issue]: The Send Gmail node did not run. The connections JSON shows that 'Route by Level' has an empty 'main' array, meaning its outputs are not wired to any downstream nodes (Send Teams Message, Send Slack M

Fetch the latest posts from the JSONPlaceholder API (GET https://jsonp / happy-path — 3/5 passed

Run [builder_issue]: The workflow did not execute completely. While Fetch Posts and Split Posts ran, the Filter Out qui Titles, Build Summary, and Post to Slack nodes did not execute. The Split Posts node is configured to
Run []: The workflow executed without errors and the Slack message was posted to #api-digest. However, the mock response from 'Fetch Posts' only returned 5 of the 8 items in the mock response array (items wit

Fetch the latest posts from the JSONPlaceholder API (GET https://jsonp / empty-response — 4/5 passed

Run [builder_issue]: The Fetch Posts node received an empty array [] as the mock response and produced no output items. This caused the downstream Filter Posts, Build Summary, and Post to Slack nodes to not run at all. Th

Fetch the latest posts from the JSONPlaceholder API (GET https://jsonp / all-filtered — 2/5 passed

Run [builder_issue]: The workflow did not crash, but it did send a message to Slack when all items were filtered out. The Post to Slack node sent the message '0 posts remain after filtering. Titles:\n' to the #api-digest
Run [builder_issue]: The scenario requires all posts to be filtered out (all contain 'qui' in their titles), but the Filter Out qui Titles node passed all 3 items through instead of filtering them out. The filter conditio
Run [builder_issue]: The workflow did not crash when all items were filtered out — the Filter and Summarize code node ran successfully and produced {summary: '0 posts remain after filtering:\n', count: 0}. However, the ch

Every hour, check the current weather for London, New York, and Tokyo / happy-path — 0/5 passed

Run [builder_issue]: The workflow failed to execute. The pre-analysis flags a builder issue: the 'Log to Airtable' node has an invalid Airtable Table ID ('Weather Logs' is a display name, not a valid table ID which should
Run [builder_issue]: The workflow failed on multiple criteria:

  1. Merge Cities produced wrong output: The Merge Cities node was configured in 'combineByPosition' mode with 3 inputs. However, since each of the three w

Run [builder_issue]: The workflow failed to execute. The pre-analysis flags a builder issue: the 'Log to Airtable' node has an invalid Airtable Table ID — the value 'Weather Logs' is a table name string, not a valid Airta
Run [mock_issue]: The workflow failed at the 'Process Weather Data' code node with the error 'Cannot read properties of undefined (reading 'temp') [line 4]'. This is because the Weather - New York mock response returne
Run [builder_issue]: The workflow failed to execute. The pre-analysis flags a builder issue: 'Log to Airtable' has an invalid Airtable Table ID ('Weather Logs' is a display name, not a valid table ID like 'tblXXXXXXXXXXXX

Every hour, check the current weather for London, New York, and Tokyo / no-alerts — 1/5 passed

Run [builder_issue]: The workflow failed to execute entirely. The pre-analysis flags a builder issue on 'Log to Airtable': the table ID value is set to 'Weather Logs' (a plain text name) instead of a valid Airtable Table
Run [builder_issue]: The workflow failed with 'Could not get parameter' on the 'Log to Airtable - No Alert' node. Additionally, the Merge Cities node (configured with combineByPosition across 3 inputs) only output a singl
Run [builder_issue]: The workflow failed to execute. The pre-analysis flags a builder issue: the 'Log to Airtable' node has an invalid Table ID — the value 'Weather Logs' is a table name string, not a valid Airtable Table
Run [builder_issue]: The workflow failed to execute. The pre-analysis flag identifies a builder issue: 'Log to Airtable' has an invalid Airtable Table ID ('Weather Logs' is a plain name string, not a valid Airtable table

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Enhancement outside /nodes-base and /editor-ui n8n team Authored by the n8n team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant