Register descriptors
An app declares its registers in a descriptor — an OpenAPI document under
lib/Settings/ carrying components.registers. OpenRegister imports it through
ConfigurationService::importFromApp(), and per
ADR-005
the import is delivered by an idempotent Repair step.
That decision settled who seeds a register. It did not give anyone a way to re-run the seeding, or to see whether it worked. This page is about the gap and the panel that closes it.
Why a register can be missing on a healthy-looking instance
Two properties combine badly.
Repair steps only run on install and occ upgrade. As soon as an app's
installed_version matches its info.xml, occ upgrade reports "No upgrade
required" and skips the repair steps entirely. There is no supported way to run
one again short of occ maintenance:repair, which fires every app's steps,
or an app disable/enable cycle.
A failed import is only logged. From ImportFlowRegister's own docblock:
Never throws — a failure logs a warning and leaves the instance otherwise healthy.
That is a defensible trade at boot, where the alternative is an app that will not install. Its cost is an instance that looks fine and is missing a register, with the only evidence in a log nobody is reading.
The consequence is not hypothetical. On a dev instance an occ upgrade that
reported complete success left 8 of 15 declared registers absent, including
flows. Two e2e suites died in beforeAll on the missing register, and
establishing why took an account listing, a register dump, and a read of the
Repair step's source.
The panel
Admin settings → Register descriptors lists every register any installed app declares, in one of three states:
| state | meaning | what it costs you |
|---|---|---|
current | present at the version the app ships | nothing |
behind | present, but older than the shipped descriptor | code runs against an older contract |
absent | the app declares it and it does not exist | the code paths that need it are dead |
absent and behind are deliberately not collapsed into one "needs
attention": they call for different actions and carry different risk.
Each row offers an import. The outcome is shown to the administrator who triggered it — a button that reports nothing is indistinguishable from one that did nothing, which is the failure being repaired.
The import is always forced
The re-import passes force: true, and that is not a convenience.
ImportHandler short-circuits on:
$force === false && version_compare($data['version'], $existingVersion, '<=')
The situation an administrator presses this button in is exactly one where the version counter is not to be trusted — the register is absent, or the write failed, while the counter says current. An unforced re-import would report success and do nothing in every case that motivates the action.
Is a forced re-import safe for customised schemas?
Yes. A schema that extends one shipped by a descriptor refers to its base
rather than copying it — Schema::getAllOf() returns "Array of schema IDs,
UUIDs, or slugs". Re-importing the base updates the base row; the extension
keeps its own properties and resolves against the new base. It is impervious to
the base moving.
That is a property of the implementation rather than a law, so it is pinned by a
test (register-descriptors.spec.ts) rather than assumed: an extension
materialised as a copy at import time would silently revert somebody's
customisation, and it would do so through the button offered as a repair.
Without a browser
The condition this diagnoses is most likely on an instance mid-setup or mid-repair, where the admin UI may itself depend on the broken thing:
occ openregister:descriptors:list # the full inventory
occ openregister:descriptors:list --problems-only # absent and behind only
occ openregister:descriptors:list --app=openregister --import=flows
The command formats the same service method the panel reads, so the two surfaces cannot drift into disagreeing about one instance.
For app authors
If your app ships a register and it never appears:
- Run
occ openregister:descriptors:listand find your app's row. If it is not listed at all, your descriptor is not being recognised — it must be a JSON document underlib/Settings/with a non-emptycomponents.registers. Recognition is by shape, not filename. - If it is listed as
absent, the descriptor is valid and the import never landed. Import it from the panel or the command, then check your Repair step: per ADR-005 Rule 1, shipping the JSON alone does nothing at runtime.
Removing example data an app loaded
Every importFromApp() call runs under its own import job id. Each audit row the
import writes carries that id. When the job created at least one object,
OpenRegister records it under the app id the import used, for example
learniq.demo or decidesk.profile.municipality. The import result returns the id
as importJobId.
Load each example set under its own app id. Then a setup wizard can offer "remove this example set" with one call:
$jobs = $configurationService->listImportJobs(appId: 'learniq.demo');
$report = $configurationService->softDeleteAppImports(appId: 'learniq.demo');
// $report: appId, jobs (one report per job), softDeleted (a count), errors.
Hide the button when listImportJobs() is empty. The removal soft-deletes only the
objects those jobs created, never objects they merely updated. It runs in-process
as a system operation, so decide in your own controller who may press it. A job
whose objects all went is forgotten. A job with errors stays recorded, so you can
retry.
An app's example data never leaves over HTTP: the import rollback route answers
409 for an app import job, because archival schemas refuse HTTP deletes. An
administrator destroys the soft-deleted rows for good from the shell:
occ openregister:objects:purge --import-job <id> # dry run
occ openregister:objects:purge --import-job <id> --apply # destroys trashed, non-archival rows
occ openregister:objects:purge --import-job <id> --apply --force # also archival and live rows
When the audit trail is off, nothing can be traced. The import then logs a warning naming the app, and no job is recorded.