Registry stack documentation: machine-readable Markdown.
Index of all pages: https://docs.registrystack.org/dev/llms.txt
Full corpus: https://docs.registrystack.org/dev/llms-full.txt

# Review an application and send an approval notice

> Run Base Registry Engine (BReg), Casework, Coordinator, and Messaging together, then prove that an unchanged approved application produces one local email.

import QuickstartMeta from '../../../components/QuickstartMeta.astro';

Run one Registry Coordinator **[workflow](../../reference/glossary/#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.

<QuickstartMeta
  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 later', 'The repository Rust toolchain through rustup', 'OpenSSL', 'Python 3.11 or later', 'curl', 'jq', 'Linux or macOS', 'A current Registry Stack source checkout']}
/>

## Build the local tools

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:

```sh
. 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.

## Read the workflow

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

```sh
coordinatorctl explain --project products/coordinator/examples/reviewed-notice
```

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

```text test-excerpt
  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](../../reference/glossary/#wait)**: it holds up to fourteen days for the review result. Later steps read
the application again before calling Messaging.

## Start the products and the run

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](../../reference/glossary/#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.

```sh
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
```

```json test-expect
{
  "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`:

```sh
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)
```

{/* Evidence: products/coordinator/examples/reviewed-notice/session.py, start, serve;
    products/coordinator/scripts/test-real-services.py, Journey.start, coordinator_service,
    tls_database, mailpit; products/coordinator/scripts/real_service_fixtures.py, write_breg,
    write_review_casework. */}

## Inspect the waiting run

```sh
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:

```yaml test-excerpt
  state: running
  step: await-review
```

```yaml test-excerpt
waits:
- acceptedStep: submit-review
  completion: result
```

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

## Claim and approve the review task

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.

```sh
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
```

```json test-excerpt
{
  "applicantName": "Amina Example",
  "benefitProgram": "housing-support",
  "requestedAmount": 1200
}
```

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

```sh
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"
```

```text test-expect
200
```

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

```sh
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"
```

```text test-expect
1
```

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

```sh
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"
```

```text test-expect
204
```

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

```sh test-skip="only after a token expires"
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)
```

## See the run finish

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:

```sh test-retry="120"
coordinatorctl status --run "$run_id" \
  --url "$coordinator_url" --token-file tutorial-work/coordinator.token
```

```yaml test-excerpt
outcome: notice-accepted
output:
  messageId: <message-id>
  resultId: <result-id>
```

```yaml test-excerpt
state: finished
```

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

```sh test-retry="30"
curl -fsS "$mailpit_url/api/v1/messages" \
  | jq '{count: (.messages | length), to: .messages[0].To[0].Address, subject: .messages[0].Subject}'
```

```json test-expect
{
  "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.

{/* Evidence: products/coordinator/examples/reviewed-notice/workflow.yaml, notice-accepted. */}

## Stop the session

The script removes only the containers and sessions it created:

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

## Next

[How Registry Coordinator works](../../explanation/how-coordinator-works/) explains the run, its
wait, who owned each effect, and how an operator recovers a run.