The idea is:
n8n's native OpenTelemetry is good. When a node throws and onError is set to continue, the node span already comes through as Error with the exception recorded. Thank you for that.
There is one path it does not cover. When a node handles the error itself per item (it calls continueOnFail(), writes the error into its output items, and returns without throwing), the engine never sets a node-level error, so the span is exported as status = OK with no error information. An HTTP Request node set to continue on a failed call is the common example: the call returns 503, the node puts the error in the item, the run stays green, and the span says OK. The only trace of the failure is inside the output item JSON, which the span does not carry.
The ask is to bring that path in line with the throw path, additively:
1. Set the node span status to Error when the node continued a per-item error, the same as the throw path already does, while the parent execution span stays OK. Some consumers count any error-status span as a failed run, so this is worth offering as opt-in (a config flag) if that is a concern. The attributes below carry the signal regardless of status.
2. Attach the error using stable OTel attributes, so any backend reads it with no custom code. The important one is error.type, which is stable and low cardinality:
error.type = "503" # the HTTP status as a string, or the error class, or \_OTHER
When the error carries an HTTP status, also expose it in its native form:
http.response.status_code = 503 # int, standard HTTP semconv
Full detail where available uses the stable exception attributes: exception.type, exception.message, exception.stacktrace. Note that the exceptions-on-spans convention is deprecated in favor of exceptions-in-logs, and exception.escaped is deprecated, so I would leave the exact vehicle (span event or a correlated log record) to your read of current OTel guidance. The ask is the standard attribute names, not a specific method.
None of this needs output-shape parsing. n8n holds the typed error at the point it decides to continue, one step before it demotes it into the node output.
3. Populate n8n's existing continuation attributes so a consumer can tell a continued error from a clean run without inspecting output. n8n.continuation.reason and n8n.node.termination_reason already exist in the constants; documenting and setting them for this case is enough:
n8n.continuation.reason = "continueOnFail" | "errorOutput"
4. Document and guarantee the item-count attributes. n8n.node.items.input and n8n.node.items.output are already emitted. Committing to them as a stable, documented convention on every node span makes volume-based checks first-class (input greater than zero, output at or near zero, is a strong drop signal).
Out of scope on purpose: this is not asking n8n to judge whether output is semantically wrong or degraded (correct shape, correct count, wrong data). That is user-specific and belongs in downstream assertions. The ask is limited to facts n8n already holds at emit time.
My use case:
I build observability on top of n8n's OpenTelemetry traces and needed to catch runs that finish green but silently did nothing, which is the most dangerous failure class because the usual signal (a failed execution) never fires.
A common, sensible setup: an HTTP Request node set to continue on fail, so one bad upstream call does not fail the whole workflow. The upstream returns a 503. The node writes the error into its output item and continues. The run is reported as success, every node green.
Here is the same HTTP node on two runs, as exported today by native OTel.
Healthy run:
span: Get Orders
status: OK
n8n.node.name = Get Orders
n8n.node.type = n8n-nodes-base.httpRequest
n8n.node.items.input = 1
n8n.node.items.output = 100
Continue-on-fail run, upstream returned 503:
span: Get Orders
status: OK (the node errored per item; the span says OK)
n8n.node.name = Get Orders
n8n.node.type = n8n-nodes-base.httpRequest
n8n.node.items.input = 1
n8n.node.items.output = 1 (an error item, not a result)
(no error.type, no message, no status code anywhere on the span)
The item counts are useful and already there. The error itself never reaches the span, so a backend cannot tell these two runs apart from the span alone. Note this is specifically the per-item path. If the same node threw instead, native OTel would already mark this span Error, which is exactly the behavior I am asking for here too.
I think it would be beneficial to add this because:
For the per-item path, the trace disagrees with reality: a node errored, and the span says OK. That forces every downstream consumer to fetch and parse node output to recover the error, and the error is shaped inconsistently by source (an HTTP failure is an object with a status field, a thrown Code error is a bare string), so each backend reinvents the same fragile parsing.
Moving it into the telemetry using OTel's standard error.type and exception attributes fixes this once, at the source, for everyone. It lights up in any OTel backend (Grafana, Honeycomb, Datadog, self-hosted collectors), not just custom tooling, and it costs almost nothing because the typed error is already in hand at the moment of continuation. It also makes the two continued-error paths (throw and per-item) consistent, which they are not today.
Any resources to support this?
- Full writeup with real traces and screenshots: ageniusailabs.com/blog/silent-but-deadly
- Technical detail, including the inconsistent error shapes across node types: silent-failure-detection.md
- OTel
error.type(stable, the primary attribute this asks for): opentelemetry.io/docs/specs/semconv/registry/attributes/error - OTel HTTP span attributes (
http.response.status_code): opentelemetry.io/docs/specs/semconv/http/http-spans - OTel exception semantics (now via logs; spans form deprecated): opentelemetry.io/docs/specs/semconv/exceptions/exceptions-logs
(This surfaced while building an open-source n8n observability dashboard that reads these traces.)
Are you willing to work on this?
Yes, happy to help. I can share sanitized traces, test any change against a live n8n to OTLP pipeline, and contribute a PR. If someone points me at where the per-item continueOnFail result is finalized in workflow-execute.ts relative to where the node span status is set, I can scope it.





