PDF Form Onboarding

How a form becomes something the platform can read and write.
Draft 2026-07-20
Take every PDF a tenant hands us and turn it into a form the platform can both write  and  read.
FoundationWhat a PDF form actually is

Two flavours of PDF matter.

Flat PDF

Call Sheet — All locations
Date/Time:
Caller Name:
Decedent Name:
Weight:

Just ink on a page. No software fields.

Filling one means printing it and writing on it by hand.

Fillable PDF (AcroForm)

Call Sheet — All locations
Date/Time:
call_datetime
Caller Name:
caller_name
Decedent Name:
Text14
Weight:
Text15

Same visual layout, but with invisible named boxes overlaid on the page.

Named boxes are what let us read and write programmatically.

OverviewThe pipeline, end to end

Nine stages. Two manual passes.

1
Intake
2
Harvest
3
Bootstrap
4 · manual
Visual binding
5 · manual
Paper sweep + new fields
6
DB mapping
7
Canonical synthesis
8 · optional
AcroForm rename
9
S3 storage
Automated
Manual pass
Duplicate detection
Stage 1Intake

Every PDF gets a stable identifier.

The tenant sends us their entire form corpus — email dump, Drive folder, SharePoint export.

We drop every PDF into one working directory. Each form gets a stable form_key; unresolved files park in _unmatched/ until a human decides what they are.

Output Every PDF resolved to exactly one form_key.
Call Sheet - All locations 03.23.pdf
→ kearney_first_call_sheet
Cremation Authorization Form.pdf
→ kearney_cremation_authorization_maple_ridge
BC-VS Death Registration.pdf
→ bc_vs_death_registration
?
Scan_089.pdf
→ _unmatched/ · needs human review
Stage 2Harvest

A script cracks open every PDF and lists its fields.

  • Flat — no widgets at all. Will need to be re-authored later.
  • Semantic — widget names describe themselves. Quick to onboard.
  • Opaque heavy — widget names like Text14. Needs a manual visual pass.
Output One JSON inventory per form, plus a triage summary.
acroform-inventories / kearney_first_call_sheet.json
"form_key": "kearney_first_call_sheet", "pdf_path": "Call Sheet - All locations 03.23.pdf", "triage": "opaque_heavy", "widget_count": 33, "widgets": [ { "name": "Text14", "type": "text", "page": 1, "rect": [104, 712, 380, 728] }, { "name": "Text15", "type": "text", "page": 1, "rect": [104, 690, 380, 706] }, { "name": "Text16", "type": "text", ... }, // ... 30 more widgets, mostly named Text17..Text46 { "name": "chk_coroner", "type": "checkbox", ... }, { "name": "chk_pre_need", "type": "checkbox", ... } ]
→ sorted into opaque_heavy — most widgets carry generic Text14-style names, so this form is heading for a manual visual pass.
Stage 3Bootstrap

Every form gets a registry file.

For each form, we generate a Markdown file — forms/<form_key>.md — that becomes the source of truth for that form's field structure.

The bootstrap seeds it from the catalogue. Human review comes at the next stage, once we can anchor the file against the paper.

Output Per-form registry file, ready for human review.
forms / kearney_first_call_sheet.md
---
form_key: kearney_first_call_sheet
display_name: Call Sheet — All locations 03.23
brand: [kearney_funeral_services]
status: catalogued
---
> Kearney first-call information sheet — initial intake when a death is reported.
## Fields
⚠ DRAFT — seeded from forms-catalog.json inputs_summary, NOT verified against the PDF.
| field_key | label | data_type |
| call_datetime | Date/Time of Call | datetime |
| received_by | Received By | text |
| caller_name | Caller Name | text |
| decedent_legal_name | Decedent Legal… | text |
| decedent_weight | Weight | number |
| … (28 more rows) |
## DB Mapping
_populated by map-form-fields.md — not yet run_
## Module Mapping
_populated by map-form-fields-to-modules.md — not yet run_
Stage 4Visual binding

What does widget #14 actually mean?

For every widget whose meaning isn't obvious from its name, we render the PDF with widget outlines drawn on top.

A human writes down what each opaque widget actually captures — in a bindings CSV that pairs the widget's PDF-internal name with a proposed field key.

Manual pass · Load-bearing
Call Sheet — All locations · Page 1
rendered with widget outlines
1
Date/Time of Call:
Text14
2
Received By:
Text15
3
Caller Name:
Text16
4
Decedent Legal Name:
Text17
5
Weight:
Text18
6
Coroner?
chk_a
bindings / kearney_first_call_sheet.csv
#widget_nameproposed_field_key
1Text14call_datetime
2Text15received_by
3Text16caller_name
4Text17decedent_legal_name
5Text18decedent_weight
6chk_acoroner_case
Stage 5Paper-only sweep & new-field authoring

Some capture areas have no widget behind them.

  • Document as paper-only — runtime fill leaves it blank; a human completes it on paper.
  • Add the widget ourselves in Acrobat, named in snake_case. Save with a - RQxx suffix in the filename.

Why the suffix? The original blank exemplar stays untouched. Every RequiemOS-added field can be told apart from tenant-authored fields at a glance.

Manual pass
Before — Call Sheet as-received
Executor:
Text23
MAID?
← no widget behind this line
Onsite Contact:
Text24
↓ Add widget in Acrobat, save with - RQ01 suffix ↓
After — Call Sheet - All locations 03.23 - RQ01.pdf
Executor:
Text23
MAID?
maid_flag · added
Onsite Contact:
Text24
Stage 6DB mapping

Where does each field live in our database?

  • Existing columncase_deceased.legal_name.
  • Proposed new column — added to the migration backlog.
  • Structured envelope — a JSONB blob for a whole subdomain.
  • Generic key-valueform_submissions.data, for one-offs.
Output A field with a DB destination is a field the platform can round-trip.
First-call sheet field
Decedent Legal Name
decedent_legal_name
Database destination
case_deceased.legal_name
EXISTING COLUMN
First-call sheet field
MAID?
maid_flag
Database destination
case_deceased.maid_flag
PROPOSED NEW COLUMN
Every field on every form → one of four destinations.
Stage 7Canonical synthesis · Duplicate detection

Which fields are asking the same question?

Before — same concept, four shapes across the corpus
kearney_first_call_sheet Decedent Legal Name
bc_vs_death_registration Legal Surname of Deceased
Given Names of Deceased
eo_cremation_authorization Name of Deceased
kearney_pre_need_funeral_service_agreement Full Legal Name
After — one canonical key
deceased.legal_name

The output of Stage 7 is a canonical field dictionary — a single vocabulary of a few hundred keys spanning the entire corpus. Every field on every form is a pointer into that dictionary.

Stage 8Optional AcroForm rename

Sometimes we rename the widgets themselves.

Tenant-owned forms (a funeral home's own paperwork) can have their AcroForm widgets renamed in the PDF itself. Text14 becomes decedent_legal_name. Simplifies runtime fill; makes future onboarding trivial.

Third-party mandated forms (provincial vital stats, CPP/OAS, insurance) are never renamed. The issuer owns them. We live with the original names and let the bindings CSV bridge.

Before — Kearney first-call sheet
Date/Time of Call:
Text14
Decedent Legal Name:
Text17
Weight:
Text18
↓ Rename widgets in the PDF ↓
After — canonical widget names in the PDF
Date/Time of Call:
call_datetime
Decedent Legal Name:
decedent_legal_name
Weight:
decedent_weight
Stage 9S3 storage

Where the blank template lives.

  • Row lands in document_templates with template_format = PDF_FORM.
  • Field mapping stored as JSONB alongside the S3 pointer.
  • Version number increments every time the PDF is re-authored.
  • Filled copies for cases live in a separate area.
S3 bucket · requiemos-documents
tenants/
  └─ kearney/
      ├─ document_templates/   ← blank templates
      │   └─ tpl_first_call_sheet/
      │       ├─ v1.pdf
      │       ├─ v2.pdf   ← after RQ01
      │       └─ v3.pdf   ← after rename
      └─ case_documents/       ← filled copies per case
          └─ case_2026_0042/
              └─ first_call_sheet.pdf
Blank templates and filled instances never share storage. Version increments when the PDF is re-authored (new field added, widgets renamed, layout changed).
End stateWhat we get

For every onboarded form.

Write

Fill

The platform produces a completed copy from live case data. Every field populates from case_deceased, case_family_members, custody_events, and so on. No re-keying.

Read

Ingest

The platform accepts a filled version — typed or scanned — and lands the values back into the database, tied to the right case.

Coherence

One source of truth

Every field ties back to a canonical key. Adding a new form doesn't add a new definition of "deceased legal name"; it adds a mapping to an existing one.

Bidirectional forms.
One canonical vocabulary.
Paper and platform, coexisting.
Read more · docs/education/pdf-form-onboarding-explainer.md
navigate  ·  P print