opencode-skills-collection 4.0.7 → 4.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundled-skills/.antigravity-install-manifest.json +19 -1
- package/bundled-skills/antigravity-maintainer-batch-release/SKILL.md +15 -6
- package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
- package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
- package/bundled-skills/docs/maintainers/aas-agent-first-control-plane-preview-profile.md +2 -2
- package/bundled-skills/docs/maintainers/aas-agent-first-control-plane-v1-worklog.md +1 -1
- package/bundled-skills/docs/maintainers/release-process.md +15 -0
- package/bundled-skills/docs/maintainers/repo-growth-seo.md +3 -3
- package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
- package/bundled-skills/docs/users/aas-core.md +32 -3
- package/bundled-skills/docs/users/bundles.md +1 -1
- package/bundled-skills/docs/users/claude-code-skills.md +1 -1
- package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
- package/bundled-skills/docs/users/kiro-integration.md +1 -1
- package/bundled-skills/docs/users/usage.md +4 -4
- package/bundled-skills/docs/users/visual-guide.md +4 -4
- package/bundled-skills/docs/vietnamese/README.vi.md +4 -4
- package/bundled-skills/hf-cloud-aws-context-discovery/SKILL.md +96 -0
- package/bundled-skills/markstream-angular/SKILL.md +76 -0
- package/bundled-skills/markstream-custom-components/SKILL.md +74 -0
- package/bundled-skills/markstream-custom-components/references/patterns.md +14 -0
- package/bundled-skills/markstream-install/SKILL.md +3 -1
- package/bundled-skills/markstream-migration/SKILL.md +78 -0
- package/bundled-skills/markstream-migration/references/adoption-checklist.md +10 -0
- package/bundled-skills/markstream-nuxt/SKILL.md +71 -0
- package/bundled-skills/markstream-react/SKILL.md +74 -0
- package/bundled-skills/markstream-svelte/SKILL.md +68 -0
- package/bundled-skills/markstream-vue/SKILL.md +71 -0
- package/bundled-skills/markstream-vue2/SKILL.md +70 -0
- package/bundled-skills/markstream-vue2-cli/SKILL.md +70 -0
- package/bundled-skills/markstream-vue2-vite/SKILL.md +69 -0
- package/bundled-skills/n8n-agents/SKILL.md +301 -0
- package/bundled-skills/n8n-agents/references/CHAT_AGENT_PATTERNS.md +228 -0
- package/bundled-skills/n8n-agents/references/EXAMPLES.md +432 -0
- package/bundled-skills/n8n-agents/references/HUMAN_REVIEW.md +180 -0
- package/bundled-skills/n8n-agents/references/MEMORY.md +139 -0
- package/bundled-skills/n8n-agents/references/RAG.md +102 -0
- package/bundled-skills/n8n-agents/references/STRUCTURED_OUTPUT.md +163 -0
- package/bundled-skills/n8n-agents/references/SUBWORKFLOW_AS_TOOL.md +199 -0
- package/bundled-skills/n8n-agents/references/SYSTEM_PROMPT.md +151 -0
- package/bundled-skills/n8n-agents/references/TOOLS.md +199 -0
- package/bundled-skills/n8n-binary-and-data/SKILL.md +269 -0
- package/bundled-skills/n8n-binary-and-data/references/AGENT_TOOL_BINARY.md +227 -0
- package/bundled-skills/n8n-binary-and-data/references/BINARY_BASICS.md +187 -0
- package/bundled-skills/n8n-binary-and-data/references/CDN_REQUIREMENT.md +109 -0
- package/bundled-skills/n8n-binary-and-data/references/MERGE_FOR_CONTEXT.md +130 -0
- package/bundled-skills/n8n-code-tool/SKILL.md +358 -0
- package/bundled-skills/n8n-code-tool/references/ERROR_PATTERNS.md +178 -0
- package/bundled-skills/n8n-code-tool/references/INPUT_SCHEMA.md +132 -0
- package/bundled-skills/n8n-error-handling/SKILL.md +289 -0
- package/bundled-skills/n8n-error-handling/references/API_WORKFLOWS.md +256 -0
- package/bundled-skills/n8n-error-handling/references/ERROR_WORKFLOWS.md +178 -0
- package/bundled-skills/n8n-error-handling/references/NODE_ERROR_OUTPUTS.md +171 -0
- package/bundled-skills/n8n-error-handling/references/RESPONSE_SHAPES.md +220 -0
- package/bundled-skills/n8n-multi-instance/SKILL.md +208 -0
- package/bundled-skills/n8n-node-configuration/references/NODE_FAMILY_GOTCHAS.md +241 -0
- package/bundled-skills/n8n-subworkflows/SKILL.md +271 -0
- package/bundled-skills/n8n-subworkflows/references/NAMING_AND_DISCOVERY.md +130 -0
- package/bundled-skills/n8n-subworkflows/references/SUBWORKFLOW_PATTERNS.md +147 -0
- package/bundled-skills/using-n8n-mcp-skills/SKILL.md +193 -0
- package/package.json +1 -1
- package/skills_index.json +547 -1
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Workflow-Level Error Workflows
|
|
2
|
+
|
|
3
|
+
Per-node error outputs handle the failures you anticipated on the nodes you remembered to wire. A **workflow-level error workflow** is the catch-all for everything else — and for an unattended workflow (scheduled, cron, queue worker), it's the difference between "the job silently stopped three days ago" and "an alert arrived the moment it broke".
|
|
4
|
+
|
|
5
|
+
What per-node outputs **don't** catch:
|
|
6
|
+
|
|
7
|
+
- Failures on nodes you forgot to wire.
|
|
8
|
+
- Crashes between nodes.
|
|
9
|
+
- Whole-workflow timeouts.
|
|
10
|
+
- Trigger failures.
|
|
11
|
+
|
|
12
|
+
When an unhandled error escapes any of those, n8n invokes the designated **error workflow** with the failure context. You build that workflow once; it serves every workflow that points at it.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## What the error workflow receives
|
|
17
|
+
|
|
18
|
+
It starts with an **Error Trigger** node, which fires with roughly this payload:
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"execution": {
|
|
23
|
+
"id": "...",
|
|
24
|
+
"url": "https://your-n8n/workflow/<wfId>/executions/<execId>",
|
|
25
|
+
"retryOf": "...",
|
|
26
|
+
"error": {
|
|
27
|
+
"name": "NodeApiError",
|
|
28
|
+
"message": "...",
|
|
29
|
+
"description": "...",
|
|
30
|
+
"timestamp": 1715000000000
|
|
31
|
+
},
|
|
32
|
+
"lastNodeExecuted": "Fetch order",
|
|
33
|
+
"mode": "trigger"
|
|
34
|
+
},
|
|
35
|
+
"workflow": { "id": "...", "name": "Sync Stripe customers" }
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Note what's **not** there: the payload carries the error message and the failed node's *name* (`lastNodeExecuted`), but **not the input data** that caused the failure. Recovering that takes an extra step (below).
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Minimal error workflow (capture → notify)
|
|
44
|
+
|
|
45
|
+
For most workflows, this is enough:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
Error Trigger → Set (build alert message) → Slack / email (post to #incidents)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Three nodes. Fast, hard to get wrong, and it turns silence into a message. Build it with `n8n_create_workflow` (or the partial-update ops), then assign it in the UI (see "Assigning it" below).
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## What to put in the alert
|
|
56
|
+
|
|
57
|
+
A good notification lets on-call act without opening n8n first. Pull these from the payload:
|
|
58
|
+
|
|
59
|
+
| Field | Expression |
|
|
60
|
+
|---|---|
|
|
61
|
+
| Workflow name | `{{ $json.workflow.name }}` |
|
|
62
|
+
| Workflow ID | `{{ $json.workflow.id }}` |
|
|
63
|
+
| Editor link | `{{ $json.execution.url.split('/executions/')[0] }}` |
|
|
64
|
+
| Execution ID | `{{ $json.execution.id }}` |
|
|
65
|
+
| Execution link | `{{ $json.execution.url }}` |
|
|
66
|
+
| Failed node | `{{ $json.execution.lastNodeExecuted }}` |
|
|
67
|
+
| Error message | `{{ $json.execution.error.message }}` |
|
|
68
|
+
| Error description | `{{ $json.execution.error.description }}` (often empty, useful when set) |
|
|
69
|
+
| Timestamp | `{{ DateTime.fromMillis($json.execution.error.timestamp).toISO() }}` |
|
|
70
|
+
|
|
71
|
+
The `timestamp` is a Unix-ms number — format it with Luxon's `DateTime.fromMillis(...)`. The execution `url` is `{base}/workflow/{id}/executions/{execId}`, so stripping the `/executions/...` tail gives the editor URL.
|
|
72
|
+
|
|
73
|
+
A useful Slack body:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
Workflow failure: *{{ $json.workflow.name }}* (`{{ $json.workflow.id }}`)
|
|
77
|
+
Open editor: {{ $json.execution.url.split('/executions/')[0] }}
|
|
78
|
+
Failed node: `{{ $json.execution.lastNodeExecuted }}`
|
|
79
|
+
Error: {{ $json.execution.error.message }}
|
|
80
|
+
Execution: {{ $json.execution.url }}
|
|
81
|
+
Time: {{ DateTime.fromMillis($json.execution.error.timestamp).toISO() }}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Two links matter: the **editor link** so on-call can start fixing, and the **execution link** so they can see the exact failed run. Skipping either costs a step. "Workflow failed." is not an alert — it's a notification that you'll have to investigate from scratch.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Featureful version: recover the failing input
|
|
89
|
+
|
|
90
|
+
The Error Trigger payload tells you *which* node failed, not *what data* broke it. To get the offending payload, fetch the execution with the **n8n** node:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
Error Trigger
|
|
94
|
+
→ n8n (resource: Execution, operation: Get,
|
|
95
|
+
Execution ID: {{ $json.execution.id }},
|
|
96
|
+
Include Execution Details: true)
|
|
97
|
+
→ Set (extract failed-node input from the execution data)
|
|
98
|
+
→ Switch (route by severity)
|
|
99
|
+
├── high → PagerDuty
|
|
100
|
+
├── med → Slack #incidents
|
|
101
|
+
└── low → Slack #monitoring
|
|
102
|
+
→ Data Table (log for tracking)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
"Include Execution Details: true" hits `GET /executions/{id}?includeData=true` and returns the full run data, so you can pluck the failed node's input out of `data.resultData.runData[<lastNodeExecuted>]`. Now the on-call message can carry the actual offending payload (which customer, which order id), not just "node X errored".
|
|
106
|
+
|
|
107
|
+
Caveats, all of which can turn the error workflow itself into a *new* silent failure:
|
|
108
|
+
|
|
109
|
+
- **Requires an n8n API credential** on this workflow (Settings → API → personal access token, then attach it to the n8n node). Without it the node throws a 401 — an unhandled error *inside the error workflow*.
|
|
110
|
+
- **Requires the failing workflow to persist execution data** (Save Execution Data, instance default or per-workflow). If it doesn't, the API returns metadata only.
|
|
111
|
+
- **The n8n node call can itself fail** (API down, rate-limited). Wire its error output (`sourceIndex: 1`) to a fallback that still notifies, or the original error vanishes behind a fetch failure.
|
|
112
|
+
|
|
113
|
+
Minimal is enough most of the time. The featureful version earns its keep on production-critical workflows where on-call minutes matter.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Assigning it (UI only — the MCP can't)
|
|
118
|
+
|
|
119
|
+
> The error workflow is assigned in the n8n **UI**: per workflow under **Workflow Settings → Error Workflow**, or as an instance-wide default. There is **no community-MCP tool** to set this assignment. `n8n_update_partial_workflow` exposes an `updateSettings` op, but the error-workflow setting is not reliably writable through it — confirm in the UI.
|
|
120
|
+
|
|
121
|
+
So the agent's job is: **build the error workflow with the MCP, then hand the user the exact UI step** — "Open the failing workflow → Settings → Error Workflow → select '<name>'" — and remind them to do it for *every* unattended workflow (or set the instance default once). Building the workflow without assigning it does nothing; the trigger only fires for workflows that point at it.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## When the error workflow fires (and when it doesn't)
|
|
126
|
+
|
|
127
|
+
**Fires** when:
|
|
128
|
+
|
|
129
|
+
- A node throws unhandled (not routed via a wired per-node error output).
|
|
130
|
+
- The workflow itself fails (timeout, OOM).
|
|
131
|
+
- A trigger fails (rare, possible for non-webhook triggers).
|
|
132
|
+
|
|
133
|
+
**Does NOT fire** when:
|
|
134
|
+
|
|
135
|
+
- A node's error output is wired — even if the handler does nothing. n8n considers the error *handled*.
|
|
136
|
+
- You manually stop an execution.
|
|
137
|
+
- The workflow is paused / inactive.
|
|
138
|
+
|
|
139
|
+
That second case is the subtle one: **a per-node error output wired to a no-op that drops the data will *suppress* the error workflow.** From n8n's perspective the error was handled, even though it was swallowed. So only catch per-node when you're genuinely acting on the error; if you want a failure to bubble up to the catch-all, leave it unwired.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## What the error workflow should NOT do
|
|
144
|
+
|
|
145
|
+
- **Make external calls that can themselves fail without a fallback.** If the error workflow fails, the original error disappears — you've added a second silent failure on top of the first.
|
|
146
|
+
- **Take significant time.** It runs synchronously; a slow error workflow compounds the original failure's impact.
|
|
147
|
+
|
|
148
|
+
Keep it fast: parse, notify, return.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## The recursion trap
|
|
153
|
+
|
|
154
|
+
If your monitored workflows alert Slack, and the *error* workflow also alerts Slack, then a Slack outage takes out both — the error workflow fails and the failure goes nowhere. n8n won't re-trigger on its own failure (no infinite loop), but you've lost the alert.
|
|
155
|
+
|
|
156
|
+
Mitigations:
|
|
157
|
+
|
|
158
|
+
- **Use a different channel than the monitored workflows.** If everything notifies Slack, the error workflow should use email (or vice versa).
|
|
159
|
+
- **Add a fallback** — write to a Data Table (`n8n_manage_datatable`) if the primary notification fails, so there's always a trace.
|
|
160
|
+
- **Lean on instance-level logging** (server logs, Sentry) so even an error-workflow failure surfaces somewhere outside n8n.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Verifying it works
|
|
165
|
+
|
|
166
|
+
After building and assigning:
|
|
167
|
+
|
|
168
|
+
1. Make a throwaway workflow that always fails — e.g. an HTTP Request to an invalid URL, with **no** error output wired so the failure is unhandled.
|
|
169
|
+
2. Run it.
|
|
170
|
+
3. Confirm the error workflow fires and the notification arrives.
|
|
171
|
+
|
|
172
|
+
This catches the setup mistakes that otherwise stay invisible until a real incident: wrong workflow assigned, wrong channel, missing API credential. Do it once before you rely on the alerting.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Drift watch
|
|
177
|
+
|
|
178
|
+
The Error Trigger payload shape can shift between n8n versions. If a field isn't where this file says, check current n8n docs and update your expressions — a renamed field fails silently as an empty alert, not a thrown error.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Per-Node Error Outputs
|
|
2
|
+
|
|
3
|
+
This file is about the **error output on a single node** — the second `main` output that fires when that node throws — and the two-step setup that trips up nearly everyone. For the workflow-level catch-all (Error Trigger workflows) and the webhook/Respond shape, see the rest of `n8n-error-handling`.
|
|
4
|
+
|
|
5
|
+
The whole point: a node failing should route somewhere *you* control, instead of halting the run. The cost of forgetting half the setup is one of the worst silent-failure modes in n8n — a run that shows green while quietly dropping its work.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## The two-step setup (both are required)
|
|
10
|
+
|
|
11
|
+
Routing a node's failure takes exactly two changes. Either one alone looks finished and misbehaves.
|
|
12
|
+
|
|
13
|
+
### Step 1 — create the error output
|
|
14
|
+
|
|
15
|
+
Set `onError: "continueErrorOutput"` on the node. This is what *adds* the second output. Until you do, `main[1]` does not exist and nothing you wire to it can fire.
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
{ type: "updateNode", nodeName: "Google Sheets",
|
|
19
|
+
changes: { onError: "continueErrorOutput" } }
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Surgical alternative if you're touching only this field:
|
|
23
|
+
|
|
24
|
+
```javascript
|
|
25
|
+
{ type: "patchNodeField", nodeName: "Google Sheets",
|
|
26
|
+
fieldPath: "onError", value: "continueErrorOutput" }
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The valid `onError` values:
|
|
30
|
+
|
|
31
|
+
| Value | Effect |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `"stopWorkflow"` (default) | Error halts the whole workflow. The right default for runs you watch. |
|
|
34
|
+
| `"continueRegularOutput"` | The error item flows out the **normal** output (`main[0]`) alongside successes. Rare and usually a mistake — downstream gets error-shaped data and keeps going. |
|
|
35
|
+
| `"continueErrorOutput"` | The error item flows out a **separate** error output (`main[1]`). This is the one you wire below. |
|
|
36
|
+
|
|
37
|
+
### Step 2 — wire the error output
|
|
38
|
+
|
|
39
|
+
With `onError: "continueErrorOutput"`, the node has two outputs:
|
|
40
|
+
|
|
41
|
+
- `main[0]` → success path (`sourceIndex: 0`)
|
|
42
|
+
- `main[1]` → error path (`sourceIndex: 1`)
|
|
43
|
+
|
|
44
|
+
Wire the error output to a real handler:
|
|
45
|
+
|
|
46
|
+
```javascript
|
|
47
|
+
{ type: "addConnection",
|
|
48
|
+
source: "Google Sheets",
|
|
49
|
+
target: "Handle Error",
|
|
50
|
+
sourceIndex: 1 }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`sourceIndex: 1` is the error output. (IF nodes accept the friendly aliases `branch: "true"`/`branch: "false"` for index 0/1; a generic fallible node has no such alias — use the explicit `sourceIndex: 1`.)
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Failure modes — why "one of two" is so dangerous
|
|
58
|
+
|
|
59
|
+
### `onError` set, error output NOT wired
|
|
60
|
+
|
|
61
|
+
```javascript
|
|
62
|
+
// onError: "continueErrorOutput" set on the node,
|
|
63
|
+
// but no addConnection from sourceIndex 1.
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
On failure the node emits to `main[1]`, which has **no targets**. The error data is silently discarded, downstream never fires, and — this is the trap — the execution is recorded as **succeeded**, because from n8n's perspective the error was "handled" by a branch that happens to go nowhere. No failed execution logged, nothing in the dashboard. The integration "just stops working" and there's no trail.
|
|
67
|
+
|
|
68
|
+
**Fix:** wire `sourceIndex: 1` to a real handler, *or* set `onError` back to `"stopWorkflow"` so the failure is loud again.
|
|
69
|
+
|
|
70
|
+
### Error output wired, `onError` NOT set
|
|
71
|
+
|
|
72
|
+
```javascript
|
|
73
|
+
// addConnection from "Some Node" sourceIndex 1 → "Handle Error" exists,
|
|
74
|
+
// but the node still has the default onError: "stopWorkflow".
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The connection sits in the JSON, but the slot it feeds from never fires. The handler is unreachable. On failure the workflow simply **halts** (default behavior). Less dangerous than the first mode — at least it's loud — but the handler you built does nothing.
|
|
78
|
+
|
|
79
|
+
**Fix:** set `onError: "continueErrorOutput"` on the node.
|
|
80
|
+
|
|
81
|
+
### Why validation won't save you
|
|
82
|
+
|
|
83
|
+
A half-wired error output **validates clean**. `validate_workflow` and `n8n_validate_workflow` don't flag "`onError` is set but `main[1]` is empty" or vice versa — both are structurally legal. This is a runtime behavior, not a schema violation. The only reliable check is to read the workflow back (see Verification below).
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Common wiring shapes
|
|
88
|
+
|
|
89
|
+
### Single fallible node → error handler
|
|
90
|
+
|
|
91
|
+
```javascript
|
|
92
|
+
// Node config: onError: "continueErrorOutput"
|
|
93
|
+
{ type: "addConnection", source: "HTTP Request", target: "Respond Error", sourceIndex: 1 }
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Success path fans out, error path goes elsewhere
|
|
97
|
+
|
|
98
|
+
```javascript
|
|
99
|
+
{ type: "addConnection", source: "HTTP Request", target: "Save Result", sourceIndex: 0 }
|
|
100
|
+
{ type: "addConnection", source: "HTTP Request", target: "Notify Slack", sourceIndex: 0 }
|
|
101
|
+
{ type: "addConnection", source: "HTTP Request", target: "Respond Error", sourceIndex: 1 }
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Multiple fallible nodes → one shared error handler (fan-in)
|
|
105
|
+
|
|
106
|
+
```javascript
|
|
107
|
+
// Each of these nodes needs onError: "continueErrorOutput" on its own config.
|
|
108
|
+
{ type: "addConnection", source: "Fetch User", target: "Respond Error", sourceIndex: 1 }
|
|
109
|
+
{ type: "addConnection", source: "Call External", target: "Respond Error", sourceIndex: 1 }
|
|
110
|
+
{ type: "addConnection", source: "Write Database", target: "Respond Error", sourceIndex: 1 }
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Fan-in keeps the graph readable: one error responder, many sources. The handler can inspect which node failed (the error payload carries the failing node's name) to differentiate the response.
|
|
114
|
+
|
|
115
|
+
### Both log AND respond on the same failure
|
|
116
|
+
|
|
117
|
+
Wiring the error output to two targets composes without conflict — both receive the error data:
|
|
118
|
+
|
|
119
|
+
```javascript
|
|
120
|
+
{ type: "addConnection", source: "Call External", target: "Log Full Error", sourceIndex: 1 }
|
|
121
|
+
{ type: "addConnection", source: "Call External", target: "Respond Error", sourceIndex: 1 }
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Useful when you want a sanitized response *and* a private full-detail log on the same failure. (Or chain them: error output → Log → Respond, so the log runs first.)
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## What counts as "fallible"
|
|
129
|
+
|
|
130
|
+
Wire an error output on anything that can throw at runtime:
|
|
131
|
+
|
|
132
|
+
- Network calls — HTTP Request, third-party API nodes, databases.
|
|
133
|
+
- Auth failures — expired credential, rotated token.
|
|
134
|
+
- Schema mismatches — missing DB column, JSON parse failure.
|
|
135
|
+
- Rate limits — 429 from upstream (configure `retryOnFail` first so these self-heal).
|
|
136
|
+
- File/binary operations — missing path, permission denied (see **n8n-binary-and-data**).
|
|
137
|
+
- Code nodes that can throw.
|
|
138
|
+
|
|
139
|
+
Usually **not** worth an error output:
|
|
140
|
+
|
|
141
|
+
- Set / Edit Fields on already-validated data.
|
|
142
|
+
- IF / Switch with simple expressions — if those throw it's a bug to fix, not a path to catch.
|
|
143
|
+
- Pure transformations with no I/O.
|
|
144
|
+
|
|
145
|
+
When unsure, wire it. The cost is one connection; the cost of not wiring it is a silent halt.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Verification (do this every time)
|
|
150
|
+
|
|
151
|
+
After any create/update, pull the workflow with `n8n_get_workflow` and check **both halves** on each fallible node:
|
|
152
|
+
|
|
153
|
+
1. **Node config** — `onError` is `"continueErrorOutput"` (or whatever you intended).
|
|
154
|
+
2. **Connections** — `connections["<node>"].main[1]` contains the expected handler(s).
|
|
155
|
+
|
|
156
|
+
If either half is missing, you have a silent-failure setup. Fix before activating.
|
|
157
|
+
|
|
158
|
+
`n8n_autofix_workflow` can repair some structural issues, but it won't infer that you *meant* to wire an error path — the intent to handle a given node's failure is yours to express. Treat the read-back as mandatory.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## When to use an error workflow instead
|
|
163
|
+
|
|
164
|
+
Per-node outputs handle the failure of *one node you remembered to wire*. They do **not** catch:
|
|
165
|
+
|
|
166
|
+
- Failures on nodes you forgot to wire.
|
|
167
|
+
- Crashes between nodes.
|
|
168
|
+
- Whole-workflow timeouts.
|
|
169
|
+
- Trigger failures.
|
|
170
|
+
|
|
171
|
+
For those, you need a workflow-level **error workflow** (Error Trigger node). And note the inverse: a per-node error output that's wired to a no-op which drops the data counts as "handled" — so it will *suppress* the error workflow. Only catch per-node when you're genuinely acting on the error. See **ERROR_WORKFLOWS.md**.
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Response Shapes
|
|
2
|
+
|
|
3
|
+
Conventions for webhook API response bodies — both success and error. The goal is **predictability**: a caller, a dashboard, or a retry loop should be able to branch on your response without guessing. Pick a shape and hold it across every endpoint on the instance.
|
|
4
|
+
|
|
5
|
+
This file is opinions with reasons. The one hard rule is consistency: **consistency within your project beats consistency with this file.** If your repo or company already has a documented API style, that wins.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## First, match what's already on the instance
|
|
10
|
+
|
|
11
|
+
Before adopting any shape here, look at the API workflows already running and reuse their conventions. A one-off custom shape is hard to undo once callers depend on it, and inconsistency across endpoints is worse than any single choice.
|
|
12
|
+
|
|
13
|
+
Search with the MCP, then read each result:
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
search_nodes({ query: "webhook" }) // find webhook-shaped workflows via templates
|
|
17
|
+
n8n_list_workflows({ /* filter */ }) // list workflows on the instance
|
|
18
|
+
n8n_get_workflow({ id: "<id>" }) // read each one's Respond to Webhook nodes
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
In each existing `Respond to Webhook`, note:
|
|
22
|
+
|
|
23
|
+
- Top-level keys — envelope vs bare, presence of `error`/`message`/`request_id`.
|
|
24
|
+
- Whether success bodies wrap the payload or return it bare.
|
|
25
|
+
- The exact error-code strings in use (`validation_error` vs `bad_request` vs `INVALID_INPUT`).
|
|
26
|
+
- Header conventions (`Content-Type`, `Retry-After`, `X-Request-Id`).
|
|
27
|
+
|
|
28
|
+
If results are sparse, mixed, or you can't tell whether a convention exists — **ask the user.** "Endpoints A and B use shape X, C uses Y; which is house style?" saves a future migration. Don't invent a domain prefix or envelope from nothing.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Success shape
|
|
33
|
+
|
|
34
|
+
Return the data bare. For requests that **create or update** a resource, prefer returning the **full resource** with a 200, not `{ "ok": true }` or just the new ID:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"customer_id": "cus_123",
|
|
39
|
+
"balance": 4200,
|
|
40
|
+
"currency": "USD",
|
|
41
|
+
"created_at": "2026-04-25T12:34:00Z"
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Returning the resource saves the caller a follow-up GET, lets them confirm what actually persisted (server defaults, normalized values, generated timestamps), and makes the endpoint a single round-trip for a UI that renders the result immediately.
|
|
46
|
+
|
|
47
|
+
Deviate only when:
|
|
48
|
+
|
|
49
|
+
- The resource is genuinely large and the caller doesn't need it → return the ID, document why.
|
|
50
|
+
- There is no resource (event ingestion, fire-and-forget) → `{}` or `204 No Content`.
|
|
51
|
+
- The payload is list-shaped → a top-level array, or `{ "items": [...] }` (friendlier to future pagination metadata).
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Error shape (the default envelope)
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"error": "<machine-readable code>",
|
|
60
|
+
"message": "<human-readable explanation>"
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- `error` is a **stable string identifier**, not a sentence. Clients branch on it.
|
|
65
|
+
- `message` is the human version — safe to log, safe to show users *after* sanitization.
|
|
66
|
+
- No `ok: false` flag — the HTTP status code already separates success from failure.
|
|
67
|
+
|
|
68
|
+
Optional fields by case:
|
|
69
|
+
|
|
70
|
+
| Field | When to include |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `details` | Validation errors, with a field-by-field map |
|
|
73
|
+
| `retry_after` | Rate limits (also set the `Retry-After` header) |
|
|
74
|
+
| `request_id` | When you run distributed tracing (then on *every* response, not just errors) |
|
|
75
|
+
| `documentation_url` | Public APIs where you want callers to RTFM |
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## `responseCode` defaults to 200 — set it on every error branch
|
|
80
|
+
|
|
81
|
+
This is the single most common API error-handling bug, and it's worth its own section because it produces a *worse-than-useless* result: the body says failure while the status says success.
|
|
82
|
+
|
|
83
|
+
**Every `Respond to Webhook` node defaults `responseCode` to 200** — including the ones you wired to error paths. An error branch that returns 200 with `{ "error": "..." }` looks like success to the caller's HTTP client, so their error handling (which keys off the status code) **never fires**. They process your error body as if it were data.
|
|
84
|
+
|
|
85
|
+
So: set `responseCode` **explicitly** on every Respond node — not just the success one. (This trap is also documented in **n8n-node-configuration** NODE_FAMILY_GOTCHAS.md, "Webhook / Respond to Webhook".) A workflow can have many Respond nodes, one per response shape; n8n returns whichever fires first.
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{ "responseCode": 502,
|
|
89
|
+
"responseBody": "={{ JSON.stringify({ error: 'upstream_error', message: 'External service failed' }) }}" }
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
For paths that differ only by number, set it with an expression instead of fanning out to N nodes — see **API_WORKFLOWS.md**, "5xx: differentiate the body".
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Status code → cause
|
|
97
|
+
|
|
98
|
+
The status code is the caller's first signal; be deliberate.
|
|
99
|
+
|
|
100
|
+
- **2xx** — success. 200 sync, 202 "accepted, processing".
|
|
101
|
+
- **4xx** — caller's fault. 400 bad input, 401 no auth, 403 not allowed, 404 not found, 409 conflict, 429 rate limited.
|
|
102
|
+
- **5xx** — your fault. 500 unexpected internal, 502 upstream broken, 503 temporarily down, 504 upstream timeout.
|
|
103
|
+
|
|
104
|
+
Distinguishing 4xx from 5xx matters because the caller's tooling depends on it:
|
|
105
|
+
|
|
106
|
+
- Caller monitoring alerts on 5xx (your fault) but not 4xx (their fault). Returning 500 for bad input fires *their* pager on *their* bug.
|
|
107
|
+
- 5xx implies "retry", 4xx implies "don't bother".
|
|
108
|
+
- Aggregated error rates segment by class — collapse everything to 500 and you lose that.
|
|
109
|
+
|
|
110
|
+
### Error codes (a small, stable set)
|
|
111
|
+
|
|
112
|
+
Adding a code is fine; renaming an existing one breaks callers.
|
|
113
|
+
|
|
114
|
+
**4xx — caller's fault**
|
|
115
|
+
|
|
116
|
+
| Code | Meaning |
|
|
117
|
+
|---|---|
|
|
118
|
+
| `validation_error` | Required field missing / type wrong |
|
|
119
|
+
| `invalid_input` | Field present but value invalid |
|
|
120
|
+
| `unauthorized` | No auth or expired auth |
|
|
121
|
+
| `forbidden` | Authenticated but not allowed |
|
|
122
|
+
| `not_found` | Resource doesn't exist |
|
|
123
|
+
| `conflict` | Conflicts with current state (duplicate key, race) |
|
|
124
|
+
| `rate_limit_exceeded` | Too many requests |
|
|
125
|
+
| `unsupported_media_type` | Content-Type wrong |
|
|
126
|
+
|
|
127
|
+
**5xx — your fault**
|
|
128
|
+
|
|
129
|
+
| Code | Meaning |
|
|
130
|
+
|---|---|
|
|
131
|
+
| `internal_error` | Catch-all, something failed unexpectedly |
|
|
132
|
+
| `upstream_error` | Third-party API returned an error |
|
|
133
|
+
| `upstream_timeout` | Third-party API didn't respond in time |
|
|
134
|
+
| `service_unavailable` | Temporarily can't process (down, or rate-limited upstream) |
|
|
135
|
+
| `not_implemented` | Operation not supported in this version |
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Validation error details (400)
|
|
140
|
+
|
|
141
|
+
For `validation_error`, include per-field detail so the caller can fix the request without guessing. The Set-node schema validator (API_WORKFLOWS.md) produces this directly:
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
{
|
|
145
|
+
"error": "validation_error",
|
|
146
|
+
"message": "Validation failed (3 issues):\n• name: Missing required field \"name\"\n• email: \"not-an-email\" is not valid - Contact email address\n• plan: \"premium\" is not allowed. Must be one of: starter, pro, enterprise - Subscription plan",
|
|
147
|
+
"details": { "name": "Missing required field \"name\"", "email": "\"not-an-email\" is not valid", "plan": "\"premium\" is not allowed" },
|
|
148
|
+
"request_schema": { "type": "object", "properties": { } }
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`message` is the human summary (safe to show), `details` is the structured per-field map (safe to bind to UI fields), and `request_schema` is the schema echoed back so an LLM-driven or programmatic caller can self-correct on the next attempt.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Rate-limit responses (429)
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"error": "rate_limit_exceeded",
|
|
161
|
+
"message": "Too many requests. Retry after 30s.",
|
|
162
|
+
"retry_after": "2026-05-08T21:10:05.135Z"
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Also set the HTTP `Retry-After` header (in the Respond node's `options.responseHeaders`). Well-behaved clients respect the header without parsing the body.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## What NOT to put in an error response
|
|
171
|
+
|
|
172
|
+
The body goes to the caller. Treat everything in it as public.
|
|
173
|
+
|
|
174
|
+
| Don't include | Why |
|
|
175
|
+
|---|---|
|
|
176
|
+
| **Stack traces** — `{ "stack": "Error at line 42 of /opt/..." }` | Reveals paths, versions, library names. A gift to attackers, useless to callers. |
|
|
177
|
+
| **Upstream errors verbatim** — `{ "details": "<raw upstream body>" }` | Upstream may embed *their* tokens and PII. Surface "upstream service failed" + a request id; details go to your logs. |
|
|
178
|
+
| **SQL queries** — `{ "query": "SELECT * FROM users WHERE ..." }` | Exposes schema and access patterns. |
|
|
179
|
+
| **Tokens / credentials / auth values** | Even innocuous-looking `headers`, `config`, or `request` fields can carry token values. Audit error bodies — leaks are easier than you'd expect. |
|
|
180
|
+
|
|
181
|
+
The pattern is always the same: **log the full error privately, return a sanitized message.** See "Don't leak internals" in API_WORKFLOWS.md for the log-then-respond wiring.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Respond node shape (JSON, for the community MCP)
|
|
186
|
+
|
|
187
|
+
Success:
|
|
188
|
+
|
|
189
|
+
```json
|
|
190
|
+
{
|
|
191
|
+
"type": "n8n-nodes-base.respondToWebhook",
|
|
192
|
+
"name": "Respond Success",
|
|
193
|
+
"parameters": {
|
|
194
|
+
"respondWith": "json",
|
|
195
|
+
"responseCode": 200,
|
|
196
|
+
"responseBody": "={{ JSON.stringify($json) }}",
|
|
197
|
+
"options": { "responseHeaders": { "entries": [{ "name": "Content-Type", "value": "application/json" }] } }
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Error:
|
|
203
|
+
|
|
204
|
+
```json
|
|
205
|
+
{
|
|
206
|
+
"type": "n8n-nodes-base.respondToWebhook",
|
|
207
|
+
"name": "Respond Error",
|
|
208
|
+
"parameters": {
|
|
209
|
+
"respondWith": "json",
|
|
210
|
+
"responseCode": 502,
|
|
211
|
+
"responseBody": "={{ JSON.stringify({ error: 'upstream_error', message: 'External service failed' }) }}",
|
|
212
|
+
"options": { "responseHeaders": { "entries": [{ "name": "Content-Type", "value": "application/json" }] } }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Two notes that bite people:
|
|
218
|
+
|
|
219
|
+
- **Always set `Content-Type: application/json` explicitly.** Default behavior depends on the body shape and isn't reliable.
|
|
220
|
+
- **With `respondWith: "json"`, pass the object, not a stringified string.** If you hand it `JSON.stringify(obj)` it serializes that string *again* and you get a double-encoded body. Either use `respondWith: "json"` with an object expression (`={{ { error: 'x' } }}`), or keep `JSON.stringify(...)` and let the node treat it as the already-final body — pick one and be consistent. (See **n8n-node-configuration** NODE_FAMILY_GOTCHAS.md.)
|