Skip to content

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.

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

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"],
});
}
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.

  • --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 nesting parallel() inside loops can’t exceed it.
  • Policy receipts are capped at 50 per run. Reaching the cap fails closed before another protected action.

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

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:

Terminal window
harn workflow approvals list --status pending
harn workflow approvals show apr-0123456789abcdef0123
harn workflow approvals approve apr-0123456789abcdef0123 \
--actor ryan --reason "reviewed the release target"
harn workflow resume wf-2026-07-21T12-00-00-000Z-a1b2c3

Requests 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().

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.

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.

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.

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.