diff --git a/docs/commercial/execute-api.md b/docs/commercial/execute-api.md
index 5d24a3e..ab4914f 100644
--- a/docs/commercial/execute-api.md
+++ b/docs/commercial/execute-api.md
@@ -236,6 +236,21 @@ This pattern also covers a response loss after your endpoint accepted an
event: OpenAdapt can retry, and your `event_id` record makes the second delivery
safe.
+## Process children are separate executions
+
+A [ProcessContract](../concepts/process-contract.md) parent sequences
+independently admitted capabilities. Each child is one `POST /v1/executions`
+with that child's `qualification_id`, `workflow_version`, `workflow_digest`,
+and its own `idempotency_key`. The parent doesn't collapse them into one
+request.
+
+Copy a handoff parameter into the next child's `parameters` only when the
+predecessor receipt `outcome` is `verified` and the predecessor's effect
+contract bound that fact. Don't replay the parent. If a predecessor returns
+`reconciliation_required`, stop and reconcile; don't dispatch the successor.
+
+See [Sequence work across two applications](../guides/compose-multi-application.md).
+
## Integration checklist
- Keep the service token and webhook secret on the server.
diff --git a/docs/concepts/index.md b/docs/concepts/index.md
index 3c86c80..c94af03 100644
--- a/docs/concepts/index.md
+++ b/docs/concepts/index.md
@@ -69,6 +69,11 @@ jump to what you need.
`lint` reports gaps, `certify` refuses an unsafe bundle before it deploys.
+- [__Process contracts__](process-contract.md)
+
+ A parent receipt over independently admitted capabilities. Handoffs copy
+ effect facts. Compose sequences recordings; process points at admissions.
+
- [__Backends: where it runs__](backends.md)
Browser (Playwright), native Windows (UIA), native macOS, native Linux
diff --git a/docs/concepts/process-contract.md b/docs/concepts/process-contract.md
new file mode 100644
index 0000000..b79e706
--- /dev/null
+++ b/docs/concepts/process-contract.md
@@ -0,0 +1,46 @@
+# Process contracts
+
+A ProcessContract is a parent receipt over independently admitted capabilities.
+Schema `openadapt.process-contract/v0`. Each child already carries a live
+`openadapt.qualification-admission/v1` envelope: one workflow version, one
+bundle digest, a counted campaign, identity and effect contract digests, a
+30-day lifetime, signed Ed25519. The parent names those admissions by
+`admission_id`. It doesn't copy recordings.
+
+`admission_id` is a UUID on the child's envelope, distinct from
+`runtime_validation_id`. Sitting next to another child doesn't extend anyone's
+validity interval or digest binding. A child whose admission is expired,
+revoked, or bound to a different digest is refused before Execute is called.
+
+## Handoffs are effect facts
+
+A handoff copies a parameter from child A into child B only when A's run ended
+`VERIFIED` and A's Effect CONFIRMed that parameter. Window titles and URLs are
+not evidence. A missing, empty, or unbound fact stops the parent before B
+starts.
+
+The parent receipt records, for each child, the `admission_id`, workflow
+version, bundle digest, terminal outcome, and model-call count. For each
+handoff it records the source, the target, and that the source was `VERIFIED`.
+Parent `VERIFIED` requires every child `VERIFIED` and zero model calls.
+
+## The recording parent and the admission parent
+
+`openadapt flow compose` writes `composition.json`
+(`openadapt.composition/v1`) and copies compiled child bundles. Use it after
+you recorded each surface, before those children are admitted. `certify` and
+`run` execute that directory. `replay` refuses it.
+
+`openadapt flow process` writes `process-contract.json` and points at
+admissions. It copies nothing. Pointing it at a `composition.json` directory
+is refused, because those children aren't admitted. Each child runs through
+[OpenAdapt Execute](../commercial/execute-api.md) with that child's envelope.
+`replay` of the process parent is refused.
+
+A compiled bundle still has its own ProgramGraph. `visualize` on a process
+parent shows admitted children, handoff edges, and a terminal labeled End of
+declared steps. Open the child bundle for its steps. See
+[Read a compiled program](program-visualizer.md).
+
+RFC-0001 is the name of this contract. Qualify each child first:
+[Qualify a workflow](../guides/qualify-a-workflow.md).
diff --git a/docs/concepts/program-visualizer.md b/docs/concepts/program-visualizer.md
index 14c6de2..5ede4c5 100644
--- a/docs/concepts/program-visualizer.md
+++ b/docs/concepts/program-visualizer.md
@@ -60,6 +60,29 @@ It does not prove `VERIFIED`. The run outcome still depends on the exact
authorization, identity, postcondition, and effect evidence required by the
execution profile.
+## Parents of children stay parents
+
+`visualize` on a compiled bundle emits that bundle's ProgramGraph: steps,
+ladders, gates, halt points.
+
+`visualize` on a compose directory (`composition.json`, schema
+`openadapt.composition/v1`) draws each compiled child as one node. Handoff
+edges are labeled with the effect-bound parameter names they copy, never a
+window title or a URL. Sequence edges follow `--after`, or `--child` order.
+
+`visualize` on a process-contract directory (`process-contract.json`, schema
+`openadapt.process-contract/v0`) draws each independently admitted child
+(name, `admission_id`) and the same kind of handoff edge.
+
+Both parents end at a terminal titled End of declared steps, not Success.
+Traversal ended. The parent is `VERIFIED` only when every child's receipt says
+so.
+
+The parent view doesn't inline a child's steps. Open the child bundle for its
+program map. See
+[Sequence work across two applications](../guides/compose-multi-application.md)
+and [Process contracts](process-contract.md).
+
## Follow one live occurrence
The same program node can run more than once inside a loop. A useful live view
diff --git a/docs/get-started/index.md b/docs/get-started/index.md
index d9eae4b..1c2f7b2 100644
--- a/docs/get-started/index.md
+++ b/docs/get-started/index.md
@@ -167,10 +167,23 @@ for the real environment.
## Where to go next
-To compile several recordings, read
-[Induce a program](../guides/induce-a-program.md). [Durable
-runs](../concepts/durable-runtime.md) explains how an operator can resume from
-the last verified checkpoint after a halt.
+To compile several recordings of the same task, read
+[Induce a program](../guides/induce-a-program.md). A task that starts in one
+application and finishes in another is two recordings. Don't record them as
+one. Sequence the compiled bundles with `compose`, or after each child is
+admitted, with a process parent:
+
+```bash
+openadapt flow compose \
+ --child intake=./intake-bundle \
+ --child posting=./posting-bundle \
+ --handoff intake.patient_id=posting.patient_id \
+ --out composed
+```
+
+See [Sequence work across two applications](../guides/compose-multi-application.md).
+[Durable runs](../concepts/durable-runtime.md) explains how an operator can
+resume from the last verified checkpoint after a halt.
diff --git a/docs/guides/compose-multi-application.md b/docs/guides/compose-multi-application.md
new file mode 100644
index 0000000..7632550
--- /dev/null
+++ b/docs/guides/compose-multi-application.md
@@ -0,0 +1,123 @@
+---
+description: >-
+ Record one program per application, sequence the compiled recordings with
+ compose, then after qualification and admission author a ProcessContract
+ parent.
+---
+
+# Sequence work across two applications
+
+One recording binds one surface. If the work starts in a browser intake form
+and finishes in a native posting app, record both.
+
+`for-each` loops one bundle over a worklist. `induce` recovers a program from
+several traces of the same task on the same backend. The two parents that
+sequence work *across* surfaces are `compose` (compiled recordings) and
+`process` (independently admitted capabilities).
+
+## Record one program per surface
+
+Record and compile each application the way you already do:
+
+```bash
+openadapt flow record --backend web --url https://intake.example --out rec-intake
+openadapt flow compile rec-intake --out intake-bundle --name intake
+
+openadapt flow record --backend windows --out rec-posting
+openadapt flow compile rec-posting --out posting-bundle --name posting
+```
+
+Decide the handoff before you sequence anything. The parent will copy
+`patient_id` only if intake's effect contract already bound it. A window title
+isn't that fact. Compose won't retarget the intake recording onto the
+posting surface.
+
+## Sequence the recordings with compose
+
+`openadapt flow compose` writes a parent directory of compiled children:
+
+```bash
+openadapt flow compose \
+ --child intake=./intake-bundle \
+ --child posting=./posting-bundle \
+ --handoff intake.patient_id=posting.patient_id \
+ --out composed
+openadapt flow certify composed --policy clinical-write
+openadapt flow run composed --config deploy.yaml
+```
+
+Default order is `--child` order. `--after NAME=PRED` declares a DAG; a cycle
+is refused at authoring. Child B starts only after child A ends `VERIFIED`, or
+a halt class you named with `--allow-halt NAME=OUTCOME`.
+
+Handoffs copy parameter values that A's confirmed effect contract already
+bound. Missing evidence stops the run. The parent won't guess a URL.
+
+The on-disk form is `composed/composition.json` (schema
+`openadapt.composition/v1`) plus `composed/children//`. Subflows and
+worklists stay inside each child. `replay` refuses this directory:
+
+```text
+replay refuses a composition artifact; use `openadapt-flow run`
+```
+
+`visualize` on `composed` shows the child bundles, the handoff edges, and a
+terminal labeled End of declared steps. Open `intake-bundle` if you need
+intake's program graph.
+
+```bash
+openadapt flow visualize composed -o composed.html
+openadapt flow visualize intake-bundle -o intake.html
+```
+
+Flags and the `certify` / `run` path are in the
+[CLI reference](../reference/cli.md#compose).
+
+## Qualify and admit each child
+
+Compose sequences recordings. A ProcessContract parent will refuse those
+copies. [Qualify each child](qualify-a-workflow.md) on its own surface, run
+its counted campaign, and keep the signed
+`openadapt.qualification-admission/v1` envelope. The envelope's
+`admission_id` is a UUID. It is distinct from `runtime_validation_id`.
+
+A recording you compiled five minutes ago and forgot to qualify is not a
+process child.
+
+## Sequence admitted capabilities with process
+
+After both envelopes exist, author the process parent:
+
+```bash
+openadapt flow process \
+ --child intake=./intake-bundle \
+ --admission intake=./intake-admission.json \
+ --child posting=./posting-bundle \
+ --admission posting=./posting-admission.json \
+ --handoff intake.patient_id=posting.patient_id \
+ --out process-parent
+openadapt flow visualize process-parent -o process.html
+```
+
+`--handoff`, `--after`, and `--allow-halt` have the same shape as `compose`.
+`--child NAME=BUNDLE` is the admitted bundle. `--admission NAME=ENVELOPE` is
+that child's signed `openadapt.qualification-admission/v1` file. A compose
+child path under `composition.json` is not an envelope. The parent file is
+`process-parent/process-contract.json`, schema
+`openadapt.process-contract/v0`. It points at the envelopes. It doesn't copy
+recordings. It doesn't become a ProgramGraph.
+
+Pointing `process` at a `composition.json` directory is refused. That check is
+what keeps unqualified recordings out of a process receipt.
+
+Each child runs through [OpenAdapt Execute](../commercial/execute-api.md)
+with that child's envelope, qualification binding, and its own idempotency
+key. `replay` of the process parent is refused.
+
+`visualize` on the process directory shows admitted children, handoff edges,
+and End of declared steps. That label means the declared sequence ended. It
+doesn't mean `VERIFIED`. Parent `VERIFIED` requires every child `VERIFIED`
+and zero model calls.
+
+Open the child bundle when you need its steps. The parent view won't inline
+them.
diff --git a/docs/guides/index.md b/docs/guides/index.md
index 092c950..0e45e2b 100644
--- a/docs/guides/index.md
+++ b/docs/guides/index.md
@@ -19,6 +19,11 @@ How-to guides for real deployments. Each assumes you have
Recover a parameterized program from several demonstrations, and run it
over a worklist.
+- [__Sequence work across two applications__](compose-multi-application.md)
+
+ Record one program per surface, sequence the compiled recordings with
+ `compose`, then after admission author a ProcessContract parent.
+
- [__Write and enforce a policy__](policy-and-certification.md)
Use `lint` to find gaps and `certify` to refuse a bundle that violates the
diff --git a/docs/reference/cli.md b/docs/reference/cli.md
index eaed15c..22721e9 100644
--- a/docs/reference/cli.md
+++ b/docs/reference/cli.md
@@ -18,6 +18,8 @@ is a subcommand of `openadapt flow`.
| [`compile`](#compile) | Compile a recording into a workflow bundle | 0 |
| [`induce`](#induce) | Induce a parameterized program from **multiple** recordings | 0 if certified, 2 if underdetermined |
| [`for-each`](#for-each) | Author a data-driven **loop** bundle: run one demonstration once per worklist record | 0 on success, nonzero on a mapping error |
+| [`compose`](#compose) | Author a parent sequencer from compiled child bundles and a handoff contract | 0 on success, nonzero on an authoring refusal |
+| [`process`](#process) | Author a ProcessContract parent over independently admitted capabilities | 0 on success, nonzero on an authoring refusal |
| [`replay`](#replay) | Replay a bundle, locally and deterministically | 0 on success, 1 on failure |
| [`run`](#run) | Execute a bundle through the regulated admission gate | 0 success, 1 execution halt, 2 refusal |
| [`resume`](#resume) | Resume a durably-paused run from its last checkpoint | 0 on success, 1/3 otherwise |
@@ -33,7 +35,7 @@ is a subcommand of `openadapt flow`.
| [`push`](#push) | Explicitly upload a recording or bundle to a control plane | 0/1 |
| [`validate-hosted`](#validate-hosted) | Bind local validation evidence to a one-time hosted challenge | 0/1 |
| [`report-break`](#report-break) | Send a scrubbed, schema-minimized halt descriptor | 0/1 |
-| [`visualize`](#visualize) | Render a bundle's program graph (steps, ladder, gates, halts) | 0 |
+| [`visualize`](#visualize) | Render a compiled bundle, a compose parent, or a process parent | 0 |
| [`bench`](#bench) | Replay a bundle N times against the sample app and aggregate | 0 if all pass |
| [`benchmark`](#benchmark) | Compare compiled replay vs a computer-use agent | 0 |
| [`emit-skill`](#emit) | Emit an Agent Skills folder for a bundle | 0 |
@@ -201,13 +203,83 @@ no demonstrated default, a ragged worklist, or a worklist longer than the bound
all **fail loudly** and write no bundle. Once authored, drive the loop with
[`replay --worklist`](#replay) or [`run --worklist`](#run).
+## compose
+
+Author a parent sequencer from two or more compiled child bundles and a handoff
+contract. Each child stays bound to the surface you recorded it on. The output
+is a directory with `composition.json` (`openadapt.composition/v1`) plus copied
+children. See
+[Sequence work across two applications](../guides/compose-multi-application.md).
+
+```bash
+openadapt flow compose \
+ --child intake=./intake-bundle \
+ --child posting=./posting-bundle \
+ --handoff intake.patient_id=posting.patient_id \
+ --out composed
+```
+
+| Argument / flag | Description |
+|---|---|
+| `--child NAME=PATH` | Named compiled child bundle (repeat; at least two). |
+| `--handoff FROM.source=TO.target` | Copy a predecessor's effect-bound parameter into a successor parameter. Repeatable. Missing evidence stops the run. |
+| `--after NAME=PRED[,PRED]` | Explicit DAG predecessors for NAME. Omit to run children in `--child` order. |
+| `--allow-halt NAME=OUTCOME` | Let NAME start when a predecessor ended OUTCOME instead of `VERIFIED`. `VERIFIED` is always allowed. |
+| `--out` (required) | Output composition directory |
+| `--name` | Composition name |
+
+`certify` and `run` execute this directory. Each child keeps its recorded
+surface. `replay` refuses a composition artifact; use `run`. A single child, a
+cycle, an unknown handoff target, and a source that is not effect-bound are
+refused at authoring.
+
+## process
+
+Author a ProcessContract parent over independently admitted capabilities.
+Schema `openadapt.process-contract/v0`. `--child NAME=BUNDLE` is the admitted
+bundle. `--admission NAME=ENVELOPE` is that child's signed
+`openadapt.qualification-admission/v1` file. A compose child under
+`composition.json` is not an envelope. The parent points at those admissions.
+It doesn't copy recordings. See
+[Process contracts](../concepts/process-contract.md).
+
+```bash
+openadapt flow process \
+ --child intake=./intake-bundle \
+ --admission intake=./intake-admission.json \
+ --child posting=./posting-bundle \
+ --admission posting=./posting-admission.json \
+ --handoff intake.patient_id=posting.patient_id \
+ --out process-parent
+```
+
+| Argument / flag | Description |
+|---|---|
+| `--child NAME=BUNDLE` | Named admitted child bundle (repeat; at least two). |
+| `--admission NAME=ENVELOPE` | Signed qualification-admission file for NAME. Required for every `--child`. |
+| `--handoff FROM.source=TO.target` | Copy a predecessor's effect-bound parameter into a successor parameter. Repeatable. Missing evidence stops the run. |
+| `--after NAME=PRED[,PRED]` | Explicit DAG predecessors for NAME. Omit to run children in `--child` order. |
+| `--allow-halt NAME=OUTCOME` | Let NAME start when a predecessor ended OUTCOME instead of `VERIFIED`. `VERIFIED` is always allowed. |
+| `--out` (required) | Output process-contract directory |
+| `--name` | Process name |
+
+`--handoff`, `--after`, and `--allow-halt` have the same shape as
+[`compose`](#compose). Pointing `process` at a `composition.json` directory is
+refused. Each child runs through
+[OpenAdapt Execute](../commercial/execute-api.md) with that child's envelope.
+`replay` of the process parent is refused. `visualize` on the directory shows
+admitted children, handoff edges, and a terminal labeled End of declared
+steps.
+
## replay
Replay a bundle against the substrate chosen by the [backend selector](#backend).
On the default `web` backend, `--url` names the target app and, with no `--url`,
replay serves the bundled sample app. For Windows, macOS, Linux, RDP, or Citrix,
select its backend and exact target flags instead of `--url`. The example below
-replays the web substrate.
+replays the web substrate. `replay` refuses a compose parent and a
+ProcessContract parent; composed recordings run with [`run`](#run), and process
+children run through [Execute](../commercial/execute-api.md).
```bash
openadapt flow replay bundle --url https://your.app --param note="Follow-up"
@@ -252,8 +324,9 @@ The same executor as [`replay`](#replay), behind a regulated admission gate:
the bundle must pass policy, identity coverage, effect coverage, approval,
encryption, and manifest-integrity checks before any action executes. Backend,
effect verification, API actuation, durable runtime, and policy come from
-`--config`. The demo-only `--drift` teaching aid is not offered here. See
-[Run a deployment](../guides/run-a-deployment.md).
+`--config`. The demo-only `--drift` teaching aid is not offered here. If the
+path is a compose parent, `run` executes each child in order under the handoff
+contract. See [Run a deployment](../guides/run-a-deployment.md).
```bash
openadapt flow run bundle --config deployment.yaml
@@ -261,7 +334,7 @@ openadapt flow run bundle --config deployment.yaml
| Flag | Description |
|---|---|
-| `bundle` (positional) | Workflow bundle directory |
+| `bundle` (positional) | Workflow bundle directory, or a compose parent directory |
| `--url` | Target app URL (default: `backend.url` from `--config`) |
| `--run-dir` | Run output directory (default `runs/replay-`) |
| `--param K=V` | Parameter substitution. Repeatable. |
@@ -386,12 +459,13 @@ openadapt flow certify bundle --config deployment.yaml
| Flag | Description |
|---|---|
-| `bundle` (positional) | Workflow bundle directory |
+| `bundle` (positional) | Workflow bundle directory, or a compose parent directory |
| `--policy` | Policy YAML path, or a built-in name (`permissive`, `clinical-write`). Defaults to `policy.policy` from `--config`. |
| `--config YAML` | [Deployment config](deployment-config.md) to read the policy from when `--policy` is omitted, so one file both certifies and runs the bundle |
Provide `--policy` or a `--config` that sets `policy.policy`; certify errors if
-neither supplies a policy. Exits 2 when the bundle fails certification — the
+neither supplies a policy. If the path is a compose parent, certify evaluates
+the policy on each child. Exits 2 when the bundle fails certification: the
gate refusing an unsafe bundle, not an error in your setup
([exit codes](run-outcomes.md#cli-exit-codes)).
@@ -679,28 +753,35 @@ sanitation protocol, and destination-aware boundary.
## visualize
See what a demonstration compiled **into**, before it runs. `visualize` reads a
-bundle and renders its program graph: the ordered steps, the resolution ladder
-each step will try, where an identity gate is armed, which writes carry an effect
-check, and every point the run can halt. It writes one of three formats from the
-same graph spec, so the CLI, Cloud, and desktop surfaces all show the same thing.
-See [Visualize a compiled program](../concepts/program-visualizer.md).
+compiled bundle and renders its program graph: the ordered steps, the resolution
+ladder each step will try, where an identity gate is armed, which writes carry
+an effect check, and every point the run can halt. Point it at a compose
+directory (`composition.json`) and it shows child bundles plus handoff edges.
+Point it at a process-contract directory and it shows admitted children plus
+handoff edges. The parent terminal is labeled End of declared steps. That isn't
+`VERIFIED`. See
+[Read a compiled program](../concepts/program-visualizer.md).
```bash
openadapt flow visualize bundle -o graph.html # self-contained page
openadapt flow visualize bundle --profile remote-safe -o review.html
openadapt flow visualize bundle --format mermaid # flowchart source, to stdout
openadapt flow visualize bundle --format json # the shared graph spec
+openadapt flow visualize composed -o composed.html
+openadapt flow visualize process-parent -o process.html
```
| Flag | Description |
|---|---|
-| `bundle` (positional) | Workflow bundle directory |
+| `bundle` (positional) | Workflow bundle directory, compose parent directory, or process-contract directory |
| `--format {html,mermaid,json}` | `html` (default): a self-contained, offline-openable page. `mermaid`: flowchart source for Markdown and docs. `json`: the shared program-graph spec every surface renders. |
| `--profile {operator-local,remote-safe,public-synthetic,sanitized-derivative}` | Select the fields allowed in the output. Non-local profiles keep topology and remove recorded values, target text, selectors, URLs, guard text, and local provenance. A projection does not sanitize the source bundle. |
| `-o`, `--out FILE` | Write to a file instead of stdout (parent directories are created) |
Reading is offline and side-effect-free: `visualize` never runs the workflow, so
-it is safe to point at any bundle, including one that would refuse to certify.
+it is safe to point at any bundle, including one that would refuse to certify. A
+compose parent and a process parent aren't one ProgramGraph; open a child
+bundle for that child's steps.
## bench
diff --git a/mkdocs.yml b/mkdocs.yml
index d0de22c..f76d0ef 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -140,10 +140,12 @@ nav:
- Multi-trace induction: concepts/multi-trace-induction.md
- Run a workflow for each record: guides/data-driven-loops.md
- Visualize a compiled program: concepts/program-visualizer.md
+ - A task that spans two apps: guides/compose-multi-application.md
- Parameters and secrets: guides/parameters-and-secrets.md
- The capability ladder: concepts/capability-ladder.md
- Write and enforce a policy: guides/policy-and-certification.md
- Qualify a workflow: guides/qualify-a-workflow.md
+ - Process contracts: concepts/process-contract.md
- Policy and certify: concepts/policy-and-certify.md
- Effect verification: concepts/effect-verification.md
- The identity gate: concepts/identity-gate.md