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.
Top comments (0)