Guide

Build an n8n document extraction workflow with review routing

All guides

This workflow takes PDF bytes, calls Velrim through an HTTP Request node, and branches on field evidence. The output is either exception_review or business_checks. The latter still needs your application rules and approval process before a downstream write.

The flow is: document input → HTTP Request → Code → IF → your review destination or business checks. It uses standard n8n nodes and a downloadable Code-node script. Start with one synthetic invoice, then add your document source. The routing script is tested offline; an end-to-end run still depends on your n8n installation and credentials.

1. Prepare document bytes and a stable request ID

Your upstream node should emit pdf_base64 containing the PDF bytes encoded as base64, plus request_key naming this logical extraction. A filename, download URL or n8n binary-storage identifier is not base64 PDF content. For small-file experiments, prepare the base64 outside n8n and enter it in an Edit Fields node. For a binary-producing integration, use its supported binary-to-base64 conversion.

Keep the same request key and exact body across retries; create a new key for a different document or schema. Do not derive it from the execution number if an entire workflow retry changes that number. Check the inline, upload and page limits; use the uploads flow for larger files and async jobs when the surrounding workflow needs asynchronous processing.

2. Configure the HTTP Request node

Set Method to POST and URL to https://api.velrim.com/v1/extract. Select Generic Credential Type → Header Auth. Store header name Authorization and value Bearer YOUR_API_KEY in an n8n credential, then select that credential on the node. Add the request header Idempotency-Key with expression {{ $json.request_key }}.

Enable Send Body, select JSON → Using JSON, switch the body editor to Expression mode and enter the body below. Confirm the preview resolves pdf_base64 before running the node. Set the response format to JSON, with the response body as the node output. Leave full-response wrapping and Never Error disabled: an authentication or extraction error must stop here, rather than reaching the field-review branch. A real request uses your Velrim balance.

HTTP Request · JSON body
{
"schema": {
"type": "object",
"properties": {
"invoice_number": {
"type": "string"
},
"total": {
"type": "number"
},
"currency": {
"type": "string",
"enum": [
"USD",
"EUR"
]
}
},
"required": [
"invoice_number",
"total",
"currency"
],
"additionalProperties": false
},
"document": {
"bytes_base64": "{{ $json.pdf_base64 }}"
}
}

3. Route the response with a Code node

Choose JavaScript and Run Once for All Items. Paste the downloaded script. It handles every input item and keeps item pairing so later nodes can find the corresponding input. Missing required metadata, non-present fields, conflicts, invalid scores and source-review cases go to exception_review. There is no default confidence cutoff. Download n8n-review.js.

The script also checks the three application fields before returning business_checks. It retains the full extraction envelope under extraction, including page anchors. It is a triage example, not a complete validator for every field in an arbitrary schema: update the required pointers and data checks together when you change the schema.

Code · JavaScript · Run Once for All Items
/**
* A conservative invoice triage example. A clear result still needs the
* application's business checks; this function never authorizes payment.
* @param {unknown} response
*/
function routeInvoice(response) {
/** @param {unknown} value @returns {value is Record<string, unknown>} */
function isRecord(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
if (!isRecord(response) || !isRecord(response.data) || !isRecord(response.fields)) {
throw new Error('Expected an extraction response with data and fields');
}
const required = ['/invoice_number', '/total', '/currency'];
const pointers = new Set([...required, ...Object.keys(response.fields)]);
const reasons = [];
for (const pointer of pointers) {
const field = response.fields[pointer];
if (!isRecord(field)) {
reasons.push({ pointer, reason: 'metadata_absent' });
continue;
}
if (field.state !== 'present') {
// This example reviews null and missing optional fields too.
reasons.push({ pointer, reason: 'not_present' });
continue;
}
if (field.conflict === true) reasons.push({ pointer, reason: 'conflict' });
if (
typeof field.confidence !== 'number' ||
!Number.isFinite(field.confidence) ||
field.confidence < 0 ||
field.confidence > 1
) {
reasons.push({ pointer, reason: 'score_absent_or_invalid' });
}
if (field.grounding !== 'verified' || !isRecord(field.anchor)) {
reasons.push({ pointer, reason: 'source_review' });
}
}
const data = response.data;
if (
typeof data.invoice_number !== 'string' ||
data.invoice_number.length === 0 ||
typeof data.total !== 'number' ||
!Number.isFinite(data.total) ||
(data.currency !== 'USD' && data.currency !== 'EUR')
) {
reasons.push({ pointer: '', reason: 'data_contract' });
}
return {
route: reasons.length ? 'exception_review' : 'business_checks',
reasons,
// Preserve source references for the reviewer; do not put them in logs.
extraction: response,
};
}
return $input.all().map((item, index) => ({
json: routeInvoice(item.json), pairedItem: { item: index }
}));

4. Add the IF node and the reviewer context

Compare the string expression {{ $json.route }} to exception_review. Send the true output to your review destination. Keep the false output attached to business checks while you validate the workflow. Do not connect it directly to payment, posting or an irreversible action.

A reviewer needs your document reference, reasons, and the affected entries under extraction.fields. An anchor contains a page, snippet, page_dims and bbox in corner coordinates [x0, y0, x1, y1]. Preserve those dimensions when drawing a highlight. The value being located on a page does not settle whether it answers the requested field.

Review n8n execution-data retention and access before processing sensitive documents: this script intentionally carries values and source snippets forward for review. An error workflow can carry just the original document reference and error code. Do not include document contents in notifications by default.

Test both branches before connecting a destination

With a synthetic response, change /total to missing, remove /currency metadata, set a conflict, and remove an anchor. Each should route to exception review. A malformed response should throw. A response with all required typed values and complete source evidence can reach business checks; that result does not certify accuracy.

For score-based routing, use the review-threshold guide and measure on your own documents first. The configuration above follows n8n’s HTTP Request node and Code node interfaces. Velrim request and replay behavior are documented in the API reference.

Written by Velrim.