Welcome to the forum @peace. This error isn’t “random” — it’s n8n’s item pairing biting you.
What’s actually happening
In your Update row in sheet node you use: {{ $(‘Loop Over Items’).item.json.row_number }}
When you reference another node with $('Node').item, n8n tries to find the paired item from that node that matches the current item (same lineage).
But in your workflow, nodes like AI Agent / Tools / Wait can sometimes drop or alter the pairing metadata (or change the item lineage). When that happens, n8n can’t figure out which “Loop Over Items” item matches the current item → so it throws:
[ERROR: Can’t determine which item to use]
“Paired item data … is unavailable…”
It can feel “sometimes works / sometimes doesn’t” because the AI/tool path can vary (tool called vs not, different internal execution path, etc.), which changes how pairing survives.
The best fix (most reliable)
Stop reaching back 3–4 nodes. Carry the row_number forward and use $json.
Change this in “Update row in sheet”:
Replace: {{ $(‘Loop Over Items’).item.json.row_number }}
With: {{ $json.row_number }}
Because Loop Over Items already outputs the current row item, and that row_number is already in the item flowing through your agent → code → wait → update chain.
This completely avoids item-linking.
If you must reference the earlier node (still reliable)
Use index-based access instead of pairing:
{{ $items(‘Loop Over Items’, 0, $itemIndex).json.row_number }}
This tells n8n: “don’t try to pair; just use the same $itemIndex”.
Only safe if your flow preserves 1 item per loop iteration (which yours should).
Fix your “Tool Used” too (same class of problem)
This is also risky: {{ $(‘Get client’).isExecuted }}
Because Get client is an AI tool node, not a normal main-line node, and it may not be paired the way you expect.
Better pattern
Set a flag in your Code node (because Code node runs on the main item you’ll update):
Example Code node change:
for (const item of $input.all()) {
const isPass = item.json.output?.IsPass;
item.json.output.status =
isPass === 1 ? 'pass' :
isPass === 0 ? 'fail' : 'unknown';
// track tool usage in a stable way (whatever signal you prefer)
item.json.tool_used = !!item.json?.output?.toolUsed; // OR set it from agent output
}
return $input.all();
Then in Sheets update: {{ $json.tool_used }}
(You’ll need to decide what signal from the agent/tool output represents “tool used” in your data — but the key idea is: write it onto the item, don’t “reach sideways” across nodes.)
Practical action plan
-
In Update row in sheet change row_number mapping to:
{{ $json.row_number }}
-
(Optional) If you still want a “back reference”, use:
{{ $items('Loop Over Items', 0, $itemIndex).json.row_number }}
-
Avoid $('SomeNode').item wherever possible inside loops + AI + wait chains.
Carry needed fields forward on $json.
If you paste the current incoming item JSON right before “Update row in sheet” (execute that node and copy the input), I can tell you exactly which fields are already present and the cleanest way to set tool_used without any fragile pairing.