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
markdownPatient 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:
bashexport 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.
pythonschema = { "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", }, }, }
javascriptconst 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.
pythonfrom 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}").
javascriptimport 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.
pythondata = 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']}")
javascriptconst 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.