Extract structured data from patient intake forms | Reducto Cookbooks
Studio

Customers

Pricing
Reducto leads independent benchmark on structured extraction with Deep Extract
Extract
June 19, 2026

Extract structured data from patient intake forms

Patient intake forms arrive as scans, faxes, and phone photos, every one laid out differently. Reducto's Extract reads them the way a nurse would, pulling demographics, insurance, and medication history into clean JSON with a source citation on every field.

Intake is where patient data enters your system, and it is rarely tidy: handwriting in the margins, checkbox grids, and a different layout from every clinic. Entering in all of that form data by hand can be tedious and error prone. With Reducto you can simply describe the fields you need once and get them back from any layout, with no per-form templates to maintain. In this cookbook, you’ll build the whole pipeline with one Extract call. For your longest, highest-stakes forms, turn on Deep Extract: a self-verifying mode that re-checks its own work until it gets every field right.

What you'll build

markdown
Patient intake form (PDF, scan, or photo) -> Reducto Extract with your schema (+ citations) -> Schema-shaped JSON, every field cited to its source -> Flip on deep_extract for long or field-heavy forms

Setup

Grab an API key from studio.reducto.ai → API Keys → Create new API key, then set it and install the SDK:

bash
export REDUCTO_API_KEY="your-api-key-here" pip install reducto # Python npm install reducto # JavaScript

Use any patient intake form: a digital PDF, a scanned packet, or a phone photo. No template setup or pre-labeling required.

Built for PHI. Reducto is SOC 2 Type II compliant on every plan, with HIPAA (under a signed BAA) and zero data retention available on Growth and Enterprise plans. For data that cannot leave your environment, Enterprise adds VPC and on-premise deployment. See Enterprise Readiness for details.

Step 1: Define your schema

Extract is schema driven. The field names and descriptions you write are the search hints the model uses to locate values, so be specific. Use enum for known value sets, and keep nesting shallow.

python
schema = { "type": "object", "properties": { "patient_name": { "type": "string", "description": "Patient's full legal name, usually at the top of the form", }, "date_of_birth": { "type": "string", "description": "Patient date of birth exactly as printed on the form", }, "sex": { "anyOf": [ {"type": "string", "enum": ["male", "female", "other"]}, {"type": "null"}, ], "description": "Patient's sex as marked on the form. Null if no checkbox is checked.", }, "insurance_provider": { "type": "string", "description": "Name of the health insurance company, in the insurance section", }, "member_id": { "type": "string", "description": "Insurance member or subscriber ID", }, "chief_complaint": { "type": "string", "description": "The patient's main reason for the visit", }, "allergies": { "type": "array", "items": {"type": "string"}, "description": "Patient-reported allergies. Return an empty list if none are listed.", }, "medications": { "type": "array", "items": { "type": "object", "properties": { "name": {"type": "string", "description": "Medication name"}, "dosage": {"type": "string", "description": "Dosage, e.g. '10 mg'"}, "frequency": {"type": "string", "description": "How often it is taken"}, }, }, "description": "All current medications listed on the form", }, }, }
javascript
const schema = { type: "object", properties: { patient_name: { type: "string", description: "Patient's full legal name, usually at the top of the form", }, date_of_birth: { type: "string", description: "Patient date of birth exactly as printed on the form", }, sex: { anyOf: [ { type: "string", enum: ["male", "female", "other"] }, { type: "null" }, ], description: "Patient's sex only if a checkbox is explicitly marked on the form. Return null if no option is checked.", }, insurance_provider: { type: "string", description: "Name of the health insurance company, in the insurance section", }, member_id: { type: "string", description: "Insurance member or subscriber ID" }, chief_complaint: { type: "string", description: "The patient's main reason for the visit", }, allergies: { type: "array", items: { type: "string" }, description: "Patient-reported allergies. Return an empty list if none are listed.", }, medications: { type: "array", items: { type: "object", properties: { name: { type: "string", description: "Medication name" }, dosage: { type: "string", description: "Dosage, e.g. '10 mg'" }, frequency: { type: "string", description: "How often it is taken" }, }, }, description: "All current medications listed on the form", }, }, };

Step 2: Run Extract

Upload the form and call Extract with your schema. Enable citations so every value comes back with the page and location it was read from, an audit trail you want for anything touching PHI. The system_prompt gives the model document-level context.

python
from pathlib import Path from reducto import Reducto client = Reducto() upload = client.upload(file=Path("intake_form.pdf")) result = client.extract.run( input=upload.file_id, instructions={ "schema": schema, "system_prompt": ( "This is a patient intake form. Extract the demographic, insurance, " "and medical history fields. Capture every medication and allergy listed; " "if a section is blank, return an empty list. " "Only extract values that are explicitly filled in or checked on the form. " "Do not infer or guess values for fields that are blank, unchecked, or show placeholder text." ), }, settings={ "citations": {"enabled": True}, }, ) print(f"Pages: {result.usage.num_pages}, fields: {result.usage.num_fields}, credits: {result.usage.credits}").
javascript
import Reducto from "reductoai"; import fs from "fs"; const client = new Reducto(); const upload = await client.upload({ file: fs.createReadStream("intake_form.pdf"), }); const result = await client.extract.run({ input: upload.file_id, instructions: { schema, system_prompt: "This is a patient intake form. Extract the demographic, insurance, " + "and medical history fields. Capture every medication and allergy listed; " + "if a section is blank, return an empty list. " + "Only extract values that are explicitly filled in or checked on the form. " + "Do not infer or guess values for fields that are blank, unchecked, or show placeholder text.", }, settings: { citations: { enabled: true }, }, }); console.log(`Pages: ${result.usage.num_pages}, fields: ${result.usage.num_fields}`);

Step 3: Read the citation-backed output

With citations enabled, each value is wrapped in an object that pairs the extracted value with the citations that prove where it came from.

python
data = result.result patient = data["patient_name"] print(f"Patient: {patient['value']}") if patient['citations']: print(f" page {patient['citations'][0]['bbox']['page']}, confidence {patient['citations'][0]['confidence']}") print(f"Insurance: {data['insurance_provider']['value']} ({data['member_id']['value']})") print("Medications:") for med in data["medications"]: print(f" {med['name']['value']}: {med['dosage']['value']}, {med['frequency']['value']}")
javascript
const data = result.result; const patient = data.patient_name; console.log(`Patient: ${patient.value}`); console.log(` page ${patient.citations[0].bbox.page}, confidence ${patient.citations[0].confidence}`); console.log(`Insurance: ${data.insurance_provider.value} (${data.member_id.value})`); console.log("Medications:"); for (const med of data.medications) { console.log(` ${med.name.value}: ${med.dosage.value}, ${med.frequency.value}`);

A trimmed example of the structured result (illustrative):

json
{ "patient_name": { "value": "Maria Sandoval", "citations": [ { "content": "Name: Maria Sandoval", "bbox": { "page": 1 }, "confidence": "high" } ] }, "insurance_provider": { "value": "Blue Cross Blue Shield", "citations": [{ "confidence": "high" }] }, "member_id": { "value": "XJL884220931", "citations": [{ "confidence": "high" }] }, "allergies": { "value": ["Penicillin", "Latex"], "citations": [{ "confidence": "high" }] }, "medications": [ { "name": { "value": "Lisinopril" }, "dosage": { "value": "10 mg" }, "frequency": { "value": "once daily" } } ] }

Step 4: Turn on Deep Extract when accuracy is critical

Standard Extract handles most intake forms in a single pass. Turn on Deep Extract for your longest and most complex forms, when you want every field verified against the source before it comes back. It is the same call with one extra setting, plus a verification instruction that tells the agent when it is done.

python
# Same extract.run call as Step 2, with two changes: settings = { "deep_extract": True, # turn on the agentic loop "citations": {"enabled": True}, } # Add one line to the system_prompt so the loop knows when to stop: # "Iterate until every field present on the form is captured."
javascript
// Same extract.run call as Step 2, with two changes: const settings = { deep_extract: true, // turn on the agentic loop citations: { enabled: true }, }; // Add one line to the system_prompt so the loop knows when to stop: // "Iterate until every field present on the form is captured."

The read code from Step 3 is unchanged: citations stay on, so result.result is still a dict of cited values.

Under the hood, Deep Extract trades the single pass for an agent-in-the-loop: it breaks the document into sub-agents, checks its output against the source, and re-extracts until your criteria are met. In Reducto's beta it reached 99-100% field accuracy, outperforming expert human labelers.

It does more work than a single pass, so it runs longer than a standard call. Both run through the same API, so pick the right one for the task: standard Extract for short, everyday forms and high volume, Deep Extract when a form is long or field-heavy, or a wrong field is costly.

Two things make the biggest difference:

  • Verification criteria in the system prompt. Tell the loop when it's done ("capture every medication; if a section is blank, return an empty list"). Leave it out and Deep Extract will infer a reasonable one.
  • A well-described schema. The agent uses your field names and descriptions on every iteration, so specific beats generic.

Prefer no code?

Do the same thing in Reducto Studio: upload an intake form, define your fields, enable Citations (and Deep Extract for the tough ones), and Run. Click any extracted field to highlight its source on the page, then deploy the config as an API endpoint for the rest of your forms.

Where this goes next

Extract is one task on one platform. Chain the rest of the toolkit onto the same intake pipeline:

  • Sort first with Classify: route new-patient packets, referrals, and insurance cards to the right schema before extracting.
  • Break apart packets with Split: divide a multi-form intake bundle into its component documents.
  • Process forms at volume with Batch Processing: parse and extract a whole day's intake packets in parallel instead of one at a time.
  • Add humans where it matters with Pipelines: flag low-confidence fields for review before they reach the EHR.
CTA patternReducto logo

Make your first API call in minutes.

Reducto logoLLM Center