Released docs. You are viewing the documentation published with v0.34.0. Development docs are available at Latest.
You are authoring a Base Registry Engine (BReg) project whose profiles already check, and some writes need more than one caller holding the right grant. A registry has three write paths. A direct mutation is a create, patch, or tombstone that a profile performs on a record and that commits at once. A change request is a record of its own that proposes a change to another entity’s record, waits for review, and is applied later by a profile that holds apply authority. An immediate action writes several records in one transaction from one typed input, with no review step. How a configured registry works explains why all three are declared in the project rather than coded.
Choose a change request when someone other than the caller must agree before the target changes, when the proposal itself must remain as a record with its own state, or when the write must wait for a later application. Choose an immediate action when the caller already holds the authority to write every target and the point is that several records change together or not at all. At the end of this page you have declared either or both, split the roles across profiles, and evaluated a planner script offline.
Change requests
Section titled “Change requests”A change request is an entity of its own whose records propose a change to another entity.
Three declarations connect them: the request entity’s changeRequest block, the target entity’s changeControl block, and the profiles that submit, read, and apply.
entities: - id: asset-placement primaryDataset: asset-registry route: placements mutationMode: mutable changeControl: requiredFor: [patch] fields: - id: asset type: reference target: asset-item required: true classification: internal - id: site type: reference target: asset-site required: true classification: internal - id: placement-correction-request primaryDataset: asset-registry route: placement-corrections mutationMode: mutable fields: - id: placement type: reference target: asset-placement required: true classification: internal - id: proposed-site type: reference target: asset-site required: true classification: internal - id: reason type: text required: true maxLength: 2000 classification: internal changeRequest: effects: - target: fromField: placement operation: patch set: site: fromField: proposed-site review: authority: casework policyId: asset-placement-correction onApproved: mode: manual retention: mode: operator_erase| Member | Effect |
|---|---|
changeControl.requiredFor | The operations on the target that must go through a request. With [patch], a direct patch of a placement is refused even for a profile that holds patch: the operation is absent from ordinary permissions, and only applying a request performs it. |
changeRequest.effects | What applying the request writes. target names the record from a reference field, operation is create or patch, set maps target fields to request fields, and clear lists target fields to null. A request entity declares either effects or a planner, never both. |
review | The logical external review authority and policy frozen into the submitted proposal. Casework owns the policy, review stages, reviewer independence, and terminal result. A request that intentionally needs no external review declares review: {mode: none}. |
onApproved | What happens after BReg has reconciled an exact approved result. manual waits for a currently authorized caller to use apply_request. automatic requires a logical executor; runtime configuration binds that executor to a separate ordinary credential that uses the same source action and guards. |
application | Optional application preconditions over frozen request facts, current target facts, or signed Evidence. It does not choose review or application authority. |
retention.mode | retain or operator_erase. With operator_erase, an operator can erase the proposal detail of an applied or canceled request while the record and its state remain. |
Decide retention.mode before the first request is submitted, because a proposal stored under retain keeps its detail for as long as the record exists; retain, erase, and audit covers what an operator can erase later.
Connect the Casework authority
Section titled “Connect the Casework authority”Use caseworkctl source add from the paired Casework project instead of copying compiled metadata
or writing integration glue. Preview first, then apply the exact reviewed changes:
caseworkctl source add ./my-registry \ --project ./casework --source-id asset-registrycaseworkctl source add ./my-registry \ --project ./casework --source-id asset-registry --applyThe source id must equal registry.id. The command verifies the Casework approval kind and
producer admission, adds the lifecycle hook and least-privilege reader profile, and writes a
candidate reviewAuthorities.<authority> runtime binding. Manual application adds no executor.
For onApproved.mode: automatic, it also writes a candidate
reviewExecutors.<executor> binding for the request’s single ordinary apply profile. Review the
endpoints and secret references before merging the fragment into the launcher-owned runtime file.
The command never activates a package or provisions a credential.
Result polling is the default. A Casework producer completion block explicitly enables
completion delivery and causes the candidate authority binding to include completionTokenRef
and completionRecipient. The BReg completion receiver acknowledges only an empty HTTP 204 No Content; other success statuses and a 204 with response bytes are retried. The notification is
a wake-up. BReg still reads and correlates the retained result before it records approval.
Split the roles across profiles
Section titled “Split the roles across profiles”accessProfiles: - id: correction-submitter default: true principalClaim: registry_principal requiredScopes: [registry:corrections:submit] requiredPurposes: [asset-correction] permissions: - entity: placement-correction-request rowBoundaries: [] operations: [create, get, patch, submit_request, revise_request, cancel_request] readableFields: [placement, proposed-site, reason] writableFields: [placement, proposed-site, reason] requestVisibility: owner - id: correction-reader principalClaim: registry_principal requiredScopes: [registry:corrections:read] requiredPurposes: [asset-correction-review] permissions: - entity: placement-correction-request rowBoundaries: [] operations: [get, list] readableFields: [placement, proposed-site, reason] readableRequestFields: [reason, review_state] - id: correction-applier principalClaim: registry_principal requiredScopes: [registry:corrections:apply] requiredPurposes: [asset-correction-apply] permissions: - entity: placement-correction-request rowBoundaries: [] operations: [get, apply_request] readableFields: [placement, proposed-site, reason] applyTargets: - entity: asset-placement rowBoundaries: []The submitter is the default for shared request reads.
The reader can inspect BReg’s source-owned review projection because its readableRequestFields includes review_state; this is disclosure authority, not authority to decide the review.
Casework policy asset-placement-correction defines the reviewers, stages, independence rules, and fields disclosed for the review task.
The manual applier is a separate current source authorization check, not an additional review decision.
An applier’s applyTargets names the entities the profile may write when it applies, with row boundaries of its own.
Give the profiles that read the target entity a requestPresence entry so they can see that a correction is pending.
A permission’s requestVisibility: owner restricts reads of the request entity to the requests the caller’s own principal created, which suits a submitter.
The source request record is stored as draft, submitted, cancelled, or applied, and bregctl explain lifecycle reports every one of them as reachable.
The external result status is projected separately and never substitutes for the source lifecycle state.
A settled result does not move the request, but it decides what its owner and applier are offered.
After a rejection the owner can only cancel.
After a send-back, or an approval whose availableUntil passed before anyone applied it, the owner can revise the request or cancel it, and apply_request is not offered; the revise_request action then carries rebase: false, so the new draft is recorded as a revision.
BReg refuses what it does not offer, and bregctl explain lifecycle reports that check as its review_outcome layer.
Applying writes the target the way a direct mutation would, so the target gains a revision and the request moves to applied.
Base Registry Engine API reference describes each action, its route, and the states it accepts.
To see everything the compiler derived, including the routes each profile receives and which request types may write each controlled entity:
bregctl explain change-requests ./my-registryThe JSON report carries a controlledWrites list, one entry per target entity with its requiredFor operations and eligibleRequestTypes, and a requests list with the actions and routes of each request entity.
Require submitter authority over targets
Section titled “Require submitter authority over targets”A change request that reaches into another record, a licence correction that names the licence it corrects, can require the submitter to still hold read authority over that record, not merely to have created the request. Add submitterTargets to the request entity’s grant, naming the entities its native reference fields target:
entities: - id: professional-license primaryDataset: directory route: professional-licenses mutationMode: mutable fields: - id: person-reference type: string required: true classification: restricted - id: licensed-activities type: structured required: true classification: restricted maxBytes: 512 schema: type: array items: type: string enum: [example-assessment, example-advisory-services, example-practical-services] minItems: 1 maxItems: 3 uniqueItems: true - id: scope-correction primaryDataset: directory route: scope-corrections mutationMode: mutable fields: - id: record type: reference target: professional-license required: true classification: restricted - id: licensed-activities type: structured required: true classification: restricted maxBytes: 512 schema: type: array items: type: string enum: [example-assessment, example-advisory-services, example-practical-services] minItems: 1 maxItems: 3 uniqueItems: true changeRequest: effects: - target: fromField: record operation: patch set: licensed-activities: fromField: licensed-activities review: authority: casework policyId: scope-correction onApproved: mode: manual retention: mode: operator_eraseaccessProfiles: - id: holder principalClaim: registry_principal requiredScopes: [starter:holder] requiredPurposes: [starter-learning] permissions: - entity: professional-license operations: [get, list] readableFields: [person-reference, licensed-activities] rowBoundaries: - field: person-reference claim: person_reference operator: equals - entity: scope-correction operations: [create, get, patch, submit_request, revise_request, cancel_request, list] readableFields: [record, licensed-activities] writableFields: [record, licensed-activities] requestVisibility: owner rowBoundaries: [] submitterTargets: [professional-license]submitterTargets must name exactly the request’s target entities: one entry for every entity its effects write, no more and no fewer. Each field the effects read a target from, record above, must be required on the request entity and readable through the permission, so admission can always read the target id it names, and writable through it wherever the profile holds create or patch on the request, so the submitter can supply the reference its own request needs. onApproved.mode must stay manual; an automatically applied request cannot declare submitter target grants. No target may itself be a change-request entity, since a request’s own read authority is the thing being checked. The profile itself must be non-anonymous and cannot hold batch on the request entity. And the same profile id must carry a permission on the target entity with get in its operations and no membershipBoundaries; a rowBoundaries restriction is fine, and the holder’s own row boundary on person-reference above is what actually limits which licences a holder may target. check refuses a permission that cannot honour all of this, with change_request.submitter_targets.invalid.
At runtime the engine reuses that same target permission’s current get authority, never the request entity’s own boundaries. Two checks carry it. The route itself refuses create, a draft patch, submit_request, and revise_request unless the caller’s claims resolve to target authority under that permission: a claim that carries no matching value, or one a boundary cannot reduce to the single value it needs, leaves the route unauthorized and answers 404, the same concealment an unrelated caller gets. Then submit_request and revise_request admit the named target rows inside the request transaction, under a share lock on each target table, and an exact idempotent replay admits them again before it replays: a target row the caller can no longer read through that permission answers 412 and precondition.failed. Reads are symmetric: once the request is retained, its target links appear only to a reader who currently holds that same get authority.
Plan effects with a Rhai script
Section titled “Plan effects with a Rhai script”When the effects depend on the request’s content, a request entity declares a planner instead of effects.
The YAML still owns the ceiling: which request fields the script may read and which targets and fields it may write.
The script only fills in values within that ceiling.
entities: - id: person-name-change-request primaryDataset: person-registry route: person-name-change-requests mutationMode: mutable fields: - id: person type: reference target: person required: true classification: internal - id: given-name type: string required: true maxLength: 80 classification: internal - id: family-name type: string required: true maxLength: 80 classification: internal - id: handling type: vocabulary-code vocabulary: name-change-handling required: true classification: internal changeRequest: planner: kind: rhai script: scripts/person-name-change.rhai abi: registry.change-request-plan/v1 requestFields: [person, given-name, family-name, handling] writes: - target: fromField: person operation: patch fields: [display-name] review: mode: none onApproved: mode: manual retention: mode: operator_eraseThe script at scripts/person-name-change.rhai builds the display name and returns the frozen effect:
fn plan(ctx) { let given = ctx.request["given-name"]; given.trim(); let family = ctx.request["family-name"]; family.trim(); let effect = #{ target: #{fromField: "person"}, operation: "patch", set: #{"display-name": given + " " + family} }; #{effects: [effect]}}| Member | Meaning |
|---|---|
requestFields | The request fields the script receives as ctx.request, keyed by their authored ids. Nothing else reaches the script: no target record, no caller identity, no other request. |
writes | Every target the script may touch, with the operation and the fields it may set or clear. target is fromField: <reference field> for an existing record or entity: <id> for a record the script creates. |
script | A path relative to the document that declares it, the project root or a module directory, ending in .rhai. The compiler reads it at check and package time, and its digest is part of the compiled contract, so editing the script changes the contract fingerprint. |
abi | The planner contract, which must be registry.change-request-plan/v1. |
The script defines fn plan(ctx) and returns a map whose effects is a non-empty list within the declared writes.
Each effect carries target, operation, and set, plus clear for a patch; a create effect also needs an id, and a later effect refers to the created record with fromEffect: <id>.
A reference field in set takes fromField or fromEffect, never a literal identifier; other values are checked against the target field’s type.
The planner never selects review or application authority. review and onApproved remain explicit authored configuration, and a script that returns an application disposition is refused.
The server runs the script once, when the request is submitted, and freezes its effects and script digest in the proposal.
Review, retry, and apply never rerun it, so a later package with an edited script leaves an already frozen proposal untouched.
The engine is closed: the script cannot import modules, evaluate strings, print, or reach a file, the database, or the network, and it runs within fixed bounds (64 KiB of source, 100,000 operations, and bounded call depth, strings, arrays, and maps).
A script that fails, leaves the ceiling, or breaks a bound refuses the submit with request.invalid.
check reports an invalid planner, review binding, or approved-application binding as change_request.* errors; it does not run the script.
Evaluate the script offline, with no database and no credentials, from a JSON file of request field values keyed by their authored ids:
{ "person": "11111111-1111-4111-8111-111111111111", "given-name": " Ada ", "family-name": " Lovelace ", "handling": "routine"}bregctl project planner-test ./my-registry \ --entity person-name-change-request --request ./routine-request.jsonRan the planner. 1 effect. compiled revision sha256:<digest> request entity person-name-change-request planner kind rhai planner ABI registry.change-request-plan/v1 planner script SHA-256 sha256:<digest> effects 1 field mutations 1 dependencies 0
effect effect-1 target existing operation patch fields display-name dependencies noneThe report names the planner identity and each effect’s target kind, operation, and fields; it never repeats the request values. The request file is limited to 64 KiB. A journey against a database remains the proof for authorization, freezing, and application.
Immediate actions
Section titled “Immediate actions”An action writes several records in one transaction from one typed input, without a review step:
actions: - id: register-asset-with-inspection inputs: - id: asset-code apiName: assetCode type: string required: true maxLength: 64 classification: internal - id: label type: string required: true maxLength: 200 classification: internal - id: jurisdiction type: string required: true maxLength: 80 classification: internal - id: observed-at apiName: observedAt type: timestamp required: true classification: internal - id: inspection-result apiName: initialResult type: vocabulary-code vocabulary: inspection-result required: true classification: internal effects: - id: asset target: entity: asset operation: create set: asset-code: fromField: asset-code label: fromField: label jurisdiction: fromField: jurisdiction - id: initial-inspection target: entity: asset-inspection operation: create set: asset: fromEffect: asset observed-at: fromField: observed-at result: fromField: inspection-result jurisdiction: fromField: jurisdictioninputs use the same types and members as fields, except that an input cannot declare validTimeRole.
Each effect has an id, a target that is either a fixed entity or a record named by an input reference through fromField, set values that come fromField or fromEffect, so a later effect can reference a record an earlier effect created, and an optional clear list for a patch.
The permission names the action, the targets it may write, and the effect results the caller gets back:
- id: asset-registrar principalClaim: registry_principal requiredScopes: [registry:asset:register] requiredPurposes: [asset-registration] permissions: - action: register-asset-with-inspection operations: [invoke] targets: - entity: asset rowBoundaries: - field: jurisdiction claim: jurisdiction operator: equals - entity: asset-inspection rowBoundaries: - field: jurisdiction claim: jurisdiction operator: equals results: [asset]Row boundaries on action targets apply to the records the action writes, so a registrar bound to one jurisdiction cannot register an asset in another.
Actions that patch existing records take preconditions: the caller first asks for target conditions, then invokes with the ETags it received, and the action fails with precondition.failed if a target moved in between.
An invocation either commits every effect or none, so a refused effect leaves no partial record behind.
bregctl explain actions ./my-registryThe report lists every compiled action with its inputs, effects, grants, contract fingerprint, and the bounds an invocation stays within. Base Registry Engine API reference documents the invoke route, the target-conditions request, and the result envelope.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause and next move |
|---|---|
check reports a change_request.* error. | effects and planner are exclusive; review must be either {mode: none} or an authority and policy id; manual application must omit an executor; automatic application must name one. check does not run the script; evaluate it with project planner-test. |
A direct patch of a controlled entity is refused although the profile holds patch. | The entity’s changeControl.requiredFor includes patch, so the write must arrive as an applied request. Submit a request of one of the eligibleRequestTypes that explain change-requests lists. |
project planner-test fails, or a submit is refused with request.invalid. | The script failed, returned an effect outside writes, attempted to return an obsolete disposition, or broke a bound. Read the message, fix the script or the ceiling, and rerun project planner-test with the same request file. |
revise_request answers 409 mutation.conflict. | The settled review result rules the shape out: a rejected request can only be cancelled, and a send-back or an expired approval is answered by a revision, not a rebase. Read the request again and follow the action it offers. |
An invoke fails with precondition.failed. | A target changed between the target-conditions request and the invoke. Ask for target conditions again and invoke with the new ETags. |
- Review changes before updating a registry: submit, review, and apply a request against a running registry.
- Test with journeys: write the submit, external-result reconciliation, apply, and refusal steps that prove the workflow.
- Control access per profile: the permission members every role on this page depends on.
- Base Registry Engine API reference: each request action, its route, and the states it accepts.
- Retain, erase, and audit: what
operator_eraselets an operator remove later.