> ## Documentation Index
>
> Fetch the complete documentation index at: https://lithi.ai/llms.txt
>
> Use this file to discover all available pages before exploring further.

---
title: Give every item a stable identity
description: >-
  Results can arrive out of order, so joining by array position is wrong. Declare
  a stable item identifier, keep your own mapping, and join every result back by
  identity.
route: /docs/compute/inputs/item-identity
page_id: docs-compute-inputs-item-identity
page_type: howTo
content_layer: guided documentation
surface_profile: guided_docs
audience: Integrators and AI agents joining Compute results back to their own records
voice: D
reader_question: How do I match Compute results back to the records I submitted?
primary_action: Join your results by declared identity
source_locale: en-US
source_status: APPROVED
source_version: docs-compute-inputs-item-identity-en-us-2026-09-05-v1
last_updated: '2026-09-05'
robots: index
claim_registry_pinned: true
claim_registry_resync: "npm run governed:tsx -- scripts/build-compute-public-claim-registry.ts --write"
---

## Before you start

You need one field that exists on every item and never repeats. You also need a pinned snapshot, so the run reads exactly the items you counted.

If that field holds a real customer value, you need one more thing: your own mapping table, held on your side.

## Declare the field that identifies an item

The `input` object carries `item_id_field`. It names the field in your data that identifies one unit of work.

```json
"input": {
  "item_id_field": "order_ref",
  "snapshot": { "version_id": "<version id>", "content_sha256": "<digest>" }
}
```

That one declaration is what every outcome points back to. An accepted row, a typed failure, an abstention and a preserved disagreement all carry it.

## Results do not arrive in your input's order

A result bundle is not a rewritten copy of your input. Its outcomes are separated by kind:

```text
data/part-*             accepted outputs
failures/part-*         typed failures and refusals
abstentions/part-*      abstained units
disagreements/part-*    preserved verifier disagreements
```

Item seven of your input may land in `failures/`, while item eight lands in `data/part-0000` as the first row there. Partial completion, retries and per-item outcomes all move rows between files. Nothing promises that row *n* of your input is row *n* of anything you receive.

Your identifier survives all of that. Result columns are appended under the `lithi_` prefix: `lithi_status`, `lithi_result`, `lithi_error_code`, `lithi_confidence`, `lithi_evidence`, `lithi_verification` and `lithi_usage`. Your own field comes back under its own name.

## Join by identity, never by position

The position join is short, reads fine, and is wrong:

```python
for record, row in zip(records, result_rows):   # wrong: position is not identity
    record.grade = row["lithi_result"]
```

It fails silently. Every record gets a grade, no error is raised, and the grades belong to other records. If any item abstained or failed, the offset is permanent from that row onward.

The identity join is barely longer:

```python
by_id = {row["order_ref"]: row for row in result_rows}   # right: key on item_id_field

for record in records:
    row = by_id.get(record.order_ref)
    if row is None:
        unresolved.append(record.order_ref)
        continue
    record.grade = row["lithi_result"]
```

Build the same index over `failures/`, `abstentions/` and `disagreements/`, and every one of your records lands in exactly one of the four.

## Duplicates, and identifiers that are missing

A repeated identifier is not a duplicate row. It is two different items claiming to be the same item, and it breaks the join in both directions.

The logical item is its own idempotency scope. The same identity with the same content digest is an exact replay and returns the earlier result. The same identity with a different digest is equivocation, and it refuses rather than picking one. So deduplicate before you submit. Where two rows are genuinely different, make the identifier different too. A natural key plus an occurrence marker usually does it.

A missing identifier is simpler and worse. A row with no value in `item_id_field` cannot be reported against, so it is refused at validation with a safe reason and a next action. Fix it at the source and re-snapshot, rather than filling the gap with a row number.

## Keep the real record on your side

Do not use a customer email address, an account number or a case reference as the item identifier. Compute never needs the real value to do the work.

Generate an opaque identifier instead. Keep the mapping from it back to the real record on your own machine, encrypted under your own key. Only you can resolve it, and the mapping never becomes part of the input, the job or the receipt.

## What reconciles, and what to do when it does not

Wait for `result-manifest.json`. It is written last and is the only complete-bundle marker; the presence of part files never means the run finished.

Then reconcile by count. Your submitted items should equal accepted plus failed plus abstained, with disagreements recorded against items you already counted. If the totals match and every identifier resolves, the join is sound.

If an identifier is missing from all four, do not assume it succeeded. Read the job status first, because a `PARTIAL` job still has more to come: [how to resume from a cursor](/docs/compute/results/partials-and-cursors).

If the job is terminal and the item is still absent, give support the item identifier and the `request_id` from your response envelope. Never send the input row itself.
