Help a patient request their complete Electronic Health Information (EHI) Export from their healthcare provider. Supports Epic and 70+ other certified EHR vendors. Explains what EHI is, why it matters, identifies the provider's EHR system, generates a vendor-specific appendix, guides through gathering details, finding forms, and producing a ready-to-submit PDF package.
Before using any scripts, install dependencies in the skill's scripts directory:
cd <skill-dir>/scripts && bun install
This installs pdf-lib for PDF generation/manipulation. All scripts in this skill use Bun (not Node.js).
Optional: pdftoppm from poppler-utils (apt install poppler-utils) is useful for rendering PDFs to images for visual verification.
Script usage pattern:
bun <skill-dir>/scripts/<script-name>.ts [arguments]
Every patient has a right to their complete medical record. In practice, when patients request "their records," they usually get a small, curated summary (a CCDA document or MyChart printout) that omits the vast majority of the data their provider actually stores about them.
The EHI Export is different. It's a certified feature (ONC § 170.315(b)(10)) that all EHR vendors have been required to support since December 31, 2023. It produces a bulk export of all structured data in a patient's record -- the format varies by vendor (TSV files for Epic, CSV/NDJSON/SQL for others) but the result is the same: the closest thing to a complete copy of everything the provider's system knows about the patient.
Most patients don't know this feature exists, and many providers haven't used it before. Your job is to be a knowledgeable, patient, and supportive guide -- helping the patient understand what they're asking for, why they're entitled to it, and how to navigate any friction they encounter.
This skill supports Epic and 70+ other certified EHR vendors. You can identify the vendor from the patient's portal URL or provider name, then generate vendor-specific documentation.
A ready-to-submit PDF package consisting of:
Your first question should be: "What's the name of the doctor or clinic you want to request records from?" A website or patient portal URL is also great if they have one.
Don't ask about EHR systems, vendors, or technical details -- that's your job to figure out, not the patient's. If the patient already told you the provider name (e.g., as an argument when invoking the skill), skip ahead.
Be ready to explain things in plain language if the patient has questions. Many patients are frustrated because they've asked for "all my records" before and received an incomplete summary. Validate that experience and explain how the EHI Export addresses it.
Once you know the provider, figure out which EHR system they use. This determines the vendor-specific details in the appendix (export format, documentation URL, entity counts, etc.). This is a web research task -- search for information about the provider online. Don't ask the patient what EHR their doctor uses.
mychart.* or *.epic.com → Epic*.athenahealth.com or *.athenanet.athenahealth.com → athenahealth*.eclinicalworks.com → eClinicalWorks*.nextgen.com → NextGen*.allscripts.com or *.veradigm.com → Veradigm/Allscripts*.elationhealth.com → Elation Health*.drchrono.com → DrChrono*.kareo.com → Kareo/Tebra*.advancedmd.com → AdvancedMD*.modmed.com → Modernizing Medicine"[provider name]" EHR or "[provider name]" "electronic health record" or "[provider name]" "patient portal"Use the vendor database at https://joshuamandel.com/ehi-export-analysis/data/vendors.json (71 vendors with detailed export information). The lookup script makes this easy:
bun <skill-dir>/scripts/lookup-vendor.ts "athena"
This returns the vendor's:
If the vendor has an analysis report, fetch it to learn:
# The analysis report URL follows this pattern:
# https://joshuamandel.com/ehi-export-analysis/data/analyses/{slug}.md
If you can't determine the vendor from web searches, ask the patient if they happen to know. If not, default to generic language in the appendix. The legal requirements are the same regardless of vendor -- every certified EHR must support EHI Export.
Ask the patient for their information. They can either:
You need:
If they provide a FHIR Patient resource or similar file, extract all details from it. Confirm the details with the patient before proceeding.
Using the provider's own ROI (Release of Information) form reduces friction -- staff recognize their own paperwork and are more likely to process it without pushback. Always attempt the provider's form first, whether it has fillable fields or not. But it's not required. HIPAA requires that the patient put the request in writing if the covered entity asks (45 CFR § 164.524(b)(1)). If you can't find the provider's form, use our generic access request template instead.
Help the patient get this form through multiple approaches:
"[provider name]" "authorization" "release" "protected health information" filetype:pdf"[provider name]" "medical records" "release form" filetype:pdf"[provider name]" "ROI" OR "release of information" form filetype:pdf"[provider name]" "request" "own records" OR "own medical records" filetype:pdf"[provider name]" "patient access" "health information" filetype:pdfsite:[provider-domain] authorization releaseProviders often have multiple forms for different purposes. When you find more than one, apply these preferences:
Once you've identified a candidate form URL, follow these steps in order:
Download with a realistic user agent to avoid bot blocking:
curl -sL -o provider_form.pdf \
-A "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" \
"<URL>"
If the download fails (network error, CAPTCHA, 403), share the link with the patient and ask them to download and share the file back to you:
"I found what looks like your provider's records release form at [URL], but I'm having trouble downloading it directly. Could you click that link, download the PDF, and share it with me? Then I can fill it out for you."
Verify this is actually a records release/authorization form. Extract the text and review it:
pdftotext ./provider_form.pdf - | head -80
Read the output and confirm this document is a form the patient can use to authorize release of their own health information. If it's something else (privacy notice, patient rights brochure, billing form, general informational document), discard it and continue searching. If pdftotext returns no text at all, the PDF is likely a scanned image — fall back to the generic form rather than attempting to transcribe from the image. If you've exhausted all search approaches, fall back to the generic form.
Check if the form has fillable fields:
bun <skill-dir>/scripts/list-form-fields.ts ./provider_form.pdf
Found provider's form?
├── Yes, has complete fillable AcroForm fields
│ └── Fill via form field API → flatten → visual check → proceed
├── Yes, but flat/scanned OR incomplete fields (missing signature, date, key sections)
│ ├── pdftotext returns text → Transcribe to markdown with filled values → convert to PDF → visual check → proceed
│ └── pdftotext returns no text (image-only scan) → Use generic form (fillable fields)
└── No form found
└── Use generic form (fillable fields)
⚠️ CRITICAL: Never invent or fabricate form content. Your transcription must faithfully reproduce the actual text from the downloaded form. If you cannot read or extract the form's content (download failed, file is corrupt, wrong document type), fall back to the generic form at templates/right-of-access-form.pdf. A real generic form is always better than a fabricated provider-specific form.
⚠️ IMPORTANT: When the form lacks fields for required information, do NOT skip straight to the generic form. The provider's own form reduces friction with records staff. Transcribe the flat form to markdown (preserving all sections, text, and structure) and convert to a clean PDF.
Signs a form needs markdown transcription (not just field filling):
Exception: If signature is the only missing field, fill the other fields via API, then add the signature image by coordinates (signature placement is forgiving - bottom of form near a signature line).
Provider forms often include questions or fields we didn't ask the patient about upfront:
Default to maximal data collection — don't ask the patient about routine choices. Fill in:
Inform, don't ask: When presenting the filled form for review, briefly note any choices made (e.g., "I selected 'all record types' and 'all dates' — let me know if you'd prefer specific date ranges or categories"). This saves the patient a conversation turn while still giving them the option to adjust.
If the provider's own form can't be found, use the generic fillable PDF at templates/right-of-access-form.pdf. This is a last resort when no provider form exists. It is a proper interactive PDF form with labeled fields that any user could open and fill in a standard PDF reader. It frames the request as an exercise of the HIPAA Right of Access (45 CFR § 164.524). It includes:
To use it programmatically, copy it to ./provider_form.pdf and fill it with pdf-lib's form field API -- the same approach used for provider forms:
const form = doc.getForm();
form.getTextField('patientName').setText(patient.name);
form.getTextField('dob').setText(patient.dob);
form.getCheckBox('ehiExport').check();
// ... etc
form.flatten();
The generic form is clean and professional. Let the patient know you're using a standard access request form and explain that providers must accept any written request for access -- they cannot insist on their own form.
While searching for the form, also look for the provider's medical records / HIM department fax number and mailing address. The patient will need these to submit the completed request. Search for:
"[provider name]" "medical records" fax"[provider name]" "release of information" fax"[provider name]" "HIM" OR "health information management" faxNote these for the delivery guidance at the end.
Use the reference script to enumerate all form fields:
bun <skill-dir>/scripts/list-form-fields.ts ./provider_form.pdf
This will show each field's type, name, current value, and widget position (x, topY, width, height). Use this to understand the form's structure.
Then write a script to fill the form using pdf-lib's form field API. Map fields intelligently:
Always flatten the form after filling so fields render as static text.
Important: Fill all pages of the provider's form that have fields to fill out. Only skip pages that have no fillable content (e.g., "For Office Use Only" pages, instruction-only pages).
When the provider's form has no fillable fields, transcribe it to markdown with filled values, then convert to PDF. This produces a clean, readable document that faithfully represents the original form's content.
Step 1: Transcribe the form to markdown
Before transcribing, extract the complete text of every page:
pdftotext ./provider_form.pdf -
Use this full text as the basis for your transcription — do not rely solely on rendered page images, which may miss pages or cut off fine print. Read the COMPLETE output before starting.
Create a markdown file that reproduces the form's structure and content with the patient's information filled in. Wrap all patient-filled values in ==...== markers so the PDF renderer can visually distinguish filled-in data (rendered in bold blue) from the original form text. This includes names, dates, addresses, checked boxes' labels, and any other values the patient is providing — but not the form's own headings, labels, or legal text. Start with a note explaining the transcription:
> **Note to Medical Records Department:** Your published authorization form is a non-fillable
> PDF (it lacks interactive form fields), which prevents electronic completion.
> Pursuant to 45 CFR § 164.524(b)(1), covered entities may not impose unreasonable
> measures that serve as barriers to individuals requesting access. This document
> faithfully reproduces all content from your authorization form with the required
> information completed.
>
> Form source: https://example-health.org/forms/medical-records-release.pdf
(Include the actual URL where the form was retrieved, when known, for easy verification.)
---
# Authorization for Release of Health Information
**Provider:** University Health Partners
---
## Patient Information
| Field | Value |
| ----- | ----- |
| Patient Name | ==Jane Doe== |
| Date of Birth | ==January 15, 1985== |
| Address | ==123 Main Street, Madison, WI 53711== |
| Phone | ==(608) 555-0123== |
## Information Requested
- [ ] Complete Medical Record
- [ ] Discharge Summary
- [x] ==Other (see below)==
> **Electronic Health Information (EHI) Export** — Complete export of all
> structured data pursuant to 45 CFR § 170.315(b)(10). See attached Appendix A.
## Authorization
I authorize the release of my protected health information as described above.
**Patient Signature:** __________________ **Date:** ==February 19, 2025==

**Printed Name:** ==Jane Doe==
Important: Do not place signature images inside table cells — the renderer does not support images in tables. Use the format above: signature label and date on one line, image below, printed name below that.
Tips for high-fidelity transcription:
[x] / [ ] for checkboxes — Renders as [X] checked or [ ] unchecked. Unicode ☑/☐ also work.> for callouts — Renders with a border boxStep 2: Add the signature
The signature can be included in two ways:
File path — If you have the signature saved to disk:

Base64 data URL — Embed the image directly:

To get a base64 data URL from a file:
echo "data:image/png;base64,$(base64 -w0 ./signature.png)"
Place the signature image in the appropriate location (usually above or next to "Patient Signature" and the date).
Step 3: Convert to PDF
bun <skill-dir>/scripts/md-to-pdf.ts ./filled_form.md ./provider_form_filled.pdf
This produces a clean PDF with:
[X] or [ ]Step 4: Verify the result
pdftoppm -png -r 150 -singlefile ./provider_form_filled.pdf ./preview
Review the rendered PDF. The markdown approach typically produces clean results on the first try, but verify the signature placement and overall layout before proceeding.
When to use markdown transcription:
What to tell the patient:
"Your provider's form isn't digitally fillable, so I've created a clean version that includes all the same information and sections. It's formatted as a standard authorization form with your details filled in. Providers are required to accept any written request that meets HIPAA requirements."
After filling the form (via form fields or markdown transcription), verify the result:
When transcribing a flat form to markdown:
"Your provider's form isn't digitally fillable, so I've created a clean version that includes all the same sections and content with your information filled in. Providers are required to accept any written request that meets HIPAA requirements."
When using the generic form:
"I'm using a standard HIPAA access request form. This is equally valid -- providers are required to accept any written request that meets the requirements of the HIPAA Right of Access."
There are three options for getting the patient's signature, in order of preference:
If the signature service is configured (check scripts/config.json -- relayUrl must be set), collect an electronic signature. The patient draws their signature on a secure webpage using their phone or computer.
When presenting this option to the patient, say something like:
"Now let's collect your electronic signature to include in the form. I'll send you a link to a secure page where you can draw your signature."
Do NOT use technical terms like "relay server", "E2EE", "encrypted session", etc. with the patient. Just call it "electronic signature" or "e-signature".
bun <skill-dir>/scripts/create-signature-session.ts \
--signer-name "Jane Doe" \
--expiry-minutes 60
Optionally pass --instructions "Custom text" to override the default instructions shown to the signer. Outputs JSON to stdout:
{
"sessionId": "62ee3034-...",
"signUrl": "https://relay.example.com/sign/62ee3034-...",
"privateKeyJwk": { "kty": "EC", "crv": "P-256", "d": "...", "x": "...", "y": "..." }
}
Save all three fields.
Present the signUrl to the patient — show them the link and tell them what to expect on the page. The patient cannot sign without this URL, so you must output it before starting the poll. On desktop, the signing page also shows a QR code so the patient can easily switch to their phone for camera access and a better signature drawing experience.
Poll for completion (run in background while the patient signs):
bun <skill-dir>/scripts/poll-signature.ts <session-id> '<private-key-jwk-json>' \
--output-dir .
This blocks until the patient signs (or the session expires). On success it writes:
./signature.png -- transparent-background PNG of the drawn signature./signature-metadata.json -- timestamp, audit log./drivers-license.png -- photo of driver's license (if the patient uploaded one)And outputs JSON to stdout:
{
"signaturePath": "./signature.png",
"metadataPath": "./signature-metadata.json",
"timestamp": "2026-02-18T18:05:00.000Z",
"driversLicensePath": "./drivers-license.png"
}
The driversLicensePath field is only present if the patient uploaded a driver's license.
Progress goes to stderr. Exits with code 1 if the session expires.
page.drawImage() — it already has a transparent background, so no ImageMagick processing is needed. See Option B steps 2-4 for positioning guidance.Ask the patient if they have a signature image to embed. If they provide one:
convert input.png -fuzz 20% -transparent white ./signature-transparent.png (or use magick depending on the ImageMagick version available)page.drawImage()If they don't have a signature image and live capture isn't available, let them know they'll need to print the final PDF and sign by hand before submitting.
The signing page always includes an optional driver's license upload section. The patient can choose to upload a photo of their ID or skip it.
When presenting this to the patient:
"The signing page will let you draw your signature. You can also optionally upload a photo of your driver's license — some providers require ID verification, but you can always provide it separately later if you prefer to skip it now."
Using the driver's license when provided:
If driversLicensePath is present in the poll output, generate a PDF page with the ID image using the template. If driversLicensePath is absent, skip this step entirely — do not prompt the patient for it separately.
# Read the template, replace placeholders, and convert
cp <skill-dir>/templates/drivers-license-page.md ./dl-page.md
# Then use sed or your preferred method to replace:
# {{PATIENT_NAME}} → patient's full name
# {{DOB}} → patient's date of birth
# {{IMAGE_PATH}} → ./drivers-license.png
bun <skill-dir>/scripts/md-to-pdf.ts ./dl-page.md ./drivers-license-page.pdf
The template at templates/drivers-license-page.md produces a page with a patient name/DOB header and the driver's license scan image. Merge this page into the final PDF package (typically after the request form, before the appendix).
The cover letter includes the patient's name and DOB (for identification if pages separate) and routes the request to the right team. Generate it with:
bun <skill-dir>/scripts/generate-cover-letter.ts '{
"patientName": "Jane Doe",
"dob": "03/15/1985",
"outputPath": "./cover-letter.pdf"
}'
The patientName and dob fields are optional (the script produces a generic version without them), but you should always include them when patient info is available. The date field defaults to today if omitted.
A pre-built generic version (without patient info) exists at templates/cover-letter.pdf as a fallback.
The appendix contains no patient-specific information -- it explains what an EHI Export is, the legal basis, how to produce it, and delivery preferences. It accepts an optional date parameter to add a self-orienting reference line ("Accompanies Request for Access to PHI dated ...").
For the quickest path, copy the pre-built templates/appendix.pdf to ./appendix.pdf. To include the date reference line, regenerate:
bun <skill-dir>/scripts/generate-appendix.ts '{"date": "02/18/2026", "outputPath": "./appendix.pdf"}'
Generate a vendor-specific appendix using the scripts/generate-appendix.ts script. Pass the vendor details from Step 2:
bun <skill-dir>/scripts/generate-appendix.ts '{
"date": "02/18/2026",
"vendor": {
"developer": "athenahealth, Inc.",
"product_name": "athenaClinicals",
"export_formats": ["NDJSON", "HTML", "PDF"],
"ehi_documentation_url": "https://docs.athenahealth.com/athenaone-dataexports/",
"entity_count": 133,
"field_count": 6809
}
}'
This generates ./appendix.pdf with:
All fields are optional -- the script gracefully falls back to generic language for any missing details.
Use pdf-lib to merge:
If the provider form was filled via AcroForm fields, scripts/fill-and-merge.ts handles both filling and merging in one step (pass coverLetterPath in the config).
If you already have a filled PDF (from markdown transcription or the user), write a simple pdf-lib merge script — load each PDF with PDFDocument.load(), copy pages with copyPages(), and save:
import { PDFDocument } from "pdf-lib";
const merged = await PDFDocument.create();
for (const path of ["./cover.pdf", "./filled_form.pdf", "./appendix.pdf"]) {
const doc = await PDFDocument.load(await Bun.file(path).arrayBuffer());
const pages = await merged.copyPages(doc, doc.getPageIndices());
pages.forEach(p => merged.addPage(p));
}
await Bun.write("./ehi-request-provider.pdf", await merged.save());
Save the final PDF to the working directory with a descriptive name like ehi-request-[provider].pdf.
⚠️ Verify the result: After generating the PDF:
Only proceed once you've verified the PDF looks correct.
This is where many patients get stuck. Don't just hand them the PDF -- help them actually submit it.
⚠️ CRITICAL: Always get explicit user approval before submitting.
Before faxing, mailing, or otherwise submitting the request:
Never auto-submit without user approval. The patient must review and approve before any transmission.
relayUrl is configured in scripts/config.json): If you found the provider's fax number and the user has approved, send the fax. Present this to the patient as simply "I can fax this directly to the provider for you" -- no need to mention technical details:bun <skill-dir>/scripts/send-fax.ts "+15551234567" ./ehi-request-provider.pdf
Outputs JSON to stdout with faxId, provider, and status (initially "queued" or "sending"). Check delivery status:
bun <skill-dir>/scripts/check-fax-status.ts <fax-id>
Returns status (queued | sending | delivered | failed), plus pages, completedAt, and errorMessage when applicable.
Also prepare them for potential pushback:
bun <script>.ts, not nodecd <skill-dir>/scripts && bun install to install pdf-libcreate-signature-session.ts, poll-signature.ts, send-fax.ts, check-fax-status.ts) read the server URL from scripts/config.json (relayUrl field). You can also pass a URL as the first argument to override. Note: Use patient-friendly language ("electronic signature", "send the fax") -- avoid technical jargon like "relay server" when communicating with the patient.md-to-pdf.ts. Do NOT attempt coordinate-based text drawing -- it's unreliable and produces poor resultstemplates/appendix.pdf) with no patient-specific content -- just copy and merge ittemplates/right-of-access-form.pdf) is a fillable PDF (16 fields) with these field names: patientName, dob, phone, patientAddress, email, providerName, providerAddress, recipientName, recipientAddress, recipientEmail, ehiExport (checkbox), includeDocuments (checkbox), additionalDescription, signature, signatureDate, representativeAuth. The form is generated by scripts/build-right-of-access-form.ts and is a Right of Access request under 45 CFR § 164.524.| Script | Usage |
|---|---|
lookup-vendor.ts | bun lookup-vendor.ts <search-term> |
list-form-fields.ts | bun list-form-fields.ts <pdf-path> |
generate-appendix.ts | bun generate-appendix.ts ['{"vendor": {...}}'] |
generate-cover-letter.ts | bun generate-cover-letter.ts ['{"outputPath": "..."}'] |
fill-and-merge.ts | bun fill-and-merge.ts <config.json> |
md-to-pdf.ts | bun md-to-pdf.ts <input.md> [output.pdf] |
create-signature-session.ts | bun create-signature-session.ts [--instructions <text>] [--signer-name <name>] |
poll-signature.ts | bun poll-signature.ts <session-id> '<private-key-jwk>' |
send-fax.ts | bun send-fax.ts <fax-number> <pdf-path> |
check-fax-status.ts | bun check-fax-status.ts <fax-id> |