Now domain-agnostic — configure any practice with Domain Packs (immigration is the reference pack)
Workflows
Document Generation

Document Generation

Audience: Engineers configuring document templates or the generation pipeline; paralegals understanding gap placeholders.
What you will accomplish: Understand the 9-step document generation pipeline — from template loading to audit record — including gap detection, versioning, and QC checks.
Prerequisites: Read Core Services first.
Estimated time: ~8 minutes.

Turn matter and contact data into filled DOCX/PDF documents — engagement letters, standard forms, routine correspondence — with gap placeholders for missing data, version tracking, and QC checks before storage.

Tools

  • document.generate — Full pipeline: template → render → QC → store → version
  • form.prefill — Fill known fields on standard forms (I-130, I-485, N-400, G-28); surface gaps for human completion

Both are risk tier write (confirm) — reversible drafts stored, nothing sent or filed externally.

Pipeline

  1. Step 1: Load template

    Retrieve template from TemplateStore by name. Template declares required variables, optional variables, and default values.

    TemplateStore.get(template_name)
  2. Step 2: Resolve context and detect gaps

    Merge Matter, Contact, and data_context overrides. Classify missing data: missing / empty / type_mismatch / enum_violation. Required vars missing → gap placeholders. Optional vars missing → empty string or default.

    Context resolver + Gap detector
  3. Step 3: Idempotency check

    Check idem_key against existing Document. Same key + same content → return existing. Same key + different content raises VersionCollisionError at the store step, not here.

    IdempotencyStore
  4. Step 4: Render DOCX

    docxtpl + Jinja2 fills the template. Gap vars render as [[MISSING: label]] placeholders in the output.

    docxtpl.render(template, context)
  5. Step 5: PDF convert

    Convert DOCX to PDF using the library-configured engine (LibreOffice headless or WeasyPrint). If engine unavailable and PDF is optional, skip. If PDF is mandatory, fail.

    to_pdf(docx_bytes)
  6. Step 6: Checksum rendered artefact

    Compute SHA-256 of the primary rendered bytes (DOCX or PDF, whichever is the primary format). Checksum stored on Document record.

    sha256(primary_bytes)
  7. Step 7: QC verification

    Run completeness + consistency checks. Are all required template vars resolved? Are names/dates consistent across all docs in the packet?

    qc.verify
  8. Step 8: Store via DocStoreConnector

    Store the rendered document in the document system. Version is monotonic per (matter_id, template_name, doc_key). Old versions immutable. Same idem_key + different content → VersionCollisionError.

    DocStoreConnector.put — write (confirm)
  9. Step 9: Audit record

    Write hash-chained audit record before returning. Record includes template name, version, checksum, and gap summary (PII-scrubbed).

    AuditService.record

Status derivation

ConditionStatusMeaning
Gaps presentincompleteNeeds human attention to fill gaps
QC failblockedCannot be used until QC issues fixed
QC skipped (unavailable)incompleteCannot certify completeness
QC pass/warn + no gapsreadyReady for routing or sending
Render/store errorfailedTechnical failure, retry or triage

Versioning

  • Monotonic per (matter_id, template_name, doc_key)
  • doc_key = semantic identifier (e.g., engagement_letter, i130_petition)
  • Old versions are immutable — no overwrite
  • Document.checksum = SHA-256 of rendered primary bytes

Gap placeholders

Unresolved required variables render as [[MISSING: label]] in the output. This is intentional — it gives the human a concrete artefact to fix, rather than a blank space they might miss.

Dear [[MISSING: client_full_name]],

Thank you for engaging [[MISSING: firm_name]] to represent you in your
[[MISSING: case_type]] matter.

Idempotency

  • idem_key = explicit key OR sha256(template_name + version + canonical(context))[:24]
  • Same key → return existing Document, no new version
  • Same key + differing content is accepted through the idempotency check; VersionCollisionError is raised later at the DocStoreConnector.put store step (version_store.py) if the existing version has different content — refused, human triage required

PDF engine

EngineStatusFidelitySpeedDependency
LibreOffice headlessPreferred (stub)High (full DOCX fidelity)Slowerlibreoffice binary
WeasyPrintFallback (stub)Medium (HTML→PDF)FasterPython package

The engine is configured in the library code, not via an environment variable.

ASSUMPTION (confirm)

  • D-001 — Priority target forms for form.prefill (I-130, I-485, N-400, G-28)
  • D-002 — Default PDF engine and primary format (DOCX vs HTML)
  • D-003 — Behaviour when PDF mandatory and conversion fails
  • D-004 — Behaviour when QC unavailable (block vs warn vs incomplete)
  • D-005 — Default privilege classification (currently: true)

Example: generating an engagement letter

from cam.core.workflows.document_gen import document_generate
 
result = await document_generate(
    template_name="engagement_letter_v2",
    matter_id="mat_abc123",
    data_context={
        "fee_structure": "flat_fee_5000",
        "retainer_amount": "2500.00",
    },
    idem_key="engagement:mat_abc123:2026-06-01",
)
 
print(result.status)      # ready, incomplete, or blocked
print(result.document_id) # doc_xyz789
print(result.gaps)        # [] or [Gap(field="client_full_name", reason="missing")]

Verification

  1. document.get(result.document_id) returns the Document with correct version and checksum
  2. Gap placeholders render as [[MISSING: label]] in the output, never empty strings
  3. Audit log shows document.generate with template name, version, checksum, and gap summary
  4. Old versions are accessible via version history API

Next steps


Last updated: 2026-06-01