> ## Documentation Index
> Fetch the complete documentation index at: https://docs.doppels.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Recipes: The Local Execution Plan for a Capability

> A Recipe implements a Capability — declaring shell steps, required tools, output mappings, and approval rules that execute locally on your machine.

A Recipe is the execution plan behind a Capability. Where a Capability answers *what* an automation produces, a Recipe answers *how*. It declares the runtime, the tools that must be present, the steps to execute, how outputs are captured, and whether any step requires human approval before it runs. Recipes live in your repository alongside Capabilities and run entirely on your local machine — no cloud execution, no token cost.

## Linking a Recipe to a Capability

A Recipe connects to one or more Capabilities through the `provides` field:

```yaml theme={null}
provides: [release-build]
```

When you run `doppels run capability/release-build`, the CLI finds the Recipe that provides it, validates the manifest, checks prerequisites, and executes the steps. A single Capability can have multiple Recipes (different implementations for different environments), and a single Recipe can provide multiple Capabilities.

## Two runtimes

Recipes support two runtimes: `shell` for automated execution and `manual` for human-driven runbooks.

### Shell runtime

The `shell` runtime runs each step as an isolated shell process. Use it for any automation that can execute non-interactively with predictable inputs and outputs.

```yaml theme={null}
apiVersion: doppels.so/v1alpha1
kind: Recipe

metadata:
  name: release-build-with-tar
  version: 1.0.0

provides: [release-build]
runtime: shell

requires:
  commands: [tar, sha256sum]

defaults:
  approval: never

steps:
  - id: build
    name: Package Release
    env:
      VERSION: "{{ inputs.version }}"
    run:
      shell: sh
      script: |
        tar -czf "release-$VERSION.tgz" dist/
        export CHECKSUM=$(sha256sum release-$VERSION.tgz | cut -d ' ' -f 1)
    produces:
      archive:
        file: "release-{{ inputs.version }}.tgz"
      checksum:
        env: CHECKSUM

returns:
  archive: "{{ steps.build.archive }}"
  checksum: "{{ steps.build.checksum }}"
```

### Manual runtime

The `manual` runtime documents a human procedure through a runbook file. Use it when work must be performed by a person — reviewing an approval, executing a GUI-only operation, or gathering evidence from an external system.

```yaml theme={null}
apiVersion: doppels.so/v1alpha1
kind: Recipe

metadata:
  name: service-status-manual
  version: 1.0.0

provides: [service-status]
runtime: manual

procedure:
  readme: ./service-status-runbook.md

evidence:
  verification-notes:
    type: string
```

The `procedure.readme` path points to a Markdown file (relative to your workspace) that describes what the human must do. The `evidence` block declares the typed values the operator must supply to confirm the procedure was completed. These become part of the run record.

## Key field reference

<ParamField path="provides" type="array" required>
  A list of Capability names this Recipe implements. Each name must match the `metadata.name` of a Capability manifest discoverable in your project.

  ```yaml theme={null}
  provides: [release-build]
  ```
</ParamField>

<ParamField path="runtime" type="string" required>
  The execution model. Must be either `shell` (automated steps) or `manual` (human runbook).
</ParamField>

<ParamField path="requires.commands" type="array">
  A list of shell commands that must be present on `PATH` before the Recipe can run. The CLI checks these before executing any steps and reports a clear error if any are missing.

  ```yaml theme={null}
  requires:
    commands: [tar, sha256sum, aws]
  ```
</ParamField>

<ParamField path="defaults.approval" type="string">
  The default approval policy applied to every step that does not set its own `approval`. Accepted values:

  * `never` — step executes without prompting.
  * `always` — step always pauses for human confirmation.
  * `local` — step pauses only when running outside a CI environment.

  Approval is never inferred. Every step must resolve its approval policy from either `defaults.approval` or its own `approval` field.
</ParamField>

<ParamField path="steps[].id" type="string" required>
  A unique identifier for this step within the Recipe. Referenced in expressions as `steps.<id>.<result>` to pass results forward to later steps or to `returns`.
</ParamField>

<ParamField path="steps[].name" type="string">
  A human-readable label shown in CLI output and run logs.
</ParamField>

<ParamField path="steps[].approval" type="string">
  The approval policy for this specific step. Overrides `defaults.approval` when set. Accepted values are the same as `defaults.approval`: `never`, `always`, or `local`.

  ```yaml theme={null}
  steps:
    - id: deploy
      name: Deploy to Production
      approval: always
      run:
        shell: sh
        script: |
          ./deploy.sh
  ```
</ParamField>

<ParamField path="steps[].env" type="object">
  A map of environment variable names to values. Use expressions here to inject input values or results from prior steps:

  ```yaml theme={null}
  env:
    VERSION: "{{ inputs.version }}"
    PREV_CHECKSUM: "{{ steps.previous-step.checksum }}"
  ```

  The script then reads these as normal shell variables (`$VERSION`, `$PREV_CHECKSUM`).
</ParamField>

<ParamField path="steps[].run.shell" type="string">
  The shell interpreter to use. Typically `sh` or `bash`.
</ParamField>

<ParamField path="steps[].run.script" type="string">
  The shell script to execute. Write the script using environment variables — never use `{{ }}` expressions directly inside a script. This prevents shell injection.

  ```yaml theme={null}
  run:
    shell: sh
    script: |
      echo "Building version $VERSION"
      tar -czf "release-$VERSION.tgz" dist/
  ```
</ParamField>

<ParamField path="steps[].produces.file" type="string">
  Declares that this step produces a file artifact at the given POSIX-relative path. The path may contain expressions:

  ```yaml theme={null}
  produces:
    archive:
      file: "release-{{ inputs.version }}.tgz"
  ```
</ParamField>

<ParamField path="steps[].produces.env" type="string">
  Declares that this step produces a scalar result from an exported environment variable. The script must `export` the variable before the step exits:

  ```yaml theme={null}
  produces:
    checksum:
      env: CHECKSUM
  ```
</ParamField>

<ParamField path="returns" type="object">
  Maps step results back to the output names declared in the Capability. Every output declared by all provided Capabilities must appear here.

  ```yaml theme={null}
  returns:
    archive: "{{ steps.build.archive }}"
    checksum: "{{ steps.build.checksum }}"
  ```
</ParamField>

<ParamField path="procedure.readme" type="string">
  (`manual` runtime only.) A POSIX-relative path to a Markdown file that describes the procedure a human operator must follow. The file is displayed to the operator at run time and becomes part of the run record.

  ```yaml theme={null}
  procedure:
    readme: ./service-status-runbook.md
  ```
</ParamField>

<ParamField path="evidence" type="object">
  (`manual` runtime only.) A map of named values the operator must supply when confirming the procedure was completed. Each entry declares a `type` using the same type vocabulary as Capability inputs and outputs (`string`, `integer`, `number`, `boolean`, `artifact`). Evidence values are recorded in the run and can be referenced by downstream automations.

  ```yaml theme={null}
  evidence:
    verification-notes:
      type: string
  ```
</ParamField>

## Using expressions safely

Expressions use the `{{ ... }}` syntax inside quoted YAML strings. Two forms are supported:

* `{{ inputs.<name> }}` — the value of a declared Capability input.
* `{{ steps.<step-id>.<result> }}` — a named result produced by a prior step.

Use expressions in `env` values, declarative path fields (like `produces.file`), and `returns` values. **Never use expressions inside `run.script`.** Instead, assign expression values to environment variables in the `env` block and read those variables in the script. This design prevents shell injection and keeps the script portable.

```yaml theme={null}
# ✅ Correct: expression in env, variable in script
env:
  TARGET_ENV: "{{ inputs.environment }}"
run:
  shell: sh
  script: |
    echo "Deploying to $TARGET_ENV"

# ❌ Incorrect: expression inside script
run:
  shell: sh
  script: |
    echo "Deploying to {{ inputs.environment }}"
```

## Path rules

All paths in a Recipe — in `produces.file`, `procedure.readme`, or any other declarative field — must use POSIX syntax relative to your workspace root. Absolute paths (starting with `/`), Windows-style paths (containing `\` or drive letters like `C:`), and paths containing `..` segments are all prohibited. This rule keeps the same Recipe portable across macOS, Linux, and Windows.

## Where to store Recipe manifests

Place Recipe manifests in the `recipes/` directory at the root of your project:

```text theme={null}
your-project/
  capabilities/
    release-build.yaml
  recipes/
    release-build-with-tar.yaml
```

File names do not need to match `metadata.name`, but keeping them aligned makes navigation straightforward. Commit both `capabilities/` and `recipes/` to your repository — they are the repeatable assets that grow your automation library.

***

<CardGroup cols={2}>
  <Card title="Capabilities" icon="file-contract" href="/concepts/capabilities">
    Understand the Capability contract that Recipes implement.
  </Card>

  <Card title="Spaces" icon="folder-tree" href="/concepts/spaces">
    See how Spaces scope discovery of your Capabilities and Recipes.
  </Card>

  <Card title="Validate and Run" icon="play" href="/guides/validate-and-run">
    Step-by-step guide to validating manifests and executing your first run.
  </Card>

  <Card title="YAML Schema Reference" icon="code" href="/reference/yaml-schemas">
    Complete schema reference for all Recipe fields.
  </Card>
</CardGroup>
