If you work in immigration legal ops, corporate mobility HR, or build document automation for law firms, you already know the arithmetic of an employment-based or family-based USCIS filing:
- A single H-1B / L-1 / O-1 nonimmigrant packet (
Form G-28+Form I-129) spans 42 pages and 1,081 interactive PDF fields (101fields inG-28+980fields inI-129). - A single concurrent EB-2 / EB-3 green card filing (
Form G-28+Form I-140+Form I-485+Form I-765) spans 43 pages and 1,283 fields (101 + 261 + 760 + 161). - Across a 40-employee H-1B cap season batch or a 25-family adjustment cohort, paralegals end up re-typing the same petitioner FEIN, attorney bar number, beneficiary passport number,
I-94record, and physical address tens of thousands of times.
Yet when engineering teams try to automate G-28, I-129, I-140, I-485, I-765, or N-400 from a clean CSV, Excel (.xlsx), or JSON payload using standard Python or Node.js PDF libraries, they immediately hit three structural walls baked into official uscis.gov PDFs:
- Owner Encryption +
/PermsUsage-Rights Locks: 24 of 26 USCIS PDFs carry empty-password owner encryption (modifyDenied: true) plus an Adobe/UR3digital signature inside/Root/Perms. - Cryptic LiveCycle Subform Paths: Instead of semantic names like
beneficiary_last_name, fields are nested inside XFA-mirrored trees likeform1[0].#subform[0].Pt1Line1a_FamilyName[0]—and inconsistent across forms (Pt1Line1a_FamilyName[0]on one form vs.Part2_Line1_FamilyName[0]on the next). - Hybrid
/XFAvs./AcroFormDesynchronization: Updating/AcroForm /Fields /Vwithout stripping/AcroForm/XFAcauses Windows Adobe Acrobat to prioritize the empty<xfa:datasets>XML stream and display every populated field 100% blank.
Below is a complete engineering breakdown of how official USCIS packet PDFs are structured, how to extract a unified cross-form schema using /TU (tooltip) metadata, and how to batch-fill multi-form USCIS packets from CSV/JSON in Python or the browser.
1. Structural Anatomy of a 6-Form USCIS Filing Packet
When we audited all 26 USCIS forms in our structural study using pypdf 6.6 and pikepdf, every core petition and application shared the same Adobe LiveCycle / AEM Forms hybrid architecture:
| Form Code | Official Purpose | Pages | Size | Total Fields | Field Breakdown (Tx / Btn / Ch) | Hybrid /XFA | Owner /Encrypt + modifyDenied |
|---|---|---|---|---|---|---|---|
| Form G-28 | Notice of Entry of Appearance as Attorney | 4 | 387 KB | 101 | 76 Tx, 23 Btn, 2 Ch | Yes | Yes |
| Form I-129 | Petition for a Nonimmigrant Worker (H-1B, L-1, O-1) | 38 | 2,242 KB | 980 | 623 Tx, 333 Btn, 24 Ch | Yes | Yes |
| Form I-140 | Immigrant Petition for Alien Workers (EB-1/2/3) | 8 | 513 KB | 261 | 179 Tx, 79 Btn, 3 Ch | Yes | Yes |
| Form I-485 | Application to Register Permanent Residence | 24 | 1,175 KB | 760 | 334 Tx, 416 Btn, 10 Ch | Yes | Yes |
| Form I-765 | Application for Employment Authorization (EAD) | 7 | 456 KB | 161 | 119 Tx, 38 Btn, 4 Ch | Yes | Yes |
| Form N-400 | Application for Naturalization | 14 | 758 KB | 440 | 237 Tx, 198 Btn, 5 Ch | Yes | Yes |
Notice two things right away:
- Zero native
/Btnradio groups (radios: 0): Even mutually exclusive "Yes / No" questions onI-485(416 checkboxes) andI-129(333 checkboxes) are implemented as individual checkbox widgets where each/AP /Ndictionary defines a specific export key (typically/Y,/N,/1, or/Yes, paired with/Off). - Every single form is both
xfa: trueandencrypted: true: Before your script can modify widgets or merge pages into a single filing packet, you must decrypt the empty-password owner lock and remove both/AcroForm/XFAand/Root/Perms.
2. Why One Canonical JSON Record Can't Naive-Match Across USCIS Forms
Suppose you have a canonical JSON or CSV row representing one beneficiary and one sponsoring employer/attorney:
{ "case_id": "2026-H1B-042", "attorney.family_name": "Vance", "attorney.given_name": "Elena", "attorney.bar_number": "CA-294810", "attorney.firm_name": "Vance Global Immigration LLP", "employer.legal_name": "Acme Robotics Inc.", "employer.fein": "94-1234567", "beneficiary.family_name": "Kowalski", "beneficiary.given_name": "Jan", "beneficiary.alien_number": "A-219847561", "beneficiary.dob": "04/19/1991", "beneficiary.i94_number": "84920194821" } If you inspect the /AcroForm /Fields trees across g-28.pdf, i-129.pdf, i-140.pdf, i-485.pdf, and i-765.pdf, the beneficiary's last name lives at a different hierarchical path on every form:
Form G-28 -> form1[0].#subform[1].Pt3Line5a_FamilyName[0] Form I-129 -> form1[0].#subform[2].Part3_Line1a_FamilyName[0] Form I-140 -> form1[0].#subform[1].Pt3Line1a_FamilyName[0] Form I-485 -> form1[0].#subform[0].Pt1Line1a_FamilyName[0] Form I-765 -> form1[0].#subform[0].Pt1Line1a_FamilyName[0] Form N-400 -> form1[0].#subform[0].Pt2Line1_FamilyName[0] Fortunately, USCIS embeds detailed human-readable descriptions inside every widget's /TU (Alternate Field Name / Tooltip) entry for Section 508 accessibility—for example:
312 0 obj << /Type /Annot /Subtype /Widget /FT /Tx /T (Pt1Line1a_FamilyName[0]) /TU (Part 1. Information About You. Line 1a. Family Name (Last Name)) /MaxLen 35 /DA (/CourierStd 10 Tf 0 g) /Rect [ 54.00 582.10 294.00 596.50 ] >> endobj By extracting (full_field_path, /FT, /TU, /MaxLen, /AP/N states) once per form edition, you can build a deterministic Canonical Packet Mapping (packet_map.json) that binds one canonical key (beneficiary.family_name, attorney.bar_number, employer.fein) to its exact target widget across G-28, I-129, I-140, I-485, I-765, and N-400.
3. Python Pipeline: Inspecting & Batch-Filling a Multi-Form USCIS Packet
Below is a complete, self-contained Python pipeline (pikepdf + pypdf) that:
- Unlocks official USCIS PDFs (
G-28,I-140,I-485,I-765,I-129,N-400) by stripping owner encryption,/Root/Perms, and/AcroForm/XFA. - Maps a single canonical CSV/JSON record across every form in the packet.
- Correctly sets both
/Vand/ASon USCIS checkboxes (NameObject("/Y"),NameObject("/1"), etc.) while clearing cached/APappearance streams on modified text fields so every viewer renders the text cleanly.
#!/usr/bin/env python3 """ uscis_packet_batch_filler.py Batch-generate multi-form USCIS immigration packets (G-28, I-129, I-140, I-485, I-765, N-400) from CSV or JSON while stripping hybrid /XFA streams and /Perms UR3 locks. """ import csv import json import sys from pathlib import Path import pikepdf from pypdf import PdfReader, PdfWriter from pypdf.generic import BooleanObject, NameObject, TextStringObject def index_terminal_widgets(node_ref, prefix="", out=None): if out is None: out = {} node = node_ref.get_object() name = str(node.get("/T", "")) full_name = f"{prefix}.{name}" if prefix and name else (name or prefix) if "/Kids" in node: for kid_ref in node["/Kids"]: index_terminal_widgets(kid_ref, full_name, out) else: out[full_name] = node if name: # Also index by terminal key for convenience when unique out.setdefault(name, node) return out def set_uscis_widget_value(widget, raw_value: str) -> None: ft = widget.get("/FT") if ft == "/Btn": ap_n = widget.get("/AP", {}).get("/N", {}) on_states = [k for k in ap_n.keys() if k != "/Off"] truthy = str(raw_value).strip().lower() in ("true", "yes", "y", "1", "x", "checked") target_state = NameObject("/Off") if truthy and on_states: # If caller passed an explicit state like "/Y" or "/1", honor it; else pick first on-state explicit = f"/{str(raw_value).strip().lstrip('/')}" target_state = NameObject(explicit if explicit in on_states else on_states[0]) widget[NameObject("/V")] = target_state widget[NameObject("/AS")] = target_state else: text = "" if raw_value is None else str(raw_value).strip() max_len = widget.get("/MaxLen") if isinstance(max_len, int) and max_len > 0: text = text[:max_len] widget[NameObject("/V")] = TextStringObject(text) # Remove cached blank appearance stream so viewer synthesizes glyphs from /V + /DA if NameObject("/AP") in widget: del widget[NameObject("/AP")] def populate_uscis_form(template_pdf: Path, field_map: dict, record: dict, out_pdf: Path) -> int: # Step 1: Remove owner encryption, /Perms UR3 signature lock, and hybrid /XFA array unlocked_tmp = out_pdf.with_suffix(".unlocked.tmp.pdf") with pikepdf.open(template_pdf, password="") as pdf: if "/Perms" in pdf.Root: del pdf.Root["/Perms"] if "/AcroForm" in pdf.Root and "/XFA" in pdf.Root.AcroForm: del pdf.Root.AcroForm["/XFA"] pdf.save(unlocked_tmp) # Step 2: Populate AcroForm widgets with pypdf reader = PdfReader(str(unlocked_tmp)) writer = PdfWriter() writer.append(reader) root = writer._root_object acroform = root[NameObject("/AcroForm")].get_object() acroform[NameObject("/NeedAppearances")] = BooleanObject(True) widgets = {} for ref in acroform.get("/Fields", []): index_terminal_widgets(ref, "", widgets) filled_count = 0 for canonical_key, pdf_field_path in field_map.items(): val = record.get(canonical_key) if val in (None, ""): continue if pdf_field_path not in widgets: raise KeyError(f"Field '{pdf_field_path}' not found in {template_pdf.name}") set_uscis_widget_value(widgets[pdf_field_path], val) filled_count += 1 out_pdf.parent.mkdir(parents=True, exist_ok=True) with open(out_pdf, "wb") as f: writer.write(f) unlocked_tmp.unlink(missing_ok=True) return filled_count def run_batch(packet_config_path: str, csv_path: str, out_root: str) -> None: """ packet_config.json structure: { "forms": { "G-28": {"template": "./templates/g-28.pdf", "map": {"attorney.family_name": "Pt1Line2a_FamilyName[0]", ...}}, "I-140": {"template": "./templates/i-140.pdf", "map": {...}}, "I-485": {"template": "./templates/i-485.pdf", "map": {...}}, "I-765": {"template": "./templates/i-765.pdf", "map": {...}} } } """ config = json.loads(Path(packet_config_path).read_text(encoding="utf-8")) out_dir = Path(out_root) with open(csv_path, newline="", encoding="utf-8-sig") as f: for row_idx, record in enumerate(csv.DictReader(f), start=1): case_id = record.get("case_id", f"case_{row_idx:03d}") for form_code, form_spec in config["forms"].items(): out_file = out_dir / case_id / f"{case_id}_{form_code}.pdf" n = populate_uscis_form(Path(form_spec["template"]), form_spec["map"], record, out_file) print(f"[{case_id}] {form_code}: populated {n} fields -> {out_file}") if __name__ == "__main__": if len(sys.argv) != 4: print("Usage: python uscis_packet_batch_filler.py packet_config.json cases.csv ./output_packets") sys.exit(1) run_batch(sys.argv[1], sys.argv[2], sys.argv[3]) 4. Handling USCIS LiveCycle Input Masks & Comb Fields (SSN, A-Number, I-94)
When batch-filling I-129, I-140, I-485, I-765, and N-400, watch out for three field-level edge cases inside USCIS PDFs:
- Comb Fields (
/Ffbit 25 =16777216+/MaxLen): Alien Registration Numbers (A-Number, 9 digits), Social Security Numbers (9 digits), USCIS Online Account Numbers (12 digits), and ZIP codes use fixed-cell comb boxes. Strip hyphens and spaces ("A-219-847-561"->"219847561") before writing to/V, or the characters will shift one cell to the right and clip the final digit. - LiveCycle JavaScript Validation Traps on Foreign Addresses &
"Present"Dates: When opened inside Adobe Acrobat with XFA active, LiveCycle scripts validate US state codes andMM/DD/YYYYdate masks—blocking common immigration entries like"PRESENT"in employment history or foreign postal codes. Stripping/AcroForm/XFAdisables those rigid XFA script traps while preserving exact courier comb spacing in/AcroForm. - macOS Preview Desynchronization: If a beneficiary edits an untouched USCIS PDF in macOS Preview, Preview writes to
/AcroForm /Fields /Vwhile leaving<xfa:datasets>empty. Running thepikepdf/XFA+/Permsstrip step above (or using our XFA to Fillable PDF Unlocker) immediately restores visibility in Windows Adobe Acrobat.
5. Browser & Spreadsheet Workflows for Legal Teams and HR Ops
If your immigration practice, in-house HR mobility team, or paralegal staff wants to fill single petitions or batch-generate USCIS packets from .xlsx / .csv spreadsheets without maintaining Python scripts or paying for per-seat desktop PDF licenses:
- Fill PDF from Excel / CSV Batch Mail Merge: Upload any USCIS or employment form alongside an Excel or CSV roster, preview 5 populated rows for free to verify comb alignment and checkbox states, and download a ZIP archive of clean, unencrypted PDFs.
- XFA to Fillable PDF Unlocker: Convert Hybrid XFA and owner-locked government PDFs into standard ISO 32000-1 AcroForm or flattened PDFs that work identically across Chrome, Safari, macOS Preview, and Windows Adobe Acrobat.
- Direct Online Fillers & Field Guides for Core USCIS Packet Forms:
- Form G-28 Online Filler — Notice of Entry of Appearance as Attorney or Accredited Representative (4 pages, 101 fields)
- Form I-129 Online Filler — Petition for a Nonimmigrant Worker: H-1B, L-1, O-1, TN (38 pages, 980 fields)
- Form I-140 Online Filler — Immigrant Petition for Alien Workers: EB-1, EB-2, EB-3 (8 pages, 261 fields)
- Form I-485 Online Filler — Application to Register Permanent Residence or Adjust Status (24 pages, 760 fields)
- Form I-765 Online Filler — Application for Employment Authorization / EAD (7 pages, 161 fields)
- Form N-400 Online Filler — Application for Naturalization (14 pages, 440 fields)
Disclosure: I am the solo developer of JustFill.
AI disclosure: AI coding agents drafted and revised this article and verified the code examples above.
