# 360 products fetched. Zero rows saved.

2026-10-20 . Mike Boscia . https://e5bravo.com/hnt/the-empty-database/

It is 12:55 on a Sunday morning, February 15, and my AI agent is writing an escalation.

I build through agents, and this one had a rule: three failures on the same problem, stop and escalate. It is escalating after six. Even the escalation was late.

The workflow is easy to describe. Hand it a store. It finds that store's top five commercial keywords, asks a shopping data API which products show up for each one, and saves those products to a table, each tagged with the keyword that found it. Eighteen nodes. The status line at the top of the report reads:

> Active but broken (0 data collected despite HTTP 200 responses)

Active, broken, 200, zero. Four things that should not share a sentence, sharing one, at five to one in the morning.

## Baggage claim

Five passengers check a bag each. Each bag gets a tag with its owner's name. Somewhere in transfer, a handler consolidates the five bags onto one cart, and the tags come off. The plane lands on time. The arrivals board says LANDED in a calm, reassuring font. The bags go round the carousel, and nobody can claim them, because nobody can prove whose they are.

By the airline's numbers it was a perfect day. Every departure on time, every arrival on time, every bag flown. Nobody got their luggage.

That is the workflow. Here it is node by node.

**Nodes 1 to 6: check-in.** A webhook takes a store ID. A lookup finds the store. A query pulls its five highest volume keywords that carry a cost per click. A Set node builds five requests, each carrying its keyword and the store ID. Five passengers, five tags.

**Node 7: departure.** Five POSTs to the API, in parallel. Five HTTP 200s. Five valid task IDs. The sample reply in the record signs off with a cheerful `"Ok."` And here, unnoticed, is the first missing tag. The reply carries the task ID. It does not carry the keyword. The keyword was in the request, not the reply. The only thing connecting each reply to the keyword that asked for it was n8n's own bookkeeping, a lineage it calls pairedItem. That bookkeeping was the last bag tag in the building.

**Node 8: the transfer.** An Aggregate node combines the five items into one item holding a list. That is its whole job and it does it perfectly. It also breaks n8n's automatic pairedItem tracking.

New Achievement!

**Economies of Scale**

You have combined 5 items into 1. Lineage tracking has been discontinued. 360 products will arrive later with no names on them.

Reward: 1 item, formerly 5. Contents: null.

**Node 9: the layover.** A Wait node pauses 90 seconds while the API works. It works. Waiting is hard to get wrong.

**Node 10: the claim desk.** A node called Extract Task IDs is supposed to rebuild five pairs of task ID and keyword. It returns null. Not a crash. A null, handed politely to the next node like a boarding pass.

**Node 11: the split.** Split Task IDs should turn that list back into five items. It receives null, so it produces exactly one item: `{task_data: null}`. Five passengers went in. One empty envelope came out.

**Node 12: the carousel.** GET Task Results builds its URL from the task ID. There is no task ID, so the URL ends in `/advanced/undefined`. The literal word undefined, sent to a paid API. The API answers 401, which is the most honest thing anyone says all night.

**Nodes 13 to 18: arrivals.** Split the products, map the fields, insert them, aggregate the results, stamp the store as collected, send the success response. Products inserted: 0. The collected timestamp stays null.

On the runs where the agent got the GET working, the API sent back 360 products. Every one fetched successfully. Every one needed its keyword and its store ID before the table would take it, and neither survived the transfer. 360 bags on the carousel. n8n's assessment: "Multiple matching items for item [0]."

What the caller saw: HTTP 200, after 91 seconds.

In fairness, the execution log did record an error at Extract Task IDs. One click down, in a screen you have no reason to open for a workflow that just answered 200. The arrivals board said LANDED. The arrivals board is the part everyone reads.

## Six repairs, six real bugs

The repair log is the funny part, if you are not me.

Attempt one: two nodes were not connected. Connected them. Still failed. Attempt two: the URL rendered as `.../advanced/[object Object]`. Fixed it, and found a new error. Attempt three: the update that fixed the URL had quietly cleared the Authorization header. 401. Put it back. Attempt four: two nodes sat at the exact same coordinates on the canvas, and validation failed. Moved one. Validation passed. Execution still failed. The validator had strong feelings about two boxes overlapping and no feelings at all about a null. Attempt five: a node referenced a node that does not exist. Pointed it at one that does. New error: "Multiple matches found." Attempt six: 360 products trying to find their way back to five keywords through a lineage that no longer existed.

Every fix was real. Every fix revealed the next bug. A seventh attempt was in flight when the escalation went out, and the report ends on a question, not an answer: "Is the data even there?" It was not. The keyword was in the request and not in the reply.

That last line is the whole post. A 200 tells you the request was received. It says nothing about whether the work happened. Automation does not fail loudly. It fails quietly, keeps reporting success, and you find out when you need the thing it was supposed to have done.

## When the 200 is enough

Concession, because there is one. Aggregate is a fine node. If the next step really does want one item (a summary, a total, one Slack message you read every time), combining is the job and nothing needs to find its way home. The trap is the round trip: collapse to one, then split back apart and expect every piece to remember where it came from.

And if a person looks at the output of every single run, you do not need any of this. You are the row count. The problem starts the day nobody looks.

The 200 was not lying, exactly. It answered the question it was asked: did the webhook get a request? I was the one who heard "did the work happen?"

## Check your carousel

Pick the workflow you would miss first and answer these tonight.

1. When it writes nothing, what does it return to whatever called it? Run it on an input you know produces nothing and look.
2. Does anything in it combine many items into one, and then split them back apart later? Count the items after the split. Is it the number you started with?
3. Does every row it writes still carry the thing that produced it (the keyword, the customer, the order)? Or is that link held by the tool's bookkeeping, where you cannot see it?
4. Put two numbers side by side: runs that succeeded today, and rows written today. If those two numbers ever disagree, the pipeline is lying to you, and it will keep lying on schedule.

Next post: the scanner that was green for 74 days.
