# Workflow V1 language reference

Reference URL: `https://docs.usefulmachinery.com/reference/workflow-v1.md`

Raw schema: `https://docs.usefulmachinery.com/schemas/workflow-v1.schema.json`

Workflow V1 is the closed YAML language accepted by `um workflow validate`. This reference summarizes its author-visible structure and semantics. Use the [agent authoring guide](https://docs.usefulmachinery.com/agent/workflow-authoring.md) for a safe repository procedure and the [first-workflow guide](https://docs.usefulmachinery.com/agent/first-workflow.md) for the guarded onboarding starter.

## Schema selection and authoritative validation

Configure a YAML language server with this comment at the top of a workflow:

```yaml
# yaml-language-server: $schema=https://docs.usefulmachinery.com/schemas/workflow-v1.schema.json
schemaVersion: 1
steps:
  check:
    kind: cmd
    command:
      argv: ["./scripts/check"]
```

The first line is a YAML comment for editor tooling. Do **not** add `$schema` as a root workflow property: the Workflow V1 root is closed and that property is invalid. The raw schema is the exact canonical structural schema packaged for public tooling.

The JSON Schema checks document structure. The supported CLI is authoritative for a complete bundle:

```sh
um workflow validate \
  --source-root ./repository \
  ./repository/.um/workflows/check.yaml \
  --json
```

`workflow validate` additionally checks graph semantics, types, references, path containment, failure policy, recovery capacity and handlers, agent-output cardinality, finalizer rules, and every resolved static prompt, recovery prompt, message, attachment, and result-schema file. A schema-only success does not establish that a bundle is valid. A valid definition also does not execute a step or prove harness installation, model access, credentials, or run readiness.

## Document shape

A workflow is one YAML 1.2 document representing JSON-shaped data.

| **Root field** | **Required** | **Meaning** |
| --- | --- | --- |
| `schemaVersion` | yes | The integer `1`. |
| `description` | no | Human-facing metadata with no execution effect. |
| `inputs` | no | A closed map of required named `text`, `json`, `file`, and `attachments` values. |
| `environmentPassthrough` | no | Unique host variable names forwarded to every command, agent, and recovery handler. |
| `agentProfiles` | no | Workflow-local, immutable harness configurations selected by agent nodes. |
| `steps` | yes | A mapping with at least one ordinary command or agent step. |
| `finalizers` | no | A second DAG considered after all ordinary steps are terminal. |
| `exports` | no | Stable public names selecting declared node outputs. |

There is no workflow ID or source-root field. Identity comes from the explicitly supplied workflow source root and selected YAML path. `steps` and `finalizers` may each contain at most 256 entries, their combined node count may not exceed 256, and `exports` may contain at most 4,096 entries.

Workflow children inherit only `PATH`, `HOME`, `USER`, `LOGNAME`, `LANG`, `LC_ALL`,
`LC_CTYPE`, `TERM`, and `TMPDIR` by default. Declare other host variables at the
workflow root, for example `environmentPassthrough: [MY_TOKEN]`. Names are unique
ASCII shell variable names (letter or underscore followed by letters, digits, or
underscores). Missing variables remain unset. Engine-reserved `UM_*` names, `SCHERZO_CONTINUATION_CONTEXT`, and
runner-private Git, SSH, and GitHub credential/helper names cannot be passed through.
Runner source revision metadata is injected by the engine, not inherited from the
host. All workflow code can read a declared value, including recovery handlers;
only declare values the workflow is authorized to use. Guard and recovery scratch
use `TMPDIR` when set (otherwise the system temporary directory). The guard launch
manifest does not persist environment values.

V1 permits comments and ordinary block or flow YAML syntax. It rejects duplicate or non-string mapping keys, anchors, aliases, merge keys, custom tags, and multiple YAML documents.

### Identifiers

Agent-profile names, ordinary step IDs, finalizer IDs, command-input names, output names, and export names are lower-camel-case ASCII identifiers:

```text
^[a-z][A-Za-z0-9]*$
```

Each identifier has 1 through 64 characters. Ordinary step and finalizer IDs share one namespace. A reference to `outputs.plan.result` means output `result` from node `plan`.

## Path bases

Validation receives two separate roots:

- The **workflow source root** is supplied with `--source-root`. The workflow YAML must be inside it.
- The **execution root** is supplied when a workflow runs. Commands, agents, and runtime outputs operate below it.

Path fields use these bases:

| **Field** | **Base and validation** |
| --- | --- |
| `agent.systemPrompt` or recovery-agent `handler.prompt` | Relative to the workflow YAML directory; must resolve to a retained UTF-8 file inside the workflow source root. |
| Message `{ file: ... }` | Relative to the workflow YAML directory; text entries must be UTF-8 and every file remains inside the source root. |
| JSON input/output `schema` | Relative to the workflow YAML directory; must be a retained, self-contained Draft 2020-12 JSON Schema inside the source root. |
| Node `cwd` | Relative to the execution root; defaults to the execution root and cannot contain parent traversal. |
| Path output `path` | Relative to the execution root, even when the producer has `cwd`; cannot contain parent traversal. |

Static source paths may contain `..` only when normalization and symbolic-link resolution remain inside the workflow source root. Runtime paths may not contain `..`. The resolver loads and pins static bytes during validation; execution does not later reread mutable workflow-source files. Runtime directory and output existence are checked when used.

## Required named inputs and typed references

The root `inputs` map declares the exact required invocation interface. This release supports these closed declarations:

```yaml
inputs:
  request:
    kind: json
    schema: schemas/request.schema.json
  notes:
    kind: text
  payload:
    kind: file
    mediaType: application/octet-stream
  evidence:
    kind: attachments
```

Each key is an identifier. Missing, extra, or differently typed bindings reject before execution. Empty Text, an empty File, JSON `null`, and an explicitly supplied empty attachment collection are present values; omission is not an empty value. A JSON declaration may omit `schema` or name one retained, self-contained Draft 2020-12 schema. A File declaration may omit `mediaType` or constrain the supplied explicit media type by exact string equality; V1 does not infer, normalize, wildcard-match, or sniff media types.

A reference is an object with exactly one field:

```yaml
ref: outputs.plan.result
```

Ordinary-node values may use `inputs.<declared-name>` or `outputs.<ordinary-step-id>.<output-name>`. Finalizers may also consume ordinary or finalizer outputs. `finalization.context` is an engine-owned JSON value (schema version 2, with run/attempt identity and bounded classified ordinary failure facts; missing projected diagnostics are null) available only to a finalizer command `inputs` binding or a finalizer agent attachment. Exports accept only `outputs.<node-id>.<output-name>`.

References preserve types; V1 performs no interpolation, string conversion, JSON conversion, or implicit file reading.

| **Source type** | **V1 sources** | **Command **`inputs` | **Agent **`message.text` | **Agent **`message.attachments` |
| --- | --- | --- | --- | --- |
| Text | declared `kind: text` inputs, `kind: text` outputs | yes | yes | no |
| Attachment collection | declared `kind: attachments` inputs | yes | no | yes; members splice in order |
| JSON | declared `kind: json` inputs, `kind: json` outputs, finalizer-only `finalization.context` | yes | no | yes, as `application/json` |
| File | declared `kind: file` inputs and `kind: file` outputs | yes | no | yes, preserving media type |
| Git branch | `kind: git_branch` output | no | no | no |

Inputs and profiles are data, not authority. They grant no filesystem, process, network, tool, secret, credential, or repository permissions.

## Ordinary steps and dependencies

Every ordinary step has `kind: cmd` or `kind: agent`. Both kinds may declare:

| **Field** | **Meaning** |
| --- | --- |
| `failurePolicy` | `required` or `advisory`; omission means `required`. |
| `recovery` | Optional bounded unchanged-target rechecks, with no handler or one command/fresh-agent handler. |
| `dependsOn` | Unique ordinary step IDs used as explicit control prerequisites. |
| `cwd` | Execution-root-relative working directory. |
| `outputs` | Named values captured atomically after successful execution. |

The ordinary graph combines two edge forms:

- `dependsOn` creates a control edge for ordering without consuming data.
- Every `outputs.<step>.<output>` reference in a command input or agent message creates a direct inferred data edge from that producer.

Do not repeat an inferred data dependency in `dependsOn`. Workflow input references create no step edge, and exports select outputs without creating edges. The resolved union must be acyclic; self-dependencies, unknown nodes or outputs, and duplicate explicit dependencies are invalid.

A control edge is satisfied by a successful producer or by an advisory producer's truthful failed disposition. A data edge requires the selected output. A required node cannot consume an advisory node's output; make the consumer advisory or use a control-only edge.

## Conditions

Ordinary nodes and finalizers support an optional `condition`. Conditions use `all`, `any`, `not`, `equals`, `exists`, or `disposition`; the closed predicate union rejects unknown forms.

Operands are declared Text or JSON references, optional JSON Pointers, or literal `value` operands. Equality never coerces kinds. Ordinary conditions can reference workflow inputs, ordinary outputs, and ordinary terminal dispositions. Finalizers additionally admit finalizer outputs, finalizer dispositions, and the supported finalization context.

```yaml
condition:
  equals:
    - ref: inputs.inspectMode
    - value: inspect
```

All referenced outputs and dispositions contribute static prerequisites even if evaluation short-circuits. The condition is evaluated at most once before body readiness. False commits `skipped`; unavailable data blocks its gate, and an evaluated missing Pointer fails condition evaluation. An export from a skipped producer is unavailable.

Bounds are depth 16, 256 predicate nodes per condition, 64 children per `all` or `any`, 4,096 predicate nodes per workflow, and 1,024 exact prerequisite descriptors per node. Conditions cannot read files, environment values, credentials, logs, provider state, clocks, randomness, or mutable source.

A finalizer’s `when` remains the ordinary-phase outcome trigger. Its optional `condition` is a separate gate. Required-failure policy still applies. See [dependencies and conditions](/guides/conditions.md) for a complete example.

### Step recovery

`recovery.retries` is an integer from 1 through 10 and counts additional target executions. Omitting `handler` immediately rechecks the unchanged target. A command handler declares `kind: cmd`, one nonempty `command.argv`, and optional `cwd`. A fresh agent handler declares `kind: agent`, one existing `profile`, one retained UTF-8 `prompt`, and optional `cwd`:

```yaml
steps:
  verify:
    kind: cmd
    recovery:
      retries: 2
      handler:
        kind: cmd
        command:
          argv: ["./scripts/repair-generated-state"]
    command:
      argv: ["./scripts/check-generated-state"]
```

A recovery handler is not a DAG node. It has no inputs, dependencies, outputs, exports, failure policy, or nested recovery. Before a command handler starts, Useful Machinery exposes a bounded read-only Recovery Context Schema 1 document at the engine-owned absolute path in `UM_RECOVERY_CONTEXT` and a fresh private result path in `UM_RECOVERY_RESULT`. Context diagnostics are untrusted, and handlers must ignore unknown context fields and tokens. After repair, the command must write exactly one closed decision document of at most 16 KiB to the result path:

```json
{
  "schemaVersion": 1,
  "decision": "recheck",
  "summary": "Repaired generated state.",
  "reason": "The unchanged target should now pass."
}
```

`decision` is exactly `recheck` or `gave_up`; `summary` and `reason` are required, nonempty UTF-8 strings of at most 4,096 bytes. The context and result are private handler ABI, never inputs, outputs, exports, or published artifacts. A missing, nonregular, symlinked, oversized, malformed, or invalid result fails the handler.

An agent handler's referenced profile and compatible installed harness are static admission and runner-placement requirements even when the target succeeds initially and recovery never activates. An admitted handler may mutate the shared execution root, then a `recheck` decision runs the original target with the same resolved command/profile, inputs, environment, `cwd`, outputs, and policy but fresh process/session/staging identities. `gave_up` and any handler failure stop recovery. Failed target output candidates never commit; only the target execution supplying terminal success owns outputs. Finalizers cannot declare recovery and wait until all ordinary recovery work quiesces.

Recovery and explicit full retry are at-least-once for external effects. Supply a stable domain idempotency key through an existing explicit input or admitted environment when repetition is not intrinsically safe. Useful Machinery invocation IDs, target execution numbers, rounds, process IDs, and native-session IDs are lifecycle facts, not author idempotency keys. V1 has no conditional recovery, transient classification, delay/backoff, handler retry, capture-only retry, workspace rollback, session continuation, automatic full retry, or exactly-once guarantee.

### Command nodes

A command node has this shape:

```yaml
steps:
  test:
    kind: cmd
    cwd: packages/api
    inputs:
      plan:
        ref: outputs.plan.result
    command:
      argv: ["./scripts/test.sh", "--unit"]
    outputs:
      report:
        kind: file
        from: path
        path: packages/api/artifacts/test-report.xml
        mediaType: application/junit+xml
```

`command.argv` is required and nonempty. `argv[0]` is a nonempty program name or path; later elements are literal strings and may be empty. Useful Machinery passes the vector directly to the operating system. It performs no shell parsing, interpolation, glob expansion, redirection, pipeline construction, or implicit shell selection. A bare program uses the admitted `PATH`; a relative path containing a separator resolves from the node `cwd`. Exit status zero and successful capture of every declared output are required for node success.

Only commands accept `inputs`. At runtime, each command receives its bound values in a fresh, engine-owned, logically read-only directory named by `UM_STEP_INPUTS`, with a `manifest.json` describing text, JSON, file, and attachment-collection payloads. Input names become safe ABI names; they are not environment-variable interpolation. A command without inputs receives no `UM_STEP_INPUTS`.

### Agent profiles and nodes

An agent node selects one root profile:

```yaml
inputs:
  request:
    kind: text
  updates:
    kind: attachments

agentProfiles:
  coding:
    harness:
      kind: pi
      config:
        model: provider/model
        thinking: high

steps:
  implement:
    kind: agent
    agent:
      profile: coding
      systemPrompt: ../prompts/implement.md
      message:
        text:
          - file: ../prompts/context.md
          - ref: inputs.request
        attachments:
          - ref: inputs.updates
```

`profile`, `systemPrompt`, and nonempty `message.text` are required. An invoker may
replace a declared profile as a whole for one run with `um workflow run --agent-profiles
<PATH>`: a closed document with `schemaVersion: 1` and an `agentProfiles` map keyed by
the workflow's declared profile names, each value a complete harness definition in this
same grammar. Replacement changes nothing else in the definition and does not alter the
source-closure digest; its structural contract is
<https://docs.usefulmachinery.com/schemas/agent-profile-replacement-v1.schema.json>.
The system prompt is
trusted static workflow text. Message text is an ordered list of static UTF-8 files and
text references, joined with two newline characters. Attachments are an optional ordered
list of static files and attachment-compatible references.

Inline harness definitions are invalid. A profile reuses configuration only: every agent node starts a fresh harness process, session, and conversation. Definition validation checks profile syntax but does not query executables, providers, credentials, models, or model capabilities.

Workflow V1 supports exactly these closed harness profiles:

| **Harness kind** | **Runtime profile** | **Required config** |
| --- | --- | --- |
| `pi` | PiJsonV1 | nonempty native `model`; `thinking` is `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max` |
| `claude_code` | ClaudeCodeStreamJsonV1 | nonempty native `model`; `effort` is `low`, `medium`, `high`, `xhigh`, or `max` |
| `codex` | CodexAppServerV1 | nonempty native `model`; nonempty native `effort` |

Codex execution admits capability-compatible undecorated stable releases in
`>=0.147.0 <0.164.0`; the exact repository qualification anchor is `0.163.0`. A
capability-compatible stable release of any harness newer than its qualification release
(Pi 1.3.0, Claude Code 2.1.310, Codex 0.163.0) is admitted as untested: it runs under the
same profile, a local run writes one stderr advisory per selected untested harness, and
the harness doctor check passes with `qualification` `untested`.

Each config is closed. There is no harness fallback, substitution, or translation. Runner doctor check IDs are `execution.harness.pi-json-v1`, `execution.harness.claude-code-stream-json-v1`, and `execution.harness.codex-app-server-v1`; those checks establish installation compatibility, not provider access.

## Outputs and exports

Outputs are named under their producer:

| **Semantic **`kind` | **Acquisition **`from` | **Producer** | **Required fields** |
| --- | --- | --- | --- |
| `text` | `path` | command or agent | `path` |
| `text` | `agent_response` | agent | none |
| `json` | `path` | command or agent | `path`, `schema` |
| `json` | `agent_result` | agent | `schema` |
| `file` | `path` | command or agent | `path`, `mediaType` |
| `git_branch` | `workspace` | command or agent | none |

Every declaration contains exactly one `kind` and `from` and matches one row. A node may declare any number of node-locally distinct paths, at most one native agent source, and at most one workspace source. Commands cannot use native agent sources. Acquisition is erased after capture, so consumers and exports observe only `text`, `json`, `file`, or `git_branch`.

A JSON output schema must be a self-contained UTF-8 JSON Schema Draft 2020-12 document whose root contains:

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema"
}
```

This `$schema` belongs in the separate result-schema JSON file, not in workflow YAML. External schema references, nested schema resources, and dialect changes are invalid.

A path-backed output captures one required, bounded, no-follow regular file beneath the execution root. Text preserves exact UTF-8 bytes; JSON rejects duplicate members, validates its schema, and publishes compact ordered bytes; File preserves bytes and `mediaType`. A Git branch has only semantic `kind: git_branch` plus `from: workspace` and may be exported, but it cannot be a downstream command input or agent message value.

`exports` assigns stable public names without copying or converting outputs:

```yaml
exports:
  testReport:
    ref: outputs.test.report
```

An export source must be a declared output of a required ordinary node or finalizer. A finalizer export additionally requires `succeeded` in that finalizer's trigger set. Exports do not add graph edges. On failed or cancelled runs, captured exports remain available and unavailable exports carry a typed source-disposition reason.

An export of any kind may also declare `presentation` with `title`, `description`, or both. Each field is exactly one literal `value` or direct `ref` to a complete Text output:

```yaml
exports:
  changes:
    ref: outputs.implement.branch
    presentation:
      title:
        value: Review the implementation
      description:
        ref: outputs.writeup.summary
```

Presentation refs may come from required or advisory steps or finalizers whose `when` includes `succeeded`; unlike the export value source, they do not add a dependency or change success. Fields resolve independently: a failed or invalid prose source falls back to Cloud-generated text for only that field when an eligible branch is published. Title text is single-line and at most 255 Unicode scalar values; description text is at most 8,192 UTF-8 bytes. Leading and trailing ASCII spaces, tabs and line endings are trimmed. Unsafe controls and bidi format characters are rejected. Publication still chooses its provider, destination, references and immutable marker; workflow prose cannot override them. A GitHub Publication's stored metadata records each field's `workflow` or `default` source with the final provider-bound title and body. Replays use that snapshot rather than rereading the current result or recomputing a default. GitLab publication is not enabled by this syntax alone.

## Failure policies

`failurePolicy: required` is the default. The first required ordinary-node failure stops new ordinary starts; already authorized work may settle, dependents become blocked, and other ready unstarted nodes become not-run. The ordinary workflow outcome is failed.

An advisory failure remains visible but does not by itself fail the workflow or close ordinary scheduling. It satisfies control-only dependents but cannot supply a missing output. Advisory nodes cannot be export sources, and required consumers cannot consume their outputs.

V1 has no workflow-declared retry, timeout, loop, matrix, fan-out, concurrency, secret, environment, or workspace field. Native commands or harnesses may have their own behavior, but it does not add Workflow V1 syntax or automatic engine retry.

## Finalizers

`finalizers` defines a separate DAG considered only after every ordinary step is terminal. A finalizer reuses the command or agent body, `cwd`, outputs, and failure policy. It replaces `dependsOn` with:

| **Field** | **Meaning** |
| --- | --- |
| `when` | Nonempty unique set of ordinary-phase triggers: `succeeded`, `failed`, and/or `cancelled`; omission selects all three. |
| `after` | Unique finalizer IDs that must become terminal first; this is a control edge. |

The engine commits one immutable ordinary-phase trigger and a JSON `finalization.context`, then considers matching finalizers. Ordinary-output references are allowed and rely on the phase barrier. A finalizer-output reference infers a data edge within the finalizer DAG. An `after` target must be another finalizer; ordinary steps cannot refer to finalizers.

If a finalizer consumes another finalizer's output, the consumer's `when` set must be a subset of the producer's set. A missing referenced output blocks that consumer as `input_unavailable`. Independent finalizers may still run; one finalizer issue does not close scheduling for the rest.

Scheduling and terminal outcome are separate. After an ordinary `succeeded` trigger, a required finalizer failure or `input_unavailable` block is a required finalizer issue and the terminal workflow outcome is `failed`. Advisory finalizer issues remain visible but do not change the outcome. Finalizer issues do not replace an ordinary `failed` or `cancelled` outcome.

### Snippet — finalizer phase, not a complete bundle

This excerpt assumes the referenced scripts exist. It shows an all-trigger cleanup, engine context input, and finalizer-only control ordering:

```yaml
steps:
  build:
    kind: cmd
    command:
      argv: ["./scripts/build.sh"]

finalizers:
  cleanup:
    kind: cmd
    when: [succeeded, failed, cancelled]
    inputs:
      context:
        ref: finalization.context
    command:
      argv: ["./scripts/cleanup.sh"]

  audit:
    kind: cmd
    after: [cleanup]
    when: [succeeded, failed]
    failurePolicy: advisory
    command:
      argv: ["./scripts/audit.sh"]
```

`cleanup` is selected for every ordinary trigger and, because it omits `failurePolicy`, is required. If it fails after an ordinary `succeeded` trigger, the workflow outcome is `failed`. `audit` is excluded after ordinary cancellation and waits for any terminal cleanup disposition. Its advisory policy keeps an audit issue visible without changing the outcome. `finalization.context` adds no graph edge.

## Focused dataflow snippet

### Snippet — inferred data and explicit control, not a complete bundle

```yaml
steps:
  plan:
    kind: cmd
    command:
      argv: ["./scripts/plan.sh"]
    outputs:
      planFile:
        kind: file
        from: path
        path: artifacts/plan.json
        mediaType: application/json

  implement:
    kind: cmd
    inputs:
      plan:
        ref: outputs.plan.planFile
    command:
      argv: ["./scripts/implement.sh"]

  verify:
    kind: cmd
    dependsOn: [implement]
    command:
      argv: ["./scripts/verify.sh"]
```

The `plan -> implement` edge is inferred because `implement` consumes `planFile` through its `UM_STEP_INPUTS` view. The `implement -> verify` edge is explicit control ordering and transfers no value.

## Complete bundle: command-only check

The following is a complete, validation-ready source bundle. Materialize both files under one source root and validate the shown workflow path. The script is also sufficient for execution in a POSIX repository environment; execution is a separate authority.

```text
command-only/
  .um/workflows/check.yaml
  scripts/check.sh
```

### `.um/workflows/check.yaml`

<!-- workflow-reference-fixture:command-workflow:begin -->
```yaml
# yaml-language-server: $schema=https://docs.usefulmachinery.com/schemas/workflow-v1.schema.json
schemaVersion: 1
description: Run one deterministic check and export its report.

steps:
  check:
    kind: cmd
    command:
      argv: ["./scripts/check.sh"]
    outputs:
      report:
        kind: file
        from: path
        path: artifacts/check.txt
        mediaType: text/plain

exports:
  checkReport:
    ref: outputs.check.report
```
<!-- workflow-reference-fixture:command-workflow:end -->

### `scripts/check.sh`

<!-- workflow-reference-fixture:command-script:begin -->
```sh
#!/usr/bin/env sh
set -eu
mkdir -p artifacts
printf 'check passed\n' >artifacts/check.txt
```
<!-- workflow-reference-fixture:command-script:end -->

From the parent of `command-only/`:

```sh
um workflow validate \
  --source-root ./command-only \
  ./command-only/.um/workflows/check.yaml \
  --json
```

## Complete bundle: mixed implementation and review

This complete bundle demonstrates an agent, inferred dataflow into a command, explicit control ordering, text and attachment references, native Text, path-backed File, native JSON, and exports. The example profile is syntax-valid; replace its illustrative native model with an exact configured value before execution.

```text
mixed/
  .um/workflows/implement-review.yaml
  .um/prompts/implement.md
  .um/prompts/review.md
  .um/schemas/review.schema.json
  scripts/check.sh
```

### `.um/workflows/implement-review.yaml`

<!-- workflow-reference-fixture:mixed-workflow:begin -->
```yaml
# yaml-language-server: $schema=https://docs.usefulmachinery.com/schemas/workflow-v1.schema.json
schemaVersion: 1
description: Implement a request, check it, and return a structured review.

inputs:
  request:
    kind: text
  evidence:
    kind: attachments

agentProfiles:
  coding:
    harness:
      kind: pi
      config:
        model: provider/model
        thinking: high

steps:
  implement:
    kind: agent
    agent:
      profile: coding
      systemPrompt: ../prompts/implement.md
      message:
        text:
          - ref: inputs.request
        attachments:
          - ref: inputs.evidence
    outputs:
      summary:
        kind: text
        from: agent_response

  check:
    kind: cmd
    inputs:
      summary:
        ref: outputs.implement.summary
    command:
      argv: ["./scripts/check.sh"]
    outputs:
      report:
        kind: file
        from: path
        path: artifacts/check.txt
        mediaType: text/plain

  review:
    kind: agent
    dependsOn: [check]
    agent:
      profile: coding
      systemPrompt: ../prompts/review.md
      message:
        text:
          - ref: outputs.implement.summary
    outputs:
      result:
        kind: json
        from: agent_result
        schema: ../schemas/review.schema.json

exports:
  implementationSummary:
    ref: outputs.implement.summary
  checkReport:
    ref: outputs.check.report
  reviewResult:
    ref: outputs.review.result
```
<!-- workflow-reference-fixture:mixed-workflow:end -->

### `.um/prompts/implement.md`

<!-- workflow-reference-fixture:mixed-implement-prompt:begin -->
```markdown
Implement only the bounded request in the invocation message. Follow repository
instructions, preserve unrelated changes, and finish with a concise summary.
```
<!-- workflow-reference-fixture:mixed-implement-prompt:end -->

### `.um/prompts/review.md`

<!-- workflow-reference-fixture:mixed-review-prompt:begin -->
```markdown
Review the implementation summary after the deterministic check gate. Submit one
structured result with a recommendation and concise rationale. Do not modify repository
files.
```
<!-- workflow-reference-fixture:mixed-review-prompt:end -->

### `.um/schemas/review.schema.json`

<!-- workflow-reference-fixture:mixed-review-schema:begin -->
```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "additionalProperties": false,
  "required": ["recommendation", "rationale"],
  "properties": {
    "recommendation": {
      "enum": ["approve", "changes_requested"]
    },
    "rationale": {
      "type": "string",
      "minLength": 1,
      "maxLength": 2000
    }
  }
}
```
<!-- workflow-reference-fixture:mixed-review-schema:end -->

### `scripts/check.sh`

<!-- workflow-reference-fixture:mixed-script:begin -->
```sh
#!/usr/bin/env sh
set -eu
inputs=${UM_STEP_INPUTS:?}
test -f "$inputs/values/summary"
mkdir -p artifacts
printf 'check passed\n' >artifacts/check.txt
```
<!-- workflow-reference-fixture:mixed-script:end -->

From the parent of `mixed/`:

```sh
um workflow validate \
  --source-root ./mixed \
  ./mixed/.um/workflows/implement-review.yaml \
  --json
```

## Closed-language behavior

Workflow V1 rejects unknown root fields, node fields, output kinds, harnesses, config fields, source forms, references, and enum values. It does not use unknown fields as extensions and does not silently ignore them. In particular, V1 has no root `$schema`, fixed import namespace, inline harnesses, shell-command string, interpolation syntax, secret reference, per-node environment, timeout, loop, matrix, directory output, glob output, or optional input/output.

Use the [raw structural schema](https://docs.usefulmachinery.com/schemas/workflow-v1.schema.json) for editor assistance, then always run `workflow validate --json` against the complete materialized source bundle before treating a definition as valid.
