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

# Author a Coordinator workflow

> Start a Registry Coordinator project, read and change its steps, check and test it offline, and package it with the digest an operator pins.

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](../../operate/coordinator/) adds them.

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

```sh test-skip="the gate builds coordinatorctl from this checkout"
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](https://github.com/registrystack/registry-stack/blob/main/products/coordinator/DEPLOYMENT.md#install-and-package).

{/* Evidence: products/coordinator/DEPLOYMENT.md, Install and package;
    scripts/cargo-runtime-library-path.sh. */}

## Start a project

```sh
coordinatorctl init --directory follow-up
```

```text test-expect
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:

```sh
coordinatorctl test --project follow-up
```

```text test-expect
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.

{/* Evidence: crates/registry-coordinator/src/project.rs, init;
    products/coordinator/examples/delayed-follow-up/;
    crates/registry-coordinator/src/scenarios.rs. */}

## Read the steps

```sh
coordinatorctl explain --project follow-up
```

```text test-excerpt
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`:

```yaml title="follow-up/workflow.yaml" test-excerpt="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](../../reference/coordinator-configuration/#host-operations) 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:

```yaml title="follow-up/workflow.yaml" test-excerpt="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.

{/* Evidence: crates/registry-coordinator/src/cli.rs, render_explain;
    crates/registry-coordinator/src/authoring.rs, AuthoredStep, Mapping;
    crates/registry-coordinator/src/operations.rs, CATALOG. */}

## Functions

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

```yaml title="follow-up/workflow.yaml" test-excerpt="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.

{/* Evidence: crates/registry-coordinator/src/functions.rs;
    crates/registry-coordinator/src/definition.rs, FunctionLimits. */}

## Check a change

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

```diff test-edit title="follow-up/workflow.yaml"
       recordId:
         type: input
-        path: /applicationId
+        path: /applicationID
```

```sh test-exit="1"
coordinatorctl check --project follow-up
```

```text test-expect
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:

```diff test-edit title="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:

```sh
coordinatorctl check --project follow-up --runtime-config follow-up/runtime.yaml
```

```text test-expect
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.

{/* Evidence: crates/registry-coordinator/src/mapping_check.rs;
    crates/registry-coordinator/src/project_check.rs;
    crates/registry-coordinator/src/definition.rs, seal_revisions, semantic_revision. */}

## Package the workflow

```sh
coordinatorctl package --project follow-up --output follow-up-package
```

```text test-expect
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.

{/* Evidence: crates/registry-coordinator/src/deployment.rs, package;
    products/coordinator/DEPLOYMENT.md, Install and package, Package digest and definition digest. */}

## Export a typed caller

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

```sh
coordinatorctl types --project follow-up --language typescript --output follow-up.ts
```

```text test-expect
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.

{/* Evidence: crates/registry-coordinator/src/sdk.rs;
    crates/registry-coordinator/tests/sdk_generation.rs. */}

## Hand over the package

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.

{/* Evidence: crates/registry-coordinator/src/deployment.rs, DeploymentConfig;
    products/coordinator/README.md, Package and deploy. */}

The
[example projects](https://github.com/registrystack/registry-stack/tree/main/products/coordinator/examples)
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.

## Next

[Deploy Registry Coordinator](../../operate/coordinator/)