Skip to content

Reference

Everything the v1alpha1 format accepts. Every rule lives on this page once. docs/workflow.schema.json gives editors the shape. The guide teaches by example and links here.

Document

KeyRequiredNotes
versionyesv1alpha1. Unknown values fail to load. A later format is v1alphaN
stepsyesNon-empty list
titlenoPicker label. Default: file name in title case
descriptionnoPicker subtitle. Wraps to two lines, then truncates
hiddennoHides it from the picker. hwf run still works
inputsnoQuestions. Only the workflow you start asks them
returnsnoWhat this workflow gives back when another one calls it
on_failurenoOne recovery action. Runs only in the workflow you start

No other top-level key.

File ruleValue
Location.hwf/workflows/ for one repo, ~/.hwf/workflows/ for every repo. Repo shadows global
Name<name>.yaml, name matches [a-z0-9][a-z0-9-_]*. .yml is not discovered

Steps

Every step has exactly one action: run, agent, herdr, or workflow. Every step also accepts id, when, and continue_on_error. Any other key fails to load. That also applies to out, wait, in, ratio, allow_fail, for, and as.

id matches [a-z][a-z0-9_]{0,31}. You need it only when something reads the result of the step.

run:

FormBehavior
listArgv. Each item accepts templates. No shell for local or open: tab
stringShell source, sh unless shell: says otherwise. Rejects templates
FieldRule
shellsh, bash, zsh, pwsh, powershell, cmd. String form only. Windows values parse but never run natively
cwdDefault: the directory that started the workflow
envValues accept templates. Runner values win over inherited ones. HWF_ keys fail at runtime
panePlacement. Refer to pane:
backgroundNever wait. Needs a pane. No result
ready_when/regex/. Needs timeout. Refer to Background and readiness
timeout<integer><ms|s|m|h>. Kills the command and its children. Invalid with background. Default: none
retryBlocking local only. Refer to Control flow
success_codesNon-empty unique integers. Default [0]. Blocking local only. failed reports against this list
Result kindFields
blocking localstdout, stderr, exit_code, failed
readinessnative wait payload plus pane, tab, and workspace IDs
backgroundnone

Inputs arrive as HWF_<name> variables. Step results do not. Pass those with env:. Local commands also receive HWF_RUN_ID, HWF_WORKFLOW, and HWF_CHECKOUT_ROOT.

A placed command with open: beside or below runs the argv as one shell-quoted line in the new pane. open: tab runs the argv directly.

agent:

The value is the prompt. Prompts always render as text.

FieldRule
usingProfile name. Starts a new agent in a new pane. Accepts templates
targetAgent name or pane ID that is already active. Must be idle or done. No pane, cwd, or env
neitherUses default_profile
bothLoad error
cwd, envAs for run:. env forwards HWF_ keys
paneDefault: new tab in the invocation workspace
backgroundNeeds a pane of its own, or a target that names an active agent
timeoutTurn deadline. Default 30 minutes. Startup has a separate 30-second deadline
expectVerdict token. Refer to expect:

Result: response, agent (herdr's AgentInfo), pane_id, and verdict with expect:.

BehaviorRule
blocked agentOne notification per episode. The step continues to wait
unknown agentNever counts as finished
context.agentThe name of the agent that invoked the workflow, or its pane ID when herdr reports no name. Started agents have no name
self-targetA workflow that targets {{context.agent}} must start while that agent is idle or done. An agent cannot run it on itself

expect:

yaml
expect:
  one_of: [APPROVE, REJECT]
  require: [APPROVE] # optional
FieldRule
one_ofNon-empty distinct tokens that match [A-Z][A-Z0-9_]{0,31}
requireOptional non-empty subset. Omit it to accept every token and branch with when:
verdictThe final non-empty line of the response. The runner trims it and matches it exactly. response keeps the full text

The runner appends the token rules to the prompt, and the agent checks its own final line with hwf response check before it finishes.

A final line that matches no token fails the step. A verdict outside require fails the step. Both are ordinary failures: continue_on_error tolerates them and on_failure receives them.

Load errors: expect with background: true, expect on any other action, and a {{steps.<id>.verdict}} reference to a step without expect.

herdr:

yaml
- herdr: notification.show
  params:
    title: done
FieldRule
paramsThe method's exact request. Templates apply recursively
retryRefer to Control flow

The loader never autofills a target. It checks method names, parameter types, and result paths at load. Success gives you the complete result of the method. Denied methods fail at load. Refer to Trust and sharing.

Required selectors for herdr 0.8.2:

MethodNeeds
tab.createworkspace_id
pane.splittarget_pane_id
layout.apply, layout.set_split_ratioexactly one of their paired selectors
worktree.list, create, openexactly one of workspace_id or cwd
pane.list, tab.listthe filters stay optional

workflow:

yaml
- workflow: child
  inputs:
    branch: "{{inputs.branch}}"
RuleValue
lookupRepo scope shadows global. Unknown name or cycle fails at load and names the path
editsMid-run edits to a child apply to the next run
inputsEvery key must be a declared child input. Every required child input needs a value. Values must be text
isolationThe child gets only its own inputs and context. Its step IDs stay private
resultThe child's returns: becomes this step's result. Without it, a reference fails at load
recoveryA child's own on_failure does not run inside a parent
dynamicChild dynamic choices still resolve for each invocation

returns:

One whole-value template, or a named map of them. Keys match [a-z][a-z0-9_]{0,31}. A whole-value template may resolve to an object or array. Load errors: literal null, an empty map, a conditional step result (no guard can prove it), and context.transcript or context.transcript_file.

pane:

yaml
pane:
  open: tab # tab | beside | below. Required
  target: "…" # pane to split. beside/below only. Default: invocation pane
  workspace: "…" # tab only. Default: invocation workspace
  size: 40 # percent for the new pane, 1-99. beside/below only
  name: "…" # tab name. tab only. Default: the step ID, or hwf-agent. Templates allowed
  focus: true # default true for foreground, false for background
  close: success # agent only: success | always
RuleValue
anchorsIDs captured when the workflow started, never current focus. beside splits right, below splits down
sizeherdr decides the effective split and may approximate an extreme value
nameLiteral beside or below with name fails at load. With a templated open, name applies only when the step creates a tab. Blank render keeps the step ID
closesuccess closes after the runner captures the response. always closes after any outcome. Invalid on commands and background steps
last tabWhen you close the last tab of a workspace, the workspace closes (herdr 0.8.0+). The runner does not guard against this. Place a pane you want to keep in a workspace that has another tab
templatedopen may be {{inputs.name}} when that input is an unconditional closed static choice whose options are only tab, beside, below
defaultAn agent step without pane: gets a new tab in the invocation workspace. A command without pane: runs unseen

Background and readiness

RuleValue
backgroundNeeds a pane of its own, or a target: that names an active agent. Invalid with ready_when, timeout, retry, close. No detached local background
placed commandNeeds exactly one of background or ready_when
lifetimeBackground processes belong to their pane. They survive client detach, not server restart. A later failure does not stop them
resultBackground and skipped steps produce no result. Nothing can reference them
ready_when/regex/, non-empty, slash-delimited, flagless, checked at load. Needs timeout
matchOne logical line of the recent pane output, with no ANSI codes. Text already on screen counts
outcomeSucceeds on match, fails when the deadline passes. Cannot detect process exit

Templates

Three roots only: {{inputs.name}}, {{steps.id.field}}, {{context.key}}. There is no {{scratch.*}}. To read scratch, run hwf scratch get and use {{steps.*.stdout}}.

RuleValue
whole valueKeeps its source type
embeddedRenders as text: strings unchanged, booleans lowercase, numbers decimal, null empty, arrays and objects compact JSON
string run:Templates are a load error. Use list form or env:
load errorsDuplicate step IDs, unknown paths, forward references, references to background, skipped, or result-less steps

Context

KeyHolds
workspace, tab, pane, worktree, agentWhere the workflow started. The runner captures them at start
cwdProject root of the invocation directory. Always set
selectionSelected text. Empty when there is none
platformmacos or linux. Refer to Portability
transcript, transcript_fileSession transcript. Sensitive. Fails preflight if unavailable
errorRecovery only, inside on_failure

A reference to an identity or transcript value that is not available fails preflight, before step 1.

Transcripts never enter the HWF_ environment or run history, and every review surface marks them. The runner always removes transcript files. Failed runs keep response and prompt files in gitignored .hwf/tmp for diagnosis.

context.error fields:

FieldHolds
messageFailure text
workflow, workflow_pathThe workflow that failed. A child failure names the child
action, step_number, step_idThe failed step. step_id only when it had one
detailsCommands: stdout, stderr, exit_code. Placed steps: pane, tab, workspace IDs. Agents: profile, kind or target, pane IDs. herdr: method and reason. Workflows: child name

Inputs

FieldRule
name[a-z][a-z0-9_]{0,31}. A declared input nothing references is a load error
typetext, choice, profile. Shorthand: a bare text, profile, or a plain option list
descriptionShown in the prompt. Write one
defaultFor a closed choice or profile, must be one of the values
whenOne clause or ordered list. References earlier inputs only
allow_customchoice only. Turns options into suggestions. Invalid on text and profile, even false
min_lengthNon-negative character floor for an active value
optionschoice only. Static list, or {run: argv}

profile lists merged profile names in stable order and never exposes their args. Inputs reach run: steps as HWF_<name>.

TopicRule
guardsAn inactive input does not prompt, resolve, apply its default, enter the namespace, or export HWF_. If you supply a value for one, collection fails. That includes --input branch=. A reference to a guarded input is a load error unless the site that reads it carries every clause that guards it
dynamic{run: argv} runs from the repo root with the environment of the invocation and no partial input exports. Output splits on newlines, trims, drops empty lines, deduplicates, keeps first-seen order
dynamic failsNonzero exit, empty output, more than 1,000 options, or output past 8 MiB fails collection
dynamic runsLoad and list validate but never run it. Collection runs each active one once. Treat the commands as read-only
cascadeAn argv element may hold {{inputs.<name>}} for an earlier input. It lands as one element, and a shell never parses it again. steps.*, context.*, self, and forward references are load errors. A guarded source needs the same clauses on the consumer. If you change an earlier answer, hwf discards the later answers and their options
promptShows name, description, position, option count, custom allowed, default, min_length. Earlier answers stay listed
yaml
inputs:
  repo:
    type: choice
    description: Repository to inspect
    options: { run: [ls, repos] }
  branch:
    type: choice
    description: Branch in that repository
    options: { run: [git, -C, "repos/{{inputs.repo}}", branch, --format=%(refname:short)] }

hwf workflow inspect <name> prints dynamic argv and does not run it. --resolve runs only active dynamic options. A cascading choice resolves only when every referenced input arrives through --input. Otherwise the command prints its unresolved argv, and the independent choices still resolve.

Control flow

ConstructRule
when:One clause or ordered list, short-circuit AND. False means skipped
clauseA whole-value template read for truthiness, or one template compared with a quoted string by == or !=
truthinessEmpty string, boolean false, numeric 0, and null are false. Every other value is true, and that includes the strings "0" and "false". Comparison uses canonical text
load errorsShell commands, expressions, OR, parentheses, structured values
guarded resultsA conditional step's result may be read only where every one of its clauses is also present
continue_on_errorRecords and continues. Does not trigger recovery for that failure. A later failure still can. The run exits nonzero
retryattempts (2 or more, first included) plus optional delay. Blocking local run: and herdr: only
success_codesRefer to run:
on_failureOne action, once, after the first non-tolerated failure anywhere in the run, including children
connection lossStop, keep panes, skip recovery, report that the step may still run

continue_on_error cannot make a result readable after a failure on agent, herdr, workflow, placed, readiness, or background steps. Spawn and runner failures stay hard failures.

on_failure rules:

RuleValue
rejectsid, when, continue_on_error, background, retry
agentusing, target, cwd, env, pane, timeout
commandshell, cwd, env, pane, ready_when, timeout
herdrparams
workflowinputs
outcomeA recovery that fails is final. Parse, validation, and preflight failures never trigger it. Success does not make the run succeed

Config

yaml
profiles:
  name:
    kind: claude # non-empty. herdr decides whether it starts
    args: ["--model", "…"] # optional
default_profile: name
transcripts:
  claude:
    command: [extractor, argv…]
RuleValue
keysOnly these three. agents: and sessions: fail
profile name[a-z][a-z0-9_-]{0,31}. Fields: kind and args only
layersGlobal plugin config dir, then .hwf/config.yaml, then .hwf/config.local.yaml. A later layer replaces a whole entry
default_profileHighest layer wins and must name a merged profile. Preflight fails when an agent step needs it and it is missing
using: literalUnknown name fails at load. The runner checks a templated name when the step runs
transcriptsKeyed by herdr agent kind. Built-in for claude. Any other kind needs an entry or transcript context fails preflight. An entry replaces built-in extraction

Extractor contract: it runs in the working directory of the agent. It must exit 0, print to stdout, finish in 30 seconds, and produce at most 8 MiB. Environment:

VariableHolds
HWF_TRANSCRIPT_PANE_IDThe pane that launched the workflow
HWF_TRANSCRIPT_AGENT_KINDThe kind herdr detected there
HWF_TRANSCRIPT_CWDThat agent's cwd, or the invocation cwd
HWF_TRANSCRIPT_SESSION_KINDSession reference type, when herdr reports one
HWF_TRANSCRIPT_SESSION_VALUESession id or path, when herdr reports one

Transcript rules: one read per run, before step 1, from the pane that invoked the workflow only. That pane must hold an agent. Built-in claude extraction needs herdr integration install claude and reads the session .jsonl under ~/.claude/projects/. It keeps the user and assistant text. Any failure stops the run before step 1. There is no partial transcript.

Scratch

A flat key-value store shared by every run. The key is the whole identifier. No scopes, no hierarchy, no template. hwf scratch never contacts herdr.

CommandResult
hwf scratch set <key> <value>Writes or replaces the value
hwf scratch get <key>Prints the value. Missing key fails
hwf scratch listPrints keys, one per line, in key order
hwf scratch delete <key>Deletes the key

A value uses the 8 MiB cap. A write that crosses it fails and leaves the previous value unchanged. hwf deletes keys that match <run-id>.* when that run expires. Other keys stay until you delete them.

Limits

WhatLimit
Generated HWF_* environment block24 KiB
Inline agent prompt, then spilled to a file the agent is told to read16 KiB
Clipboard paste into a picker text field16 KiB
Command output, agent response, transcript, dynamic-option output, or scratch value (each)8 MiB
Raw claude session file loaded by built-in extraction256 MiB
One record in a raw claude session file32 MiB
Dynamic options1,000
Dynamic option command10s
Transcript extractor30s
Agent turn (default timeout)30m
Agent startup30s

A value that crosses a cap fails the step and names the source and the limit. The runner never truncates. The transcript cap applies to extracted text, not the raw file.

Trust and sharing

A workflow file is code you choose to run. There is no sandbox. A run: step can call the whole herdr CLI or socket as you. When you open a repository, that never runs a workflow. The picker and CLI label repo or global provenance and mark commands, transcript references, and sensitive herdr methods.

Denied methods
Every server.*, plugin.*, events.*, integration.*, pane.graphics.*
session.snapshot, popup.close, pane.report_agent, pane.report_agent_session, pane.clear_agent_authority, pane.release_agent, agent.view.set, agent.view.clear

Allowed: workspace.*, tab.*, pane.*, worktree.*, agent.*, layout.*, notification.show, client.window_title.*, ping. Each denial states the rule it protects. This is a rail against accidental misuse, not a security boundary.

BundlesRule
formathwf workflow import "<bundle>". The bundle is one opaque string
contentThe selected workflow and every workflow: child it reaches, repo first then global. Exact YAML bodies, with a $schema pointer when one exists. No version, root, source, or config
exportA missing child or a cycle fails the export
importPreviews every body and warning. Needs one repo or global destination. The scope becomes wholly the bundle or stays wholly as it was. Conflicts need --force. Without a terminal, needs --yes and --to. Neither surface can run what it imported
legacyA bundle from a version before 0.6 fails. Re-export it

Portability

v1alpha1 syntax and argv behavior are the same on Linux and macOS. Windows runs herdr and the plugin inside WSL2, where Linux behavior applies. A string run: without shell: uses sh. For OS-specific steps, pair {{context.platform}} with when:. That is the only OS selection the format has.