# Definition Authoring
{{< tier level="sovereign" note="Workflow authoring turns repeatable delivery practice into validated, versioned automation." >}}

{{< callout type="info" >}}
This is the advanced path. Start with the [Workflow Builder](/valdr/docs/ui/workflows/) if you prefer visual authoring.
{{< /callout >}}

Author a definition when a repeated process should become portable, reviewable, and executable across projects. The YAML records its inputs, dependencies, decisions, and published results as one versioned contract.

## File placement

Pack-authored workflows are standalone `.workflow.yaml` or `.workflow.yml` files under a `pack.yaml` include root:

```text
example-workflows/
  pack.yaml
  workflows/
    task-delivery-review.workflow.yaml
```

## Minimal definition

```yaml
schemaVersion: 1.0
workflow:
  key: example.task.delivery-review
  name: Example Task Delivery Review
  version: 0.1.0
  pack: example-workflows
  source:
    kind: capability
    capabilityKey: example.workflow.task-delivery-review
  start:
    mode: manual
  inputs:
    taskKey:
      type: string
      required: true
  outputs:
    taskKey:
      type: string
      required: true
  outputMappings:
    taskKey: "${workflow.inputs.taskKey}"
  steps:
    - key: load_task
      name: Load task
      kind: tool
      tool:
        id: pm_task
        action: get
      inputs:
        taskKey: "${workflow.inputs.taskKey}"
      outputs:
        status: "$.normalized.task.status"
```

Every workflow needs a schema version, workflow key, name, version, start configuration, inputs, and steps. Add only the outputs the completed run should publish.

## Definition fields

| Field | Required | Purpose |
| --- | --- | --- |
| `schemaVersion` | Yes | Definition format; use `1.0` |
| `workflow.key` | Yes | Stable workflow identity |
| `workflow.name` | Yes | Name shown in Valdr UI |
| `workflow.description` | No | Short explanation of the workflow outcome |
| `workflow.version` | Yes | Exact version selected by runs and parent workflows |
| `workflow.pack` | Pack source | Owning pack key |
| `workflow.source` | Pack source | Capability that owns the workflow source |
| `workflow.start.mode` | Yes | Use `manual` |
| `workflow.start.suggestedWhen` | No | Catalog hints for when the workflow is useful |
| `workflow.inputs` | Yes | Values callers may or must provide |
| `workflow.outputs` | No | Results the completed run publishes |
| `workflow.outputMappings` | For required outputs | Maps each required result from an input or step output |
| `workflow.steps` | Yes | Executable steps and their dependencies |

Input and output descriptors support `string`, `boolean`, `number`, `integer`, `object`, and `array`:

```yaml
inputs:
  taskKey:
    type: string
    required: true
    description: Task to deliver
  includeDraft:
    type: boolean
    default: true
```

Use `description` to make Builder launch forms self-explanatory. A default makes an optional input usable without a supplied value. Output names must match `[A-Za-z_][A-Za-z0-9_]*`. Every required workflow output needs an `outputMappings` entry unless its descriptor supplies a type-correct `default`.

## Version changes deliberately

Treat the workflow key and version as a published identity. When behavior changes, create a new version so existing runs and parent workflows keep the behavior they selected.

Prefer updating pack workflows through their source pack. A guarded same-version overwrite is supported for local and pack-managed definitions when the caller preserves the exact provenance, sets `overwrite: true`, and supplies the current content hash. Existing runs keep their frozen routing and child revisions, and reruns reuse those frozen revisions. An eligible same-run manual retry of a saved Command step is the deliberate exception: its next attempt can snapshot the active saved definition's `command`, `cwd`, and `timeoutSeconds`; prior attempts remain unchanged. For a one-off variation, use **Use as starting point** in Valdr UI and save it under a new identity.

## Step kinds

| Kind | Use it to |
| --- | --- |
| `tool` | Call an action-based [Valdr MCP tool](../steps/tool/), bounded [Git](../steps/git/) or [GitHub](../steps/github/) action, or trusted [shell command](../steps/command/) |
| [`session`](../steps/session/) | Launch or continue an agent session |
| [`review`](../steps/review/) | Launch an independent reviewer |
| [`await_condition`](../steps/await-condition/) | Wait for an expected result or evidence |
| [`human_gate`](../steps/human-gate/) | Require an operator decision |
| [`condition`](../steps/condition/) | Outcome route: evaluate a value for branching or repeat a bounded correction loop |
| [`subworkflow`](../steps/subworkflow/) | Run an exact version of another workflow |

## Expressions and outputs

Use expressions to connect declared data:

| Syntax | Meaning |
| --- | --- |
| `${workflow.inputs.taskKey}` | A workflow input |
| `${steps.load_task.outputs.taskStatus}` | A named output from an upstream step |
| `${runtime.actor}` | The operator recorded for the run |
| `${runtime.loop.currentPass}` | The current pass inside a bounded loop |
| `TASK-${workflow.inputs.taskKey}` | Text interpolation |

Other runtime references include `runUlid`, `rootRunUlid`, `taskKey`, `projectKey`, `contextRef`, `createdFromSessionUlid`, `stepKey`, `attempt`, `startedAt`, `clientRequestId`, and `actorHandle`. Inside a loop corridor, `runtime.loop` also exposes `targetStepKey`, `sourceStepKey`, and `previous`; loop references outside a corridor are invalid.

Map only values later steps or the final result need:

```yaml
- key: load_task
  name: Load Task
  kind: tool
  tool:
    id: pm_task
    action: get
  inputs:
    taskKey: "${workflow.inputs.taskKey}"
  outputs:
    taskStatus: "$.normalized.task.status"
```

Later steps use `${steps.load_task.outputs.taskStatus}`. The Workflow Builder offers supported output choices for the selected action.

## Dependencies

Use `needs` whenever a step depends on another step or consumes its outputs:

```yaml
- key: mark_in_progress
  name: Mark Task In Progress
  kind: tool
  needs: [load_task]
  tool:
    id: pm_task
    action: change_status
  inputs:
    taskKey: "${workflow.inputs.taskKey}"
    to: in_progress
    actorHandle: "${runtime.actor}"
```

## Compose another workflow

A `subworkflow` step selects an exact saved child version and binds its declared inputs:

```yaml
- key: verify_delivery
  name: Verify Delivery
  kind: subworkflow
  workflow:
    key: example.task.verify-delivery
    version: 1.0.0
  inputs:
    taskKey: "${workflow.inputs.taskKey}"
  outputs:
    status: "$.normalized.child.outputs.status"
```

The child must declare every bound input and selected output. Publishing a newer child version does not update the parent automatically; change the parent's selected version when you want the new behavior.

## Wait for agent or review evidence

An `await_condition` step lists the outcomes it accepts and the agent or session allowed to provide them:

```yaml
- key: await_executor
  name: Await Executor
  kind: await_condition
  needs: [launch_executor]
  waitsFor:
    kind: workflow_input
    expected: [executor_completed]
    authorizedHandles: ["${workflow.inputs.executorHandle}"]
    sourceSessionUlid: "${steps.launch_executor.outputs.executorSessionUlid}"
  outputs:
    outcome: "$.normalized.input.outcome"
```

Keep the accepted outcomes narrow and bind the wait to the session, review, or task that owns the evidence.

## Add a human gate

```yaml
- key: approve_release
  name: Approve Release
  kind: human_gate
  needs: [prepare_release]
  humanGate:
    prompt: "Approve ${steps.prepare_release.outputs.releaseName}?"
    approvalText: Ship
    rejectionText: Hold
  waitsFor:
    kind: workflow_input
    expected: [approved, rejected]
    authorizedHandles: ["@release-operator"]
  outputs:
    outcome: "$.normalized.input.outcome"
```

Human gates may also present one to five reviewed documents and send feedback to the configured writer session. Feedback keeps the gate open; the operator approves or rejects only after reviewing the refreshed result.

## Add a bounded correction loop

Use an [Outcome route](../steps/condition/) with `loop_back` when requested changes should return to an earlier step:

```yaml
- key: route_review
  name: Route Review
  kind: condition
  needs: [await_review]
  checks:
    - kind: value_equals
      value: "${steps.await_review.outputs.outcome}"
      equals: review_approved
      domain: [review_approved, review_changes_requested]
  outputs:
    outcome: "${steps.await_review.outputs.outcome}"
  onFailure:
    action: loop_back
    to: implement
    max: 3
    exhausted: block
```

`max` is from 1 to 10 and counts the first pass and every correction pass. The `domain` must be a non-empty, unique list containing `equals` and every possible observed value. Keep loops narrow and bounded: corridor steps cannot declare retry, and corridor subworkflows cannot use `forEach` or `detached`.

## Validate and import

Validate the pack source:

```bash
valdr validate-pack example-workflows
```

Generate an archive:

```bash
valdr generate-valdr-pack example-workflows \
  --output scratch/example-workflows.valdr-pack.tar.gz
```

Open **Settings** in Valdr UI to preview and import the pack. Fix validation errors at the reported workflow and step before importing.

## Next step

Open the [Workflow Steps reference](../steps/) for per-kind examples, or use the [Workflow Builder](/valdr/docs/ui/workflows/) to compose the same definition visually.

