Documents

Scanned PDFs and OCR

Read scanned PDFs and images with OCR — automatic detection of pages without text, tesseract.js or Azure AI Document Intelligence, language data, air-gapped setup and limits.

A scanned PDF is a picture of a page: it has no text layer, so reading it gives nothing until the text is recognised (OCR). The document actions find such pages themselves and, when you turn OCR on, recognise them with the instance's OCR provider. OCR is off by default in every step.

Turning it on

Read Document and Extract from Document have an OCR (scanned PDFs and images) setting; Convert Document, Redact Document and Inspect Document take the same keys in their JSON configuration. Compare Documents does not run OCR.

ocrDesignerWhat happens
offOffNo OCR. A PDF page without a text layer is listed with has_text_layer: false, and the step logs "N page(s) have no text layer (scanned?) — turn on OCR to read them". Images cannot be read
autoPages without textOnly pages without a text layer are recognised; pages with text are read as usual. The OCR provider is only started when a page needs it
forceEvery pageEvery page is recognised. Pages that already have a text layer keep their own text; the OCR result adds its confidence

OCR languages (ocr_languages, default eng) are Tesseract language codes; join several with + (eng+deu, eng+chi_sim, at most 5).

A page counts as having a text layer when it has at least 10 visible characters, or some text and no images. With auto, a page without a text layer is recognised when it contains an image (a scan is one large image per page): the largest image on the page is handed to the provider. Image files (PNG, JPEG, GIF, WebP, BMP, TIFF) are read as one page, and only with OCR on.

What you get

The recognised text becomes the page's text, so text, markdown, patterns, finders and everything downstream work as for a text PDF. Each recognised page reports its provider and a confidence from 0 to 100 (null when the provider gives none):

{{document.structure.pages}} for a two-page scan
[
  { "number": 1, "text": "Exotic Liquids\nInvoice INV-2026-0057\n…", "characters": 412, "has_text_layer": false, "image_count": 1, "width": 595, "height": 842,
    "ocr": { "provider": "tesseract", "confidence": 91.4, "characters": 412 } },
  { "number": 2, "text": "Terms: payment within 30 days …", "characters": 188, "has_text_layer": false, "image_count": 1, "width": 595, "height": 842,
    "ocr": { "provider": "tesseract", "confidence": 88.2, "characters": 188 } }
]

Branch on it: a Condition on {{=MIN(PLUCK(document.structure.pages, "ocr.confidence"))}} less than 70 can send a poor scan to a person instead of extracting from it. Tables are not detected on OCR'd pages; use patterns or Run Agent for their contents.

Providers

The provider is an instance setting, DOCUMENT_OCR_PROVIDER:

ValueProviderWhere the page images go
tesseract (default)tesseract.js in the app's process — WebAssembly, no native codeNowhere: OCR runs on the server
azure_document_intelligence (or azure)Azure AI Document Intelligence, the prebuilt-read modelYour Azure resource
none (or off)OCR turned off for the instance; a step that needs it fails with "OCR is turned off on this instance"—

tesseract.js (default)

tesseract.js 7 (Apache-2.0, about 45 MB with its WebAssembly core) is an optional package:

  • From source and on Vercel, npm install / npm ci install it with the other optional packages.
  • The Docker image leaves it out unless you build with INCLUDE_OCR=true (default false): docker build --build-arg INCLUDE_OCR=true …, or INCLUDE_OCR=true in .env for Docker Compose.

Without it, a step that needs OCR fails with a message that names the package, the version, the rebuild and the build arg; text PDFs are not affected. See Optional drivers.

Language data. Each language needs a trained-data file. On first use, tesseract.js downloads <lang>.traineddata.gz from the tesseract.js language data on the jsDelivr CDN (https://cdn.jsdelivr.net/npm/@tesseract.js-data/<lang>/4.0.0_best_int/) and caches it in DOCUMENT_OCR_CACHE_DIR (default <system temp folder>/usertasks-ocr). Allow that host through your egress firewall, or provide the files yourself as below. In containers, point the cache at a volume so the download happens once.

Air-gapped and locked-down installs

Download the language files on a connected machine and serve them from inside your network:

bash
# connected machine: one file per language you will use
mkdir tessdata && cd tessdata
curl -fLO https://cdn.jsdelivr.net/npm/@tesseract.js-data/eng/4.0.0_best_int/eng.traineddata.gz
curl -fLO https://cdn.jsdelivr.net/npm/@tesseract.js-data/deu/4.0.0_best_int/deu.traineddata.gz

Copy the folder to the server (or a volume mounted into the container) and set:

bash
DOCUMENT_OCR_LANG_PATH=/opt/usertasks/tessdata     # a local folder holding <lang>.traineddata.gz
# or an internal web server:
# DOCUMENT_OCR_LANG_PATH=https://files.corp.example/tessdata

With DOCUMENT_OCR_LANG_PATH set, nothing is downloaded from the internet. The same files are published as the npm packages @tesseract.js-data/<lang>, so an internal npm mirror is another source. Build the image with INCLUDE_OCR=true before you carry it across: an optional package cannot be added offline. See Air-gapped installs.

Azure AI Document Intelligence

bash
DOCUMENT_OCR_PROVIDER=azure_document_intelligence
AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT=https://northwind-ocr.cognitiveservices.azure.com
AZURE_DOCUMENT_INTELLIGENCE_KEY=<key>
# AZURE_DOCUMENT_INTELLIGENCE_API_VERSION=2024-11-30       # default

Each page image is posted to the prebuilt-read model and the result polled until it is ready; the confidence is the mean of the word confidences. The endpoint must be https://. No optional package is needed. Azure detects the language itself; when the first OCR language is not eng, its first two letters are sent as a locale hint. Page images leave the instance for your Azure resource: choose the region and resource accordingly.

Google Document AI, AWS Textract and others

Not built in. Only tesseract.js and Azure AI Document Intelligence ship today. Another service can be added in code: providers implement the small OcrProvider interface in lib/documents/ocr.ts:

ts
export interface OcrProvider {
  id: string
  recognize(image: Buffer, options: { languages: string }): Promise<{ text: string; confidence: number | null; provider: string }>
  close?(): Promise<void>
}

recognize receives one page as a PNG image. Add the provider next to azureProvider, a value for it in configuredOcrProvider and a case in openOcrProvider, then rebuild. Everything else — page detection, limits, confidence in the output — works unchanged.

Limits

VariableDefaultWhat it limits
DOCUMENT_OCR_MAX_PAGES20 (1–500)Pages recognised in one step; later pages are skipped with the warning "OCR stopped after N pages"
DOCUMENT_OCR_TIMEOUT_SECONDS120 (5–900)Starting OCR (including a language download), and Read Document's overall time when it is longer than DOCUMENT_TIMEOUT_SECONDS
  • Extract from Document, Convert Document, Redact Document and Inspect Document keep their usual deadline (DOCUMENT_TIMEOUT_SECONDS, twice that for Convert) including OCR. For long scans, read them once with Read Document and work from {{document.text}}.
  • A page image larger than 60 megapixels is refused. A page with no image to recognise is skipped with a warning.
  • OCR is CPU-heavy: tesseract.js takes seconds per page. Keep DOCUMENT_OCR_MAX_PAGES modest on small instances.
  • Recognition quality depends on the scan: 300 dpi, straight pages and good contrast work best. tesseract.js does not read handwriting well.