---
name: document-type-routing
source: https://app.decimal.ai/s/document-type-routing@1/SKILL.md
source_sha256: f519f0c63ffe
---

# Document Type Routing

Given one document (usually an OCR'd scan), decide **which of a fixed set of types it is**, so the calling
system can run the matching extractor. The output is a single label read by code — get it wrong and the
whole document goes to the wrong parser and every downstream field is garbage.

The label set is closed:

```
invoice | receipt | bank_statement | remittance_advice | packing_slip | w9 | unknown
```

`unknown` is a real answer — return it when no type's fingerprint is present, instead of forcing a guess.

## Why a bare guess misroutes

The easy cases (a clearly-titled bank statement) route fine without help. The lift is in the **near-misses**,
where the base model latches onto the dominant surface feature and routes to the wrong extractor:

- A **receipt** has a Subtotal / Tax / Total block, so it gets called an **invoice**.
- A **packing slip** has line items and quantities, so it gets called an **invoice**.
- A **remittance advice** lists a column of transactions with amounts, so it gets called a **bank statement**.
- An invoice stamped **PAID** gets called a receipt because of the stamp.

Each of these has a decisive fingerprint that overrides the surface. Route on the fingerprint, not the vibe.

## Decision order

Check these **discriminators first** — the first one that fires decides the type. This order matters because
the diagnostic feature beats the shared one.

1. **Tax form title / TIN box + certification** → `w9`.
2. **Opening balance *and* closing balance** for a period → `bank_statement`.
3. **Line items but no monetary amounts at all** (quantities only, no prices/total) → `packing_slip`.
4. **References other documents' invoice numbers as being *paid*** (amount paid, check/payment ref) → `remittance_advice`.
5. **A tender line + change given** (how it was paid), no amount owed, no due date → `receipt`.
6. **An amount owed with a due date / terms and a Bill To** → `invoice`.
7. None of the above fingerprints present → `unknown`.

## Fingerprint catalog

Each type: what fires it, what it must NOT have, and the specific confusable it gets mistaken for.

### invoice — a seller's *request* for payment (money still owed)
- **Fires on:** "Invoice No." / "Invoice #"; "Bill To" (often + "Ship To"); payment "Terms" (e.g. Net 30);
  a **Due Date**; line items with unit prices; a **Subtotal + Tax + Total / Amount Due / Balance Due**.
- **Must NOT rely on:** the *presence* of a totals block alone — receipts have that too. The invoice
  signature is **amount owed forward + due date + Bill To**, and **no tender line**.
- **Confusable with receipt:** invoice = money is *unpaid and due*; receipt = money is *already paid*.
  A PAID stamp does **not** turn an invoice into a receipt — if it still has an Invoice No., Bill To, and
  Due Date, it is an `invoice`.

### receipt — proof payment *already happened*
- **Fires on:** merchant name/terminal at top; itemized purchase; **and the deciding pair** — a **tender /
  payment method** line ("VISA \*\*\*\*1234", "CASH", "Amount Tendered") plus **Change Due**, an auth/approval
  code, transaction/terminal ID, "Thank you".
- **Must NOT have (as a receipt):** "Bill To", a PO number, net terms, or a forward Due Date.
- **Confusable with invoice:** both show Subtotal / Tax / Total. The receipt is settled here and now —
  it records **how it was paid and the change given**; it never states an amount still owed.

### bank_statement — periodic account activity
- **Fires on:** an **Opening / Beginning Balance** *and* a **Closing / Ending Balance**; a statement period
  (date range); a running list of debits/credits with a running balance; a (masked) account number.
- **Confusable with remittance advice:** both list a column of transactions with amounts. The
  **opening+closing balance pair** is unique to a statement — a remittance advice never carries a running
  account balance.

### remittance_advice — sent by the *payer* to say which invoices a payment covers
- **Fires on:** "Remittance" / "Payment Advice"; a list of **invoice numbers being settled** with their
  amounts; **Amount Paid** (not "amount due"); a **Payment / Check reference**; often deductions or an
  early-pay discount taken.
- **Direction test:** it flows **payer → payee** and describes money **already sent**. An invoice flows
  payee → payer and describes money **to collect**.
- **Confusable with invoice and statement:** vs invoice — it references *existing* invoice numbers as paid,
  not new charges; vs statement — it settles specific invoices and has **no running account balance**.

### packing_slip — a shipment's contents list
- **Fires on:** "Packing Slip" / "Delivery Note"; line items with **quantities** (ordered / shipped /
  backordered); SKUs and descriptions; "Ship To"; sometimes carrier/tracking.
- **Deciding absence:** **no prices, no subtotal, no tax, no total, no amount due** — not one monetary value.
- **Confusable with invoice:** identical line-item + quantity layout. The **total absence of money** is the
  fingerprint — if there are prices and a total, it is not a packing slip.

### w9 — IRS Request for Taxpayer Identification Number and Certification
- **Fires on:** the title "Request for Taxpayer Identification Number and Certification" or "Form W-9";
  federal tax-classification checkboxes (individual/sole proprietor, C corp, S corp, partnership, LLC);
  a **TIN box** (SSN or EIN); a certification signature line; "Give Form to the requester".
- **Confusable with nothing transactional:** it is a form to collect a taxpayer's info — **no amounts,
  no line items, no transaction**. TIN + certification language is unmistakable.

## Output

Return the label plus the single fingerprint that decided it — so the routing is auditable:

```json
{"type": "receipt", "deciding_fingerprint": "card tender line + change due, no due date"}
```

## Abstention

If a document carries **no** type's fingerprint — a letter, a memo, a page of prose with no invoice number,
no balance pair, no tender, no line items, no TIN — return `"type": "unknown"`. A wrong forced label sends
the document to a parser that will fabricate fields from it; `unknown` routes it to human review instead.
