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

# Order processing: PO matching

> Purchase orders arrive in every shape. Match the buyer to customer master, line items to item master, resolve shipping, draft the confirmation — with a starter SOP you can copy.

## The job

Purchase orders arrive by email and ticket in every shape a customer feels like sending: a clean PDF from an ERP, a free-text email that says "same as last time plus 200 of the blue ones", a phone photo of a signed order form. Someone in order entry reads each one, finds the customer in the ERP, matches every line to an article number, works out the shipping, and types it in.

It's high volume, it's judgment-heavy in exactly the places that are hard to script, and a mistake ships the wrong goods to the wrong company.

## What you'll build

```
order documents ──▶ Order Match (reconcile table) ──▶ review ──▶ confirmation draft
                            │
                            ├── queries ──▶ Customer Master (storage)
                            └── queries ──▶ Item Master (storage)
```

* Orders land as cases in a reconcile table — from upload, a mailbox, or a ticket webhook.
* The agent reads the document (including photos and scans), matches the buyer against customer master, matches every line against item master, resolves the shipping method, and fills the output fields.
* Anything it can't resolve confidently is flagged **with its candidates** rather than guessed.
* Clean cases carry a drafted order confirmation, waiting on approval.

## Ingredients

|                     |                                                                                            |
| ------------------- | ------------------------------------------------------------------------------------------ |
| **Reconcile table** | `Order Match` — where the process runs                                                     |
| **Storage tables**  | `customer_master`, `item_master` — granted read access                                     |
| **Model tier**      | **Balanced** to start; **Powerful** if your documents are photos or dense multi-page forms |
| **Integrations**    | Optional: a mailbox or ticket webhook to trigger cases. Start with manual upload.          |
| **Auto Review**     | Off at first. Turn it on once your SOP reflects a few hundred reviewed cases.              |

**Master data shape** (invent your own column names to match your system):

```
customer_master: customer_number, company_name, street, postal_code, city,
                 country_code, contact_email, vat_id, default_shipping_agent

item_master:     article_number, description, vendor_item_number, unit,
                 pack_size, weight_kg, hazard_class
```

## The starter SOP

Create this as `SOP.md` in your project's Files. Change the field names to yours; the structure is the part that matters.

```md theme={null}
# SOP — Incoming order matching

Version 1.0 · Owner: Order Entry · Last approved by: <name>, <date>

## Purpose
Turn an incoming purchase order document into a structured, verified order
record: the right customer, the right articles, the right shipping method.
Flag anything that cannot be resolved confidently. Never guess.

## Field mapping — what to read off the document

| Output field        | Where to find it on the order                          |
|---------------------|--------------------------------------------------------|
| order_number        | Buyer's PO number. Header, often top right.            |
| order_date          | Header. Formats vary — see Conventions.                |
| customer_number     | Resolved by matching, not read off the document.       |
| buyer_name          | The company in the buyer/bill-to block.                |
| buyer_address       | Street, postal code, city, country of the buyer block. |
| contact_email       | Sender address, or the contact block on the document.  |
| delivery_address    | Ship-to block if present, otherwise the buyer address. |
| requested_date      | Delivery/arrival date requested by the buyer.          |
| shipping_agent_code | Resolved by the shipping rules below.                  |
| line_items          | One entry per ordered position.                        |

## Conventions
- Dates are DD.MM.YYYY unless the document is clearly US-formatted
  (month name spelled out, or a value > 12 in the first position).
- Quantities are pieces unless a unit is stated. Note the stated unit.
- Prices are net. If the document says "incl. VAT", record it and flag.

## Step 1 — Customer: name + address
Match the buyer block against `customer_master` on company name AND
postal code. Normalize before comparing:
- Strip legal-form suffixes: GmbH, AG, Ltd, B.V., S.p.A., Inc., KG
- Handle umlauts both ways: ü ⇄ ue, ö ⇄ oe, ä ⇄ ae, ß ⇄ ss
- Ignore case, punctuation, and extra whitespace
- Treat "Str.", "Strasse", "Straße" as equivalent
- Common abbreviations are equal: "Intl" = "International", "&" = "und"

A match on normalized name + postal code is a **confident match**.

## Step 2 — Customer: email tiebreaker
If Step 1 returns more than one candidate, compare the sender address and
any contact email on the document against `customer_master.contact_email`.
An exact email match resolves the tie. A domain-only match does NOT —
several sites of one group share a domain.

## Step 3 — Customer: decide or flag
- Exactly one confident match → set `customer_number`,
  `customer_match_status: matched`.
- Two or more candidates after Step 2 → `customer_match_status: ambiguous`,
  put the candidate customer numbers in `match_candidates`, stop.
- No candidate → `customer_match_status: unmatched`, note what you searched
  for in the comment. Do NOT create a customer.

**Never invent a customer number.** An unmatched order is a normal outcome.
A wrong customer number is a shipment to the wrong company.

## Step 4 — Line items, in priority order
For each ordered position, try in this order and stop at the first hit:
1. **Exact article number** — the buyer quotes our `article_number`.
2. **Vendor/supplier item number** — matches `item_master.vendor_item_number`.
3. **Buyer-prefixed number** — strip a leading customer prefix
   (e.g. "AC-", "KM/") and retry rules 1 and 2.
4. **Description match** — only when 1–3 fail. Requires a clear match on
   product type AND size/variant. Set `line_confidence: low` and say in
   the comment which description matched what.
5. **No match** — set `line_status: unmatched`, list up to five candidate
   article numbers with their descriptions. Do not pick one.

A quantity or unit that does not fit the article's `pack_size` is not an
error to correct — record it as stated and flag the line.

## Step 5 — Shipping method
Resolve `shipping_agent_code` by the first rule that applies:
1. Buyer explicitly requests express, next-day, or a named carrier → use it.
2. Any line has a `hazard_class` → hazardous-goods carrier. This overrides
   an express request; if the buyer asked for express, note the conflict.
3. Total order weight over 500 kg → freight carrier.
4. Otherwise → the customer's `default_shipping_agent`.
5. No rule resolves and no customer default exists → leave empty and flag.

## Escalation — always stop and flag
- No order number or no order date on the document.
- Customer ambiguous or unmatched (Step 3).
- Any line unmatched, or matched only by description (Step 4 rules 4–5).
- Requested delivery date is in the past.
- Total on the document does not equal the sum of the lines.
- The document says "incl. VAT" or quotes prices that differ from ours.

Flagging is a correct outcome, not a failure. List the candidates and what
you checked so the reviewer can decide in seconds.

## Outputs
Fill every output field. In the comment, state in plain sentences: how the
customer was matched, how many lines matched by which rule, which shipping
rule fired, and what — if anything — you flagged and why.
```

## Walkthrough

<Steps>
  <Step title="Create the storage tables">
    Create `customer_master` and `item_master` as **Storage** tables and load your CSVs. Give every column a name the agent can read — `postal_code`, not `col_7` — and add descriptions where a name isn't self-explanatory.
  </Step>

  <Step title="Create the reconcile table">
    Create `Order Match` as a **Reconcile** table. In **Configure**:

    * **Operating Procedures**: `Match incoming purchase orders according to the SOP.`
    * **Output Fields**: `order_number`, `order_date`, `customer_number`, `customer_match_status`, `match_candidates`, `contact_email`, `delivery_address`, `requested_date`, `shipping_agent_code`, `line_items` (array), `flags`. Describe each one — the agent reads the descriptions.
    * **Data Access**: grant `customer_master` and `item_master`.
    * **AI Model**: **Balanced**.
  </Step>

  <Step title="Add the SOP">
    Paste the starter SOP above into `SOP.md` in the project's Files, then ask the agent in chat to review it against your actual master data:

    > Read SOP.md, then look at customer\_master and item\_master. Does the SOP reference fields that don't exist, or miss fields you'd need? List open questions.

    It will tell you where your data doesn't match the template — that's the fastest way to adapt it.
  </Step>

  <Step title="Run your first cases">
    Upload five or six real orders, deliberately mixed: two clean PDFs, one free-text email, one photo, one with an article the buyer numbers differently. Add a task per order and let them run.
  </Step>

  <Step title="Review">
    Open each finished case with the source document beside the result. Check the reasoning against the document, not the other way around.
  </Step>
</Steps>

### What good looks like

**A fully automated case.** A clean PDF from a regular customer:

> Matched buyer "Kestrel Instrumente GmbH", postal code 44227, to customer 30412 on normalized name + postal code — single candidate, no tiebreaker needed. All 6 lines matched on exact article number. No hazard class, total weight 84 kg, no express request → customer default agent `SPD-02`. Order total 4,180.00 EUR equals the sum of lines. No flags.

Six lines, one query path, nothing ambiguous. This is the case Auto Review will eventually clear without you.

**A case the agent correctly refuses to resolve.** A photo of a signed order form:

> Buyer block reads "Meridian Handels", postal code 8010. Two candidates in customer\_master: 40118 (Meridian Handels GmbH, Ostbahnstrasse 14) and 40233 (Meridian Handels GmbH, Ostbahnstrasse 14a). The document carries no VAT ID and no contact email, and the sender domain matches both records. Cannot resolve — `customer_match_status: ambiguous`, candidates 40118 and 40233.
>
> Line 3 "blue housing, 40mm" matched no article number or vendor number. Closest by description: `ART-88120` (Housing 40mm, blue), `ART-88122` (Housing 40mm, blue, reinforced). Description alone is not sufficient per Step 4 — `line_status: unmatched`, both listed.

Two flags, both correct, both resolvable by a human in about ten seconds because the candidates are right there. That's the output you want from a hard case — not a confident guess.

## Iterating

Fix the procedure, not the case. When a reviewer corrects the same thing twice, change the SOP by chat:

> Reviewers keep correcting orders from Kestrel: they use their own item numbers with a "KM/" prefix and we match them by description instead. Add the prefix to the buyer-prefix rule in Step 4.

The agent edits `SOP.md`, explains the diff, and flags any decision it had to make for you. After a few weeks of reviews, run [the improvement loop](/concepts/improvement-loop):

> Look at the last month of reviews in Order Match. What are reviewers correcting most often, and what SOP change would fix it?

## Hardening

<CardGroup cols={2}>
  <Card title="Trigger from real intake" icon="inbox" href="/cookbooks/inbox-intake">
    Replace manual upload with a mailbox or a Zendesk ticket webhook: ticket in → attachments fetched → case created → ticket updated with the outcome.
  </Card>

  <Card title="Turn on agentic approval" icon="user-check" href="/concepts/review-approvals">
    Once your SOP reflects real review feedback, enable **Auto Review** so full clean matches clear automatically and your team sees only the flagged cases.
  </Card>

  <Card title="Add extraction upstream" icon="file-export" href="/quickstart-extraction">
    At high volume with stable document shapes, an extraction table structures orders first and the reconcile table works rows instead of raw files.
  </Card>

  <Card title="Drive it from the API" icon="code" href="/concepts/async-run-pattern">
    Create tasks and poll for results programmatically when the trigger lives in your own system.
  </Card>
</CardGroup>

**Thresholds worth setting explicitly** as you harden: the amount above which a case always goes to a human regardless of confidence, the number of unmatched lines that sends the whole order to review, and whether a new customer number ever auto-resolves (it shouldn't).

<Note>
  All names, article numbers, carrier codes, and values in this cookbook are invented. Replace them with your own master data.
</Note>
