Skip to content

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:

  1. Which health-insurance carrier(s) does this employer use?
  2. 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

BenefitsInsight:

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. Form5500Source is 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 a SemaphoreSlim so the download happens exactly once) — so a resync that enriches many employers never re-downloads the national file per employer. It reuses ResiliencePipelines.Default (retries + backoff). For production, prefer a locally-mirrored copy of the official dataset (pass its path/URL to the Form5500Source constructor); 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 — Flurl HttpTest stubs 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 mocked IBenefitsSourceAdapters (Bogus for employer names).
  • Stryker.NET is scoped to the new logic via the existing **/Enrichment/*.cs glob in stryker-config.json.