Skip to content
Registry StackDocsDevelopment (unreleased)

Review an application and send an approval notice

For the operator

View as Markdown

Run one Registry Coordinator workflow, reviewed-notice, from an application in Base Registry Engine (BReg), through your decision as reviewer in Registry Casework, to an approval email from Registry Messaging. Coordinator does the rest, even across a restart.

Outcome
One approved, unchanged application produces one finished run and one email in local Mailpit.
Time
About 20 minutes
Level
Local evaluation only
Prerequisites
Docker 24 or laterThe repository Rust toolchain through rustupOpenSSLPython 3.11 or latercurljqLinux or macOSA current Registry Stack source checkout

From the source checkout, build the four products, their tools, and the example’s input helper. On macOS, the sourced function also tells your shell where the binaries’ FIPS library lives:

Terminal window
. scripts/cargo-runtime-library-path.sh
registry_cargo_build "$PWD" --locked \
-p registry-coordinator --example reviewed-notice-input \
-p registry-breg --features registry-breg/runtime \
-p registry-bregctl -p registry-casework -p registry-caseworkctl \
-p registry-messaging -p registry-messagingctl --bins
export PATH="$PWD/target/debug:$PATH"

Keep this terminal open.

A workflow is a finite list of named steps. Ask coordinatorctl to explain this one:

Terminal window
coordinatorctl explain --project products/coordinator/examples/reviewed-notice

The output ends with the steps from start, among them:

verify-review-subject: choose by review-subject-route(): changed -> changed-before-review, current -> submit-review
changed-before-review: finish application-changed
submit-review: call review review-submit-review-request (casework mutation, recovery same-command) with review-request(), then await-review
await-review: wait for the result of submit-review, at most 1209600 seconds, then decide; on timeout withdraw-timeout; on cancel withdraw-operator

Each line is one step. A call reaches a product through a named connection, here review for Casework; a choose picks the next step; a finish ends with an outcome. await-review is a wait: it holds up to fourteen days for the review result. Later steps read the application again before calling Messaging.

The session script starts the four products from target/debug, with their databases and the Mailpit mail sink in Docker. It seeds one application for Amina Example and starts one run of the workflow for it with POST /v1/runs: one admitted execution that Coordinator keeps in its database. The registry project, review policy, and notice template it uses are in products/coordinator/scripts/real_service_fixtures.py. Every service listens on loopback HTTP (tlsTermination: development-loopback), and Coordinator applies its schema as the database superuser and serves as a separate runtime role: a local setup only.

Terminal window
mkdir -p tutorial-work
python3 products/coordinator/examples/reviewed-notice/session.py start \
--session-dir tutorial-work/reviewed-notice > tutorial-work/session.json
jq '{status, workflow}' tutorial-work/session.json
{
"status": "ready",
"workflow": "reviewed-notice"
}

session.json holds service addresses and owner-only token file paths. Keep them in variables, and copy the Coordinator token out of its header file for coordinatorctl:

Terminal window
session=tutorial-work/session.json
run_id="$(jq -r .runId "$session")"
coordinator_url="$(jq -r .coordinatorApi "$session")"
casework_url="$(jq -r .caseworkApi "$session")"
mailpit_url="$(jq -r .mailpit "$session")"
staff_headers="$(jq -r .staffHeaderFile "$session")"
(umask 077 && sed -n 's/^Authorization: Bearer //p' \
"$(jq -r .coordinatorHeaderFile "$session")" > tutorial-work/coordinator.token)
Terminal window
coordinatorctl inspect --run "$run_id" \
--url "$coordinator_url" --token-file tutorial-work/coordinator.token

The run is at await-review, after Casework accepted the review request:

state: running
step: await-review
waits:
- acceptedStep: submit-review
completion: result

Nothing has gone to Messaging. Inspection never shows the application data or the commands sent.

As staff, find the open task in the applications queue and read the facts you decide on. The applicant’s email address stays in BReg.

Terminal window
curl -fsS --header @"$staff_headers" --header 'Registry-Casework-Profile: staff' \
"$casework_url/v1/review-tasks?queue=applications" > tutorial-work/tasks.json
task_id="$(jq -r '.items[] | select(.state == "open") | .taskId' tutorial-work/tasks.json)"
curl -fsS --header @"$staff_headers" --header 'Registry-Casework-Profile: staff' \
"$casework_url/v1/review-tasks/$task_id/context" | jq .context.snapshot
{
"applicantName": "Amina Example",
"benefitProgram": "housing-support",
"requestedAmount": 1200
}

Claim the task at the revision you read. Casework answers 200:

Terminal window
revision="$(jq -r '.items[] | select(.state == "open") | .revision' tutorial-work/tasks.json)"
curl -sS --output /dev/null --write-out '%{http_code}\n' -X POST \
--header @"$staff_headers" --header 'Registry-Casework-Profile: staff' \
--header "If-Match: \"$revision\"" --header "Idempotency-Key: $(openssl rand -hex 16)" \
"$casework_url/v1/review-tasks/$task_id/claim"
200

Before you decide, restart Coordinator. The run is durable state, not a running process:

Terminal window
python3 products/coordinator/examples/reviewed-notice/session.py restart-coordinator \
--session-dir tutorial-work/reviewed-notice > "$session"
coordinator_url="$(jq -r .coordinatorApi "$session")"
jq .coordinatorRestarts "$session"
1

Now approve at the task’s current revision, with the policy’s approved outcome. Casework answers 204:

Terminal window
revision="$(curl -fsS --header @"$staff_headers" --header 'Registry-Casework-Profile: staff' \
"$casework_url/v1/review-tasks/$task_id" | jq -r .revision)"
curl -sS --output /dev/null --write-out '%{http_code}\n' -X POST \
--header @"$staff_headers" --header 'Registry-Casework-Profile: staff' \
--header 'Content-Type: application/json' \
--header "If-Match: \"$revision\"" --header "Idempotency-Key: $(openssl rand -hex 16)" \
--data '{"decision":{"type":"approve","outcome":"approved"}}' \
"$casework_url/v1/review-tasks/$task_id/decisions"
204

If a request answers 401, the local token expired. Refresh the staff token, and the Coordinator token for coordinatorctl, then repeat:

Terminal window
owner="$(jq -r .ownerProject "$session")"
bregctl dev token staff "$owner"
bregctl dev token coordinator-producer "$owner"
(umask 077 && sed -n 's/^Authorization: Bearer //p' \
"$(jq -r .coordinatorHeaderFile "$session")" > tutorial-work/coordinator.token)

Casework tells the restarted Coordinator a result is ready. The workflow reads the application again, checks it is unchanged and still allows a notice, and asks Messaging to send to the address on that record. Check the run:

Terminal window
coordinatorctl status --run "$run_id" \
--url "$coordinator_url" --token-file tutorial-work/coordinator.token
outcome: notice-accepted
output:
messageId: <message-id>
resultId: <result-id>
state: finished

If it is still running, repeat it after a few seconds. Then look at the local mail server:

Terminal window
curl -fsS "$mailpit_url/api/v1/messages" \
| jq '{count: (.messages | length), to: .messages[0].To[0].Address, subject: .messages[0].Subject}'
{
"count": 1,
"to": "amina@example.invalid",
"subject": "Application approved"
}

One email reached Amina’s address on record. A changed application, a withdrawn noticeAllowed, or a rejection would each have finished without calling Messaging.

The script removes only the containers and sessions it created:

Terminal window
python3 products/coordinator/examples/reviewed-notice/session.py stop \
--session-dir tutorial-work/reviewed-notice

How Registry Coordinator works explains the run, its wait, who owned each effect, and how an operator recovers a run.