Skip to content
Registry StackDocsDevelopment (unreleased)

Author a Coordinator workflow

View as Markdown

You know which product calls your process makes and in what order, and you want Registry Coordinator to make them, with waits in between that survive restarts. Write the process as a project, check and test it with no service running, and package it for the operator who serves it. Endpoints, credentials, and caller policies never enter the package; Deploy Registry Coordinator adds them.

You need coordinatorctl on your PATH. Build it from a Registry Stack checkout of the release you deploy:

Terminal window
cargo build --locked --release -p registry-coordinator --bins
export PATH="$PWD/target/release:$PATH"

On macOS the binary loads the FIPS library its build produced, so run it from that checkout or follow the macOS install in the deployment runbook.

Terminal window
coordinatorctl init --directory follow-up
created: follow-up
workflow.yaml
functions.rhai
runtime.yaml
scenarios.yaml
.gitignore
next: coordinatorctl test --project follow-up

The starter is a delayed follow-up: wait until a given time, read an application from a Base Registry Engine (BReg) registry, and send a notice through Registry Messaging only if the record still allows one. The project ID is the directory name. Run the test it suggests:

Terminal window
coordinatorctl test --project follow-up
PASS notice-accepted-after-virtual-wait
PASS permission-withdrawn
PASS same-command-after-response-loss
PASS read-only-reconciliation-after-response-loss
PASS expired-receipt-does-not-prove-no-effect
PASS refusal-after-unknown-preserves-unknown
6 passed, 0 failed

Each case in scenarios.yaml runs the workflow on a virtual clock against fake product replies: the record allows a notice, the record no longer allows one, and four cases where a response is lost or refused after the call may have taken effect. No case sends a request or reads a credential.

Terminal window
coordinatorctl explain --project follow-up
Steps from start:
due: wait until input /sendAfter, then read-application
read-application: call applications applications-read-record (breg read, recovery read-again) with {collection: "applications", recordId: input /applicationId}, then permission
permission: choose by notice-route(): send -> notify, skip -> skipped
notify: call notices notices-submit-message (messaging mutation, recovery same-command-and-receipt) with notice-request(), then accepted
skipped: finish no-current-permission
accepted: finish message-accepted with {messageId: step notify /id}

Each line is one step of workflow.yaml, in order from workflow.start. A step’s type is wait-until, wait-for, call, choose, or finish. A call names an operation alias declared under operations:

follow-up/workflow.yaml
operations:
applications-read-record:
uses: breg.read-record/v1
connection: applications
notices-submit-message:
uses: messaging.submit-message/v1
connection: notices

uses picks one versioned host operation; the configuration reference lists all ten with the effect and recovery explain printed. connection is a logical name the operator binds to an endpoint and credentials in runtime.yaml. There is no other way to reach a product: no arbitrary HTTP method, SQL, or computed destination.

The call’s input is a mapping:

follow-up/workflow.yaml
read-application:
type: call
operation: applications-read-record
input:
type: fields
fields:
collection:
type: literal
value: applications
recordId:
type: input
path: /applicationId
next: permission

type: input reads the run’s input, type: step an earlier call’s output, and type: literal a fixed value. A path is a JSON Pointer into it. type: fields builds an object of up to 64 members, and type: function computes the value with a Function, as permission and notify do.

notice-route and notice-request are Rhai entrypoints in functions.rhai. workflow.yaml declares each one’s inputs and output by schema, here the read operation’s output and the Messaging operation’s input:

follow-up/workflow.yaml
notice-request:
runtime: rhai
source: functions.rhai
entrypoint: notice_request
inputs:
input:
typeRef: workflow-input
record:
operationOutput: applications-read-record
output:
operationInput: notices-submit-message

A Function is pure: it has no clock, network, database, file, or credential. The host checks its inputs and output against those schemas and stops it at the limits check --explain prints. add_seconds and seconds_between compute over instants a Function is given.

check fits every mapping to the schemas it reads and feeds. Misspell the input member the read uses:

follow-up/workflow.yaml
recordId:
type: input
path: /applicationId
path: /applicationID
Terminal window
coordinatorctl check --project follow-up
error[coordinator.mapping.path] <file> /workflow/steps/read-application/input/fields/recordId/path
expected a path that the workflow input schema declares; it declares nothing at /applicationID
next: Correct the path to a member the workflow input schema declares, or omit path to use the whole value.
1 error, 0 warnings in 1 file

The refusal names the file, line, and column, the step and field, and the repair. Some refusals hide the checks that depend on them, so check again after each repair. Put the path back:

follow-up/workflow.yaml
recordId:
type: input
path: /applicationID
path: /applicationId

Check the project with the runtime file the starter carries, which binds applications and notices to a BReg and a Messaging connection:

Terminal window
coordinatorctl check --project follow-up --runtime-config follow-up/runtime.yaml
Valid workflow follow-up version 1
Definition: sha256:<digest>
Valid runtime configuration

The Definition: digest is the workflow revision. It hashes what execution depends on: input, steps, outcomes, and the operation and Function revisions used. Titles, descriptions, and project.version do not change it. Every run records the revision it was admitted under.

Terminal window
coordinatorctl package --project follow-up --output follow-up-package
created: follow-up-package
package digest: sha256:<digest>

package runs scenarios.yaml first and writes nothing unless every case passes. It writes definition.json and SHA256SUMS into a new directory and refuses one that exists. The package digest is the SHA-256 of SHA256SUMS, so it covers the exact bytes; the operator pins it. project.version is a label for people; the revision is what tells two definitions apart.

A caller starts a run with the workflow name, the revision, and an input the workflow accepts. Generate those as code:

Terminal window
coordinatorctl types --project follow-up --language typescript --output follow-up.ts
output: follow-up.ts
status: exported
workflow: follow-up
workflowRevision: sha256:<digest>

follow-up.ts exports the workflow name and revision, an Input type, an Outcome type with one member per outcome, and a client with start, get, and cancel. --language python writes the same for Python. Regenerate after any change that moves the revision.

Give the operator the package directory, its package digest, and a runtime.yaml template with secret references and no secret values. For each connection, list the product, the access profile or review policy it needs, and for Messaging the template and sender profile the Functions name. The operator pins the package in deployment.packages by root and expectedDigest. One deployment serves up to sixteen workflows; each is applied, upgraded, and rolled back on its own, and a run keeps the definition it started with.

The example projects show the other step and operation shapes: a review wait, a governed BReg action, a typed decision, an external read, and an appointment. Each runs under coordinatorctl test the same way.

Deploy Registry Coordinator