---
title: Playbook Reference
description: "Complete YAML schema for Cuitty Pilot playbooks — inputs, steps, actions, assertions, and captures."
section: Pilot
order: 2
updatedAt: 2026-06-01
slug: pilot/playbook-reference
---
# Playbook Reference

A playbook is a YAML document that describes an automated workflow. Pilot validates every playbook against a Zod schema before execution.

## Top-level structure

```yaml
playbook:
  id: <dotted.id>          # e.g. cloudflare.dns.add-a-record
  version: 1               # integer, incremented on each edit
  target: <provider>       # cloudflare, aws, vercel, etc.
  title: Human-readable title
  description: Optional longer description
  executor: browser        # default executor for all steps (optional)
  executor_config: {}      # executor-specific config map (optional)
  inputs: []               # input definitions
  outputs: []              # output definitions
  preconditions: []        # conditions checked before run starts
  steps: []                # ordered list of steps (min 1)
```

Required fields: `id` (dotted lowercase string), `version` (integer), `target`, `title`, `steps` (min 1). Optional: `description`, `executor` (default `browser`), `executor_config`, `inputs`, `outputs`, `preconditions`.

## Inputs

Each input defines a variable the caller must supply. Referenced in steps as `{{name}}`.

```yaml
inputs:
  - name: zone
    type: string
    required: true
  - name: proxied
    type: boolean
    default: true
```

Fields: `name` (pattern `^[a-z_][a-z0-9_]*$`), `type` (one of `string`, `number`, `boolean`, `ipv4`, `ipv6`, `url`, `email`, `secret`), `required` (default `true`), `default`, `description`.

## Outputs

Outputs reference values captured during the run: `{ name, type, source }` where `source` is e.g. `capture.record_id`.

## Steps

Each step is an atomic action. Steps run in order unless the composite executor orchestrates them differently.

```yaml
steps:
  - id: goto-dns
    action: navigate
    executor: browser        # override playbook-level executor
    url: "https://example.com"
    timeout_ms: 15000
    ai_fallback: true
    destructive: false
    breakpoint: false
    intent: Human-readable goal for this step.
    observe: [text, a11y, screenshot]
```

Required: `id` (pattern `^[a-z][a-z0-9-]*$`, unique within playbook), `action`. Optional: `executor` (overrides playbook default), `selector`, `url`, `value`, `timeout_ms` (default 15000), `ai_fallback` (default false), `destructive` (default false), `breakpoint` (default false), `intent`, `observe` (`["text", "a11y", "screenshot"]`), `capture_as`, `assert`.

### Selectors

A selector can be a plain string or an object with fallback chain:

```yaml
selector:
  primary: "button:has-text('Save')"
  fallbacks:
    - "[data-testid='save-btn']"
    - "role=button[name=/save/i]"
```

## Browser actions (14)

These are the default executor's actions:

| Action | Required fields | Description |
|--------|----------------|-------------|
| `navigate` | `url` | Navigate to a URL. Supports `{{input}}` interpolation. |
| `click` | `selector` | Click an element. |
| `fill` | `selector`, `value` | Type a value into an input field. |
| `select` | `selector`, `value` | Choose a dropdown option. |
| `set_toggle` | `selector`, `value` | Set a checkbox or toggle to true/false. |
| `wait_for` | `selector` | Wait until an element is visible. |
| `assert` | `assert` array | Verify page state without acting. |
| `capture` | `capture_as`, `from` | Extract a value from the page or network. |
| `capture_secret` | `capture_as`, `from` | Like `capture`, but the value is masked in logs. |
| `ai_decide` | `allowed_outcomes` | Let the AI model choose from a list of outcomes. |
| `scroll` | `selector` (optional) | Scroll the page or a specific element. |
| `hover` | `selector` | Hover over an element. |
| `press` | `value` | Press a keyboard key (e.g. `Enter`, `Tab`). |
| `upload` | `selector`, `value` | Upload a file via a file input. |

## Assertions

Post-step assertions verify expected page state:

```yaml
assert:
  - visible: "[data-testid='dns-records-table']"
  - text_present: "Record added"
  - url_matches: "/dns"
  - network_complete:
      url_pattern: "/dns_records"
      method: POST
      status: 200
```

| Assertion | Description |
|-----------|-------------|
| `visible: <selector>` | Element is visible on page |
| `hidden: <selector>` | Element is not visible |
| `text_present: <text>` | Text appears in page body |
| `text_absent: <text>` | Text does not appear |
| `url_matches: <pattern>` | Current URL matches pattern |
| `network_complete` | A matching network request completed |

## Capture sources

The `from` field on `capture` / `capture_secret` supports three source types:

```yaml
# From a DOM element
from:
  selector: ".result-id"
  attribute: textContent    # optional, defaults to textContent

# From a network response
from:
  network_response:
    url_pattern: "/dns_records"
    method: POST
    json_path: "$.result.id"

# From OCR on a screenshot region
from:
  screenshot_ocr:
    region: "top-right"
```

## Executor actions

Actions prefixed with the executor name are routed automatically:

### Terraform (`tf_*`)

`tf_init`, `tf_validate`, `tf_plan`, `tf_apply`, `tf_destroy`, `tf_output`, `tf_import`, `tf_state`

### Git (`git_*`)

`git_clone`, `git_branch`, `git_commit`, `git_push`, `git_pr_create`, `git_pr_merge`, `git_tag`, `git_release`

### Persist (`persist_*`)

`persist_provision`, `persist_migrate`, `persist_sync`, `persist_backup`, `persist_restore`, `persist_proxy_start`, `persist_proxy_stop`

### Shell (`shell_*`)

`shell_exec`, `shell_script`, `shell_assert`

### HTTP (`http_*`)

`http_request`, `http_assert`, `http_poll`

### Composite

`run_playbook`, `parallel`, `conditional`, `loop`

See [Executors](/docs/pilot/executors) for full field documentation on each action.

## Example

A condensed playbook that adds a DNS A record on Cloudflare:

```yaml
playbook:
  id: cloudflare.dns.add-a-record
  version: 1
  target: cloudflare
  title: Add an A record to a DNS zone
  inputs:
    - { name: zone, type: string, required: true }
    - { name: record_name, type: string, required: true }
    - { name: ip_address, type: ipv4, required: true }
  outputs:
    - { name: record_id, type: string, source: capture.record_id }
  steps:
    - id: goto-dns
      action: navigate
      url: "https://dash.cloudflare.com/?to=/:account/{{zone}}/dns"
      ai_fallback: true
    - id: fill-name
      action: fill
      selector: "[name='dns-record-name']"
      value: "{{record_name}}"
    - id: save
      action: click
      selector: "button:has-text('Save')"
      assert: [{ text_present: "Record added" }]
    - id: capture-id
      action: capture
      capture_as: record_id
      from:
        network_response:
          url_pattern: "/dns_records"
          method: POST
          json_path: "$.result.id"
```

For a longer example using `ai_decide` with multiple outcomes and `breakpoint` on destructive steps, see the R2 playbook in `playbooks/cloudflare/r2.enable-and-create-buckets.yaml`.

## See also

- [Executors](/docs/pilot/executors) — detailed executor reference
- [JavaScript SDK](/docs/pilot/sdk/javascript) — run playbooks programmatically
- [Quickstart](/docs/pilot/quickstart) — record and replay your first workflow