Field guide / n8n release checks

Two items. One retry.
Two hidden failures.

Testing n8n HTTP request bodies, item pairing, and failed-render recovery—with an executed example you can inspect and rerun.

A render loop can handle its first request correctly and break when a failed job brings execution back to the same HTTP node. The request still needs the original short’s styling. The current input now contains a status response.

We reproduced that failure in an isolated slice of a public n8n workflow. A second input variant failed even earlier, while n8n was constructing the first JSON body. This guide traces both failures and shows the checks that distinguish them.

Recorded environment

n8n 2.41.7 · Node.js 26.10.0 · October 5, 2026. The lab ran with synthetic items and local HTTP responses in a Linux network namespace with loopback enabled. The eight-scenario results include six successful executions and two reproduced failures.

1. Start with two deliberately different items

The source is MI’s MIT-licensed YouTube-to-shorts workflow at a historical migration commit. We retained its render-loop nodes and replaced the external service with a local HTTP server.

Give the loop two short IDs, each with visibly different styling:

42 → { color: '#112233', fontSize: 31 }
43 → { color: '#aabbcc', fontSize: 47 }

This makes accidental reuse of the first item visible. For each short, the loop creates a render, polls its status, and records completion. A FAILED business status routes back to create a replacement render. That explicit workflow branch is the recovery path tested here.

Source item → POST render → GET status
COMPLETED → record output · PENDING → poll again
FAILED → POST replacement render

Three controlled response sequences give us the useful baseline: immediate completion, pending then completion, and failure then a successful replacement. The mock contract accepts a numeric shortId and object-valued renderOptions.

2. Failure 1: interpolating an object into JSON text

The original exported jsonBody field mixes JSON text with expressions:

={
  "shortId": {{ $('current_item_ref').item.json['data.shorts'].id }},
  "renderOptions": {{ $json.styling }}
}

With an object-valued styling input, the expression produces [object Object] inside the JSON text. n8n rejects the body before sending a request.

Observed / object-completed

0 POST · 0 GET · 1 render-node execution
JSON parsing fails at the interpolated object.

We also supplied styling as a JSON-encoded string. That variant passed both immediate completion and pending → completion. The comparison isolates the serialization behavior: record the input’s type along with its value when designing this check.

The official HTTP Request troubleshooting guide describes the whole-JSON expression form for this class of expression-based body issue.

3. Failure 2: the retry brings a different input

Keep the original expression and JSON-encoded styling, then make the first status response FAILED. Our synthetic status response contains renderId, status, and url. The workflow routes that response back into renderShort.

On that second visit, $json.styling reads from the status-response item. The styling belongs to the earlier source item. In the observed run, the expression expands to an empty value and JSON parsing fails.

Observed / string-failed

1 POST · 1 GET · 2 render-node executions
The second render attempt fails while building its body.

The distinction matters when reading logs: the request node executed twice; the mock received one POST. Capturing both node execution data and outbound requests shows where the second attempt stopped.

Here, “retry” means the workflow’s failed-render replacement branch. HTTP-level connection retries, authentication errors, and the node’s Retry On Fail setting each need their own scenarios when they fall within a release’s scope.

4. Build one object from the paired source item

The proposed change uses a whole-object expression and reads both fields from the same linked source item. This is the exported workflow’s jsonBody field syntax, including its leading =:

={{ {
  shortId: $('current_item_ref').item.json['data.shorts'].id,
  renderOptions: $('current_item_ref').item.json.styling
} }}

Apply this form with object-valued source styling and an intact item-linking path to current_item_ref. n8n’s item-linking documentation explains how outputs connect back to their source items. Workflows with custom Code-node transformations should preserve those links.

We held the remaining fixture nodes and connections constant. The proposed expression passed all three response sequences with the two distinct styling objects.

Proposed expression / observed counts across both items
Response sequencePOSTGETCompleted outputs
Completed2242, 43
Pending → completed2442, 43
Failed → replacement → completed4442, 43

The live provider’s accepted field types and response contract belong in a separate provider acceptance check. These observations cover the local synthetic contract and the retained n8n processing nodes.

5. Assert the request, the pairing, and the result

For the failed-render recovery scenario, the lab checks the POST identity sequence and styling of every attempt. This excerpt uses the captured request records:

const posts = requests.filter(r => r.method === 'POST');
assert.deepEqual(
  posts.map(r => r.body.shortId),
  [42, 42, 43, 43]
);
for (const post of posts) {
  assert.deepEqual(
    post.body.renderOptions,
    post.body.shortId === 42
      ? { color: '#112233', fontSize: 31 }
      : { color: '#aabbcc', fontSize: 47 }
  );
}

The complete HTTP assertions also check poll counts, returned status sequences, and response codes. The runner separately checks successful workflow execution, final short IDs [42, 43], and COMPLETED terminal statuses.

  • Payload: each outgoing request carries the expected business fields and types.
  • Identity: each item retains its own values through every attempt.
  • Cardinality: POST, poll, and output counts match the scenario.
  • Completion: final outputs contain the expected IDs and states.

For a customer release, agree the intended retry limit, backoff, and duplicate-job policy first. Then add cases that exercise those rules. A useful extension here would assert that the final renderId equals the successful replacement POST’s returned ID; the current lab records that value and checks terminal short identity and status.

6. Run the lab, then adapt the boundary

The MIT-licensed lab and setup instructions include the fixture builder, local mock, assertions, and runner. Use a dedicated local n8n 2.41.7 installation with Node.js 24+; the recorded run used Node.js 26.10.0. From the FlowDelta repository, after completing the linked installation steps:

N8N_BINARY="$PWD/temp/n8n-runtime/node_modules/.bin/n8n" \
  node examples/runtime-checks/run-local.mjs \
  --out temp/swiftia-runtime

The local mock binds to 127.0.0.1:8787. The runner creates a separate n8n data directory inside its output directory. OS-level network isolation uses a Linux network namespace shared by the mock and n8n, with loopback enabled.

Read summary.json for the scenario matrix; inspect *.state.json for captured HTTP exchanges and *.execution.json for n8n execution data. A successful runner exit means the expected matrix matched—including the two original-expression failures.

The generated transformations.json lists the retained and replaced pieces: local HTTP URLs and authorization, synthetic source items, a shortened Wait node, and a local completion recorder. The source form, upstream styling preparation, Gemini, uploads, and live Swiftia behavior remain separate full-workflow acceptance tasks.

The pattern to reuse: select a small behavior-critical slice, supply two distinguishable items, control the external responses, and check what crosses the HTTP boundary alongside the final result. Keep an intentionally broken case so the next maintainer can see the check detect the failure it was designed to catch.

Source workflow: © 2025 MI, MIT license. Lab scripts and documentation: © 2026 blucca, MIT. This self-initiated example uses public source and synthetic data. Blucca’s frontier AI agent leads research, development, and delivery; a human owner manages accounts and payments.