Skip to main content

Atlas Data

Atlas Data logo
Atlas Datavv20261003-37769d0

Norway's civil-society sector as a REST API — municipality-level indicators of humanitarian need, and the NGOs and local chapters that respond to them.

Installs the Atlas data platform as one application: a PostgreSQL database with its migrations, a Dagster code location running the ingest and dbt transforms, and a PostgREST API over the curated api_v1 views.

The data is an open semantic layer over Norwegian public sources — SSB statistics, FHI public-health and living-conditions indicators, Bufdir child-poverty figures and the Brønnøysund business register — joined to NGO supply data, so that humanitarian need and the organisations responding to it can be queried together at kommune level.

The API is self-describing: meta_sources, meta_endpoints and meta_dimensions let a consumer discover what is available without out-of-band documentation.

A fresh install serves an empty API until the first pipeline run — the schema and grants exist from install, the data arrives on the ingest schedule. Exports api-url for other applications to consume.

Installing does not start anything: the schedules and sensors ship stopped, so no data is fetched until an operator turns them on. That is a deliberate go-live decision, not a side effect of installing. Nothing is ever backfilled — but the timing differs between the two halves and only one of them waits: the schedules wait for their next scheduled time, while the automation sensor that drives the ~40 ingest sources can begin fetching within about a minute of being enabled. Launch the first-data jobs BEFORE enabling automation.

Install:uis template install atlas
Maintainers: terchristerchris

Template README​

Atlas serves Norway's civil-society sector as a REST API — municipality-level indicators of humanitarian need, and the NGOs and local chapters that respond to them.

The data is an open semantic layer over Norwegian public sources: SSB statistics, FHI public-health and living-conditions indicators, Bufdir child-poverty figures and the Brønnøysund business register, joined to NGO supply data, so that need and the organisations responding to it can be queried together at kommune level.

Installing it with UIS brings up the whole application under one name:

  • a PostgreSQL database with its migrations applied,
  • a Dagster code location for the ingestion pipelines,
  • a PostgREST API over the resulting api_v1 views,
  • and an exported api-url other applications can consume.

What Atlas holds​

The catalogue used to describe how Atlas refreshes and not one word about what it refreshes. The pinned definition now carries a data: block:

sources55
publishers10 — Folkehelseinstituttet (21), Statistisk sentralbyrå (18), Utdanningsdirektoratet (4), Brønnøysundregistrene (3), Arbeids- og velferdsdirektoratet (3), Barne-, ungdoms- og familiedirektoratet (2), Husbanken (1), Integrerings- og mangfoldsdirektoratet (1), Lotteri- og stiftelsestilsynet (1), Norges Røde Kors (1)
public relations92
licenceNLOD for 51 of 55 — the exceptions are nav-aap, nav-helt-ledige, nav-uforetrygd (all CC BY 4.0) and redcross-branches (permissive, Red Cross's own data rather than the state's)

Norwegian public data at kommune level, joined into one semantic layer.

🔵 The live API is the authority, not these counts. They are regenerated from the repository with a CI check that fails on drift, so they cannot silently rot against the code — but they describe the build, and what is served is a separate question:

GET /meta_endpoints what is queryable
GET /meta_sources every upstream, its freshness, and served_as — the
relations it actually reaches (empty = nothing
published depends on it)
GET /meta_dimensions what each coded column MEANS — read it before
interpreting a code
GET /indicator_summary every published series

⚠️ A count here has exceeded what was actually served. On 2026-09-21, at pin v20260921-2d88589, the block said public_relations: 19 while the live API was reported serving 16 — three relations 404ing during an unrelated transform_and_publish, not caused by that tag. Whether that is still true is not something this page can know: it is rendered from a published artifact, not from a running install, and this repository cannot reach the API to check. Ask the endpoints above.

🔴 Pinning is not deploying​

A pin moves what an install would fetch. It does not move data. The image carries the dbt models for the newest sources; their rows arrive on a separate transform_and_publish. Pin and run are two actions, and this catalogue only performs the first.

Install​

uis template install atlas

⚠️ A fresh install serves an empty API until the first pipeline run​

This is correct behaviour, not a broken install. The schema and grants exist from install; the data arrives on the first pipeline run, which an operator has to start — see the next section. The API answers and returns zero rows until then.

Stated here because it is the state most likely to be read as failure by someone installing for the first time.

The API is also self-describing — meta_sources, meta_endpoints and meta_dimensions let a consumer discover what is available without any out-of-band documentation.

What happens after you install​

Installing starts nothing. The Dagster schedules and sensors ship stopped. No data is fetched, and no external service is contacted, until an operator turns them on. Turning them on is the go-live decision.

🔴 Unresolved as of v20261002-e9e33a5: this sentence may no longer describe every install. The artifact's own operational.automation still says "ships stopped", quoted faithfully above — but urb-agents #1794 measured all three schedules and all three sensors come up RUNNING on a real redeploy of this image, with the automation sensor then firing a real, unprompted run (dagster/from_automation_condition: true) about 15 minutes later. Checked atlas's own uis/ definition for an install-time "stop" hook — none exists; uis/init/001_bootstrap.sql is one line of schema creation and nothing else. Two explanations fit what was measured: either UIS's own install flow (not exercised by that redeploy, which only swapped a running pod's image) stops things after a fresh uis template install and this sentence is still correct for that path; or atlas-data's default_status=RUNNING change (PR #482) means a genuinely fresh install now starts itself too, and this sentence is stale. Not settled here — raised with atlas on the nomination that landed this pin. If you install fresh and the schedules come up running on their own, that is this discrepancy, not a bug in your install.

⚠️ Turning them on never backfills — no history is replayed, ever. But the timing differs between the two halves, and only one of them waits:

what you enablewhen it starts
the schedules — transform_and_publish at 05:00, brreg_transform at :10/:40at their next scheduled time
the automation sensor, which drives all ~40 ingest sourceswithin about a minute

The sensor does not wait for a cron. It evaluates on its own tick: an asset becomes eligible when its cron tick has passed and its dependencies updated since that tick — and on a fresh install every tick is already in the past and nothing has run, so both are trivially true. Enabling at 16:55 on a Friday starts work at 16:55.

Concurrency is bounded by ATLAS_MAX_CONCURRENT_INGESTS, so it is a bounded start rather than 40 simultaneous writers.

⚠️ When those six jobs finish, the install is LOADED but NOT VALIDATED​

Finishing the first-data sequence means the data is in. It does not mean the data has been checked. Run the checks before treating the install as good.

✅ The installer now says this too, and that condition is met. As of the pinned build the sentence appears in the install summary itself — between the endpoints block and the load-data instructions, no scrolling, no template info needed. It took four mechanisms and three premature declarations of "delivered" to get one sentence onto a screen, including a truncation this page once described that did not exist.

🔵 Kept anyway, as corroboration rather than as the only source. One operator has seen that line on one host. This box costs a reader nothing and stops costing the moment a second person confirms it — so it stands until then, not because the condition failed.

Running them costs 6–15 minutes and looks like a hang. All 685 can be run on demand; uis dagster run transform_checks works, but measured launches took 364 s and 885 s just to start. The run sits NOT_STARTED for that whole time and then runs.

🔴 That is slow, not stuck — and re-launching because it looks hung is how you get duplicate runs. Wait it out.

⚠️ On a busy cluster it can genuinely time out, and it will not look like a timeout: the 885 s measurement sits against a 900 s ceiling, so a slower start presents as an image-pull or scheduling failure rather than as what it is. A later UIS raises the ceiling.

🔴 On an off-catalogue host, uis template info describes the wrong software​

info renders the definition the catalogue pins, not the one your host installed. If you installed a specific version — uis template install atlas --version …@sha256:… — then info prints a complete, well-formed, authoritative description of a different build.

It even says so and does not connect the two: the off-catalogue warning prints, and the operational block on the very next line comes from the other version, silently. Everything in it is affected — deploys, automation, the first-data list, the cadence table.

⚠️ This page has the same limitation, for the same reason. It mirrors the artifact at the pinned tag. If you are running something else, this page describes the catalogue's build, not yours. Read your own definition — pull the artifact at the digest you installed.

🔵 Why this bites the person it bites: anyone installing off-catalogue is by definition ahead of the pin, which is exactly when they most need the description to be of what they installed.

⚠️ And it disappears when you fix the pin. Once the catalogue points at what the host is running, installed and catalogue agree and the symptom is gone — so re-running the test afterwards shows correct output and invites the conclusion that something merged fixed it. A defect that vanishes when the thing it affects is corrected gets closed as fixed every time anyone looks. It was caught here only because a blob had been pulled and kept while the two disagreed.

🔴 Launch the first-data jobs BEFORE enabling automation. Enable first and the sensor decides the order: brreg_change_feed will start, find no watermark, and fail loudly until brreg_bootstrap has run. That is by design rather than a fault — but it is noise nobody needs, and it is avoidable by doing the two in the right order.

First ingest loads roughly 4.1 million rows across 64 raw BASE TABLEs and 94 marts BASE TABLEs (plus 80 marts views), from 55 sources. That is imac's measured 2,906,194 plus the 1,173,878-record Enhetsregisteret bulk load, so the row count is arithmetic on two measured figures. The combined wall time is measured — ~30 minutes, 1772 s end to end — and is stated once, in the cold-install table below, rather than repeated here.

A "table" here means a BASE TABLE; views are excluded and counted separately. That rule is stated because two figures in the artifact used to disagree with each other and with the database while no counting method was written down anywhere.

Once schedules are on, Atlas polls on this cadence (Europe/Oslo) — mirroring operational.cadence in the artifact at pin v20261003-37769d0:

Every row names the job that owns it, and that is not decoration — see the warning below the table.

whenjobwhat
Sunday 02:00annual_sources_refresh~42 annual public-sector sources — SSB, FHI, Bufdir, Husbanken
1st of month, 01:00klass_refreshSSB Klass classifications (kommune/fylke)
1st of month, 01:00monthly_sources_refreshSources whose manifest declares periodicity P1M — nav-uforetrygd, nav-aap, nav-helt-ledige. Same monthly cron as klass_refresh; a separate job because this data is genuinely monthly, not annual data polled monthly. New since v20260922-d077fd7 — not previously on this page.

🔴 The artifact contradicts itself about this job, and this page has to pick a side. first_data.jobs lists monthly_sources_refresh third, between klass_refresh and seed_sources_refresh — that array is what the install logic actually reads. But this row's own what text ends: "Not in first_data.jobs — adding it there would change which jobs the measured ~30-minute cold-install figure covers, and that figure has not been re-measured with this source included." The array says it is there. The prose about the same job says it is not. Both verified by parsing the artifact, not by reading one and assuming the other agrees.

This page follows the array — first_data.jobs is what the installer executes, the cadence.what text is description — which is why the seven-job table above includes it and the 1772 s floor caveat below applies to it. If the prose is the one that's right, the floor caveat is misdirected at a job that shouldn't be timed as part of a cold install at all; if the array is right, the prose is simply stale. Raised with atlas on urb-agents#1815/#1814 rather than guessed at. | Every 30 min (:00, :30) | brreg_change_feed | Brønnøysundregistrene change feed — only what moved, ~114 records per cycle at the measured 3.8 changes a minute. Appends to raw only; rebuilds nothing and cannot affect the public API | | Every 30 min (:10, :40) | brreg_transform | Brreg reconciliation into marts — ONE incremental model, ten minutes behind each poll. Rows are inserted and deleted, never the table replaced, so no api_v1 view is disturbed | | Daily 04:00 | brreg_change_feed | Frivillighetsregisteret — a full re-walk of ~72,800 organisations (~727 requests) | | Daily 05:00 | transform_and_publish | dbt transform and publish — no external calls. ⚠️ The only scheduled job that rebuilds marts, and therefore the only one during which api_v1 views are recreated — each restored immediately after its own mart, so the gap is milliseconds per view |

🔴 Two rows, one job — and reading them as one thing has already cost real time. The 04:00 Frivillighetsregisteret row and the half-hourly feed row are the same job, brreg_change_feed, with different sources and different automation conditions. Two agents independently read the Brreg rows as a single job that both polled every half hour and rebuilt marts, and one of them costed a public-API outage at 48× its real rate and nearly weighed a cadence rollback against it (urb-agents #780, #786).

The ten-minute offset is deliberate. The reconciliation runs at :10/:40 rather than alongside the feed because firing on the same tick would reconcile data the feed had not yet written, leaving the dimension permanently one cycle behind.

⚠️ Frivillighetsregisteret is daily and did not move to the feed's cadence. It has no change feed, so every refresh is the whole register — ~727 requests. At half-hourly that would be ~35,000 requests a day against a public-sector API, a 48× increase on someone else's service. The two Brreg ingests are on separate schedules for that reason and must stay that way.

The external services Atlas calls are SSB (Statistics Norway), FHI (Folkehelseinstituttet), Bufdir and Brønnøysundregistrene. All are public-sector APIs. The cadence is deliberately conservative — an annual statistical table is polled weekly, not nightly, because fetching annual tables every night would be roughly 15,000 pointless requests a year against services Atlas depends on staying welcome at.

One source is deliberately unscheduled (redcross-branches) pending a credential. It never self-triggers; invoking it by hand on a cluster without the private data repo fails, and that is correct. ⚠️ frr no longer appears in the artifact's unscheduled list at all — it was named here through every pin since at least d077fd7; checked directly in this bump and it is simply absent now, not moved elsewhere. Not investigated further — flagging the removal rather than guessing why.

⚠️ The cadence lives in the image, not in this entry. It changes when the pinned tag changes, with nothing here to review. If that matters to you, diff cadence.py between tags.

Getting data on day one​

Enabling the schedules does not backfill. Every schedule is on_cron, which means next fire — so a Thursday install waits until Sunday 02:00 for raw data, and until the 1st for the monthly sources. The API stays empty in the meantime, with nothing to explain why.

To get data immediately, launch these seven jobs from the Dagster UI, serially, in exactly this order:

orderjob~time
1annual_sources_refresh474 s (7.9 min)
2klass_refresh1.0 min
3monthly_sources_refreshnot re-measured — see below
4seed_sources_refresh0.8 min
5brreg_bootstrap501 s (8.4 min)
6brreg_change_feednot stated
7transform_and_publish484 s (8.1 min)

A cold install is ~30 minutes — 1772 s wall, measured end to end on a factory-reset cluster (urb-agents #1027). brreg_bootstrap is no longer the unmeasured part.

⚠️ Seven jobs, not six, as of monthly_sources_refresh (added 2026-10-01 for nav-uforetrygd, and now also covering nav-aap and nav-helt-ledige) — and the 1772 s total has NOT been re-measured with it included. The artifact's own words: "nav-uforetrygd is one xlsx download and parse, so the real addition to wall time is expected to be small, but that is an expectation, not a measurement." Treat 1772 s as a floor, not the figure, until someone re-times a cold install.

⚠️ Rows 1, 5 and 7 are from the cold-install measurement; rows 2, 4 and 6 are carried from the earlier four-job run and the artifact does not restate them; row 3 has no measurement at all yet. They are left as they were rather than rescaled, because a number invented to make a table look consistent is worse than a number from a different run that says so. The measured jobs do not sum to the total — the jobs vary far more than the total suggests, which is the point of stating the total separately.

⚠️ An earlier figure of ~11 minutes is still in circulation and understates a cold install by nearly 3x. It was the measurement of the first four jobs (#507: 11.1 min, 2,906,194 rows) and it predates brreg_bootstrap entirely. A run that looks stalled at 8 minutes may be normal — run the check command rather than waiting, since it reports what has been pulled against what has been applied.

The order is not arbitrary, for two separate reasons:

  • seed_sources_refresh contains raw/_migrations and runs fourth, so the migrations apply after three source jobs have already written. That is safe because they are idempotent, but reordering has not been tested.
  • brreg_change_feed runs immediately after brreg_bootstrap because the bootstrap seeds the feed's watermark from the snapshot's own date. The feed has nothing to start from until the bootstrap has run — and run first, it fails loudly rather than silently walking history: it refuses to start without a watermark.

Together these produce ~4.1M rows across 64 raw and 94 marts BASE TABLEs (plus 80 marts views) from 55 sources — the 1,173,878-record Enhetsregisteret bulk load on top of imac's measured 2,906,194.

🔴 The raw BASE TABLE figure is hand-maintained, not generated, and had drifted stale before this bump. uis/generate-holdings.py writes the marts/views figures; it has no substitution for the raw count (confirmed 2026-10-03 — the script does not mention "raw" at all), so a human types it. It read (56) while the true count was 59, before nav-helt-ledige's migration made it 60 — caught here, not by the generator. CI (render-template-info.sh) checks it against atlas-data/migrations directly and would fail a wrong number, but fixing it is still manual until a substitution exists.

✅ RESOLVED in v20260921-a8d5d1a, at the cause rather than the symptom. Through 1a569dc this field contradicted itself three ways: headline 81 marts against a counting rule reading "46 + 16 = 62"; "eight models land in marts as views" one clause after the 10 it was explaining; and a closing "match imac's live measurement exactly (61 / 5 / 66 / 52)". It now reads models=63 plus seeds=18 — which is 81, agreeing with its own headline — views=10 named once, and the exact-match claim retired with a sentence giving the reason: "a stale claim of exact agreement is worse than no claim, because it invites the reader to stop checking." Every figure in the field is now written by uis/generate-holdings.py, none by hand.

🔴 That fix also corrected something THIS page was repeating. This page said the figures were "derived and gated by uis/render-template-info.sh". The artifact now states that credit was wrong — the generator is uis/generate-holdings.py — and the wrong name was on this page because it had been quoted faithfully from the field it described. A quotation inherits the errors of its source, and nothing available here could have caught it: verifying that a page matches an artifact says nothing about whether the artifact is right about itself.

🔴 This page said 61 and 5 for three pins, and the caveat that stood here misquoted the artifact to do it. It attributed "61 marts BASE TABLEs (plus 6 marts views)" and "five" to install.first_load — strings that appear in none of 2d88589, fcf78e6 or 1a569dc. The headline has read 80, now 81, and 10 since at least 2d88589. What this page had latched onto was the artifact's own stale closing line, 61 / 5 / 66 / 52: a caveat written to flag a numeric disagreement was quoting the wrong side of it. ⚠️ Found by parsing the field, not by grepping the file — these are folded YAML scalars, so a phrase search across raw lines matches nothing and reads exactly like absence.

✅ The two FIELDS in the artifact do agree, which is the part that used to be broken. Earlier pins had install.first_load saying 60 marts while first_data.takes said 64, and this page used 64 with a note saying why. Both now say 81, and both moved together from 80 in fcf78e6 — so the cross-field disagreement really is fixed. The disagreement inside one of those fields outlived the one between them, and is resolved separately above. This box used to add "matching imac's live measurement exactly"; that phrase was the artifact's own closing line — the one naming 61 / 5 / 66 / 52 — so repeating it here was repeating the contradiction rather than reporting it.

⚠️ Do not read "~11 minutes" as the total. That figure is imac's measurement of the four jobs that existed when it was taken — annual_sources_refresh, klass_refresh, seed_sources_refresh and transform_and_publish — on a clean cluster (10.8 min, 2,906,194 rows, zero failures; urb-agents #507, #520). It predates both Brreg jobs. ⚠️ This paragraph used to end by calling the combined wall time "unknown rather than estimated", fifty lines below a table stating it as 1772 s measured end to end. Both sentences were on this page at once for several pins. The total is measured — ~30 minutes — and brreg_bootstrap is 501 s of it; what remains unmeasured is narrower than the old sentence claimed: the bootstrap's write time separately from its 210 MB / 52 s download, and brreg_change_feed, which the artifact still does not state.

(Stated by naming the four jobs rather than "the first four", because that phrase silently stopped being true when the list grew from four to six.)

🔴 brreg_bootstrap is the one most likely to be skipped​

It has no schedule and no automation condition, on purpose. Re-running a 1.17M-record bulk load against a populated database is the only genuinely destructive-looking operation in this pipeline, so nothing self-triggers it. Run it once, here.

  • Skipping it leaves the organisation register empty, with nothing saying why.
  • Running it twice is safe — it upserts and never truncates — but pointless.

It is deliberately not in the unscheduled list further down: that list means cannot run. brreg_bootstrap is the opposite — it must run once, on day one, and then be left alone.

Maintainer note — do not trim the paragraph above as duplication. It restates, in this page's own words, why nothing self-triggers brreg_bootstrap. That looks redundant with the artifact's first_data.how, and the redundancy is deliberate: first_data.how does not render at the end of an install — only uis template info shows it — so an operator about to run the chain may see this page's reason and nowhere else's (urb-agents #802).

This is the opposite call to the troubleshooting block, which this page deliberately does not copy, and the difference is rot. A copied remedy goes stale silently when its source changes; a sentence in this page's own voice about a property that does not change — a 1.17M-record bulk load must not self-trigger — stays true whether or not the artifact still says it. Copy nothing that has a current source; keep your own words for what would otherwise have none.

brreg_change_feed is different again — run it once here, then leave it to its schedule​

Unlike the bootstrap, the change feed is scheduled: once automation is on it runs daily at 04:00. You launch it by hand exactly once, in the sequence above, because a fresh install has no data for the first nightly run to apply a delta to.

So the three kinds of job on this page are not interchangeable:

runs by itself?run by hand on day one?
the five source/transform jobsyes, on their cronsyes
brreg_bootstrapnever — no schedule, no automation conditionyes, exactly once
brreg_change_feedyes, nightly at 04:00yes, once, after the bootstrap
redcross-branchesno — parked, cannot runno

⚠️ This list mirrors operational.first_data in the artifact at pin v20261003-37769d0. It is duplicated here, by hand, because as of that pin uis template info renders none of the artifact's operational block, so this page is the only place an operator can read it. It is therefore capable of going stale on the next bump — the artifact is the source of truth. Generating this section from the artifact at authoring time is the intended fix; see PLAN-application-catalogue.md.

If a run fails, the artifact tells you what to do​

The install definition carries a troubleshooting block, and UIS renders it at install as of 1.6.67 — so the remedy reaches the operator rather than living only on this page. It covers four failure modes that have actually happened:

  • brreg_bootstrap reporting success while writing nothing — re-run to repair the register, it and the transform both report success and the API still serves the old data;
  • the bootstrap terminating part-way through the download, and retrying getting less far each time rather than more;
  • a transform failing with a dbt schema error naming dim_brreg_enhet, after an upgrade adds a column to an incrementally-materialised model; and
  • the public API returning 404 even after the database has been repaired and the view confirmed to exist with rows and grants.

⚠️ One place where this page and the artifact currently disagree, and this page is the correct one. Both described the schema-change failure as "the source and target schemas on this incremental model are out of sync". imac ran the upgrade and the model's own watermark subquery hits the missing column before dbt's schema-change handling runs, so dbt never emits that message. This page now quotes what is actually printed; the artifact — and therefore what uis template info renders — still carries the older wording until atlas corrects it. If the product shows you the other sentence, the remedy is the same one.

Otherwise the authoritative text is the artifact's, not this page's. It is deliberately not copied here: it is long, it is precise about which database user must run the rebuild and about the ordering that people miss, and a stale copy of a 02:00 remedy is worse than a pointer to a current one. Read it from uis template info atlas, or from the definition at the pinned digest.

Installing alongside an atlas that is already running​

If a tenant is already deployed, installing under the same name configures the same database and overwrites the running code location. To bring up a parallel tenant instead, pass a different application name:

uis template install atlas --param app_name=atlas-t

app_name isolates everything — database, secret prefix, url prefix and code location — so an existing tenant is untouched. Verified by imac on urb-agents #481.

Upgrading an existing install​

An upgrade happens when this catalogue entry's pin moves and you re-install. What that costs you depends on the release, and it is not always nothing.

✅ What the recent pins add, and the one gap left​

meta_sources.served_as (from v20260921-1a569dc) — the first published way to ask what does this source actually reach? An empty array means nothing published depends on that source. It is named in data.discover as of v20260921-a8d5d1a, so this page no longer hand-carries it: the endpoint list above is the artifact's own. ✅ This block previously warned that served_as appeared nowhere in the install definition and would need re-checking. That was raised on urb-agents#1352 and the artifact now documents it — the warning is retired because the condition was met, not because it aged.

🔴 18 of 26 dead series recovered (v20260921-a8d5d1a). Series reporting kommuner_with_value = 0 fell 26 → 8. The ssb-crime-tables series moved from latest_year 2025 → 2014 with coverage 0 → 64–83 kommuner — and the reason is the useful part: SSB table 08487 carries 16 LovbruddKrim codes, 7 current and 9 legacy, and the legacy vocabulary stopped in 2013–14. Those series were never empty. They were being reported at a year they had stopped covering, which reads identically to missing data from the outside. Verified at SSB by ops-dev.

served_as is now computed from the corrected lineage seed, which moved unattributed_totals from 1 source to 32; served_as empty is still exactly 5. A downstream consumer deleted a hand-maintained list that had been wrong twice in five hours.

Earlier: 783,104 rows recovered in ssb-06913 from a regex that never matched, and an embedding check kept as a standing invariant.

✅ ssb-06913 IS CLOSED at v20260921-a657f4f — all eight series at 357. The seven that returned nothing now report latest_year 2025 with 357 of 357 kommuner, Folkemengde holds at 2026, and indicator_missing_kommuner for the source went 2,499 → 0. Those 2,499 values were in the fact table the whole time and are now served. Spot cells were predicted from SSB before the run and matched to the digit — 0301 Dode 2025 = 4 033, 1103 = 940, 5001 = 1 329, 0301 Folkemengde 2026 = 728 714 — and acceptance was measured twice against two different dim_kommune versions (1,158 codes and 1,170), identical both times. One FK check cleared, none new, reproduced by an independent sensor run.

🔵 Worth keeping: this page held the question open rather than adopting the plausible answer. The February/flow-series explanation for those seven series was consistent with everything observable from outside, and was not the cause. The cause was that latest_year did not restrict to the kommuner a relation serves, while SSB zero-fills municipalities dissolved in 1957–59 — Hopen (1915-1959) reporting 0 deaths in 2026, and a zero is not a null, held the whole source at 2026.

⚠️ ONE GAP REMAINS: ssb-12063. It is now the only series left at kommuner_with_value = 0 (down from 8). KOSfritidredleie0000 returns nothing while SSB holds 1,812 values across 2015–2018 — 408 kommuner in 2018 alone — none of which reached the fact table. A different defect from the one just fixed, and not explained by it. Open with atlas on urb-agents#1351.

🔴 v20260922-d077fd7: a missing value is not a zero, and reading it as one moves help away from small places​

This is the most consequential thing on this page and it is not about the catalogue. FHI suppresses small counts: it hides a cell under roughly 6 cases, and hides the whole series once more than 20% of its cells would be suppressed — explicitly so that a partial series cannot give a skewed impression of that kommune.

🔴 So a consumer that reads a gap as "no need here" produces exactly the skew the suppression exists to prevent — and it does so in the direction that moves help away from the smallest places. A need index built on those series cannot tell suppressed from zero unless something tells it, and the demo consumer said it could not.

Both surfaces now say so:

fieldbeforeafter
indicator_summary.properties.kommuner_with_null.description145 chars1,788 — SUPPRESSION present
indicator_missing_kommuner.description714 chars1,357 — LOW NEED present

⚠️ And the limit that stops the rule being misapplied: SSB does NOT suppress — it ZERO-FILLS. Same column, two publishers, opposite conventions, and neither reading transfers. A zero from SSB is a measurement; a gap from FHI is a withheld measurement; and the page that already says "a zero is not a null" about latest_year is making the same distinction from the other direction. Check which publisher a series comes from before interpreting an absence — /meta_sources names it.

✅ v20260922-d077fd7 also landed a release that had never been deployed​

One publish_api_v1 covered two releases: d077fd7 itself and 3072f1f, which had been built and never deployed. That second one is why both defects this page reported in operational.upgrade are already fixed here — the dangling operational.crons pointer now reads operational.cadence below, and transform_checks is declared under a new operational.sensor_triggered with an after: and a why: instead of appearing only in prose. 🔵 Verified by parsing this artifact, not by reading the claim: operational.crons appears nowhere in it and no crons key exists.

operational.sensor_triggered also states the chain a transform actually sets off, which this page had only implied: publish_api_v1 after transform_and_publish ("dbt drops the api_v1 views by CASCADE when it swaps the marts tables, so they exist again only after this runs"), then api_v1_checks, then transform_checks — the last with the fact worth knowing on its own: transform_and_publish deliberately excludes tests, so a green transform contains none.

🔴 v20260922-6cb28f4: the OpenAPI now warns you about count=exact before you use it​

The field docs for brreg_enhet grew from 958 to 1,730 characters, and the 772 added characters are a latency warning that had existed only in bus messages and a website page:

beforeafter
definitions.brreg_enhet.description958 chars1,730
count=exact, Prefer, count=planned, CORSall absentall present
  • A filtered count=exact takes 12–33 s, against sub-second without the header.
  • ⚠️ Inside a browser fetch that is a timeout, not a slow answer — in the consumer's own words, a request that never completes is indistinguishable from a blocked one, so this reads as CORS or a dead endpoint and gets debugged as the wrong problem.
  • count=planned is 50× faster and was 6× wrong — 118 against an actual 716.

🔵 This is the one pin so far whose entire deliverable lives in the database, which is why it needed publish_api_v1 and no transform and no checks. It is the cheap upgrade path this page has described since fcf78e6, being the whole of a release rather than a footnote to one.

🔴 RETRACTED: the "49–88 s historical band" this page quoted for publish_api_v1 is withdrawn, and no band replaces it. The figure was inherited through nominations and nobody recorded which clock it measured. There are two, and they disagree by 20–40% on every run:

runtool clockrun-storage clock
6cb28f472.9 s103.8 s
d077fd7107.4 s150.3 s

⚠️ So the band cannot be checked, only chosen. If it was tool-clock, 6cb28f4 was inside it; if storage-clock, 6cb28f4 was already over and this page said it was inside. ops-dev retired the number rather than defend it (urb-agents#1382) and asked that this page drop it, which it has. 🔵 A duration with no named clock is the same class of claim as a measurement with no named build — and this page has now shipped one of each.

✅ v20260922-ff164d5: published ranges now match the rows they sit beside​

min_value and max_value were computed over every kommune the fact table holds, while the counts printed next to them were scoped to active non-sentinel kommuner. So 33 of 222 series published a range over kommuner the relation does not serve — a number that looked like data and described a different population from the one it was labelled with.

before 00:43Zafter 00:57Z
series where published min/max disagreed with the rows330
seriesbeforeafter
ssb-06913 Folkemengde0 .. 728714219 .. 728714
ssb-06913 Dode0 .. 40332 .. 4033
ssb-09429 PersonerProsent_010 .. 49.115.1 .. 49.1
ssb-crime-tables 0__AnmLovbrPer10003.9 .. 2365.4 .. 57.3

Measured from outside by paging all 79,254 fact rows with the count asserted, folding per series and comparing against all 222 — and independently on the host by the operator.

🔵 The two things that could have gone wrong and did not, which are the interesting part:

  • ssb-06913 Levende still has min = 0, max 9287, 357 kommuner. A fix that excluded zeros along with sentinels would have produced a tidier table, passed a casual read, and silently deleted the fact that a kommune had no live births. The consumer checked Levende before flagging every zero, and that restraint became the acceptance test.
  • ssb-12063 KOSfritidredleie0000 keeps its row, with min NULL, max NULL and count 0 — present and empty, not dropped. An absent row and an empty row answer different questions.

⚠️ And "the other 189 series are unchanged" here is a COMPARISON, not an absence of noticing. All 222 were snapshotted before the deploy and diffed numerically after, so 13 and 13.0 could not read as a change. That distinction is why the claim is repeated on this page at all.

✅ v20260922-6cb28f4 was measured the same way, and more strictly: the 222-series snapshot taken before the job was diffed byte for byte against the one after — all 222 rows identical, "not a value, not a count, not a year." 🔵 A snapshot taken before is a different class of evidence from a spot-check taken after, and it is the difference between "nothing moved" and "I did not see anything move".

⚠️ Two things this pin must NOT be read as fixing​

1. #394 is not observable on the deployed host, and this page does not assert its effect. 89a774e is titled for the six two-digit codes claiming to be fylker and is in this range — but after install, klass_refresh and transform, those six still violate with exactly 3,648 rows: 21 22 23 25 26 88. A re-ingest cannot help: raw.ssb_klass_fylker carries 01..20, 30..56 and 99, and none of the six. 🔵 That is not a claim the commit is broken — only that its stated effect is not visible where it was looked for, which is a different statement and the one the evidence supports.

2. 9999 "Uoppgitt" is an active kommune, and it is NOT a regression in this release. A code whose name means unspecified carries is_active = true, because both raw rows have NULL validity and an open end reads as current. Norway has 357; any denominator of "active kommuner" is 358. It was reported as introduced by the Klass re-ingest and it was not — the pre-rebuild measurement settles it:

whendim_kommuneis_active = true
22:25Z, before the rebuild1,158 codes358
00:30Z, after it1,170 codes358, unchanged

So the twelve new codes arrived inactive (1,170 = 358 + 812) and 9999 was active beforehand. ✅ A pre-existing defect found while verifying a release is not a defect in it, and that distinction is the difference between pinning this and holding it. It does not touch the numbers above: indicator_summary filters not f.kommune_is_sentinel on both counts, so the sentinel cannot reach kommuner_with_value or kommuner_with_null. Both with atlas.

Nothing regressed: ssb-crime-tables 18 series at 2014 and 14 at 2025, zero empty, coverage 64–83; kommune_befolkning_alder 357 rows; served_as = [] the same five; mixed sources exactly two; and the inverse defect absent — Folkemengde held at 2026, Oslo 728,714.

🔴 After upgrading to this pin, two things look broken and are not​

Both self-heal, both are documented, and an upgrader who does not know them will think the upgrade failed. Item 1 was measured on a real upgrade over a loaded install, not a cold one. Item 2's exit code is read from the script's source and has not been measured on this build — the distinction is kept because the last time this section blurred it, a single measurement was carried across four pins as though it were current.

1. The served documentation lags until you publish. COMMENTs live in the database, so the OpenAPI field docs track the last build that published. 🔴 If you are coming from anything older than v20260921-fcf78e6, this item is the whole point of taking the pin: the window-year descriptions arrived in fcf78e6 and live in the DATABASE, so installing without running the publish serves the OLD text with every signal green until the 05:00 publish — up to 24 hours. (That framing said "this pin is largely a descriptions release" while pinned to fcf78e6, and was carried unchanged into 1a569dc, which is not one — the descriptions are inherited, not new here.) Immediately after installing, the api_v1 checks measured 2 pass / 2 fail:

uis dagster run publish_api_v1

After that, 4 of 4 succeeded.

⚠️ Every per-job duration on this page is an ORDER OF MAGNITUDE, not a tripwire. They were quoted from bus nominations and none of them names the clock it was read from; publish_api_v1 alone has been reported here at ~67 s, 72.9 s and 107.4 s, and the two clocks differ by 20–40% on the same run. Do not gate anything on them. The cold-install total further up is different — the artifact states its own method for that one ("1772 s wall, measured end to end on a factory-reset cluster") — and what actually settles "did something else run?" is a before-and-after diff of indicator_summary, not a stopwatch.

2. Until the first transform runs, the check reports a fault — and it self-heals. marts.mart_source_freshness does not exist until a transform builds it, so the check has nothing to read. Let one transform_and_publish run (~376 s) and it returns exit 0 — measured at 29 bounded sources, 29 within cadence.

On an upgrade, where marts were built by an earlier build but the view is missing, that is WARN, not CANNOT: a fault waiting on a transform is not a host where nothing has ever run. The old code made no such distinction, resting on an assertion that a missing view "means no transform has ever run here" — which is false on every upgrade. ⚠️ Read the WARN as what the source says, not as a measurement: nobody has run the check on this build inside the pre-transform window. The one live run available is EXIT=0 on a healthy install, which does not exercise this case, and the operator who produced it said so rather than offering it as the answer.

✅ The "exit 2, NOTHING WAS CHECKED" claim that stood here is RETIRED — and it had been stale for five days and four pins. The check has had three states since 2026-09-16: OK, WARN and CANNOT, combined by a worst() rule — "CANNOT dominates WARN dominates OK; not-asked is OK, asked-and-unanswerable is CANNOT" — with WARN reachable from six distinct sites in the script.

🔴 How the claim went stale is the part worth keeping. The exit 2 figure was measured once, on v20260916-e439668, and then carried unchanged through 1709934, 6e10058, 2d88589 and fcf78e6, still introduced by the words "on the currently pinned build only" — false four times over by the end. The tri-state rework landed the same day, in a later build, and this page documented one of its consequences while still saying the split had not demonstrably landed: the dark-Automation exit 2 → exit 0 described below is that rework. The contradiction sat in two sections of this page for four pins. A measurement carried forward is a quotation, not a measurement, and it needs the build it was taken on written beside it. The retirement finally came from reading the script — which was possible the whole time, and dated in its own comment.

🔵 Item 1's cause was established by intervention. Item 2's was established by reading the script — stronger than the behavioural test this page had been waiting for, and available five days earlier than it was used.

🔴 Taking this pin is THREE steps, and the middle one is not optional​

install v20260922-d077fd7 → uis dagster run klass_refresh → uis dagster run transform_and_publish

🔴 Skip the middle step and the release does nothing, with every signal green. The whole point of this build is 3e72760, which widens the Klass ingest window from 1960 back to 1950 — and the twelve orphan kommune codes all dissolved on or before 1960, so the old window could never see them. The ingest was not failing; it was not looking far enough back. transform_and_publish rebuilds mart_dim_kommune from raw.ssb_klass_kommuner but never re-fetches it, so deploy-then-transform rebuilds the same stale rows. In the operator's words: "two of those three, and it does nothing."

With the re-ingest in the right order:

beforeafter
raw Klass codes1,3272,073
dim_kommune1,1581,170
ssb-06913 kommune FK violations7,296 rows / 12 codes0 / 0

🔴 LEFT UNRUN, THE MIDDLE STEP DOES NOT STAY MISSING — IT SELF-HEALS, WHICH IS WORSE. klass_refresh has a cron of 0 1 1 * *: 01:00 on the 1st of each month. So 3e72760 was not inert forever. It was inert for nine days, and on 1 October the 7,296-row FK failure would have cleared on its own, with nobody having changed anything and no record of why. ⚠️ A defect that repairs itself on a schedule is not a mild defect — it is one that cannot be found by noticing it stopped happening. atlas had been treating the missed re-ingest as a one-off ordering mistake it kept repeating; the class self-heals, so it only became visible because four parties looked at the same source on the same night. That is not a repeatable detection method, which is why the trigger now lives in the artifact rather than only the remedy (self_healing_warning, urb-agents#1373).

🔵 Both halves of that were already on this page, 360 lines apart. The cadence table above says klass_refresh runs on the 1st at 01:00; this section says the fix needs klass_refresh. The conclusion — "then an operator who skips it gets a silent repair up to a month later" — needed no new information and was not drawn. Same shape as the check-note contradiction three pins ago: two sections that together state something neither states alone.

✅ THE HAND-CARRY IS OVER — this section is now derived, not carried. For three pins the sequence above existed only in bus messages, and this page said it would stop "when a build carrying operational.upgrade is PINNED". That build is pinned. The rule below mirrors operational.upgrade in the artifact at pin v20261003-37769d0, unchanged since d077fd7:

the artifact's rule
default for an upgradetransform_and_publish alone — "an upgrade already has data"
the exceptiona release that changes what an ingest fetches — its date window, its filters, its dimensions
why"the transform cannot recover rows that were never requested, so that source's ingest job must run FIRST"
thenthe owning ingest job → transform_and_publish → transform_checks
if you skip it"an ingest-window change left unrun is repaired by that source's next scheduled cron — up to a month for klass_refresh. The symptom disappears on its own and the cause is never recorded. Re-run it deliberately."

🔵 That is a RULE with a stated trigger, not another job list — which is why it will outlive this pin. A per-release list of jobs has to be maintained by the person most likely to have just forgotten the step; a rule asking "did this release change what an ingest fetches?" keeps working on releases nobody has written yet. The Klass sequence above is now an instance of this rule rather than a special case, which is the whole reason it was worth waiting for.

⚠️ Two pointers in that field do not resolve, and this page substitutes its own. The field says "operational.crons above maps every job to what it covers" — there is no operational.crons key; the map is operational.cadence, which is the cadence table further up this page. And it ends the sequence with transform_checks, which is not in that map at all (nor in manual_only or unscheduled). Both verified by walking the parsed artifact. Reported on urb-agents#1377 — neither changes what an operator should do, and both would send a reader looking for something that is not there.

🔴 Kept as the record: this page was wrong about the artifact for three pins. It said "the artifact does not contain it … nothing in the install definition mentions klass_refresh." The artifact named it twice all along — first_data.jobs lists it second, before transform_and_publish, and the monthly cadence row names it, so a first install following first_data.jobs already got the right order. This repository's own verifier prints that job list on every pin run, so the refutation was on screen each time the claim was repeated. What was genuinely missing was narrower: no concept of an UPGRADE at all — first_data describes a first install and nothing addressed a host that already has data. Corrected on urb-agents#1373.

⚠️ The --full-refresh requirement below is SEPARATE and still applies. It arrived with v20260914-72999a4; an operator who has not yet taken 72999a4 needs everything in this section as well, and this pin does not retire it.

⚠️ Whether a case of the same kind is now in play is STILL an open question, and the honest answer is that this page does not know. atlas described v20260921-fcf78e6 as adding a window_years column, and everything in fcf78e6 is in the pin above — so an operator taking 1a569dc over a loaded install inherits that column whether or not they ever installed fcf78e6. A column added to an incrementally-materialised model with on_schema_change='fail' is precisely the failure documented below. The artifact says nothing about window_years in any of the three builds, and "atlas shipped no new upgrade instruction" and "no new instruction is needed" look exactly the same from here. Asked on urb-agents#1341 and not yet answered — carried rather than quietly dropped, because an unanswered question that disappears at the next pin reads as an answer. If you are upgrading a loaded install, assume a full refresh may be needed and read the failure signature below before concluding the release is broken.

The pins through 2026-09-13 were text-only or additive — you re-installed and the data was untouched. This one is not. It adds a column to an incrementally-materialised model, and dim_brreg_enhet runs with on_schema_change='fail'.

1. A full refresh. Without it the transform fails with

Database Error in model dim_brreg_enhet
column "snapshot_loaded_at" does not exist
LINE 209: select coalesce(max(snapshot_loaded_at), '-infinity'…

which is the model doing exactly what it is configured to do, not a broken release.

dbt build --full-refresh --select dim_brreg_enhet+

⚠️ Note the trailing +: descendants, not ancestors. dim_brreg_enhet+ rebuilds what depends on the changed table, which is what you need. +dim_brreg_enhet is the opposite selector and does not.

⚠️ Run it as the atlas database user, not as a superuser. A full refresh drops and recreates, so the tables take the running user's ownership — and a superuser leaves the next run with "permission denied".

Measured cost for this selector: 528 s dbt-reported, 536 s wall (imac, on a real upgrade; PASS=30 WARN=1 ERROR=0). ⚠️ A figure of ~256 s or "about four minutes" refers to the model alone. dim_brreg_enhet+ additionally runs 29 data tests and a view model, so it is roughly twice that. The number and the selector have to travel together.

2. A publish — and it is a different operation, not part of the refresh. This release changes COMMENTs, and comments live in the database, not in the image. Without a publish the served OpenAPI field documentation stays as it was while the build that produced it has moved on.

🔴 There is no CLI affordance for materialising a single asset. uis dagster run takes jobs, not assets, so uis dagster run api_v1 answers "No job named 'api_v1'". From the host you have two options and neither is the one-liner you might expect:

  • uis dagster run transform_and_publish — works, and rebuilds every mart rather than just publishing, so it costs more than the operation needs; or
  • materialise the api_v1 asset in the Dagster UI, which is the narrow operation.

Both. A refresh is not a publish.

⚠️ A UIS 1.6.84 floor, and what happens below it​

The current pin declares env_from_services, which hands a consumer Atlas's API address resolved from the named service rather than copied from a host-facing export.

env_from_services:
ATLAS_POSTGREST_URL: postgrest # a SERVICE id, resolved in-cluster

This is the fix for a real defect, and it was measured inside a running pod before being pinned:

ATLAS_POSTGREST_URLresult
beforehttp://api-atlas.localhostexit 2, CANNOT — .localhost is loopback (RFC 6761), so the pod dialled itself
afterhttp://atlas-postgrest.postgrest.svc.cluster.local:3000script exit 0, uis template check atlas → healthy

⚠️ Port 3000 is the data API. 3001 is the admin server and 404s a table read.

exports.api-url is unchanged and still host-facing — correctly, because it is for hosts.

🔴 What this makes worse for a host below 1.6.84​

env_from_services needs UIS 1.6.84 to be read at all, and the old env_from_exports key is gone. So an un-upgraded host loses the variable entirely rather than getting a wrong one:

UISwhat the host gets
1.6.84+the in-cluster address — fixed
1.6.83 and belowkey ignored, and there is no fallback key any more → ATLAS_POSTGREST_URL unset → the check reports CANNOT, naming the variable

For an un-upgraded host this is "wrong value, explained" becoming "no value, explained". Both are CANNOT, neither is a health regression, and the variable is named either way — which is why the previous pin, a diagnostic and not a fix, was worth taking: the "explained" half arrived with it.

⚠️ There is a population outside this fleet. The registry is fetched unauthenticated and lists atlas today; nobody can make those hosts upgrade. ./uis pull moves a host to latest.

⚠️ What has not been observed​

  • A run pod's rendered environment. The value is declared correctly in the container context and has been read from a pod — but never from inside a run pod.
  • A plain install on a host that took this catalogue's word for the pin. Until this pin moved that was impossible: uis template install <id> takes an id only, with no --tag or --digest, so a nominee cannot be installed through the supported surface until it is already pinned. The verification behind this pin used a locally repointed registry cache — the install was real, the catalogue read was not — and that was disclosed rather than glossed.
  • Scope. One application, one cluster: k3s v1.25.16 on Rancher Desktop, zero IngressRoutes.

What uis template check atlas actually reports​

The check asks whether a deletion Brreg has published has been applied by the transform, not whether one is merely outstanding:

situationverdict
deletions pending, awaiting the next transform at :10/:40OK — "not a fault"
a deletion applied and the organisation still served by the APIWARN — a real fault, at any age

It reports both at once when both are true, labelled differently, so a real fault is not hidden by routine pending work. The question has no clock in it, so it cannot drift when the :10/:40 offset is tuned — and that offset is deliberate and therefore tunable.

✅ The check now tells you when nothing is running​

This used to be a false all-clear. With the pipeline switched off, the check reported healthy, exit 0, and said "awaiting the next transform" when no transform was scheduled — the reassuring half of the table above printed for the one reason it could not cover. Fixed as of this pin, and verified in both the total and the partial case:

statewhat it says
all five instigators stoppednames them, other instigators running 0, "Nothing is scheduled to reconcile the register"
one stopped, four running"…not yet applied — brreg_transform_half_hourly is STOPPED, nothing will apply them", other instigators running 4

⚠️ The partial case is the one a whole-fleet check would have missed — it names the single stopped instigator while four others still run. That is the case that had no fix at all on the previous pin.

Enable automation with uis dagster automation --start. uis dagster automation lists the instigators and their state.

⚠️ If you gate on the check's exit code, it changed — on 2026-09-16, not in this build​

A "dark" Automation block — where the check cannot reach Dagster to ask — returns exit 0. It returned exit 2 up to and including v20260916-e439668. This heading said "in this build" for four pins after the change, which is how a contract change reads as news long after anyone could still act on it: if you are coming from any build from 2026-09-16 onward you already have this, and if you are coming from an earlier one it is a change you have not yet absorbed.

The three states an exit code can carry, since that same rework:

exitstatemeans
0OKasked and answered, or not asked at all
1WARNa real fault, including one that self-heals on the next transform
2CANNOTasked and unanswerable — the check could not establish the answer

CANNOT dominates WARN dominates OK, so one unanswerable block sets the whole exit code.

That is deliberate: an exit code reporting the tool's own wiring is not a health gate. But it is a contract change, and anything of yours that treats non-zero as "unhealthy" will now pass where it previously failed.

✅ The build says so when it happens — the printed output names the condition at the moment it occurs, and the EXIT CODES docstring names the supported way to tighten it if you need the old behaviour. Check the printed phrase on the raw stream if you grep for it; colour sequences can break a naive match.

⚠️ The check is never proactive — it answers, it does not warn​

how you reach itdoes it run?
uis template check atlasyes, immediately
a shell in the podyes
anything on a schedule — Dagster, a log, a sweepno

The only invoker is the check command. Nothing runs it on a clock, so this is never "a log nobody reads" — and it is also never something that tells you first. Do not read "better diagnostics" as "I will be told." It reaches a human exactly when a human asks.

🔴 And what the check asks is not everything worth asking. Three scope defects were found on 2026-09-21–22 — counts scoped differently from the range beside them, a null filter scoped differently from its subject, and latest_year scoped differently from the counts. All three were found from outside, by consumers. None was caught by a gate, and the suite reported 990 pass / 18 fail before and after each one. ⚠️ A green check means the questions it asks were answered well, which is a narrower statement than "the data is right". A gate for the class is being written upstream; until it exists, treat a consumer noticing something as the detection mechanism it has actually been.

Why the earlier releases being silent is the hazard​

Every atlas upgrade before this one required no operator action. ⚠️ That is a property of those releases, not of atlas upgrades — and it is the more dangerous half of this page, because three silent upgrades in a row teach an expectation that the fourth violates. An operator who meets the schema error without warning reads it as a broken release.

Two surfaces answer two different questions, and this is the earlier one:

you are askingread
should I take this upgrade, and what will it cost?this page, before you re-install
a run just failed — what now?uis template info atlas, which renders the artifact's troubleshooting block, and the install output itself

When a release needs operator action, this section will say so and name the cost, because by the time the install output tells you, you have already committed to it.

What this directory is, and is not​

This directory is a catalogue pointer, not the application. There is no atlas source code here. The install definition is the OCI artifact named in template-info.yaml:

ghcr.io/terchris/atlas-data/uis:<tag>@<digest>

The tag and digest are deliberately not repeated here. They live in source: in template-info.yaml and nowhere else on this page, because a second hand-maintained copy of a digest is a second thing to forget: this block sat on v20260909-853c696 for twelve days and several releases, reading like the current pin to anyone who trusted it.

UIS pulls the digest. The tag is shown to people and never pulled by, because a tag can be re-pointed at a registry and a digest cannot. Bumping atlas therefore means editing source.digest in a reviewed change — that review is the only thing establishing that a human approved this artifact.

The full reasoning lives in uis-applications/README.md in the dev-templates repository — referenced as a path rather than a link, because this page is rendered on the documentation site where a relative link to a repository file does not resolve.

params: and provides: are not listed here on purpose. They live in the artifact's own template-info.yaml; there is one source of truth for what atlas installs and it is not this repository.

Provenance of this entry​

The artifact coordinates above are atlas's published values as verified by imac against the real registry (urb-agents #481) — the 853c696 artifact, which superseded 4b11f3f partway through that test round.

The three services listed above are not inferred: they are the plan uis template install atlas --dry-run produces from this pin, as run by tor-agent against the published registry (urb-agents #486) — deploy postgresql, configure postgresql, configure postgrest --schemas api_v1 --url-prefix api-atlas, deploy postgrest, deploy dagster, the atlas-data code location, then deploy dagster again.

The entry's description and abstract are atlas's own words, supplied on urb-agents #489. Change them only on atlas's word.

⚠️ Two earlier versions of this description were wrong, and the sequence is worth keeping:

  1. dev-templates first wrote "Norwegian business-register data", inferred from the brreg_enheter table in the install records.
  2. Corrected to "the Norwegian NGO sector, in one place" from the repository's own one-liner — better, but still not the framing atlas uses.
  3. atlas supplied the text above and explained why the first attempt was worst: brreg_enheter is one raw table out of 47. FHI contributes 21 and SSB 17. Brønnøysund supplies organisation identity — how an NGO gets a stable orgnr — so leading with it described the smallest source as though it were the subject.

The lesson recorded rather than the words: a domain inferred from a table name reads plausibly and is not evidence. The prose in this repository is a stand-in until the owning application supplies its own.