Plan: Ingest kommune-level sykefravær (sick leave) statistics
🔴 The investigation's framing was wrong on the mechanism, not just a detail — read this before
anything else. "nav-sykefravaer" was assumed to be a third NAV-Excel-scrape source, same shape as
nav-uforetrygd/nav-aap. It is not, for a structural reason: NAV's own sykefravær statistics
pages do not publish kommune-level data at all — checked live, every downloadable table under
both the sykefravar and sykepenger sub-pages is fylke-level or coarser (industry, sector, age,
diagnosis, occupation — never kommune). The real kommune-resolved data lives at SSB, table
12451 ("Bostedskommune- og kjønnsfordelt sykefravær"), reached through this project's existing,
already-proven lib/pxweb.ts PXWebAPI client — the same mechanism every ssb-* source already
uses, not a new NAV-family Excel parser. Recommendation: build this as ssb-12451, not
nav-sykefravaer — see [Q1].
IMPLEMENTATION RULES: Before implementing this plan, read and follow:
- WORKFLOW.md - The implementation process
- PLANS.md - Plan structure and best practices
Status: Completed
Goal: Add kommune-level sick-leave statistics to Atlas, completing the NAV-adjacent welfare
triad alongside nav-uforetrygd (long-tail disability outcome) and nav-aap (transitional
work-assessment benefit) — this one covers the much larger, earlier-stage population: everyone
with a physician-certified sick-leave spell.
Last Updated: 2026-10-02
Investigation: INVESTIGATE-new-norwegian-public-sources.md §Tier 1 #2 ([Q4]–[Q7], [Q32])
Prerequisites: None. ssb is already a valid publishers.yaml provider, and lib/pxweb.ts
already exists and is already proven across a dozen+ ssb-* sources — no new mechanism, no new
Dagster job (reuses annual_sources_refresh-style weekly polling, same as every other ssb-*
source; see [Q4] on cadence).
Phase 1: Find the real mechanism — DONE (verified 2026-10-02)
Tasks
-
1.1 Checked NAV's own sykefravær statistics pages for a kommune-level table, the same way
nav-uforetrygd/nav-aapfound their kommune-levelPST302/AAP155tables. Not found — genuinely absent, not just hard to locate. Fetched both sub-pages live:nav.no/.../sykefravar-statistikk/sykefravar— 14 downloadable tables (SYFRA 510-SYFRA 2501), covering bosted (residence), sektor, næring, alder, yrke, diagnose, varighet, innvandrerstatus, fødeland. DownloadedSYFRA 510("etter bosted") directly and inspected it: row labels are fylke names ("Agder", "Oslo - Oslove", …), zero kommune codes anywhere. Confirmed by grepping the whole page's HTML for the literal string "kommune" — zero hits.nav.no/.../sykefravar-statistikk/sykepenger— same pattern. DownloadedSYKP 510("etter bosted") directly: fylke-level rows only ("Landet", "Agder", "Akershus", …), zero kommune codes, zero "kommune" mentions on the page.- Both page's own file-naming convention ("etter bosted") means "by place of residence (fylke)", not "by kommune" — a different meaning from what the name suggests on first read.
-
1.2 Found the real kommune-resolved data at SSB instead of NAV. Queried SSB's own table search (
data.ssb.no/api/pxwebapi/v2/tables?query=sykefravær, the same real API this project'sssb-*sources already use) rather than guessing a table number. Of ~20 matching tables, three explicitly key onBostedskommune(kommune of residence):12451— Region × Kjønn × ContentsCode × Tid. The simplest, most direct candidate.12448— adds Alder (age, 10 groups).12449— adds Næring (industry, 2 groups).
12451is the right v1 scope — same "start with the plainest geo-resolved table, breakdowns are a fast-follow" philosophy asudir-gsi's grand-total slice. -
1.3 Verified
12451's real shape live via its metadata (.../tables/12451/metadata), not assumed from the search result's label alone:- Region: 937 codes, real SSB-format (
0301Oslo,1101Eigersund,1151Utsira, …) — same conventionclassify_region_codealready handles, confirmed by spot-checking the known sentinel shapes:XX99("Uoppgitt kommune"), 9999("Uoppgitt kommune" national),2111(Longyearbyen/Svalbard),2199/2299(Svalbard/Jan Mayen unspecified). No new sentinel shape — unlike every NAV-Excel source this session, which each needed a new one (udir-gsi/husbanken-bostotte's Svalbard-as-kommune bug class,nav-aap's non-numericUkjent). SSB's own Region dimension already carries full history back to kommune mergers, same asdim_kommune. - Kjonn: 3 codes (
0Begge kjønn,1Menn,2Kvinner).0is the server's own elimination default — a query that omitsKjonnentirely still returnsBegge kjønnonly, confirmed live, though this plan recommends passing it explicitly for robustness against a future default change. - ContentsCode: 9 measures, not 1 —
Sykefraversprosent(%),Sykefraversdagsverk(lost workdays),ArbeidstakereSykefra/ArbeidstakereSykePro(a different metric: count/share of employees who had any sick-leave spell, not days-weighted), plus four*Endr*variants that are pure year-over-year deltas of the measures above. See [Q2] for v1 scope. - Tid: 105 quarters,
2000K2-2026K2. - Full cartesian product: 937 × 3 × 9 × 105 = 2,655,315 cells — over SSB's 800,000-cell
request limit (documented in
lib/pxweb.ts's own header). Filtering is required, not optional — same category of constraint as Husbanken's Qlik pagination limit and Udir's 420-column cross-tab, a different shape each time but the same lesson: check the real size before assuming one request covers it.
- Region: 937 codes, real SSB-format (
-
1.4 Proved end-to-end by pulling real data, not just confirming the schema. A live, filtered query (
valuecodes[Region]=0301,1101,1151&valuecodes[Kjonn]=0&valuecodes[ContentsCode]=Sykefraversprosent,Sykefraversdagsverk&valuecodes[Tid]=TOP(4)) returned real, plausible figures for all three test kommuner — Oslo, Eigersund (an ordinary kommune), and Utsira (Norway's smallest by population):Oslo 2025K3-2026K2: 4.1, 4.9, 5.1, 4.8 % 914600, 1047600, 1067700, 942500 dagsverkEigersund 2025K3-2026K2: 4.3, 5.6, 6.0, 5.4 % 18400, 22900, 23700, 20000 dagsverkUtsira 2025K3-2026K2: 4.3, 5.4, 4.6, 6.3 % 240, 300, 230, 310 dagsverkNo suppression observed on any of the nine cells, including Utsira's small-kommune ones — not a blanket claim of "no suppression anywhere in this table," just what this specific sample showed. Confirm against the smallest kommune by activity (not necessarily population) during Phase 2.
-
1.5 Licence correction — this is SSB data, not NAV's, despite covering NAV-administered benefits. The investigation's licence note for this family ("CC BY 4.0... unverified for sykefravær") assumed the NAV-Excel mechanism and inherited NAV's licence by analogy. Since the real source is SSB's own PXWebAPI table, the real licence is NLOD — SSB's standard, same as every other
ssb-*source in this repo, confirmed by the table's own metadata ("copyright": falsein thepxextension block) and by precedent (everyssb-*manifest in this repo declares NLOD). Not CC BY 4.0 — the two NAV-Excel sources' licence correction does not transfer to this candidate, because the candidate itself changed.
Validation
✅ Confirmed 2026-10-02. Real metadata fetched, real filtered query executed, real figures returned for three kommuner of differing size — not inferred from the table search result's label.
Open Questions
- [Q1] Name this
ssb-12451, notnav-sykefravaer. The investigation's original [Q4] decision ("NAV: one folder per indicator family") assumed an NAV-Excel mechanism needing its own per-family parser, the same reasonnav-uforetrygd/nav-aapeach got their own folder. This candidate reuseslib/pxweb.tsunchanged — structurally identical to everyssb-*source already in the repo, not a new mechanism. Recommendation:source_id: ssb-12451,provider: ssb, folderatlas-data/ingest/src/sources/ssb-12451/, dbt modelindicators__ssb_12451— matching this repo's unbroken "ssb-sourced table = ssb-" convention, not the NAV-family folder convention. This is a real deviation from how this candidate was filed in the investigation and backlog docs; the rename should be reflected when this ships (see "Files to Modify"). - [Q2] ContentsCode scope for v1 — 9 measures exist, how many ship?
Recommendation:
Sykefraversprosent+Sykefraversdagsverkonly — the two headline measures (percentage and absolute lost workdays), matchingssb-12944's single-measure-family style. DeferArbeidstakereSykefra/ArbeidstakereSykePro(a materially different metric — people who had any spell, not days-weighted) and all four*Endr*year-over-year-delta variants, which are trivially re-derivable from the raw quarterly series Atlas already carries and which no other Atlas source ingests as separate stored rows. - [Q3] Kjonn scope for v1 — total only, or total + sex-split?
Recommendation:
Kjonn=0(Begge kjønn) only for v1, matching every other Atlas source's "kommune total first, breakdowns are a fast-follow" default. A future source can add the sex-split without re-deriving the mechanism. - [Q4] Cadence — annual-style weekly poll, or something tighter? SSB updates this table
quarterly (confirmed via its own
updatedtimestamp andTiddimension). Recommendation: weekly poll viaannual_sources_refresh(the existing job already polls several quarterly/annual SSB tables this way — e.g.ssb-06913is a long time series, polled weekly to catch a quarterly release promptly) — not a new Dagster job, and notmonthly_sources_refresh(that job's own docstring scopes it to genuinely-monthly data, and this is quarterly). - [Q5]
topics.yamlcategory — social or health? The investigation's EU-theme note flagged this as a real open question ("possibly split with HEAL for sickness statistics"). Sykefravær is fundamentally a labour-market/welfare-claim statistic (who is absent from paid work and why), closer in kind tonav-uforetrygd/nav-aapthan to FHI's health-outcome sources. Recommendation:social, matching the sibling NAV-adjacent sources — not resolved with certainty, confirm againsttopics.yamlbefore writing the manifest, per [Q4]'s own note onnav-aap's Phase 2 about catching this before a CI failure rather than from one. - [Q6] Historical depth — all 105 quarters (back to 2000K2), or a shorter window? Recommendation: all of them — SSB serves the full series in one request within the 800,000-cell budget once ContentsCode/Kjonn are scoped per [Q2]/[Q3] (937 × 1 × 2 × 105 = 196,770 cells, comfortably under the limit, no pagination needed), and a longer time series is strictly more useful with no extra ingest cost. Confirm the exact cell count against whatever [Q2] actually settles on before relying on this number.
- [Q7] The two documented data-quality notes in the table's own metadata — represent, don't
correct. SSB's own metadata names: (a) a trend break 2007→2008 (industry-standard change) and
2014→2015 (NAV register replaced by A-ordningen); (b) a since-corrected error affecting Q1 2026
(25,656 employment relationships misassigned between two Østfold/Akershus kommuner, fixed by SSB
before this plan's research). Neither needs an Atlas-side fix — SSB already applied its own
correction, and the trend breaks are a fact about the series, not a defect in it. Record both in
the manifest's
methodology_notes, same as every prior source's upstream caveats.
Phase 2: Ingest module + raw table (not started)
Tasks
- 2.1 Create
atlas-data/ingest/src/sources/ssb-12451/(per [Q1]) — same shape asssb-12944:index.tscallingfetchPxTableData/parseJsonStat2fromlib/pxweb.tswith explicit filters forRegion=*(or the full code list),Kjonn=0,ContentsCode=<Q2's choice>,Tid=*; no new parser module needed —lib/pxweb.ts's existingparseJsonStat2is already generic across dimension shapes.manifest.yml—source_id: ssb-12451,provider: ssb,license: NLOD,periodicity: P1Y(annual-cadence-style weekly poll per [Q4], even though the underlying data is quarterly — confirm this matches how other quarterly-but-weekly-polledssb-*sources declare it, e.g.ssb-06913, before committing toP1YvsP3M/irregular).README.mdand__tests__/— golden-file test against a real captured JSON-stat2 response (reusessb-12944's test pattern: a real fixture file, not a hand-built mock).
- 2.2 Migration
raw.ssb_12451(region_code, period, contents_code, contents_label, value, status, loaded_at)— same shape asraw.ssb_12944, PK(region_code, period, contents_code). - 2.3 Dagster registration — add
ssb-12451to the existing weekly-polledSSB_SOURCESlist (raw_ssb.py/schedules.py's_ANNUAL_SOURCE_IDS), same job every otherssb-*source uses. No new job.
Validation
cd atlas-data/ingest && npm run ingest:ssb-12451 # the REAL npm-run invocation, not a direct tsx call
🔴 Run it this exact way. check-every-source-has-an-ingest-script.sh (added after
husbanken-bostotte's urb-agents#1807) will catch a missing script entry in CI, but verify with
the real invocation anyway rather than relying solely on the gate, per that incident's own lesson.
Real filtered query returns the expected cell count for whatever [Q2]/[Q3] settle on; a spot-check against Oslo, Eigersund and Utsira matches this plan's own Phase 1.4 figures exactly (same table, same period range, no republish should have happened between research and ingest — if it has, that is itself worth noting, not silently absorbed).
Phase 3: dbt staging and marts (not started)
Tasks
- 3.1 Add
raw.ssb_12451tomodels/indicators/sources.yml. - 3.2
indicators__ssb_12451.sql—kommune_nr/region_kindviaregion_code_to_kommune_nr/classify_region_codefrom the first commit. Expect this to pass clean, not need a correction — unlike every NAV-Excel source this session, SSB's own Region dimension already matches this macro's existing patterns exactly (confirmed in Phase 1.3); if it doesn't, that is itself a more surprising finding worth stopping on. - 3.3 Document columns in
schema.yml; validate against a local Postgres loaded with the real ingest. - 3.4
mart_indicators__ssb_12451.sqlapi passthrough +marts/api/schema.ymlentry. - 3.5
dbt build --select indicators__ssb_12451 mart_indicators__ssb_12451against real loaded data.
Validation
Real local Postgres, not an empty schema. Confirm the sentinel region codes (XX99, 9999,
2111, 2199, 2299) resolve through classify_region_code exactly as Phase 1.3 predicted,
rather than assuming the existing macro "just works" without checking.
Phase 4: Deploy and verify arrival (COMPLETE, 2026-10-02)
Deploy request filed to imac: urb-agents#1811.
Tag v20261002-15e7b13, both digests labelled, LANDS WITH derived via lands-with.sh
(annual_sources_refresh then transform_and_publish), row-count prediction stated (196,770 for
raw.ssb_12451 / indicators__ssb_12451 / mart_indicators__ssb_12451, confirmed via a real
local-Postgres ingest run). Also flagged and explained a false-positive in the derived range —
lands-with.sh named nav-aap as "ingest changed" too, traced to a one-line README link fix from
PLAN-012's close-out commit, not a code or data-path change.
⚠️ Self-caught transcription error: the code-location digest in the first message to imac was
mistyped (...16705b8120f... instead of the real ...16705b2120f...). Caught by re-verifying
against the build log immediately after sending, before any reply — corrected with a follow-up
comment on the same task rather than leaving the wrong value standing.
annual_sources_refresh and transform_and_publish both succeeded clean on the first attempt —
raw.ssb_12451 landed 196,770 rows, exact match to the prediction. No dim_brreg_enhet
corruption recurrence (imac watched PostgreSQL's own log for the entire run and checked the table
directly before and after).
Independently verified live (not just trusting the report), 2026-10-02, against
https://api-atlas.urbalurba.com (confirmed cf-cache-status: MISS, not a stale cached read):
GET /meta_sources?source_id=eq.ssb-12451&select=served_as→["indicators__ssb_12451"]GET /indicators__ssb_12451?limit=1→ a real row (region 3101, period 2000K2 —value: null,status: ".", SSB's own documented convention for a historical period with no data, not an ingest error)HEADwithPrefer: count=exact→content-range: 0-196769/196770— 196,770, exactGET /atlas_inventory?endpoint=eq.indicators__ssb_12451→row_count=196770, is_empty=false- Spot-checked Oslo/Eigersund/Utsira's
Sykefraversprosentvalues directly against this plan's own Phase 1 live-captured figures — exact match, same source, no republish happened in between.
All match imac's report exactly. No regression on nav-aap, husbanken-bostotte, or udir-gsi.
Acceptance Criteria
- The real kommune-level mechanism is identified and verified live — SSB table 12451 via
lib/pxweb.ts, not a NAV Excel scrape. NAV's own sykefravær/sykepenger pages confirmed to have no kommune-level table at all. - Licence independently confirmed for this specific candidate — NLOD (SSB's own,
confirmed via the table's own metadata and this repo's unbroken
ssb-*precedent), not CC BY 4.0 inherited from the NAV-family assumption. - The source ships as
ssb-12451(per [Q1]), notnav-sykefravaer— carried through Phase 2 exactly as decided, including the folder name, source_id, dbt model names, and Dagster registration inraw_ssb.py'sSSB_SOURCESlist. -
ssb-12451ingests cleanly with zero rows silently dropped —Kjonn=0,Sykefraversprosent/Sykefraversdagsverkonly (per [Q2]/[Q3]), 196,770 rows, confirmed live and against a real local Postgres. -
indicators__ssb_12451andmart_indicators__ssb_12451build and test clean against real loaded data (13/13 PASS), with every sentinel region code — including the one unanticipated shape,0716u— resolving throughclassify_region_codeexactly as predicted. -
ssb-12451appears inmeta_sources.served_asafter a real deploy, independently verified via livecurl. - The investigation and
1PRIORITY.mdare updated to mark this candidate shipped, with the "nav-sykefravaer" framing corrected to namessb-12451throughout.
Implementation Notes
- This is not a new mechanism — it is the simplest Phase 2 of any source this session.
lib/pxweb.tsandparseJsonStat2already exist, are already proven, and need zero changes. The entire Phase 2 is: writemanifest.yml, write a thinindex.tsmatchingssb-12944's shape with this table's own filters, write the migration, register with Dagster, write tests against a real captured fixture. No discovery-tier HTML scraping, no sheet-shape quirks, no suppression-marker handling (none observed), no new sentinel shape. - Do not build this as
nav-sykefravaer. See [Q1]. If something about Phase 2 reveals a reason the NAV-family folder convention should apply after all, name that reason explicitly rather than defaulting back to it for consistency with the other two NAV sources alone. - The 800,000-cell request limit is real and this table trips it unfiltered. 2,655,315 cells
for the full cartesian product. Scope
ContentsCode/Kjonnper [Q2]/[Q3] before writing the first live query inindex.ts, not after acurlto production returns an error. - SSB's own metadata already documents two data-quality caveats for this table — a trend
break and a since-corrected assignment error ([Q7]). Carry them into
methodology_notesverbatim; this is upstream's own account of its data, not something Atlas is discovering or correcting.
Outcome
Shipped end to end, 2026-10-02: ingest (196,770 rows, zero dropped) → dbt staging and api_v1
publication → live cluster deploy → independently verified arrival. Implements what the
investigation originally called "nav-sykefravaer" — the real mechanism turned out to be SSB's
existing lib/pxweb.ts client, not a new NAV-family Excel parser, because NAV's own sykefravær
statistics pages publish no kommune-level table at all. This was the simplest Phase 2 of any
source shipped this session: no discovery-tier HTML scraping, no sheet-shape quirks, no
suppression-marker handling, and the ingest passed clean on the first dbt build with zero
relationship-test failures — unlike every NAV-Excel source this session.
One genuinely new finding surfaced anyway, caught by querying the built table rather than assumed
from metadata: a single region code, 0716u ("Våle (-2001)"), is SSB's own disambiguation suffix
for a historical kommune whose numeric code was later reused by a different kommune (0716 is Re,
2002-2019). Not digit-only, so it correctly fell through classify_region_code's existing
branches to unknown rather than needing a new one — confirmed directly against the built table,
not assumed to work.
Also applied, not just inherited: ingest:ssb-12451 was added and verified via the real npm run
invocation before any other Phase 2 work began, for the third source running after
husbanken-bostotte's urb-agents#1807 finding.
Plugs the largest-population welfare-claim signal in the NAV-adjacent triad — sykefravær covers
everyone with a physician-certified sick-leave spell, a much broader population than
nav-uforetrygd's long-tail disability outcome or nav-aap's transitional work-assessment
benefit — completing Report #4 (Mental-Health Triangulation) and Report #5 (Income & Welfare
Trajectory)'s registry-side welfare signal.
Files to Modify
atlas-data/ingest/src/sources/ssb-12451/manifest.yml(new — per [Q1], notnav-sykefravaer/)atlas-data/ingest/src/sources/ssb-12451/index.ts(new)atlas-data/ingest/src/sources/ssb-12451/README.md(new — no__tests__/: checked while implementing, and nossb-*PXWebAPI source in this repo has per-source tests,lib/pxweb.tsincluded; followed that unbroken precedent rather than inventing one)atlas-data/ingest/package.json(ingest:ssb-12451script, added first, verified viacheck-every-source-has-an-ingest-script.sh)atlas-data/migrations/062_raw_ssb_12451.sql(new)atlas-data/dagster/atlas_data/assets/raw_ssb.py(SSB_SOURCESlist — existing weekly job, no new Dagster infrastructure)atlas-data/dbt/models/indicators/sources.yml,indicators__ssb_12451.sql(new),schema.ymlatlas-data/dbt/models/marts/api/mart_indicators__ssb_12451.sql(new),schema.ymlatlas-data/dbt/models/marts/api/mart_atlas_inventory.sql(depends_on + counted list)atlas-data/dbt/scripts/generate_api_v1.py(SCHEMA_COMMENT'sssblisting — bump the count)atlas-data/template-info.yaml,website/docs/developers/index.md(regenerated counts)website/docs/ai-developer/plans/backlog/INVESTIGATE-new-norwegian-public-sources.md(mark shipped; correct the "nav-sykefravaer" framing to namessb-12451, per [Q1])website/docs/ai-developer/plans/backlog/1PRIORITY.md(mark shipped)