Workflow runtime
Harn's workflow runtime is the layer above raw llm_call() and
agent_loop(). It gives host applications a typed, inspectable, replayable
orchestration boundary instead of pushing orchestration logic into app code.
Pipeline vs. workflow. Two different things that are deliberately not renamed — learn the distinction once:
pipelineis the language keyword: a named, callable, function-like composition (pipeline name(args) { ... }) that serves as a program entrypoint and container. Not itself agentic. See Pipeline lifecycle.- Workflow is the stage-graph runtime: the typed, replayable graph of stages (
stage,verify,join,condition,fork,map,reduce,subagent,escalation) executed byworkflow_execute— this page.On the abstraction ladder:
llm_call= one request <agent_loop= one goal < workflow = multiple goals, attempts, or models. (agent_presetis how you buildagent_loopoptions, not a tier of its own.) See Choosing an agent abstraction and the glossary.
Core concepts
Workflow graphs
Use workflow_graph(...) to normalize a workflow definition into a typed
graph with:
- named nodes
- explicit edges
- node kinds such as stage, verify, join, condition, fork, map, reduce, subagent, and escalation
- typed stage input/output contracts
- explicit branch semantics and typed run transitions
- per-node model, transcript, context, retry, and capability policies
- workflow-level capability ceiling
- mutation audit log entries
subagent nodes are now a real delegated execution boundary. They run through
the worker lifecycle, attach worker metadata to their stage records, and tag
their produced artifacts with delegated provenance so parent workflows can
inspect and reduce child results explicitly.
Start with a helper that registers the tools the workflow will expose to each node. Each tool carries its own capability policy so validation can enforce them automatically:
import { StageSpec } from "std/workflow/options"
fn review_tools() {
let tools = tool_registry()
tools = tool_define(tools, "read", "Read a file", {
parameters: {path: {type: "string"}},
returns: {type: "string"},
handler: nil,
policy: {
capabilities: {workspace: ["read_text"]},
side_effect_level: "read_only",
path_params: ["path"],
mutation_classification: "read_only"
}
})
tools = tool_define(tools, "edit", "Edit a file", {
parameters: {path: {type: "string"}},
returns: {type: "string"},
handler: nil,
policy: {
capabilities: {workspace: ["write_text"]},
side_effect_level: "workspace_write",
path_params: ["path"],
mutation_classification: "apply_workspace"
}
})
tools = tool_define(tools, "run", "Run a command", {
parameters: {command: {type: "string"}},
returns: {type: "string"},
handler: nil,
policy: {
capabilities: {process: ["exec"]},
side_effect_level: "process_exec",
mutation_classification: "ambient_side_effect"
}
})
return tools
}
// Build each node through the typed `StageSpec` alias (or the
// `workflow_stage_spec(...)` constructor) from `std/workflow/options`
// so stage-spec typos fail at check time.
const act: StageSpec = {kind: "stage", mode: "agent", tools: review_tools()}
const verify: StageSpec = {kind: "verify", mode: "agent", tools: tool_select(review_tools(), ["run"])}
const repair: StageSpec = {kind: "stage", mode: "agent", tools: tool_select(review_tools(), ["edit", "run"])}
const graph = workflow_graph({
name: "repair_loop",
entry: "act",
nodes: {act: act, verify: verify, repair: repair},
edges: [
{from: "act", to: "verify"},
{from: "verify", to: "repair", branch: "failed"},
{from: "repair", to: "verify", branch: "retry"}
]
})
const report = workflow_validate(graph)
assert(report.valid)
When tool entries include policy, Harn folds that metadata into workflow
validation and execution automatically. That keeps the registry itself as the
source of truth for capability requirements instead of forcing products to
repeat the same information in both tool definitions and node policy blocks.
Action graphs
std/agents now exposes an action-graph layer above raw workflow graphs for
planner-driven orchestration:
action_graph(raw, options?)canonicalizes planner output variants into a stable{_type: "action_graph", actions: [...]}envelope.action_graph_batches(graph, completed?)repairs missing cross-phase dependencies and groups ready work by phase plus tool class.action_graph_flow(graph, config?)turns that plan envelope into a typed workflow graph with one scheduled batch stage per ready batch.action_graph_run(task, graph, config?, overrides?)attaches a durableplanartifact and executes the generated workflow viaworkflow_execute.
This is the intended shared substrate for "research -> plan -> execute -> verify" style pipelines when the planner output is unstable but the executor should still see a canonical schedule.
import "std/agents"
const raw_plan = {
steps: [
{id: "inspect", kind: "research", title: "Inspect parser", tools: ["read", "search"]},
{id: "patch", title: "Patch diagnostics", tools: ["edit"]},
{id: "docs", title: "Update release notes", tools: ["edit"]}
]
}
const plan = action_graph(raw_plan, {task: "Fix parser diagnostics"})
const run = action_graph_run("Fix parser diagnostics", plan, {
research: {mode: "llm", model_policy: {provider: "mock"}},
execute: {mode: "llm", model_policy: {provider: "mock"}},
verify: {command: "cargo test --workspace --quiet", expect_status: 0}
})
log(run.status)
log(len(run.batches))
Artifacts and resources
Artifacts are the real context boundary. Instead of building context mostly by concatenating strings, Harn selects typed artifacts under policy and budget.
Core artifact kinds that ship in the runtime include:
artifactresourcesummaryanalysis_notedifftest_resultverification_resultplan
Artifacts carry provenance fields such as:
sourcecreated_atfreshnesslineagerelevanceestimated_tokensmetadata
Example:
const selection = artifact({
kind: "resource",
title: "Selected code",
text: read_file("src/parser.rs"),
source: "workspace",
relevance: 0.95
})
const plan = artifact_derive(selection, "plan", {
text: "Update the parser diagnostic wording and preserve spans."
})
const context = artifact_context([selection, plan], {
include_kinds: ["resource", "plan"],
max_tokens: 1200
})
Executing workflows
workflow_execute(task, graph, artifacts?, options?) executes a typed
workflow and persists a structured run record.
Build run options through the typed WorkflowExecuteOptions alias from
std/workflow/options (inline dict literals in the options slot are
flagged by the unnormalized-options lint):
import { WorkflowExecuteOptions } from "std/workflow/options"
const run_options: WorkflowExecuteOptions = {max_steps: 8}
const run = workflow_execute(
"Fix the diagnostic regression and verify the tests.",
graph,
[selection, plan],
run_options,
)
log(run.status)
log(run.path)
Use harn runs view --json <path> on run.path for the stable
harn.run_view.v1 projection, including stage summaries.
Agent-backed stages pass model_policy.iteration_budget directly into the
per-stage agent_loop. Treat that structured budget as the source of truth for
adaptive or fixed loop limits; scripts no longer need to copy that cap into
max_iterations. max_iterations remains accepted as a scalar fixed cap, and
when both fields are present iteration_budget.max is the cap used by the agent
loop. Invalid budget fields fail at loop startup instead of being ignored.
verify nodes can also run deterministic checks without an LLM loop:
verify: {
kind: "verify",
verify: {
command: "cargo test --workspace --quiet",
expect_status: 0,
assert_text: "test result: ok"
}
}
Command-based verification records stdout, stderr, exit_status, and a
derived success flag on the stage result while still flowing through the same
workflow branch/outcome machinery as LLM-backed verification.
A stage's verify may also be a function (fn-verify mode) when the check
is easier to express as Harn logic than as a command or an assertion dict:
goal: {
kind: "subagent",
retry_policy: {max_attempts: 3, feedback: true},
verify: { result ->
const text = to_string(result?.artifacts[0]?.text)
return {ok: contains(text, "SUMMARY:"), findings: ["output is missing a SUMMARY: section"]}
},
}
The verifier receives the settled attempt result and returns either a bool or a
verdict dict {ok, findings?} (findings may be a list<string> or a single
string). A failing fn-verify forces the retry-eligible failed branch and its
findings thread into the next attempt's repair prompt exactly like the
structured-check findings above. It applies on the same VM-executed stage paths
that honor max_attempts (subagent stages and deterministic execute stages) —
so a subagent stage can self-verify its own output and repair with feedback,
without a separate verify node. Because the verifier is a closure it runs in
Harn (workflow_evaluate_verification, std/workflow/stage.harn) and never
crosses into the host.
node.retry_policy.max_attempts uses total-attempt semantics for VM-executed
stage paths: command/compact/manual stages, subagent, fork/join, condition,
reduce, escalation, map branches, and deterministic command verify nodes.
Attempts stop on the first success and every attempt is recorded under the
stage's attempts array. Agent-backed stages still rely on their
agent_loop/LLM retry and iteration policies. Backoff fields are accepted in
the normalized policy shape but deterministic workflow execution does not sleep
between attempts yet; use host/orchestrator retry policy for scheduled delivery
retries and provider failover rather than relying on workflow backoff fields.
Retry with feedback
A stage's retry_policy is the typed WorkflowRetryPolicy
(std/workflow/options): {max_attempts?, feedback?, repair_prompt_builder?, verify?, repair?, backoff_ms?, backoff_multiplier?}. Build it through the
StageSpec alias so a typo fails at check time.
By default a retry re-issues the unmodified task on every attempt (a blind
retry — replayed runs are byte-identical). Two retry_policy keys turn the
retry into a repair loop that threads the prior attempt's verification findings
into the next attempt's task:
-
feedback: trueappends a bounded default template to the retry task —Previous attempt N failed: <findings>, where the findings are the failed verification checks (or the prior attempt's error/output when there are no structured checks).feedback: {max_chars: N}bounds the injected findings (default ~2000 characters). -
repair_prompt_builderis a closure that receives the full retry context and returns the complete replacement task. Its return value becomes the next attempt's task verbatim (it takes precedence overfeedback). The context dict has exactly these keys:{ task, // the original (base) task string attempt, // the just-failed attempt number (its return runs as attempt N+1) findings, // list<string> of failed verification checks verification, // the prior attempt's verification dict error, // the prior attempt's error message, if any prior_text, // the prior attempt's visible text stage, // the stage node }verify_stage: { kind: "subagent", retry_policy: { max_attempts: 3, repair_prompt_builder: { ctx -> return ctx.task + "\n\nFix these findings from attempt " + to_string(ctx.attempt) + ":\n" + join(ctx.findings, "\n") }, }, }
Retry-with-feedback applies to the VM-executed stage paths that consume the
task (subagent stages, and deterministic execute stages) — the same paths that
honor max_attempts. The mechanism lives in the embedded stage loop
(std/workflow/stage.harn), so the closure runs in Harn and never crosses into
the host. workflow_repair_stage_graph (std/workflow/patterns) is the
one-stage sugar over this policy: a single delegated goal stage that retries
with feedback until it settles.
workflow_run_repair (std/workflow/repair) goes one step further and runs
that pattern for you — the run→validate→repair loop as a first-class helper:
const out = workflow_run_repair({
task: "Write the release notes for v1.2.",
model_policy: {provider: "anthropic", model: "claude-sonnet"},
verify: {command: "scripts/lint_release_notes.sh", expect_status: 0},
max_attempts: 3,
})
// out = {ok, status, text, findings, verification, attempts, result, run}
It runs one agent stage, validates its output with the supplied verifier
(a callable, a {command, expect_status?} check, or a
{assert_text?, expect_status?} assertion — command/assertion verifiers are
wrapped into fn-verify closures so they gate + retry), and re-prompts with the
findings up to max_attempts times. It owns no loop of its own; the retry and
findings-threading run in the same attempt machinery described above.
The stage executor
By default an agent stage runs its attempt by delegating to a spawned worker.
Set executor on the stage node to run the attempt as an in-process Harn
closure instead. The closure is the attempt: it receives the attempt context
and returns the attempt result. Reach for it when the work is easier to express
as Harn code than as a delegated agent (a deterministic transform, a call into
your own module, a hand-built repair step), while still getting the stage's
retry, feedback threading, and fn-verify gate for free.
{
id: "act",
kind: "stage",
retry_policy: {max_attempts: 3, feedback: true},
executor: { ctx ->
const patched = my_patch_step(ctx.task, ctx.prior_findings)
return {text: patched.summary, artifacts: patched.artifacts}
},
}
The context it receives. The closure is called with one dict of exactly these keys:
{
task, // the (possibly repaired) task string for this attempt
attempt, // 1-based attempt number
prior_findings, // list<string> of findings from the previous attempt, [] on the first
prior_verification, // the previous attempt's verification dict, nil on the first
prior_text, // the previous attempt's visible text, "" on the first
artifacts, // the artifacts selected for this stage
}
The feedback / repair_prompt_builder policy has already been applied to
task before the closure sees it, so ctx.task on attempt 2+ already carries
the prior findings when feedback is on. The prior_* keys are there when you
want the raw signal rather than the templated task. (Note the distinction from
the repair_prompt_builder context, which keys findings as findings and adds
error and stage; the executor context uses prior_findings and adds
artifacts.)
What it returns. Return a dict shaped as:
{
result?, // the full stage result dict; or use `text` for the common case
text?, // convenience: wrapped into {status: "completed", visible_text: text}
artifacts?, // produced artifacts, defaults to []
transcript?, // falls back to the stage's input transcript
verification?, // a verdict dict {ok, findings?} to self-gate this attempt
}
Supply result for a full stage result, or text for the common "here's my
output" case. A returned verification runs through the same fn-verify gate as
any other stage, so an executor can grade its own attempt and drive the retry.
A throw is a failed attempt. If the closure throws, the stage contains it as
{ok: false, error} and the attempt fails like any other; it does not abort the
run. The next attempt fires with the incremented attempt and the failure
threaded into prior_findings (and into task when feedback is on). This
mirrors the delegated worker's success/failure contract exactly, so a stage
behaves identically whether it delegates or runs your closure.
Where it composes. executor is a field on any agent stage node, so it works
directly in workflow_stages (set executor on a stage row) and in
workflow_run_repair (pass executor in the config to replace the delegated
goal stage with your closure). workflow_repair_stage_graph
(std/workflow/patterns) wires executor onto its single goal stage when you
supply one.
Building linear stage graphs
workflow_stages (std/workflow/patterns) is ergonomic sugar for the common
case of a linear stage pipeline. It expands a concise WorkflowStagesSpec
(a list<StageSpec>, or {stages, name?, entry?, edges?}) into the
{entry, nodes, edges} graph workflow_execute consumes — each stage's id
becomes the nodes-map key, stages are wired head-to-tail, and entry defaults
to the first stage. It is pure sugar over workflow_graph: the result is
byte-identical to the hand-authored equivalent, so there is no new node shape
or runtime concept to learn.
const graph = workflow_stages({
name: "implement",
stages: [
{id: "act", kind: "stage", mode: "agent", model_policy: {provider: "mock"}},
{id: "check", kind: "verify", mode: "command", verify: {expect_status: 0}},
],
})
Stage option flattening and the capability ceiling
Before a stage runs its agent loop, its policy structs — model policy,
auto-compaction, tool spec, capability + approval policy, the workflow skill
registry, and nested-execution attribution — are flattened into the single
options dict the loop consumes. That flattening lives in Harn
(workflow_flatten_agent_loop_options in std/workflow/stage.harn): Harn
decides what options the loop gets.
The host keeps exactly one thing here — enforcement. Rust re-derives the
stage's capability ceiling (the intersection of the tool spec's implied policy
with the stage capability_policy) and, when the flattened dict crosses back
into the host, checks that its policy never widens that ceiling. A
flattener may narrow a capability, budget (recursion_limit), root allowlist,
side-effect level, or sandbox profile, but any attempt to add a tool or
capability, raise a budget, add a root, or loosen the sandbox is rejected with
a tool_rejected error naming the widened dimension. The ceiling is authority;
Harn is trusted only for shape, so the host re-checks rather than assuming the
flattener narrowed correctly.
Verifier requirements can also be published as structured contract inputs for earlier planning and execution stages. Harn injects these contracts into the stage prompt automatically so the model sees exact verifier-owned identifiers, paths, and wiring text before it starts editing:
verify: {
kind: "verify",
verify: {
command: "python scripts/verify_rate_limit.py",
expect_status: 0,
required_identifiers: ["rateLimit"],
required_paths: ["src/middleware/rateLimit.ts"],
required_text: ["app.use(rateLimit)"],
notes: ["Use the verifier-exact symbol names. Do not rename them."]
}
}
When the verifier contract lives outside the workflow file, point contract_path
at a JSON file relative to the workflow execution context:
verify: {
kind: "verify",
verify: {
command: "python scripts/verify_rate_limit.py",
contract_path: "scripts/verify_rate_limit.contract.json",
expect_status: 0
}
}
Options currently include (typed as WorkflowExecuteOptions in
std/workflow/options):
max_stepspersist_pathresume_pathresume_runreplay_pathreplay_runreplay_mode: "deterministic"auditmutation_scopeapproval_policy
Resuming is practical rather than magical: if a saved run has unfinished successor stages, Harn continues from persisted ready-node checkpoints with saved artifacts, transcript state, and traversed run-graph edges.
Deterministic replay is now a runtime mode rather than a CLI-only inspection
tool: passing a prior run via replay_run or replay_path replays saved stage
records and artifacts through the workflow engine without calling providers or
tools again. For delegated stages, replay also preserves the recorded worker
envelope from stage metadata so replayed parent runs keep the same child
run/snapshot pointers for inspection and evals.
Delegated runs surface child worker lineage in each delegated stage's metadata.
This makes replay/eval and host timelines able to distinguish parent execution
from child execution without reconstructing that structure from plain text.
Persisted runs also retain explicit parent_run_id, root_run_id, and
child_runs lineage, and load_run_tree(path) materializes that hierarchy
recursively for inspection or host-side task views. When a process exits after a
stage record is written but before the parent child_runs list is refreshed,
subsequent save/load/normalize passes recover the child entry from the stage's
worker metadata before exposing or replaying the run.
Map nodes can now execute branch work in parallel. node.join_policy.strategy
accepts:
"all"to wait for every branch result"first"to return after the first completed branch"quorum"to return afterjoin_policy.min_completedbranches finish
node.map_policy.max_concurrent limits branch fan-out, and partial failures are
retained alongside successful branch artifacts instead of aborting the whole map
stage on the first error.
Workflow state channels are a design-stage extension for workflows whose
fan-out branches should merge structured state by name instead of only
producing artifacts. The v0 proposal keeps artifacts and transcripts as the
default runtime model, then adds explicit state_channels, node reads /
writes, and deterministic reducers for cases that need LangGraph-style typed
state. See Workflow state channels v0.
Runs may also include metadata.mutation_session, a normalized audit record
used to tie tool gates, workers, and artifacts back to one mutation boundary:
session_idparent_session_idrun_idworker_idexecution_kindmutation_scopeapproval_policy
This is not an editor undo stack. It is the runtime-side provenance contract that hosts can map onto their own approval and undo/redo UX.
Durable workflow messages
Workflows can also expose a durable mailbox/query surface that lives alongside
run records under .harn/workflows/<workflow_id>/state.json. This is the
shared substrate for external workflow control over Harn builtins, ACP, and
A2A without requiring a live in-memory handle.
The mailbox builtins are:
workflow.signal(target, name, payload?)workflow.query(target, name)workflow.publish_query(target, name, value?)workflow.update(target, name, payload?, options?)workflow.receive(target)workflow.respond_update(target, request_id, value, name?)workflow.pause(target)workflow.resume(target)workflow.status(target)workflow.continue_as_new(target)continue_as_new(target)
target may be a workflow-id string or a dict with workflow_id /
workflow. When you already have a saved run, passing
{workflow_id, persisted_path} lets Harn derive the correct workspace root
without an extra lookup.
Use signals for one-way notifications, queries for last-known published state, and updates when the caller needs a response:
const workflow_id = "customer-journey-42"
workflow.signal(workflow_id, "customer_joined", {customer_id: 7})
workflow.publish_query(workflow_id, "progress_pct", 25)
const next = workflow.receive(workflow_id)
log(next?.kind == "signal")
log(workflow.query(workflow_id, "progress_pct"))
workflow.update(...) enqueues a request and waits until
workflow.respond_update(...) publishes a response for the generated
request_id:
const response = workflow.update(
"review-42",
"approve_budget",
{max_usd: 10},
{timeout_ms: 5000}
)
log(response?.approved)
Pause and resume are durable state transitions, not ephemeral process-local
flags. They set paused in workflow state and enqueue a control message so the
workflow can observe that transition through workflow.receive(...).
workflow.continue_as_new(...) increments the workflow generation counter and
clears pending update responses. The std/agents helper
continue_as_new(prev, options?) pairs that state transition with a transcript
reset so long-running workflows can roll forward without losing their durable
workflow identity.
Transcripts and sessions
Stage transcripts are owned by the session store, not by
a per-node transcript_policy dict. Each node picks up a session id from
model_policy.session_id; two nodes that share an id share their
conversation automatically. Unset ids get a stable stage-scoped default.
To shape transcript behavior on a node, use the dedicated workflow setters plus the lifecycle builtins:
workflow_set_auto_compact(graph, node_id, policy)— setsauto_compact,compact_threshold,tool_output_max_chars,compact_strategy,hard_limit_tokens,hard_limit_strategy.workflow_set_output_visibility(graph, node_id, visibility)—"public" | "private" | nil.agent_session_reset(id),agent_session_fork(src, dst?),agent_session_fork_at(src, keep_first, dst?),agent_session_trim(id, keep_last),agent_session_compact(id, opts)— call these in the pipeline beforeworkflow_executeto branch, reset, or compact a stage's conversation explicitly.
The old transcript_policy dict (with mode: "continue" | "reset" | "fork") was removed in 0.7.0; see Sessions for
migration.
Meta-orchestration builtins
Harn exposes typed workflow editing builtins so orchestration changes can be audited and validated against the workflow IR:
workflow_inspect(..., ceiling?)workflow_clone(...)workflow_insert_node(...)workflow_replace_node(...)workflow_rewire(...)workflow_set_model_policy(...)workflow_set_context_policy(...)workflow_set_auto_compact(...)workflow_set_output_visibility(...)workflow_diff(...)workflow_validate(..., ceiling?)workflow_policy_report(..., ceiling?)workflow_commit(...)
These mutate structured workflow graphs, not free-form prompt text.
For common graph shapes, prefer std/workflow/patterns over ad hoc graph
assembly:
workflow_self_verifying_graph(config?)buildsact -> verify.workflow_command_verify_graph(config?)buildsimplement -> verify -> repair -> verify.workflow_verification_only_graph(config?)runs only a deterministic verifier.workflow_failover(config)runs a typed failover loop over opaque route handles while the caller/host owns provider HTTP, credentials, endpoint policy, and billing.
Capability ceilings
Workflows and sub-orchestration may narrow capabilities, but they must not exceed the host/runtime ceiling.
This is enforced explicitly by capability-policy intersection during validation and execution setup. If a node requests tools or host operations outside the ceiling, validation fails.
Run records, replay, and evals
Workflow execution produces a persisted run record containing:
- workflow identity
- task
- stage records
- stage attempts, outcomes, and branch decisions
- traversed graph transitions
- ready-node checkpoints for resume
- stage transcripts
- visible output
- private reasoning metadata
- tool intent and tool execution events
- provider payload metadata kept separate from visible text
- verification outcomes
- artifacts
- policy metadata
- parent/root run lineage and delegated child runs
- a derived observability block summarizing planner rounds, research facts, action-graph nodes/edges, verification outcomes, and transcript pointers
- execution status
CLI support:
harn portal
harn runs view --json .harn-runs/<run>.json
harn runs view --json --session .harn-runs/
harn replay .harn-runs/<run>.json
harn eval .harn-runs/<run>.json
harn eval .harn-runs/
harn eval evals/regression.json
The replay/eval surface is intentionally tied to saved typed run records so host applications do not need to build their own provenance layer.
For a local visual view over the same persisted data, harn portal reads the
run directory directly and renders stages, the derived action graph, trace
spans, transcript sections, and delegated child runs without introducing a
second storage format.
For host/runtime consumers that want the same logic inside Harn code, the VM also exposes:
run_record_fixture(...)run_record_eval(...)run_record_eval_suite(...)run_record_diff(...)eval_suite_manifest(...)eval_suite_run(...)
Eval manifests group persisted runs, optional explicit replay fixtures, and optional baseline run comparisons under a single typed document. This lets hosts treat replay/eval suites as data rather than external scripts.
Host artifact handoff
Hosts and editor bridges should hand Harn typed artifacts instead of embedding their own orchestration rules in ad hoc prompt strings. The VM now exposes helpers for the most common host surfaces:
artifact_workspace_file(...)artifact_workspace_snapshot(...)artifact_editor_selection(...)artifact_verification_result(...)artifact_test_result(...)artifact_command_result(...)artifact_diff(...)artifact_git_diff(...)artifact_diff_review(...)artifact_review_decision(...)artifact_patch_proposal(...)artifact_verification_bundle(...)artifact_apply_intent(...)
These helpers normalize kind names, token estimates, priority defaults, lineage, and metadata so host products can pass editor/test/diff state into Harn without recreating artifact taxonomy and provenance logic externally.