Case studies
CASE STUDY — Automation

Contract intake automation — how to pick an OCR engine, and validation that never invents a value

Deployed at Legal / Administration7 days to build
WebhookOCR / document extractionPostgresSlack

Every time a contract arrived, someone opened the PDF and typed the parties, the amount, and the term into the system by hand. When volume spiked, entry fell behind — and transcription introduced its own errors.

This is read-it-and-retype-it work. So reading was handed to OCR and extraction, checking was handed to rules, and people were left with only the exceptions.

contract-intake.workflowLIVE
UPContract uploadOCRExtract fieldsVLValidateDBRegister

The pipeline

  1. The uploaded contract is converted to text by OCR.
  2. Key fields — parties, amount, term — are extracted automatically.
  3. Missing required fields and malformed values are flagged by validation.
  4. Clean contracts are registered; only exceptions reach a person.

How to choose an OCR engine — the question we get most

An earlier version of this write-up listed only OCR in the stack. But the single thing readers actually want to know is which OCR. There isn't one answer; it splits along four axes.

  • Accuracy in your language — support for non-Latin scripts varies enormously between engines. English benchmark figures do not transfer to Korean contracts. Measure it yourself on real sample documents.
  • Table recognition — contracts routinely put amounts and dates inside tables. And **engines that handle table structure *and* non-Latin script well are rare** — a Korean contract is exactly that combination. The n8n community repeatedly reports that document-structure services outperform general-purpose OCR on table-heavy files.
  • Cost structure — per page or per document, and is there a free tier? 100 documents a month and 10,000 a month lead to entirely different choices. Work out expected volume × unit price before you commit.
  • Whether documents may leave your networkthis is the first question corporate teams ask. If policy forbids sending contracts to an external API, the other three axes don't matter: your only candidates are local OCR plus a local model. Our on-premise case covers that arrangement.
Don't pick the engine first. Settling the data-egress question alone halves the candidate list, and measuring per-field accuracy on 20–30 real contracts settles the rest. This document recommends no specific engine — engines and pricing change often, and we can't know your organisation's policy. That's why the OCR node in the workflow is left blank.

Do scanned (image-only) PDFs work?

This is the second most common question. A PDF with a text layer and a PDF that is just scanned paper are completely different things. Plain PDF text-extraction nodes return nothing at all for the latter — an empty string, not an error. Left unhandled, that passes as "extraction succeeded, no content."

  • Check which kind you have first. Open the PDF in a viewer: if you can drag-select the body text, it has a text layer.
  • Image-only PDFs require a genuine OCR step. Text extraction alone will not do.
  • The validation rules below are the safety net here. Empty extraction trips the required-field check and becomes needs_review, so a blank contract never gets registered.

Never invent a value — the validation rules

Extraction is never 100%. So this workflow has one governing rule: if any required field is missing or malformed, don't register it — hand it to a person with the reason. A wrong amount on a contract is far worse than a missing one.

Required: party_a · party_b · amount · start_date · end_date

Amount parsing   strip currency symbols, commas, spaces, then check it's numeric
                 "11,000,000 KRW" → 11000000
                 "to be agreed"   → fails → needs_review

Date parsing     2026-01-31 / 2026.01.31 / 2026/01/31 / 20260131 all accepted
                 2026-02-31       → date does not exist → fails → needs_review

Cross-check      end_date < start_date → needs_review
Catching a date like 2026-02-31 matters more than it appears. In most languages that silently rolls over to March 3 with no error. A contract end date lands two days late in the system and nobody notices. So this workflow formats the parsed date back to a string and compares it against the original.

The extraction schema

CREATE TABLE IF NOT EXISTS contracts (
  id             bigserial PRIMARY KEY,
  source_file    text,
  party_a        text,
  party_b        text,
  amount         numeric,
  start_date     date,
  end_date       date,
  auto_renew     boolean NOT NULL DEFAULT false,
  status         text    NOT NULL DEFAULT 'needs_review',  -- registered|needs_review
  review_reasons jsonb   NOT NULL DEFAULT '[]'::jsonb,
  created_at     timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS contracts_status_idx ON contracts (status, created_at DESC);

review_reasons preserves exactly why something was flagged. Whoever opens the record knows immediately what to check — and over time you can aggregate that column to see which field fails most often. That's your next improvement.

What you need

  • n8n — uploads arrive from outside, so a publicly addressable URL is required.
  • One Postgres database — a single contracts table.
  • An OCR / document-extraction service — chosen on the four axes above. Start sample testing on something with a free tier.
  • A Slack Bot User OAuth Token — scopes chat:write and channels:read, for review notifications.
  • 20–30 real sample contracts — non-negotiable for measuring accuracy. Choose an engine without them and you'll be choosing again after rollout.
⬇︎ Download the workflow (contract-intake.json)
One Postgres credential and one Slack credential gets validation, registration, and exception alerts running. Fill the OCR node with whichever engine the criteria above led you to.
Being precise about what was verified. The validation logic was executed in Node.js across 8 inputs and passed all of them — a clean contract, an amount carrying a currency symbol, a date with no separators (20260101), a missing amount, an amount reading as free text, a non-existent date (`2026-02-31`), an end date before the start date, and a missing party. Parsing "11,000,000 KRW" to the number 11000000 was confirmed. Node types and versions match our already-published workflows. Not verified: any specific OCR engine's accuracy on Korean text or tables — we did not measure it, so no accuracy figures for any engine appear in this document. The 12 min → 1 min and −90% figures come from the original engagement and were not re-measured with this workflow. Last verified: 2026-08-15.

Setup (30 minutes, plus engine selection)

  1. Gather samples first — 20–30 real contracts. A mix of scans and text PDFs is better.
  2. Settle the data-egress question — confirm whether policy permits sending contracts to an external API. If not, your candidates narrow to local engines.
  3. Sample-test 2–3 engines — run the same documents through each free tier and count per-field hits. Keep that comparison in writing.
  4. Create the table — run the SQL above.
  5. Register the Postgres and Slack credentialsTest each, and /invite @your-bot in the Slack channel.
  6. Import the workflow — n8n → Workflows → ...Import from File.
  7. Fill in the OCR node — endpoint, auth, and request body for your chosen engine. Map its response to party_a, party_b, amount, start_date, end_date.
  8. Activate, then connect the upload path — point your upload form at the Production URL of Contract Upload Webhook.
  9. Test with a deliberately broken contract — upload one with the amount removed. It should be stored as needs_review with the reason posted to Slack. Testing only clean documents leaves half this workflow untested.

What happens in edge cases

  • Poor scan quality — extracted values come back empty or garbled, trip required-field validation, and become needs_review. No wrong value gets registered.
  • Handwritten signatures and stamps — this workflow makes no judgement about whether a contract is signed. If signature verification matters, keep a human review step.
  • Several contracts in one PDF — only the first is extracted. Separate uploads are assumed; automatic splitting needs an extra stage.
  • Non-PDF attachments — anything that isn't a PDF never enters this workflow. Convert at upload time, or add a node that reads that format first.
  • A contract whose amount reads "to be agreed" — genuinely common. It fails format validation and becomes needs_review. If those are frequent, drop `amount` from the required list and track it as its own status instead. The required-field list is at the top of the Validate Extracted Fields node.
  • The OCR service being down — that execution ends in error. The upload already returned 200, so the user sees success. If you need a replay path, adopt the queue structure from our settlement and payment cases. The base structure has no queue.
12 min → 1 min
per-contract handling
−90%
data-entry errors
Exceptions only
human review
Zero
guessed values registered
Typing them in one by one feels like a story I made up. Now we just upload.Administration team

Your operation belongs
in here, too.

Tell us the most repetitive task you have. We'll map an automation scenario for it.