Pilot

Playbook Reference

Complete YAML schema for Cuitty Pilot playbooks — inputs, steps, actions, assertions, and captures.

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

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}}.

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.

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:

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:

ActionRequired fieldsDescription
navigateurlNavigate to a URL. Supports {{input}} interpolation.
clickselectorClick an element.
fillselector, valueType a value into an input field.
selectselector, valueChoose a dropdown option.
set_toggleselector, valueSet a checkbox or toggle to true/false.
wait_forselectorWait until an element is visible.
assertassert arrayVerify page state without acting.
capturecapture_as, fromExtract a value from the page or network.
capture_secretcapture_as, fromLike capture, but the value is masked in logs.
ai_decideallowed_outcomesLet the AI model choose from a list of outcomes.
scrollselector (optional)Scroll the page or a specific element.
hoverselectorHover over an element.
pressvaluePress a keyboard key (e.g. Enter, Tab).
uploadselector, valueUpload a file via a file input.

Assertions

Post-step assertions verify expected page state:

assert:
  - visible: "[data-testid='dns-records-table']"
  - text_present: "Record added"
  - url_matches: "/dns"
  - network_complete:
      url_pattern: "/dns_records"
      method: POST
      status: 200
AssertionDescription
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_completeA matching network request completed

Capture sources

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

# 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 for full field documentation on each action.

Example

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

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