Skip to content

Build1 publisher3 min readPublished

n8n execution traces can prove less about branch coverage than the tooling assumes

A dev.to teardown finds IF and Switch coverage readable straight from the trace, while filter counts are reconstruction. The gap is what coverage tools quietly assume.

The Engineer · Build desk

Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

What happened

  • The author of a dev.to post set out to answer which branches a given n8n execution exercised, expecting it to be a lookup: find the nodes, inspect their outputs, and observe which items were present.
  • The author concluded that the n8n execution trace does not record everything: it records specific things, elides others, and sometimes gives enough information to reconstruct what it did not record.
  • In the trace, node outputs are structured as arrays of items, each item having a JSON field, with multiple outputs per node. The example given for a Switch shows output 0 containing an item with id 101, output 1 empty, and output 2 containing an item with id 103.
  • The trace yields three states: an output with items means the branch was taken, an empty output means the branch was skipped, and a node not present at all means the branch did not exist in that run.
  • If a node was not executed, it will not appear in the execution trace.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

A dev.to write-up set out to answer what looked like a lookup question about an n8n workflow, which branches did this execution exercise, and found that the execution trace records specific things, elides others, and only sometimes leaves enough behind to reconstruct the rest [1][2]. That matters because a coverage report that mixes observation with inference gets trusted as if all of it were observation.

The part that works is genuinely useful and quite narrow. Node outputs in the trace are arrays of items, each item carrying a json field, and a node can have several outputs, so a Switch that fired on its first and third paths appears as a populated array, an empty array, and a populated array [3]. That gives three readable states: items present means the branch was taken, an empty output means the branch was skipped, and a node missing from the trace means it did not run at all, because unexecuted nodes do not appear [4][5]. For IF-style branching nodes, that is enough to build a basic coverage report directly out of the trace, with no inference in the middle [6]. The graph tells you a branch exists; only an execution tells you whether anything went down it [7]. In the author's Switch example, two of three outputs carried items, so a third of that node's branches went unexercised in the run [12].

Then the model stops holding. The trap is that third state. An empty output means one thing, an absent node means something else entirely, and the trace does not tell you which you are looking at [8]. So the two most interesting failure signals for a test run, a branch that ran and produced nothing versus a branch that was never reached, collapse into the same absence unless you bring the graph back in.

Filters make the accounting problem concrete. Feed 100 items into a Filter, have the condition match 70, and the trace holds the 70 output items, according to the post. There is no field anywhere in the trace containing the number 100, so the input count is not directly observable and the trace cannot tell you how many items were dropped [9]. You can recover the number as 100 minus 70, but only if you already know the input count from upstream, and that is an inference the trace does not support on its own [10]. Thirty of a hundred is a 30 percent drop rate, which is exactly the kind of figure a dashboard will present without a provenance label [13]. The author's preferred output keeps the seam visible: Input 100 (inferred from upstream), Output 70, Dropped 30 (inferred), rather than the flat claim that the filter dropped 30 items [11].

Filters are only the first stop. The post names loops as the next place the lookup model fails, and the available text breaks off mid-argument before working through them [14][15], so treat loop coverage as an open question rather than a solved one.

The test to apply to any n8n coverage tool, including one you write yourself: ask it to distinguish a node that ran and emitted nothing from a node that never ran, and ask which numbers in its report are read from the trace versus computed from upstream assumptions [16]. If it cannot answer the first, its absences are guesses. If it will not mark the second, its item counts are opinions formatted as measurements.

Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories