harn workflow
harn workflow run <script> [--harness <name>] [--resume-from <run-id>] [--subscription-only] [--allow-api-billing] [--policy <file>] [--isolation <mode>] [--approval-to <address>] [--max-agents <n>] [--concurrency <n>] [--cwd <dir>] [--json]harn workflow resume <run-id> [--json]harn workflow approvals list [--status pending|approved|denied] [--json]harn workflow approvals show <approval-id> [--json]harn workflow approvals approve|deny <approval-id> [--actor <name>] [--reason <text>] [--json]harn workflow proof <run-id> [--json]A workflow is a small throwaway JS script with staged fan-out: the script (deterministic code), not any model, decides what runs next, and the run always terminates when the script returns. Subagents spawn as headless harness-CLI subprocesses and are born coordination-registered — they heartbeat, emit canonical events, and stay visible to peers, so a workflow child editing a file is subject to the same claim/guard protections as any other agent. Design record: decision 0015.
Spawn adapters: claude-code (default), codex (codex exec --output-last-message), and cursor (cursor-agent -p --output-format json --trust — headless cursor refuses untrusted workspaces without the flag).
All three are live-verified end-to-end (codex against codex-cli 0.144.5,
cursor against cursor-agent 2026.07.16). A missing harness
CLI fails loud with the vendor’s install one-liner and login command in the
error, and harn doctor reports all three spawn targets up front
(installed? authenticated? how will children bill?). Select the run default
with --harness, or per agent via opts.harness — mixed-harness workflows
are legal (triage on one CLI, deep work on another). Codex and cursor expose
no per-run cost or max-turns controls, so costUsd is unreported and
maxTurns is ignored on those harnesses.
Auth: children ride the logged-in harness session
Section titled “Auth: children ride the logged-in harness session”The engine never handles credentials. Each child is the plain harness CLI, so
it authenticates exactly the way that CLI already does on the machine:
claude uses the OAuth login in ~/.claude (a subscription seat), codex
uses its ChatGPT login, cursor-agent uses cursor-agent login. A
subscription login is the default and preferred path — no per-token API
billing is required or implied by running workflows.
The child-env scrub deletes session variables (CLAUDE*, CODEX*,
CURSOR*), not auth: credential files and keychains are untouched
(CURSOR_API_KEY is carved out of the CURSOR* scrub — it’s a credential,
not session state). The one way to end up on API billing is an API-key
variable in the parent shell (ANTHROPIC_API_KEY, OPENAI_API_KEY,
CURSOR_API_KEY): harness CLIs prefer a key over a stored login when both
are present.
Billing safeguards
Section titled “Billing safeguards”The engine probes each harness’s billing state on its first spawn of the
run (journaled as billing.probe, echoed as a [billing] line, and reported
per harness in the RunReport and web view):
subscription— no key exported; children ride the stored login.api-key— key present, no stored login detected: a deliberate key-only host (e.g. CI). Proceeds with a printed notice.api-key-override— key present AND a stored login exists: the key is silently shadowing subsidized auth, almost always by accident. The engine refuses to spawn unless you pass--allow-api-billing.
--subscription-only goes further: every API-key var is deleted from every
child env, so children can only authenticate via their stored login, and a
harness whose login is provably absent fails loud before spawning. Pin it
repo-wide with .harnery/config.jsonc — harn init stamps this pin by
default into any project without a committed workflow key, so new setups
are subscription-only out of the box:
{ "workflow": { "subscriptionOnly": true } }HARNERY_WORKFLOW_SUBSCRIPTION_ONLY=1|0 overrides per process (the 0
escape hatch exists for a key-only CI job inside a pinned repo).
--allow-api-billing and subscription-only mode contradict each other and
are rejected as a pair.
Login detection is a heuristic over well-known credential locations (Claude
Code’s ~/.claude/.credentials.json, codex’s $CODEX_HOME/auth.json — a
codex login --api-key stored key is classified as API-key billing, not a
login). Where presence can’t be proven either way (macOS keychain storage,
cursor), the state is unknown and never hard-fails — the harness CLI itself
is the final authority.
Two accounting consequences: the costUsd the claude adapter journals is the
CLI’s notional cost figure — under subscription auth it is not an invoice,
just a burn gauge — and a large fan-out consumes the subscription’s usage
allowance the same way interactive sessions do, so the agent cap and
concurrency pool are still your throttles.
Script shape
Section titled “Script shape”export const meta = { name: "review-prs", description: "triage + deep-dive open PRs", objective: "Produce an inspectable risk assessment for every routed PR", acceptance: [ { id: "tests", statement: "The focused test suite passes" }, { id: "review", statement: "The routed changes receive a final review" }, ],};
const TRIAGE = { type: "object", required: ["route", "reason"], properties: { route: { enum: ["close", "analyze", "keep"] }, reason: { type: "string" }, },};
export default async function run({ agent, parallel, stage, log, evidence }) { stage("triage"); const verdicts = await parallel( prs.map((pr) => () => agent(`Classify this PR… Return ONLY JSON {route, reason}.`, { schema: TRIAGE, model: "claude-haiku-4-5", maxTurns: 1, })), );
stage("deep-dive"); for (const [i, v] of verdicts.entries()) { if (v?.route !== "analyze") continue; // conditional routing: code decides await agent(`Risk-assess: ${prs[i]}`, { label: `deep: ${prs[i]}` }); }
evidence({ kind: "test", status: "passed", label: "Focused workflow tests", ref: "bun test src/workflows/review-prs.test.ts", acceptanceIds: ["tests"], });}The ctx API
Section titled “The ctx API”| Function | Contract |
|---|---|
agent(prompt, opts?) |
Spawn one subagent. With opts.schema the reply must strict-parse as JSON and validate against the schema subset (type, properties, required, items, enum); failures re-prompt with the validation errors appended, up to opts.maxAttempts (default 2), then throw. Without a schema, resolves to the raw reply text. Other opts: model, effort, maxTurns (default 25; use 1 for pure classification), timeoutMs (default 300000), label. |
parallel(thunks) |
Run thunks against the run-wide concurrency pool. A rejected thunk resolves to null (journaled), so one bad item can’t kill the batch — filter and route. |
stage(title) |
Declare the current stage: journal marker + progress grouping. |
log(message) |
Narrate progress (stderr + journal). |
evidence(input) |
Attach a bounded receipt to the run and optional acceptance IDs. Returns a stable ID such as e1. Kinds: test, command, artifact, change, review, observation. Statuses: passed, failed, observed, unknown. |
authorize(input) |
Authorize one host-mediated external mutation before performing it. Input includes a short action plus optional path, network, service, and bounded target. Returns the final allow decision; denial or missing host policy throws before control returns. |
effort is validated and mapped per registered adapter: Claude Code accepts
low | medium | high | xhigh | max; Codex accepts
none | minimal | low | medium | high | xhigh. Cursor does not accept a
separate effort option through Harnery because its effort syntax is embedded
inside some parameterized model ids. See harn harness for the
executable capability declaration.
Bounds
Section titled “Bounds”--max-agents(default 50): total-agent ceiling for the run. The cap failing loud is the runaway backstop — there is no recursive spawning path, because subagents are leaf processes.--concurrency(default 4): one shared slot pool for every spawn in the run, so nestingparallel()inside loops can’t exceed it.- Policy receipts are capped at 50 per run. Reaching the cap fails closed before another protected action.
Host policy
Section titled “Host policy”--policy <file> applies a host-owned JSON/JSONC policy immediately before
every uncached agent dispatch. The policy can constrain run cost, unknown
pricing, harness, model, working path, network state, and the host-declared
isolation mode. It is not part of workflow metadata, so the script and child
prompts cannot weaken it.
Rules return allow, ask, or deny. The standalone CLI parks on ask and
writes a durable request under .harnery/approvals/; no protected operation
runs while it is pending. Library callers remain fail-closed by default and
must select EngineOpts.approvalMode = "park" deliberately. An embedding host
can still supply EngineOpts.resolvePolicyAsk for immediate decisions. Use
harn policy to validate a document before a run.
--isolation declares the boundary the host actually created: shared,
worktree, sandbox, or remote. It does not create that boundary. Ordinary
CLI harness subprocesses inherit host networking, so the CLI declares network
enabled and a network: deny policy blocks dispatch.
Workflow code may use ctx.authorize() before an external mutation:
await authorize({ action: "publish release", network: true, service: "package registry", target: "https://registry.example.test/package",});// Perform the operation only after authorization returns.This seam protects host-mediated operations. It does not intercept arbitrary
tools invoked inside an opaque harness subprocess. Adapter-native policy
mapping remains a separate capability claim in harn harness show.
Resume
Section titled “Resume”--resume-from <run-id> reuses completed agent results from a prior run’s
journal: an agent() call whose identity — stage, harness, model, effort, maxTurns,
schema, and the ORIGINAL prompt — matches a journaled agent.end returns the
recorded result instantly without spawning (journaled as agent.cached).
Changed or previously-failed calls re-run live. Same script + same inputs →
100% cache, $0. A typo’d run id fails loud rather than silently re-running
everything.
Durable approval parking
Section titled “Durable approval parking”A CLI run that reaches policy ask writes run.parked, prints its approval
ID, and exits without run.end or a terminal proof. Inspect and resolve it:
harn workflow approvals list --status pendingharn workflow approvals show apr-0123456789abcdef0123harn workflow approvals approve apr-0123456789abcdef0123 \ --actor ryan --reason "reviewed the release target"harn workflow resume wf-2026-07-21T12-00-00-000Z-a1b2c3Requests are immutable private records. Resolution creates a separate exclusive decision record: the first verdict wins, the same-verdict retry is idempotent, and a conflicting retry is refused. Approval does not auto-spawn work. Resume must be invoked explicitly, and an exclusive resume lease refuses concurrent attempts for the same run.
workflow resume reuses the same run ID and journal. It verifies the frozen
script SHA-256, reconstructs completed agent results and committed cost, and
reruns the deterministic script from its entry point. Engine-managed
agent() calls return cached results and the matching approval decision is
replayed. Plain JavaScript side effects executed directly before the park run
again; keep them idempotent or put protected effects behind authorize().
Context-cost heads-up
Section titled “Context-cost heads-up”Children spawn in --cwd (default: the coord root) and load that directory’s
repo-instructions file (CLAUDE.md / AGENTS.md) into their system prompt —
a per-child cache-write a fan-out multiplies. The engine prints the estimate
up front ([context] each child cache-writes ~51K tokens …, bytes/4
heuristic) and reports it as contextTokensPerChildEstimate in the
RunReport. For mechanical stages that don’t need repo instructions, point
--cwd at a directory without one.
Output
Section titled “Output”Human mode prints a run summary (agents, cached count, cost, duration, journal
path, proof path, acceptance summary, result). --json emits the full
RunReport. Every run journals to
.harnery/workflows/<run-id>/journal.jsonl: run.start, stage.start,
agent.start/end (cost, duration, child session id, result), agent.cached,
agent.schema_retry, evidence.recorded, run.end. New journal lines carry
schema_version and run_id; older journals remain resumable.
Durable approval runs also use approval.requested, approval.resolved,
approval.consumed, run.parked, and run.resume.
Proof packets
Section titled “Proof packets”Every terminal run, including a failed one, writes
.harnery/workflows/<run-id>/proof.json. The packet is written atomically with
mode 0600 and contains:
- the objective, terminal status, timestamps, and acceptance rollup;
- typed evidence declared through
ctx.evidence(); - agent outcomes, attempts, duration, cost, and session ID when reported;
- SHA-256 digests and byte counts for agent and workflow results, not their raw text;
- engine-observed repository snapshots before and after the run;
- adapter tool-evidence coverage from the harness capability registry;
- explicit unknowns for missing capability claims, cost, session IDs, and incomplete repository drift;
- a SHA-256 digest of the final journal.
- the normalized host policy, its digest, host isolation/network declarations, and bounded initial/final decision receipts when policy was configured.
Acceptance is deliberately simple. Any attached failed evidence makes a
criterion unsatisfied. At least one passed item and no failure makes it
satisfied. observed, unknown, or no attached evidence leaves it unknown.
An item recorded through ctx.evidence() has source workflow: Harnery stores
the declaration but does not claim it independently ran the command or test.
Repository snapshots and packet integrity are marked separately as
engine-observed.
Inspect a packet with harn workflow proof <run-id>. Human mode renders the
status, criteria, evidence count, repository drift, unknowns, and journal
digest. --json emits the stored schema unchanged. Failed workflow run
commands include the proof path in their error output.
Proof packets are bounded to 50 criteria, 200 evidence records, and 512 KiB. Labels, summaries, and references have field-level limits. Unknown acceptance IDs and duplicate criterion IDs fail loud. The packet does not run a secret redactor; do not put credentials into objectives, evidence summaries, or references. The detailed journal continues to retain full agent results for resume compatibility.
The programmatic surface is exported from harnery/core/workflow, including
the engine, proof reader and renderer, schema types, and
WORKFLOW_PROOF_SCHEMA_VERSION.
Web view
Section titled “Web view”The dashboard (harn web up) ships a /workflows page: journal-driven list
of runs (status, agents, cached count, cost, stage chain) and a per-run detail
view rendering the stages → agents tree with per-agent harness, attempts,
duration, and cost. Parked runs have a distinct awaiting-approval status and
show the durable approval ID and inspection command. When a terminal proof packet exists, the detail page also
shows the objective, acceptance criteria, declared evidence, repository drift,
host-policy decisions, and explicit unknowns. Journal-driven means a run stays
inspectable while live
and after the orchestrator exits, with no terminal paging limits.
Workflow children and the end-of-turn ritual
Section titled “Workflow children and the end-of-turn ritual”Children run with the host’s hooks on (that’s what makes them
coordination-visible) but with HARNERY_WORKFLOW_CHILD=1, which the stop-hook
rule honors as an exemption (stop-hook.workflow_child): the human-facing
end-of-turn ritual doesn’t apply to a headless child reporting to a journal.
Everything else — heartbeats, canonical events, claims — behaves normally.