Skip to main content
Moneyline
All guides
Advanced9 minWorkflows

Route low-confidence fields to review

Use per-field confidence to send the 2% that needs a human to a human, and accept the rest.

Every extracted field carries its own confidence. The point of that number is to make review selective: instead of a person checking every document or none, a person checks the fields that are actually uncertain.

Confidence is per field

A document-level score hides the thing you need. A statement can be parsed almost perfectly while one smudged transaction descriptor is a guess, and averaging that into a single number loses which one it was.

jsonjson
{
  "endingBalanceMinor": { "value": 4523167, "confidence": 1.0 },
  "accountHolder":      { "value": "R. Kumar", "confidence": 0.99 },
  "accountNumber":      { "value": "****4471", "confidence": 0.72 }
}

Set thresholds by consequence

One global threshold is the wrong shape, because fields do not cost the same when they are wrong. A misread account number sends money to the wrong place; a misread merchant name makes a category slightly off.

Thresholds are policy, not a model propertytypescript
const THRESHOLDS: Record<string, number> = {
  accountNumber: 0.95, // Wrong value sends money somewhere else.
  routingNumber: 0.98,
  endingBalanceMinor: 0.9,
  accountHolder: 0.85,
  descriptor: 0.6, // Only affects categorization.
};

const DEFAULT_THRESHOLD = 0.8;

export function fieldsNeedingReview(doc: ParsedDocument): string[] {
  return Object.entries(doc.fields)
    .filter(([name, f]) => f.confidence < (THRESHOLDS[name] ?? DEFAULT_THRESHOLD))
    .map(([name]) => name);
}

Branch inside a workflow

Rather than writing the routing yourself, express it as a workflow so the review step, its assignment, and its history are recorded with the run.

Workflow definition, trimmedjson
{
  "steps": [
    { "id": "parse", "type": "parse" },
    {
      "id": "gate",
      "type": "branch",
      "when": [
        {
          "if": "parse.reconciliation.ok == false",
          "then": "review"
        },
        {
          "if": "any(parse.fields, .confidence < 0.85)",
          "then": "review"
        }
      ],
      "else": "deliver"
    },
    { "id": "review", "type": "human", "queue": "ops", "next": "deliver" },
    { "id": "deliver", "type": "relay", "target": "webhook" }
  ]
}

Tune with the outcomes

Reviewers either correct a field or confirm it. Both are labels. Track the rate at which flagged fields turn out to be correct: if reviewers confirm 98% of what a threshold catches, that threshold is too high and is spending human attention for nothing.

The goal is not fewer review queues. It is a review queue whose contents are usually wrong.

Keep going

Run it yourself

Everything in this guide works against the open-source stack. Clone it, or start on the hosted API.