Extracting Structured Fields From a Background Check Report
10 min read · updated August 11, 2026
The single most damaging field you can put in a background check schema is overall_status. The report is a bundle of searches with different scopes, different completion states and different meanings, and no one value summarises them without lying about at least one.
One PDF, several independent searches
A consumer report ordered for employment purposes is assembled from components that were run separately: an identity or address-history trace, one or more criminal searches at county, state or federal level, a national database scan, a sex-offender registry check, employment verifications, education verifications, professional licence verifications, and sometimes a motor-vehicle record. Each component has its own vendor process, its own turnaround, and its own outcome.
They arrive bound into one PDF with a shared header, which is why they get extracted as one object. Structurally they are a list, and the document belongs to the class that multi-entity document schema design exists for. Each element needs the search type, the scope actually searched, the date it was run, its completion state, and its findings — and the report’s own summary page is a rendering of that list, not a separate fact.
The consequence people hit first is that reports are commonly returned before every component is finished. A report with four completed searches and one pending county search is a normal, valid document. A schema that cannot express “three complete, one pending, one unable to verify” will express it as something else, and every option it has available is wrong.
What “clear” actually means
This is the mechanism that makes this document specific, and it is almost always lost. A criminal search returns results from the jurisdictions and index it searched. “Clear”, “no records found” and “nothing to report” are statements about a scope: these counties, this state repository, this database, for these name and date-of-birth variants, over this period. They are not statements that no record exists anywhere.
So the scope is a field, and dropping it converts a bounded finding into an unbounded one. Capture the jurisdictions listed, the search period where stated, and the name variants and aliases the search was run against — reports usually print all of it, precisely because the scope is the qualification on the result.
- Database scans and court searches are different instruments. A multi-state database scan has broad coverage and uneven currency; a county-court search is authoritative for one county on the day it ran. Reports label which is which, and merging them into one findings array erases the distinction.
- Identity matching is probabilistic. Records are matched on name, date of birth and sometimes partial identifiers, so a common name produces candidate records belonging to somebody else. Reports carry match-quality language for this reason. Extract it.
- “Unable to verify” is not “false”. An employment verification that failed because the employer no longer exists says nothing about whether the applicant worked there. It is a third state and it is common.
A record is not a conviction
Where a criminal search does return something, the returned item has structure: a case number, a jurisdiction and court, a filing date, a charge description, a charge level, and a disposition. The disposition is the outcome, and it is the field that determines what the record means. Dispositions include convictions, but also dismissals, nolle prosequi, acquittals, deferred adjudications, pending matters and cases with no disposition recorded at all.
A field called has_criminal_record derived from the presence of any returned item is therefore factually wrong as well as consequential. A dismissed charge is a returned record and not a conviction; a pending matter is not an outcome. Model each item with its disposition as a first-class field, allow not_reported as a distinct value from pending, and never derive a boolean from item presence.
Reportability limits are their own field, not something to infer. Under the US Fair Credit Reporting Act, 15 U.S.C. § 1681c places time limits on reporting certain adverse items — among them civil judgments, paid tax liens, accounts placed for collection and arrest records — with an exception where the report is used in connection with employment at an annual salary at or above a threshold stated in the statute. Several states impose stricter limits than the federal floor, and the interaction is genuinely complicated.
Per-section status, and the field to avoid
{
"report_id": "SYNTHETIC-REPORT-0001",
"requested_on": "2026-05-02",
"returned_on": "2026-05-06",
"components": [
{ "type": "county_criminal",
"scope": { "jurisdictions": ["Example County, ST"],
"period_years": 7,
"names_searched": ["Alex Sample", "A. Sample"] },
"state": "complete",
"items": [
{ "case_number": "SYN-0000-EX", "court": "Example County District",
"filed": "2019-08-14", "charge": "Example misdemeanour",
"level": "misdemeanour", "disposition": "dismissed",
"disposition_date": "2019-11-02",
"match_quality_text": "name and date of birth match" }
] },
{ "type": "employment_verification",
"scope": { "employer": "Former Example Co" },
"state": "unable_to_verify",
"state_reason_text": "employer no longer in operation",
"items": [] },
{ "type": "education_verification",
"scope": { "institution": "Example University" },
"state": "pending", "items": [] }
]
}There is deliberately no overall_status in that record. If a user interface needs a headline, derive it at render time from the component states and show what it is derived from — “1 pending, 1 unable to verify” is honest and just as short as a green tick. What must not happen is that the derived value is stored, because a stored summary outlives the components it summarised and gets read as a fact about a person.
Two further extraction details. Findings sections are physically long and repeat across pages, so the record boundary is a layout problem of the kind tables that continue across pages produce generally: a case that spans a page break is regularly split into two half-records or merged with the next. And reports embed the adverse-action and summary-of-rights notices as attached pages of boilerplate; those are not part of the findings and should be excluded by structure rather than by hoping the model ignores them.
Handling: FCRA, PII and third-party models
Two obligations attach to this document that do not attach to an invoice, and they are worth stating plainly because they shape the pipeline rather than only the paperwork.
The first is that a consumer report used for employment purposes sits inside a defined process. The Fair Credit Reporting Act requires disclosure and authorisation before the report is obtained, and where an adverse action is to be taken based in whole or in part on the report, it requires a pre-adverse-action step in which the person receives a copy of the report and a summary of their rights, and a notice afterwards. The FTC and EEOC publish joint guidance for employers on background checks and what employers need to know, and the statute is at 15 U.S.C. § 1681b. The engineering consequence is narrow and firm: an extraction pipeline may produce fields, and it may not produce a recommendation, a score or an automatic disqualification. A person makes the decision and the process exists so that the person can be shown what it was based on.
The second is data handling. These reports contain some of the most sensitive identifiers a company holds — date of birth, address history, government identifiers, case detail. Redact before the document reaches a model you do not host, keep the mapping between the redacted token and the real value on your side, and expect your redaction step to be the thing auditors look at first. Retention deserves an explicit decision rather than a default: the extracted fields and the source PDF are two separate retention questions, and request and response logging is a third that is easy to forget until a log search finds a date of birth in it.