Workflow File Format Reference

July 6, 2026 · View on GitHub

Canonical workflow files live at docs/plans/YYYY-MM-DD-<slug>-workflow.md and define work to be executed through governed-execution or an explicit loop-execution, executing-plans, or subagent-execution compatibility profile. The <slug> is a short kebab-case semantic identity derived from the intent. The date makes revisions sortable; HOTL still derives the execution branch from the semantic slug, for example docs/plans/2026-04-22-add-rate-limiting-workflow.md becomes hotl/add-rate-limiting.

Legacy root files such as hotl-workflow-add-rate-limiting.md remain readable during migration, but new workflow files should use docs/plans/.

Frontmatter Fields

FieldTypeRequiredDescription
intentstringyesOne sentence describing what this builds
success_criteriastringyesHow you know the workflow is done
risk_levellow|medium|highyesDetermines auto-approve behavior
auto_approvebooleanno (default: false)Skip gate: human for non-high-risk steps
branchstringnoOverride branch name (default: derived as hotl/<slug> from the canonical or legacy workflow filename)
worktreetrue|false|hostno (default: true, or host inside an unpinned named linked worktree)Choose the execution checkout. true creates an isolated HOTL worktree, false uses the current checkout and may create/switch to the target branch, and host uses the current feature branch exactly as provided by the host tool.
progressverbosenoEnable verbose progress view — prints full step list at each step transition
report_detailfullnoInclude all verify output in the execution report, not just failures
dirty_worktreeallownoProceed even if non-HOTL files are uncommitted (HOTL artifacts are always excluded automatically)
policy_profilestringno (default: standard)Portable policy label recorded in normalized workflows and receipts; it never weakens host policy
max_total_attemptsnumbernoPause when the state-derived total step attempts exceeds this value
max_agentsnumbernoCompare against explicitly observed agent count; unknown remains unknown
max_cost_usdnumbernoCompare against explicitly observed cost; unknown remains unknown
max_elapsed_minutesnumbernoCompare against explicitly observed elapsed time; unknown remains unknown

Sensitive actions are not frontmatter permissions. Each external_write, production_change, or secret_access requires a bounded hotl-rt action request with a stable idempotency key, an explicit human decision, action begin before the effect, and action complete or verify-first action reconcile evidence afterward. Host security controls remain authoritative.

Step Fields

FieldTypeRequiredDescription
actionstringyesWhat to do in this step
loopfalse|"until [condition]"yesWhether to retry
max_iterationsintegerno (default: 3)Safety stop for loops
verifystring | object | listnoHow to check success (see Verification Types)
gatehuman|autonoApproval behavior

Verification Types

The verify field supports 4 types. A scalar string is shorthand for type: shell.

Scalar Shorthand (backward-compatible)

verify: pytest tests/
# equivalent to:
verify:
  type: shell
  command: pytest tests/

shell

Run a command and check exit code. Stdout/stderr captured for reporting.

verify:
  type: shell
  command: pytest tests/test_rate_limit.py -v

browser

Verify UI behavior via browser tooling. Capability-gated: if browser tooling is unavailable inside the runtime, execution downgrades to human-review with the url and check text as the prompt. Never silently skipped. When the host agent has Browser, Computer Use, or equivalent UI tooling, the controller can perform that check before approving or rejecting the pause.

verify:
  type: browser
  url: http://localhost:3000/dashboard
  check: priority badge is visible with correct color

human-review

Mandatory pause for human inspection. Always pauses regardless of auto_approve.

verify:
  type: human-review
  prompt: Check that priority colors match the approved design spec

artifact

Verify a file or output exists and meets an assertion.

verify:
  type: artifact
  path: migrations
  assert:
    kind: matches-glob
    value: "*.sql"

Supported assert kinds:

  • exists — file or directory at path exists (no value needed)
  • contains — file at path contains the text in value
  • matches-glob — directory at path contains at least one file matching value glob Use a filename glob such as *.sql or *.md; do not include directory segments like src/* in value

Multiple Checks Per Step

verify can be a list. All checks must pass for the step to pass.

verify:
  - type: shell
    command: npm test
  - type: browser
    url: http://localhost:3000
    check: login page renders correctly
  - type: artifact
    path: coverage/lcov.info
    assert:
      kind: exists

Auto-Approve Logic

if auto_approve: true AND risk_level != high:
  gate: human → auto-approved
else:
  gate: human → pause for human

risk_level: high always forces human gates regardless of auto_approve.

Branch/Worktree Preflight

Execution skills (loop-execution, executing-plans, subagent-execution) run structural lint and branch/worktree preflight after resolving the workflow file and before step 1. This preflight resolves an isolated execution root so work does not land in the wrong checkout or on main/master.

Branch Name Derivation

ScenarioBranch Name
branch: feat/add-auth in frontmatterfeat/add-auth
No branch:, file is docs/plans/2026-04-22-add-auth-workflow.mdhotl/add-auth
No branch:, file is docs/plans/2026-04-22-fix-login-timeout-workflow.mdhotl/fix-login-timeout
No branch:, legacy file is hotl-workflow-add-auth.mdhotl/add-auth

Preflight Steps

1. Is this a git repo with at least one commit?
   - No  → log "Skipping branch setup (no git history)" → proceed to step 1
   - Yes → continue

2. Check for uncommitted changes
   - First, exclude HOTL-owned transient artifacts from the dirty check:
     - `docs/plans/*-workflow.md`
     - `hotl-workflow-*.md`
     - `docs/designs/*.md`
     - `docs/plans/*-design.md` and `docs/plans/*-plan.md`
     - `.hotl/`
   - If only HOTL artifacts are dirty → treat as clean, continue
   - If non-HOTL files are dirty and `dirty_worktree: allow` is set → proceed without prompting
   - Otherwise → HARD-FAIL. Show choices:
     a. Clean up manually, then re-run
     b. Stash manually, then re-run
     c. Explicitly approve HOTL to stash and continue
   - Clean → continue

3. Determine branch name
   - If branch: field exists in frontmatter → use it
   - Otherwise → derive `hotl/<slug>` from the workflow filename:
     - Canonical: strip `YYYY-MM-DD-` prefix and `-workflow.md` suffix from `docs/plans/YYYY-MM-DD-<slug>-workflow.md`
     - Legacy: strip `hotl-workflow-` prefix and `.md` suffix from `hotl-workflow-<slug>.md`

4. Capture authoring origin
   - Record the current branch name (if any) and current HEAD commit as the workflow's authoring origin
   - If the current branch is neither `main` nor `master`, and the workflow does not already pin `branch:` or `worktree:`, pause and ask whether to:
     a. continue on the current branch in this checkout (`branch: <current-branch>` + `worktree: false`)
     b. use HOTL's isolated execution branch/worktree (recommended)
     c. use a custom execution branch

5. Determine isolation mode
   - If `worktree: host` → use the current checkout and current feature branch exactly as provided by the host tool; reject `main` and `master`
   - If `worktree: false` → stay in the current checkout and use a dedicated branch there
   - If running inside a named linked git worktree and neither `branch:` nor `worktree:` is set → default to host mode to avoid stacking a second worktree
   - Otherwise → use an isolated git worktree by default

Before creating or reusing an execution branch/worktree, resume-aware executors run `scripts/hotl-locate-run.sh --workflow <workflow-file>`. The locator scans the current checkout, linked git worktrees, and HOTL's default `.hotl-worktrees/<repo>/` directory for matching `.hotl/state/*.json`. If it finds an interrupted run, resume from the sidecar's recorded `execution_root` and `run_id` instead of creating a new worktree.

6. Check if the target branch/worktree already exists
   - Current helper behavior: branch/worktree collisions stop with a clear error; interactive reuse/recreate is not implemented yet
   - If a collision occurs, pause and ask the user whether to resolve it manually, delete and recreate manually, or abort
   - Does not exist → create (no prompt)

7. Resolve the execution root with `scripts/hotl-prepare-execution-root.sh <workflow-file> --executor-mode <mode>`
   - Returns JSON with: `branch`, `repo_root`, `execution_root`, `workflow_path`, `source_workflow_path`, `source_branch`, `source_head`, `worktree_path`
   - By default → create a linked git worktree with the branch, copy the current workflow into it, and execute from that worktree
   - If `worktree: false` → create/switch to the dedicated branch in the current checkout and execute from the repo root
   - If `worktree: host` → keep the current branch and execute from the current checkout; if `branch:` is set, it must match the current branch
   - If `branch:` matches the currently checked-out branch while worktree isolation is still enabled, STOP and tell the user to use `worktree: false` or `worktree: host` for same-branch continuity
8. Change into `execution_root`
   - Every later git command, runtime call, helper call, and review command for that run must execute from this directory

Common Branch Scenarios

Starting checkoutWorkflow frontmatterWhat HOTL does
Normal repo on mainno branch, no worktreeCreates a HOTL worktree on derived branch hotl/<slug> from current HEAD
Normal repo on feature/enhance-worktreeno branch, no worktreeExecution skills pause and ask whether to use the current branch, a HOTL worktree, or a custom branch
Codex or other linked worktree on feature/enhance-worktreeno branch, no worktreeUses host mode automatically: executes in the current linked worktree on feature/enhance-worktree
Any checkout on main or masterworktree: hostRejected; use worktree: true or switch to a feature branch
Any checkoutworktree: trueCreates a separate HOTL-managed worktree unless the target branch is already checked out
Any checkoutworktree: falseUses the current checkout and creates/switches to the target branch if needed
Any checkoutworktree: hostUses the current checkout's current branch; branch: must be absent or match that branch

Design Principles

  • No auto-stash. Hidden state mutation weakens governance.
  • Existing branch/worktree collisions stop clearly. Interactive reuse/recreate is not implemented yet; resolve the collision manually, then re-run.
  • Non-git repos skip entirely. HOTL works for POCs and new projects without git ceremony.
  • Structural lint runs before any git mutation. Catches format issues before execution begins.

Execution State (.hotl/state/)

The hotl-rt shared runtime (runtime/hotl-rt) manages all execution state. Agents call hotl-rt subcommands; they do not manage state files directly.

State is persisted in .hotl/state/<run-id>.json — the authoritative, revisioned source of truth for execution progress. Serialized runtime writes prevent concurrent lost updates. The runtime also maintains .hotl/reports/<run-id>.md as a durable Markdown report, updated incrementally on each state transition and reconstructed from state when missing or when its summary drifts from authoritative state.

Workflow checkboxes (- [x]) are a human-visible mirror updated by the agent on step completion. If a chat transcript or native progress card disagrees with the runtime artifacts, trust the artifacts.

Runtime API:

hotl-rt init <workflow-file> [metadata flags] → creates run, state JSON, report
hotl-rt owner claim|status|heartbeat|handoff|release|takeover → renewable controller ownership
hotl-rt step <N> start|verify|retry|block     [--run-id <run-id>] → step state transitions
hotl-rt gate <N> approved|rejected            [--run-id <run-id>] → records gate decisions
hotl-rt action request|decide|begin|complete|reconcile            → bounded effect lifecycle
hotl-rt budget record|check                    [--run-id <run-id>] → aggregate budget enforcement
hotl-rt finalize [--json]                     [--run-id <run-id>] → validates evidence; successful run becomes ready_to_finish
hotl-rt finish <disposition>                  [--run-id <run-id>] → records disposition; ready_to_finish becomes completed
hotl-rt summary <run-id> [--json]             → read-only query
hotl-rt receipt <run-id> [--json]             → state-derived completion evidence

Run ID format: <slug>-<YYYYMMDDTHHMMSSZ>-<12-hex-nonce>, with a final numeric suffix such as -2 only for a local entropy collision. The slug and UTC timestamp remain human-readable and sortable; the operating-system entropy suffix prevents simultaneous runs in independent worktrees from sharing an identity. Legacy timestamp-only IDs remain readable.

Execution-root and owner rule: driver-managed runs initialize with --require-owner. After init returns a run id, the controller must owner claim, retain the one-time token only as HOTL_OWNER_TOKEN, renew with owner heartbeat, and use the same execution_root and run_id for every later mutation. Handoff, release, and takeover are explicit; state age alone is never ownership authority. Do not rely on "latest file in .hotl/state" when multiple runs exist.

Run status lifecycle: running / paused / blocked describe execution. A successful finalize produces ready_to_finish; the run becomes completed only after finish records kept, merged, published, or discarded. Step, gate, and budget evidence cannot be mutated after ready_to_finish; only the governed finish/external-effect and controller lifecycle may continue. A completion receipt is sufficient only after all verification, gate, effect, budget, and finish evidence is terminal.

Gitignore: Add .hotl/ to your project's .gitignore — execution state should not be committed.

Step Syntax

Preferred syntax uses checkboxes so progress is visible in the workflow file itself:

- [ ] **Step 1: Write failing tests**
action: Write tests for rate limit behavior (429 response after N requests)
loop: false
verify: pytest tests/test_rate_limit.py -v

Legacy numbered headings are still accepted during transition:

### 1. Write failing tests
action: Write tests for rate limit behavior (429 response after N requests)
loop: false
verify: pytest tests/test_rate_limit.py -v

Example

---
intent: Add rate limiting to the API
success_criteria: Rate limit tests pass, no existing tests broken
risk_level: low
auto_approve: true
---

## Steps

- [ ] **Step 1: Write failing tests**
action: Write tests for rate limit behavior (429 response after N requests)
loop: false
verify: pytest tests/test_rate_limit.py -v

- [ ] **Step 2: Implement rate limiting**
action: Add rate limiting middleware
loop: until tests pass
max_iterations: 5
verify: pytest tests/test_rate_limit.py -v

- [ ] **Step 3: Full regression check**
action: Run complete test suite
loop: false
verify: pytest -v

- [ ] **Step 4: Final approval**
action: Summarize what was implemented
loop: false
gate: human