Skip to content
Blog

How to Preserve Item Linking in a Programmatic n8n Node

Item linking tells n8n which earlier item produced the item you are looking at now. A downstream expression such as $('Fetch contacts').item.json.email depends on that connection. If a programmatic node creates new output items without the link, the expression can fail or point to the wrong source.

n8n handles simple item relationships automatically in several cases. A programmatic node still needs an explicit link when it changes the number of items, creates new items, combines inputs, or otherwise makes the source ambiguous. The field that carries this relationship is pairedItem.

This article covers the documented pattern, the n8n 3.0 change that affects old workflows, and a test plan. Adapt the code examples to your node and run them before you ship.

Here is what correct linking looks like when a node filters, reorders and expands three inputs:

Output item Came from pairedItem
contact-c (sorted first) Input 2 { item: 2 }
contact-a Input 0 { item: 0 }
contact-a-note (expanded child) Input 0 { item: 0 }
(input 1 was filtered out)

What item linking does

An n8n execution contains data items, not one undifferentiated response. Each item can carry JSON, binary data, and metadata that lets n8n trace it back through earlier nodes. That trace powers expressions that use .item to select the corresponding data from a previous node.

For example, a workflow might look like this:

  1. A trigger emits three customer records.
  2. A custom node calls an API for each record.
  3. A Set node uses $('Customer input').item.json.customerId to label the result.

The third step needs n8n to know which API response came from which customer record. If the custom node returns three new JSON objects but drops their item relationships, the workflow has lost the thread that .item follows.

The n8n item-linking concepts documentation describes this as a chain through the workflow. The chain can survive sorting and other changes, but only when n8n can identify the source item.

When n8n can infer the relationship

n8n can add item-linking information automatically when the input and output shape makes the relationship clear. The current documentation describes these common cases:

Input and output shape What n8n can infer
One input item becomes one output item The output links to the input.
One input item becomes several outputs Each output links to that input.
Several inputs and outputs keep the same items but change order or remove some n8n can preserve the links for the items it can match.
The number of outputs equals the number of inputs n8n can match them by position.
The node creates new items or changes the count in an ambiguous way The node must provide the link information.

Automatic inference is useful, but it should not be your only design check. A small refactor can change the output count or reorder items. Test the relationship after the node's real transformation, rather than assuming the current array position will always remain meaningful.

When a programmatic node must set pairedItem

The n8n guidance for node creators applies to programmatic-style nodes, including trigger nodes. Declarative-style nodes receive different handling, so first confirm which node style you are building.

Set pairedItem when your code knows which input produced an output and n8n cannot infer it. The simplest case is a one-to-one API loop. Each request uses input index i, so each returned item can point back to that index.

const items = this.getInputData();
const returnData: INodeExecutionData[] = [];

for (let i = 0; i < items.length; i++) {
	const response = await this.helpers.httpRequest({
		method: 'GET',
		url: 'https://api.example.com/records',
		qs: { id: items[i].json.id },
	});

	const executionData = this.helpers.constructExecutionMetaData(
		this.helpers.returnJsonArray(response),
		{ itemData: { item: i } },
	);

	returnData.push(...executionData);
}

return [returnData];

The official programmatic-node tutorial uses constructExecutionMetaData with itemData: { item: i } for this kind of per-input request. The helper attaches the source information to the returned execution data. If you build the output object yourself, the equivalent direct form is:

const inputItems = this.getInputData();

const outputItems = inputItems.map((input, index) => ({
	json: {
		id: input.json.id,
		status: 'received',
	},
	pairedItem: { item: index },
}));

return [outputItems];

Use one approach for each returned item. Do not attach an index from a temporary array after you have sorted or filtered the inputs unless that index still refers to the original input item.

Preserve the source after filtering or reordering

Filtering and sorting often look harmless because the output still contains familiar records. The danger appears when a later node uses .item and the position no longer matches the original input.

Keep the original index while you transform the data:

const items = this.getInputData();
const returnData: INodeExecutionData[] = [];

for (let index = 0; index < items.length; index++) {
	const input = items[index];
	if (input.json.enabled !== true) continue;

	returnData.push({
		json: {
			id: input.json.id,
			ready: true,
		},
		pairedItem: { item: index },
	});
}

return [returnData];

The output array can contain fewer items and still preserve the correct relationships because every output carries the original input index. A later sort does not change what the pairedItem value means.

One input can create several outputs

Suppose one API record expands into several child records. Every child output came from the same input, so each child should link to that input. The output position is not the source identity.

const input = this.getInputData()[0];
const children = [
	{ id: 'child-a', parentId: input.json.id },
	{ id: 'child-b', parentId: input.json.id },
];

return [[
	...children.map((child) => ({
		json: child,
		pairedItem: { item: 0 },
	})),
]];

This is a documented pattern for a one-to-many relationship. If a child combines data from multiple input items, the relationship becomes more complex. Decide which previous items a downstream expression should be allowed to resolve, and represent that relationship explicitly. If there are several possible matches, .item can be ambiguous by design.

Multiple inputs need the input number too

A node with more than one input can produce an item from input 1, input 2, or both. In that situation, the source item index alone is not enough. The pairedItem object can include an input value to identify the input connection.

const output = {
	json: { source: 'second-input' },
	pairedItem: {
		item: 0,
		input: 1,
	},
};

Use the input index that matches the node's connection order. If one output combines several source items, document the relationship in the node behavior and test how downstream expressions should access it. The n8n item-linking error guide explains why .item fails when a thread points to multiple possible matches.

Outputs built from several items

An aggregate, such as a summary of all input records or a batch API response that covers several inputs, came from more than one source. pairedItem accepts an array for this case:

const items = this.getInputData();

return [[
	{
		json: { total: items.length, ids: items.map((item) => item.json.id) },
		pairedItem: items.map((_, index) => ({ item: index })),
	},
]];

With several sources, $('Node').item downstream is ambiguous by design and will raise an error, while .first(), .last(), or .all() still work. Say so in the node's docs so users pick the right expression.

Error outputs need links as well

Continue on Fail changes what the node returns when an API request fails. If the error item represents input index i, keep the same source link on the error output. Otherwise, a later support or notification step may lose the customer record that caused the failure.

if (this.continueOnFail()) {
	returnData.push({
		json: { error: 'The API request failed' },
		pairedItem: { item: index },
	});
} else {
	throw new NodeApiError(this.getNode(), error as JsonObject, { itemIndex: index });
}

The message should be safe to expose in the workflow output. Do not place tokens, full request headers, or private response bodies in the error object. Keep the original error in the execution log only when the project's logging and data-retention rules allow it.

n8n 3.0 removes $getPairedItem

n8n 3.0 removes the deprecated $getPairedItem expression helper, and any expression that still calls it fails when the node runs (see the v3.0 breaking changes). Three standard forms replace it:

Expression Returns
$('Node').item The item in Node that the current item links back to
$('Node').itemMatching(index) The item in Node that the input item at index links back to
$('Node').pairedItem(index) The paired item for index directly

All three depend on the pairedItem data your node returns. If your README or example workflows use $getPairedItem, update them before n8n 3.0 ships to stable, and test the replacements against the output of your node. A node that drops item links will make every one of these fail.

Test item linking before you publish

Pagination is a common place to lose links, because one input becomes many pages of results; the pagination guide covers that loop, and the custom-node guide covers the development setup. Run npm run dev to load the node locally (from n8n 3.0 this needs Docker or Podman, or --external-n8n with your own instance started with N8N_DEV_RELOAD=true). Then use a small workflow to test the link behavior separately from the API response.

  1. Create three input items with unmistakable IDs such as input-a, input-b, and input-c.
  2. Run the node with a one-to-one transformation and reference the source node with .item in a following node.
  3. Filter out the middle item and confirm the remaining outputs still reference the correct IDs.
  4. Sort the output in a different order and confirm each expression follows its source rather than its new position.
  5. Expand one input into two outputs and check that both outputs point to the original item.
  6. Trigger one controlled error with Continue on Fail and confirm the error output still carries its source.
  7. If the node has multiple inputs, test an output from each input and a combined result separately.

Capture the n8n version, node package version, node style, input shape, output shape, expression used, and observed result. If a test shows "Paired item data unavailable", the paired item troubleshooting guide walks through the usual causes, and the debugging guide shows how to step through execute and inspect each returned pairedItem.

Keep item linking part of the node contract

Item linking is easy to overlook because the node can appear to return correct JSON in the editor. The failure often appears in the next node, when a user maps a field from an earlier step or handles an error path.

Write the relationship down when you design the operation. State whether each output represents one input, several inputs, or a new event with no direct input. Then keep the pairedItem logic beside the transformation code and repeat the test when you add pagination, batching, retries, sorting, or a second input.

If you are changing the node's behavior in a later release, review the n8n node versioning guide and test a saved workflow that uses the affected expression. A new output shape can break a workflow even when the API request itself still succeeds.

Frequently asked questions

Item linking is the metadata that connects an output item to the input item or items that produced it. n8n uses that connection to resolve expressions that refer to a matching previous item.

No. n8n can infer several simple one-to-one, one-to-many, and equal-count relationships. A programmatic node must add explicit linking when the output shape is ambiguous or it creates new items.

Use it when your code knows the source input for an output and n8n cannot infer the relationship, such as after filtering, expanding, combining, or returning an error item.

It identifies which input connection produced the item. Add it when the node has multiple inputs and the item index alone cannot identify the source.

The thread may be missing, may point to several possible source items, or may come from another branch. Fix the node that produced the output, or use .first(), .last(), or .all()[index] when that access pattern is intentional. Also replace any $getPairedItem call, which n8n 3.0 removes.

Use input items with unique IDs, test one-to-one, filtered, reordered, expanded, combined, and error outputs, then reference the source node with .item in a following node. Record the package and n8n versions with the result.

No. It proves only that the node preserves workflow data relationships for the tested shapes. Credentials, request behavior, response mapping, pagination, errors, and the real API still need their own tests.

Ready to launch your n8n, Make, Zapier or Activepieces integration?

Give your customers another way to use your product.

Get started