WorkWingman — Employer Benefits Insight (Technical)¶
A third enrichment signal on a job posting, built to the same hard rule as the others: every source must be legally/officially published — never a bulk scrape that violates a site's Terms of Service. This signal answers two questions a job seeker may need before accepting an offer:
- Which health-insurance carrier(s) does this employer use?
- Does this employer offer gender-affirming (trans-inclusive) health coverage?
Both are health-relevant and, like every other signal in this app, stay entirely local — nothing about an employer's benefits, and certainly nothing about a user's interest in gender-affirming care, ever leaves the machine.
The one caveat that governs this whole feature: coverage of gender-affirming care is decided at the employer's plan level, not the carrier level. A carrier can publish a clinical policy that covers a service, and a specific employer's plan can still carve it out. So the honest output is a signal to confirm — carrier named (from Form 5500) + an employer-level CEI signal + a link to the carrier's own public policy — with an explicit "confirm the plan documents during the offer stage" note. It is never a guarantee. This wording lives in one place,
BenefitsCaveat.PlanVsCarrier, and is attached to every insight.
Sources evaluated and the decision¶
| Source | Verdict | Why |
|---|---|---|
| DOL Form 5500, Schedule A | Used (Form5500Source) |
ERISA (29 U.S.C. § 1021 et seq.) requires the administrator of a covered employee welfare benefit plan (health, dental, vision, life) to file a Form 5500 annually. Schedule A ("Insurance Information") names each insurance carrier the plan contracts with. EBSA publishes every filing — including Schedule A — as public bulk data on the EFAST2 electronic-FOIA portal as zipped, comma-delimited CSV datasets. This is the strongest legal primary source for which insurer an employer uses. |
| HRC Corporate Equality Index (CEI) | Used (HrcCeiSource) |
The CEI is HRC's public annual benchmark of employer LGBTQ+ inclusion. Trans-inclusive health coverage (equal coverage with no blanket exclusion for medically necessary care) is a distinct 25-point criterion; a perfect "Equality 100" score requires it. HRC publishes per-employer scores in its annual report (Appendix A) and a public employer-search tool. |
| Carrier clinical-policy documents | Used as a link only (CarrierPolicyCatalog) |
Carriers publish their own gender-affirming-care clinical policies (e.g. Aetna CPB 615, Cigna, UnitedHealthcare, Anthem/BCBS). These describe what a carrier can cover as medically necessary — they are surfaced as a reference link for a named carrier, not as a statement about any employer's plan (the plan-vs-carrier caveat). |
Why HRC data is a curated snapshot, not a live fetch¶
HRC exposes no public API and no bulk export — the CEI lives in an annual PDF report and an
interactive search UI. Programmatically scraping that UI would be exactly the ToS-risky bulk-scrape
pattern this codebase deliberately avoids (the same reasoning as LayoffsFyiSource in
job-signals). So HrcCeiSource reads a curated, hand-transcribed, attributed
snapshot of publicly-reported CEI scores, shipped as an embedded JSON resource
(Enrichment/Data/hrc-cei-snapshot.json),
each entry citing its CEI edition. The loader is injectable, so refreshing the snapshot each edition
is a data-only change and tests supply their own fixture with zero network.
Which fields are reliable vs. caveated¶
| Field | Type | Reliability |
|---|---|---|
CarrierNames |
string[] |
Reliable. Directly present on Form 5500 Schedule A (INS_CARRIER_NAME) for any employer whose welfare plan filed. Empty when no filing/carrier matched. |
CeiScore |
int? |
Reliable within its vintage. The employer's CEI score (0–100) as publicly reported, only as current as the shipped snapshot's edition (surfaced via AsOf). null when the employer is unrated. |
TransInclusiveCoverage |
bool? |
An employer-level signal, not a plan guarantee. Reflects the CEI's distinct trans-inclusive-coverage criterion. true/false when the CEI reports it; null when the employer is unrated. Does not read a specific plan document. |
CarrierPolicyUrl |
string? |
A reference to what the carrier can cover — the carrier's own public clinical policy — not what this employer's plan does. null when no named carrier maps to a catalogued policy. |
Source |
enum | Form5500, HrcCei, or Merged when both contributed. |
SourceUrl |
string |
Primary attribution link (the EFAST2 dataset page when a carrier was found; otherwise the HRC employer search). |
CeiSourceUrl |
string? |
HRC attribution for the CEI-derived fields, preserved separately so those signals stay traceable in the common Merged case. null when no CEI data contributed. |
AsOf |
DateOnly? |
Vintage of the freshest contributing signal (5500 plan year / CEI edition), so a stale signal is visibly stale. |
Caveat |
string |
Always populated with the plan-vs-carrier caveat. |
Bottom line: CarrierNames is a hard fact. CeiScore and TransInclusiveCoverage are honest
employer-level signals bounded by the snapshot vintage. CarrierPolicyUrl is a "read the carrier's
own criteria" pointer. None of them substitute for reading the actual plan documents at the offer
stage — which is exactly what Caveat says.
Normalized model¶
public class BenefitsInsight
{
public IReadOnlyList<string> CarrierNames { get; set; } // from Form 5500 Schedule A
public int? CeiScore { get; set; } // from HRC CEI (null = unrated)
public bool? TransInclusiveCoverage { get; set; } // CEI criterion (employer-level signal)
public string? CarrierPolicyUrl { get; set; } // carrier's public clinical policy
public BenefitsSourceKind Source { get; set; } // Form5500 | HrcCei | Merged
public string SourceUrl { get; set; } // attribution
public DateOnly? AsOf { get; set; } // data vintage
public string Caveat { get; set; } // always: plan-vs-carrier
}
Aggregation¶
CompanyBenefitsService
queries every registered IBenefitsSourceAdapter, then merges the partial insights: carrier
fields come only from the Form 5500 partial, CEI fields only from the HRC partial (so registration
order can never drop a field one source uniquely owns), AsOf is the most-recent contributing
vintage, and the caveat is always attached. When both sources contribute, Source is Merged. One
source failing (network error, malformed dataset) never blocks the other; each adapter's LookupAsync
degrades to null rather than throwing, and cancellation propagates.
public interface IBenefitsSourceAdapter
{
BenefitsSourceKind Source { get; }
Task<BenefitsInsight?> LookupAsync(string employerName, CancellationToken ct = default);
}
This mirrors the layoff-enrichment pattern (ICompanyLayoffService / ILayoffSourceAdapter) exactly
— adapters plus graceful per-source try/catch degradation.
Wiring¶
Registered in Program.cs as plain singletons, alongside the
layoff sources:
builder.Services.AddSingleton<IBenefitsSourceAdapter, Form5500Source>();
builder.Services.AddSingleton<IBenefitsSourceAdapter, HrcCeiSource>();
builder.Services.AddSingleton<ICompanyBenefitsService, CompanyBenefitsService>();
JobPosting carries the insight directly on the
job model, next to LayoffHistory:
public BenefitsInsight? Benefits { get; set; }
It is populated in
JobQueueService.ResyncFromLinkedInAsync
— for each newly-added job, ICompanyBenefitsService.GetBenefitsInsightAsync(job.Company) is
called once and the result assigned before the job is persisted. A failure/empty result leaves
Benefits null, never blocking the sync — the same graceful-degradation contract as the layoff
enrichment.
The Form 5500 CSV shape¶
The real EFAST2 Schedule A datasets are comma-delimited CSV with a header row of field names, keyed
by ACK_ID back to the parent Form 5500. Form5500Source targets a per-year Schedule A feed
(parameterized by URL, so a new plan year or a locally-mirrored copy is a one-line change).
Form5500ScheduleACsv is a
dependency-free reader that maps columns by header name (case-insensitive, with historical name
variants like SPONS_DFE_NAME / CARRIER_NAME), so a column-ordering change between plan years never
silently reads the wrong field. It handles RFC-4180 quoting (commas + doubled "" inside fields) and
CRLF/LF endings, and it never throws on a malformed row.
Employer matching is token-boundary, not raw substring. Form5500Source.SponsorMatches normalizes
both the queried employer and the Schedule A sponsor name to lower-case word tokens and requires the
employer's tokens to appear as a contiguous run. So "Apple" matches "Apple Inc" / "Apple, Inc." (legal
suffixes) but is correctly rejected by "Snapple Beverage Corp" and "Applebee's" — carriers are never
mis-attributed to an unrelated employer whose name merely contains the query text as part of a larger
word.
Privacy note¶
This is health-relevant data. Consistent with WorkWingman's local-first design
(security): the Form 5500 lookup hits only the official public EFAST2 dataset; the CEI
data is a bundled local snapshot (no HRC request is made at all); and the resulting BenefitsInsight
is stored only in the local job store, never transmitted anywhere. A user's interest in
gender-affirming-care coverage is never observable off-device.
Caching and rate guidance¶
- Form 5500 (EFAST2 dataset): the datasets are large per-year files.
Form5500Sourceis a DI singleton and fetches + parses the feed once per source lifetime, then serves every subsequent employer lookup from an in-memory cache (concurrent first-callers are serialized via aSemaphoreSlimso the download happens exactly once) — so a resync that enriches many employers never re-downloads the national file per employer. It reusesResiliencePipelines.Default(retries + backoff). For production, prefer a locally-mirrored copy of the official dataset (pass its path/URL to theForm5500Sourceconstructor); to pick up a newer plan-year file, use a fresh source instance. - HRC CEI: no network calls are made — the snapshot is embedded — so no rate guidance applies. Refresh the snapshot when a new CEI edition publishes.
Tests¶
Form5500ScheduleACsvTests.cs— header-by-name mapping, quoting, CRLF, plan-year extraction, graceful degradation (pure parser).Form5500SourceTests.cs— FlurlHttpTeststubs with the real EFAST2 CSV shape; employer→carrier extraction, caveat, policy link, degradation. Zero real network.HrcCeiSourceTests.cs— injected fixture loader (no HRC site touched) plus a test that the bundled snapshot loads + parses.CompanyBenefitsServiceTests.cs— merge/authoritative-field/degradation/cancellation logic against mockedIBenefitsSourceAdapters (Bogus for employer names).- Stryker.NET is scoped to the new logic via the existing
**/Enrichment/*.csglob instryker-config.json.
Related docs¶
- Plain-language version: ../plain/benefits-insight.md
- The other two enrichment signals: job-signals
- Testing philosophy and tooling: testing
- Local-first safety: security ```