n8n - 💡(How to fix) Fix Bug: SDK '.onError()' connections not rendered on canvas and corrupted by copy-paste (Apify community nodes) [1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
n8n-io/n8n#28637Fetched 2026-04-18 05:56:55
View on GitHub
Comments
1
Participants
2
Timeline
4
Reactions
0
Author
Timeline (top)
commented ×1labeled ×1mentioned ×1subscribed ×1

Error Message

const apifyNode = node({ type: '@apify/n8n-nodes-apify.apify', version: 1, config: { name: 'RunActor', onError: 'continueErrorOutput', parameters: { /* ... / }, credentials: { apifyApi: newCredential('Apify account') } }, output: [{ / sample */ }] });

const mergeNode = merge({ version: 3.2, config: { name: 'MergePaths', parameters: { mode: 'append', numberInputs: 9 } } });

export default workflow('id', 'name') .add(trigger) .to(apifyNode.to(otherNode)) .add(apifyNode.onError(mergeNode.input(8))) // ← the error wiring under test .add(mergeNode);

Code Example

const apifyNode = node({
     type: '@apify/n8n-nodes-apify.apify',
     version: 1,
     config: {
       name: 'RunActor',
       onError: 'continueErrorOutput',
       parameters: { /* ... */ },
       credentials: { apifyApi: newCredential('Apify account') }
     },
     output: [{ /* sample */ }]
   });

   const mergeNode = merge({
     version: 3.2,
     config: {
       name: 'MergePaths',
       parameters: { mode: 'append', numberInputs: 9 }
     }
   });

   export default workflow('id', 'name')
     .add(trigger)
     .to(apifyNode.to(otherNode))
     .add(apifyNode.onError(mergeNode.input(8)))   // ← the error wiring under test
     .add(mergeNode);

---

"RunActor": {
     "main":  [[{ "node": "OtherNode",  "type": "main", "index": 0 }]],
     "error": [[{ "node": "MergePaths", "type": "main", "index": 8 }]]
   }

---

"RunActor2": {
     "main": [[
       { "node": "OtherNode2",  "type": "main", "index": 0 },
       { "node": "MergePaths2", "type": "main", "index": 8 }
     ]]
   }

---

{
  "name": "Repro_OnError_NotRendered",
  "nodes": [
    { "id": "node-1", "name": "Trigger",     "type": "n8n-nodes-base.executeWorkflowTrigger", "typeVersion": 1.1, "position": [0, 0],   "parameters": { "inputSource": "passthrough" } },
    { "id": "node-2", "name": "RunActor",    "type": "@apify/n8n-nodes-apify.apify",          "typeVersion": 1,   "position": [240, 0], "parameters": { "actorSource": "store", "actorId": { "__rl": true, "value": "PLACEHOLDER_ACTOR_ID", "mode": "id" }, "customBody": "={{ JSON.stringify($json) }}", "timeout": 300, "memory": 256 }, "onError": "continueErrorOutput", "credentials": { "apifyApi": { "id": "PLACEHOLDER_CRED_ID", "name": "Apify account" } } },
    { "id": "node-3", "name": "OtherNode",   "type": "@apify/n8n-nodes-apify.apify",          "typeVersion": 1,   "position": [480, 0], "parameters": { "resource": "Datasets", "datasetId": "={{ $json.defaultDatasetId }}", "limit": 1000, "options": {} }, "onError": "continueErrorOutput", "credentials": { "apifyApi": { "id": "PLACEHOLDER_CRED_ID", "name": "Apify account" } } },
    { "id": "node-4", "name": "MergePaths",  "type": "n8n-nodes-base.merge",                  "typeVersion": 3.2, "position": [720, 0], "parameters": { "numberInputs": 9 } }
  ],
  "connections": {
    "Trigger":    { "main":  [[{ "node": "RunActor",   "type": "main", "index": 0 }]] },
    "RunActor":   { "main":  [[{ "node": "OtherNode",  "type": "main", "index": 0 }]],
                    "error": [[{ "node": "MergePaths", "type": "main", "index": 8 }]] },
    "OtherNode":  { "main":  [[{ "node": "MergePaths", "type": "main", "index": 7 }]],
                    "error": [[{ "node": "MergePaths", "type": "main", "index": 8 }]] }
  }
}

---

"connections": {
  "RunActor2": {
    "main": [[
      { "node": "OtherNode2",  "type": "main", "index": 0 },
      { "node": "MergePaths2", "type": "main", "index": 8 }
    ]]
  },
  "OtherNode2": {
    "main": [[
      { "node": "MergePaths2", "type": "main", "index": 7 },
      { "node": "MergePaths2", "type": "main", "index": 8 }
    ]]
  }
}
RAW_BUFFERClick to expand / collapse

Bug Description

Bug Description

When the AI Builder / MCP integration creates a workflow that wires a node's error output pin to a downstream node (using the .onError(target) SDK helper combined with onError: 'continueErrorOutput' on the source node), the resulting connection is serialized to the workflow JSON under a top-level "error" key in the connections object — a format that the n8n editor canvas does not render visually for at least some custom community nodes (reproduced with @apify/n8n-nodes-apify.apify v1).

Worse, when the user copies the affected nodes (Ctrl/Cmd-C) and pastes them anywhere (Ctrl/Cmd-V), the editor's serializer silently flattens the error connection into the source node's main outputs array, losing the error-pin semantics. The pasted copy now has two lines coming out of the Success pin and none from Error.

The runtime engine itself respects the legacy format — workflows execute correctly when triggered — so the bug is invisible in test runs and only surfaces (a) visually on the canvas, and (b) structurally when the user copy-pastes. It is therefore easy to ship a workflow that looks fine to the engine but is silently corrupt in the editor and will be permanently broken the moment it's duplicated.

<img width="635" height="636" alt="Image" src="https://github.com/user-attachments/assets/d3796692-f9ef-4388-b457-f6df8e853043" />

To Reproduce

To Reproduce

  1. Use the AI Builder (or any direct caller of @n8n/workflow-sdk via the MCP integration) to create a workflow containing a custom community node configured with onError: 'continueErrorOutput', whose error pin is wired to a downstream Merge node:

    const apifyNode = node({
      type: '@apify/n8n-nodes-apify.apify',
      version: 1,
      config: {
        name: 'RunActor',
        onError: 'continueErrorOutput',
        parameters: { /* ... */ },
        credentials: { apifyApi: newCredential('Apify account') }
      },
      output: [{ /* sample */ }]
    });
    
    const mergeNode = merge({
      version: 3.2,
      config: {
        name: 'MergePaths',
        parameters: { mode: 'append', numberInputs: 9 }
      }
    });
    
    export default workflow('id', 'name')
      .add(trigger)
      .to(apifyNode.to(otherNode))
      .add(apifyNode.onError(mergeNode.input(8)))   // ← the error wiring under test
      .add(mergeNode);
  2. Persist the workflow (the SDK / MCP update_workflow call returns success).

  3. Inspect the saved workflow JSON via the REST API. The connections object contains:

    "RunActor": {
      "main":  [[{ "node": "OtherNode",  "type": "main", "index": 0 }]],
      "error": [[{ "node": "MergePaths", "type": "main", "index": 8 }]]
    }

    Note the top-level "error" key as a sibling of "main".

  4. Open the workflow in the editor (hard-refresh to defeat any cache).

    👉 Observed: the Error pin on RunActor shows the unconnected ─ + affordance. No connection line is drawn.

  5. Trigger the workflow (e.g. via test_workflow with pinned input) so that RunActor produces an error item.

    👉 Observed: the runtime correctly routes the error item to MergePaths input 8. The execution succeeds.

  6. In the editor, select RunActor + OtherNode + MergePaths, copy them (Ctrl/Cmd-C), and paste them (Ctrl/Cmd-V) — into the same workflow or a new one. Inspect the resulting JSON of the pasted copy:

    "RunActor2": {
      "main": [[
        { "node": "OtherNode2",  "type": "main", "index": 0 },
        { "node": "MergePaths2", "type": "main", "index": 8 }
      ]]
    }

    👉 Observed: the error connection (formerly under "error") is now flattened into "main". The pasted Success pin shows two outgoing lines; the Error pin shows none. Any future error from RunActor2 will no longer reach MergePaths2(8).

Expected behavior

Expected behavior

  • The editor canvas should render the connection on the Error output pin of RunActor, regardless of whether the connection was created through the editor UI or through the SDK / MCP integration.
  • Copying nodes that have an error-output connection should preserve the connection on the error pin, not flatten it into the success pin.
  • Either of (a) the SDK should emit the connection in whatever shape the editor and serializer expect for error pins, or (b) both the editor renderer and the copy-paste serializer should accept the legacy "error": [[...]] top-level key produced by the SDK.

Debug Info

Workflow JSON

⚠️ Sensitive values (credential IDs, API tokens, real actor IDs, real targets) have been replaced with placeholders below.

The pre-paste state (saved by the SDK / MCP integration — runtime works, canvas does not render the error connection):

{
  "name": "Repro_OnError_NotRendered",
  "nodes": [
    { "id": "node-1", "name": "Trigger",     "type": "n8n-nodes-base.executeWorkflowTrigger", "typeVersion": 1.1, "position": [0, 0],   "parameters": { "inputSource": "passthrough" } },
    { "id": "node-2", "name": "RunActor",    "type": "@apify/n8n-nodes-apify.apify",          "typeVersion": 1,   "position": [240, 0], "parameters": { "actorSource": "store", "actorId": { "__rl": true, "value": "PLACEHOLDER_ACTOR_ID", "mode": "id" }, "customBody": "={{ JSON.stringify($json) }}", "timeout": 300, "memory": 256 }, "onError": "continueErrorOutput", "credentials": { "apifyApi": { "id": "PLACEHOLDER_CRED_ID", "name": "Apify account" } } },
    { "id": "node-3", "name": "OtherNode",   "type": "@apify/n8n-nodes-apify.apify",          "typeVersion": 1,   "position": [480, 0], "parameters": { "resource": "Datasets", "datasetId": "={{ $json.defaultDatasetId }}", "limit": 1000, "options": {} }, "onError": "continueErrorOutput", "credentials": { "apifyApi": { "id": "PLACEHOLDER_CRED_ID", "name": "Apify account" } } },
    { "id": "node-4", "name": "MergePaths",  "type": "n8n-nodes-base.merge",                  "typeVersion": 3.2, "position": [720, 0], "parameters": { "numberInputs": 9 } }
  ],
  "connections": {
    "Trigger":    { "main":  [[{ "node": "RunActor",   "type": "main", "index": 0 }]] },
    "RunActor":   { "main":  [[{ "node": "OtherNode",  "type": "main", "index": 0 }]],
                    "error": [[{ "node": "MergePaths", "type": "main", "index": 8 }]] },
    "OtherNode":  { "main":  [[{ "node": "MergePaths", "type": "main", "index": 7 }]],
                    "error": [[{ "node": "MergePaths", "type": "main", "index": 8 }]] }
  }
}

The post-copy-paste state (showing the corruption):

"connections": {
  "RunActor2": {
    "main": [[
      { "node": "OtherNode2",  "type": "main", "index": 0 },
      { "node": "MergePaths2", "type": "main", "index": 8 }
    ]]
  },
  "OtherNode2": {
    "main": [[
      { "node": "MergePaths2", "type": "main", "index": 7 },
      { "node": "MergePaths2", "type": "main", "index": 8 }
    ]]
  }
}

Both index: 8 entries belonged on a separate "error" key. The flattening into "main" is the smoking gun.

Operating System

Ubuntu 24.04

n8n Version

2.16.1 Community Edition, self-hosted via Docker

Node.js Version

22.x

Database

PostgreSQL

Execution mode

main (default)

Hosting

self hosted

extent analysis

TL;DR

The issue can be resolved by modifying the SDK to emit connections in the shape expected by the editor, or by updating the editor renderer and copy-paste serializer to accept the legacy "error": [[...]] top-level key.

Guidance

  • Verify that the issue is specific to the @apify/n8n-nodes-apify.apify node version 1 and the n8n-nodes-base.merge node version 3.2.
  • Check if the problem persists when using different node versions or types.
  • Test if manually editing the workflow JSON to use the expected connection shape resolves the issue.
  • Consider updating the SDK to use the expected connection shape or modifying the editor to accept the legacy connection shape.

Example

No code snippet is provided as the issue is related to the interaction between the SDK, editor, and node versions.

Notes

The issue seems to be specific to the combination of node versions and the self-hosted n8n Community Edition. It is unclear if this issue affects other node versions or the cloud-hosted version of n8n.

Recommendation

Apply a workaround by manually editing the workflow JSON to use the expected connection shape until a permanent fix is available. This will ensure that the error connections are properly rendered in the editor and preserved when copying and pasting nodes.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

Expected behavior

  • The editor canvas should render the connection on the Error output pin of RunActor, regardless of whether the connection was created through the editor UI or through the SDK / MCP integration.
  • Copying nodes that have an error-output connection should preserve the connection on the error pin, not flatten it into the success pin.
  • Either of (a) the SDK should emit the connection in whatever shape the editor and serializer expect for error pins, or (b) both the editor renderer and the copy-paste serializer should accept the legacy "error": [[...]] top-level key produced by the SDK.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

n8n - 💡(How to fix) Fix Bug: SDK '.onError()' connections not rendered on canvas and corrupted by copy-paste (Apify community nodes) [1 comments, 2 participants]