На этой странице
- Blueprint a Harness
- Use / don't use
- 0. Load project context
- Look for an existing harness first
- Then look in the template library
- 1. Interview
- 2. Derive the stack & architecture profile
- The block
- 3. Build
- 4. Graph presets
- Shared deliberation — how a council actually meets
- Architecture decision (type: council / debate)
- Review board (type: review_board)
- Spec audit (type: pipeline)
- 5. Branching
- 6. Budget, policies, timeouts — only what is enforced
- 7. Validate & publish
- Share it (optional)
- 8. Run & link back
- Rules
harness-blueprint
Use when a project needs a new multi-agent harness — a council, review board or debate — and no existing one fits. Triggers on "build a council", "set up a review board", "we need agents to deliberate on X", "собери совет", "нужен консилиум
Blueprint a Harness
Take a request like "we need a council to decide X" all the way to a validated, published harness version tailored to this project. Prerequisite: read flownix-basics for the hierarchy, statuses, the project config file and the harness subsystem.
Everything below describes what harness-service actually executes. If a field, policy or node type is not listed here as doing something, assume the runtime ignores it — and never promise the user otherwise.
Use / don't use
Use it when a project needs a new council / review board / debate and none of the existing harnesses fit, or when an existing one needs a materially different graph, role set or policy set — that is a new version (or a clone) through this same procedure, not an edit of a published one.
Not this skill: you hold a role in a running harness → flownix-harness-participant; you
hold judge/moderator → flownix-harness-moderator; you just want to look at one →
harness.get / harness.export / harness.get_run.
0. Load project context
cat .flownix 2>/dev/null || cat .ai-flowParse the YAML for project_id and org_id. If neither file exists, run flownix-init first
and do not create anything until project_id is known.
workspace_idin everyharness.*call is theproject_idfrom the project config. harness-service storesworkspace_idas a plain UUID with no foreign key and no validation: pass the wrong one and nothing errors — the harness is simply created in a scope nobody looks at. There is no second source for this value; never invent one.
Then get_project_policy({ project_id }). In spec_mode: "strict" any decision this council
produces will need a spec delta before the work can be closed — say so in the roles' instructions
so the council's output is written to be applicable, not just persuasive.
Look for an existing harness first
harness.list({ workspace_id }) // id, name, type, status, current_version (published number)Before any harness.create: if a harness with the same purpose already exists, name it to the
user and ask which of three they want:
- a new version of it —
harness.create_versionon that harness. The new draft starts empty: roles, nodes, edges and policies are not copied, so rebuild them (read the current one withharness.get/harness.export); - a clone —
harness.clone({ harness_id, new_name })deep-copies roles, nodes, edges and policies into a new harness whose version 1 is an editable draft; - a separate harness anyway — only on an explicit answer.
Then look in the template library
No suitable harness in the project? Before building from scratch, search the public library — other Flownix users may already have published the council or pipeline you need:
harness.library_list({ query: "<purpose in a few words>", type: "council", sort: "rating" })
harness.library_get({ package_id }) // full composition, versions, rating, statsShow the user the best matches (title, summary, rating, clones) and offer to clone one with
harness.library_clone({ package_id, workspace_id, number? }). The clone is an independent
draft: roles, steps, transitions, policies and schemas are copied, but project policy links and
participants are not — adapt the instructions to this project, then continue from step 7
(validate & publish). Build from scratch only when nothing fits or the user says so.
1. Interview
Ask all questions in one batch, each with its default shown, and let the user answer only what they care about. Never ask about the stack, architecture, conventions or spec mode — those are derived in step 2. Eight questions, no more.
| # | Question | Default if unanswered | Maps to |
|---|---|---|---|
| 1 | What is this council for, and what decision must it hand back? | (no default — must be answered; without it there is nothing to build) | harness.create name + description; picks the graph preset |
| 2 | Type: review / debate / design / audit? | inferred from Q1, else council | harness.create type (single_agent, pipeline, council, review_board, debate, custom); picks the graph preset |
| 3 | What goes in — a Flownix node, a diff, the spec, an external document? | the node the request came from; nothing else | version input_schema; input and source_node_ids on harness.start_run |
| 4 | Who takes part, and what is each participant's specialization? | preset's default roles (see step 4) | one harness.add_role per participant: key, name, objective, system_instructions, min_instances/max_instances |
| 5 | How is the winner picked — vote or judge? | judge for review/audit, vote for debate/design | a vote node with config.method/quorum/threshold/allow_abstain/timeout_seconds, or a judge node — both on the council's deliberation_key |
| 6 | Is a human gate needed, and at which step? | no gate (except before create_flow_nodes, where it is mandatory) | human_gate node (+ a rejection branch); or a tools policy require_approval on work.complete |
| 7 | Any tool or data restrictions? | none beyond the roles' prohibited_actions | tools policies on work.* / council.*; everything else goes into prohibited_actions as an instruction (see step 6) |
| 8 | Budget — how many rounds, what time/token/cost ceiling? | 1 debate round, no explicit ceiling | version budget (harness.set_budget); config.max_rounds on debate; a loop with max_iterations for rework rounds |
Rules for the interview:
- Defaults are applied silently, and reported back. After the batch, restate the resolved
configuration (all eight lines, defaults marked as such) and get one confirmation before
the first
harness.create. A user who answers nothing but Q1 still gets a working harness. - Q1 is the only blocking question. If it is not answered, ask it again and build nothing.
- Anything derivable is not asked. If you are about to ask what language the project is in, you skipped step 2.
- The answers, not the preset, win. The preset from Q1/Q2 is a starting layout; Q4–Q8 reshape its nodes, roles and policies.
2. Derive the stack & architecture profile
A council that proposes a Kafka bus to a project that has none is worse than no council. Build
the profile once, before any role exists, and paste the identical block into every role's
system_instructions.
Extraction order — stop as soon as a line is answered, and prefer the cheap source:
get_project_policy({ project_id })— spec mode. Instrict, every role's instructions must also say: the decision has to be expressible as a spec delta on an existing doc node.list_docs({ project_id })plusrag_context/rag_queryon"architecture","stack","conventions". Ifbootstrap-spechas been run on this project, the profile comes almost entirely from these doc nodes and the repo is only spot-checked.- The repository, which wins on any disagreement with the docs — docs go stale, manifests
do not:
- manifests:
go.work, eachgo.mod, eachpackage.json; - service layout:
services/**, top-level apps (web,admin,landing, …); - contracts:
proto/**plusbuf.yaml/buf.gen.yaml; - storage and schema history:
migrations/**; - deploy:
k3s/**,Dockerfile*,docker-compose.yml, CI workflows; - build and test commands: the root
Makefile.
- manifests:
When the repo and the docs disagree, record the repo's answer in the profile and tell the user which doc is stale — it is a finding, not noise to swallow.
The block
Fill this template and reuse it verbatim across all roles of the harness. Same text everywhere: two roles working from different pictures of the same system is exactly the failure this section exists to prevent.
## Project stack & architecture (authoritative)
- Languages & runtimes: <…>
- Services & boundaries: <…>
- Transport & contracts: <…>
- Storage & migrations: <…>
- Build, test, deploy: <…>
- Conventions that constrain solutions: <…>
Propose solutions that fit this stack. A proposal that requires a new language,
framework, datastore, or transport must be marked as an explicit alternative and
carry a justification plus a migration cost estimate.Rules for filling it:
- Every line is backed by a file or a node. A category you could not confirm is written as
unknown — not found in <where you looked>, never guessed and never quietly dropped. - The closing paragraph is a constraint on the role, not advice. Keep it verbatim; it is what makes an off-stack proposal a labelled alternative instead of the default answer.
- Keep it to structure and contracts. No source code, no file-by-file inventory — the roles get a map, not the territory.
Worked example — this repository, filled from go.work, services/**, proto/** + buf.*,
migrations/**, docker-compose.yml, k3s/** and the root Makefile:
## Project stack & architecture (authoritative)
- Languages & runtimes: Go 1.25 (go.work workspace: pkg, gen/go and one module per
service); TypeScript/Next.js for the three frontends; a TypeScript CLI in cli/.
- Services & boundaries: seven Go services under services/** — core-service (nodes, docs,
spec mode), auth-service, admin-service (back office: users, organizations, projects,
feedback), gateway (HTTP edge), mcp-server, harness-service, rag-service.
Frontends: web, admin, landing.
- Transport & contracts: gRPC between services, proto/{core,auth,admin,harness,rag}/v1
generated with buf into gen/go; HTTP at the gateway and the MCP server; NATS client
in core-service and rag-service.
- Storage & migrations: PostgreSQL 16 with pgvector; one shared, numbered migrations/**
sequence (NNNN_name.up.sql / .down.sql); Qdrant for RAG.
- Build, test, deploy: root Makefile (proto, migrate-up, test, up/down), docker-compose.yml,
Dockerfile*, k3s/** Helm charts, GitHub Actions deploy and contract workflows.
- Conventions that constrain solutions: contract changes start in proto/** and are regenerated
with buf, never hand-edited; schema changes ship as a new migration, never as an edit to an
applied one; spec_mode=strict means closing a task needs an applied doc delta or an explicit
declare_no_spec_impact.3. Build
Fixed order — each call needs an ID the previous one returned:
// 1. The definition (only after harness.list and the user's confirmation).
// workspace_id === project_id from the project config.
harness.create({ workspace_id, name, description, type }) // → harness_id
// 2. A draft version. Everything below hangs off this version.
harness.create_version({ harness_id, changelog: "initial blueprint",
input_schema,
default_budget: { max_run_duration_seconds, max_work_items,
max_tokens, max_cost_usd } }) // → harness_version_id
// 3. One call per participant from Q4. Paste the step-2 profile block into
// system_instructions, identically for every role.
harness.add_role({ harness_version_id, key: "architect", name: "Architect",
objective: "…", system_instructions: "…profile block…",
responsibilities: [...], prohibited_actions: [...],
output_schema: { type: "object", required: ["summary"],
properties: { summary: { type: "string" } } },
min_instances: 2, max_instances: 2 }) // → role_id
// 4. Nodes, then edges.
harness.add_workflow_node({ harness_version_id, type: "debate", name: "proposals",
config: { role_key: "architect", deliberation_key: "decision",
max_rounds: 2, lease_seconds: 900 },
timeout_seconds: 3600, retry_policy: { max_attempts: 2 } })
harness.connect_workflow_nodes({ harness_version_id, from_node_id, to_node_id,
condition: { field: "outcome", op: "eq", value: "winner" } })
// 5. Budget, context, output contract and policies from Q6–Q8.
harness.set_budget({ harness_version_id, budget: { … } })
harness.set_context_policy({ harness_version_id, include: ["run_input", "all_steps"],
exclude: ["flow_nodes"], max_tokens: 16000 }) // optional
harness.set_output_schema({ harness_version_id,
output_schema: { type: "object", required: ["report"] } }) // optional
harness.set_policy({ harness_version_id, category: "tools", effect: "deny",
subject: { role_key: "critic" }, action: "council.cast_vote" })Things that bite:
-
Nodes reference roles by
config.role_key, not byrole_id.agent_task,review,debate,voteandjudgeare executed by an agent and must have one (node.role_missing/node.role_unknownotherwise). -
Node types accepted:
agent_task,parallel_group,review,debate,vote,judge,human_gate,transform,create_flow_nodes,condition,loop,end.parallel_groupandtransformare not executed — they complete immediately with an empty output (node.unimplemented_type). Do not use them: parallelism is several outgoing edges from one node, or several start nodes (nodes without incoming edges all start at once). -
reviewruns exactly likeagent_task. No deliberation is opened for it. -
configkeys the runtime reads, per type (anything else, apart fromtitleanddescription, isnode.config_ignored):Type configkeysagent_task,reviewrole_key,lease_secondsdebate,judgerole_key,subject,max_rounds,deliberation_key,lease_secondsvotethe above + method,quorum,threshold,allow_abstain,timeout_secondshuman_gateoptions,approval_type,timeout_seconds(+title,descriptionshown to the human)conditionconditionloopmax_iterations,exit_conditioncreate_flow_nodes,endnone timeout_secondsandretry_policyare top-level parameters ofharness.add_workflow_node, notconfigkeys — exceptconfig.timeout_secondsonvoteandhuman_gate, which is the ballot / approval deadline. -
input_bindings,output_bindings, edgemappingandpriorityare not executed (node.field_ignored,edge.field_ignored). A step receives its predecessors' outputs andresult_artifact_idsthroughwork.get_context; that is the only data path. -
Instances: for
debateandvotethe runtime createsmin_instanceswork items of the node's role per round / ballot.max_instancesis only checked (1 ≤ min ≤ max); it does not add work. Every other agent node gets exactly one work item. -
Edge conditions are JSON over the source step's output: a leaf
{ "field": "<dotted.path>", "op": "…", "value": … }withopone ofeq,neq,in,not_in,gt,gte,lt,lte,contains,exists,not_exists(defaulteq;in/not_inneed an array), or{ "all": [...] }/{ "any": [...] }. A missing field does not match. No condition = always taken (except fromhuman_gate, see step 5). -
output_schemais enforced. A role'soutput_schema(JSON Schema 2020-12) is checked on everywork.completeof that role — output that does not match is refused withoutput.schema_violationand the agent retries; the version'soutput_schema(harness.set_output_schema) is checked on steps whose node has an edge intoend. Give a schema to every role whose output something downstream reads:outcome-driven edges read the step output,create_flow_nodesreadsoutput.flow_nodes, synthesis reads the named fields. A schema that is not valid JSON Schema is refused withoutput.schema_invalidand blocks publish. Checked beforerequire_approval, so humans only see well-formed results. -
Context policy (
harness.set_context_policy) decides whatwork.get_contextreturns:includefromrun_input,role,predecessors,all_steps(every completed step, in order — for synthesis and triage nodes that are not directly wired to the research),excludetop-level output keys (e.g.flow_nodes, large blobs),max_tokenscapping step outputs (JSON bytes / 4, earliest dropped first). Without a policy agents getrun_input,role,predecessors. There is no retrieval strategy: agents search withrag_querythemselves. -
Only a draft can be changed. Any
harness.add_*,harness.update_role,harness.connect_workflow_nodes,harness.set_policy,harness.set_budget,harness.set_context_policyorharness.set_output_schemaon a published version is refused withversion.published_immutable— that refusal means you are editing the wrong version, not that you should retry.
4. Graph presets
Pick by the answers to Q1/Q2, then reshape with Q4–Q8. These are starting layouts, not doctrine: drop a node the interview didn't ask for, add the human gate where the user wanted it.
Shared deliberation — how a council actually meets
debate,voteandjudgeopen a deliberation (proposals, critiques, ballots, decisions). Nodes with the sameconfig.deliberation_keyshare one deliberation per run; a node without a key gets its own, empty one.- Give every
debate,voteandjudgenode of one council the samedeliberation_key, and putconfig.max_roundson thedebatenodes. Avote/judgewhose key is missing or matches nodebateproducescouncil.isolated_vote: it has nothing to choose from. Fix it, never publish it as "accepted". debaterounds: each round createsmin_instanceswork items; when they are all closed and major/blocking critiques are still unanswered, the next round opens, untilmax_rounds. The step outputoutcomeissettledorrounds_exhausted.voteopens a ballot when the step starts: options are the deliberation's proposals that are not withdrawn or rejected (none → the step fails withballot.no_options, sovotemust come after the debate). It givesmin_instancesvoters of its role a work item; they vote withcouncil.cast_vote/council.abstain. The ballot closes on quorum, when all voter work is closed, or atconfig.timeout_seconds; the stepoutcomeiswinner,tied,no_quorumorno_consensus.- Vote rules live in the
votenode'sconfig— there is no separate "create ballot" step:method(single_choicedefault,approval,ranked_choice,score,consensus,veto),quorum(number of votes),threshold(minimum score forscore),allow_abstain(defaulttrue),timeout_seconds. judgegets one work item; the judge reads the deliberation and closes the step withcouncil.submit_judgement(outputdecision_id,selected_proposal_ids).
Architecture decision (type: council / debate)
debate(proposer A) ─┐
├─→ vote | judge ─→ end (all three: deliberation_key "decision")
debate(proposer B) ─┘Two or more debate start nodes (different proposer roles, or one role with min_instances > 1)
feed a vote or judge on the same key. A node with several incoming edges waits until every
predecessor has finished. Default roles: two-plus proposers with different angles, one
critic (a debate node of its own on the same key), plus judge if Q5 said judge. A useful
reshape when Q5 said vote: vote ─(outcome in ["tied","no_quorum","no_consensus"])→ judge → end
and vote ─(outcome eq "winner")→ end.
Review board (type: review_board)
start ─┬─→ review(correctness) ─┐
├─→ review(security) ─┼─→ judge ─→ end
└─→ review(performance) ─┘Default roles: correctness, security, performance reviewers + judge; dimensions come
from Q4. start is any first node (an agent_task preparing the input, or drop it and make the
reviews start nodes). Reviewers do not see each other's work — each is a separate step; the judge
receives all their outputs and result_artifact_ids through work.get_context. review opens no
deliberation, so a judge here still reports council.isolated_vote; if the judge must select
among positions via the council tools, make the reviewers debate nodes on the judge's
deliberation_key instead of review.
Spec audit (type: pipeline)
agent_task(analyze spec) → human_gate → create_flow_nodes → end
└─(outcome eq "rejected")→ end- The
human_gateis mandatory on every path tocreate_flow_nodes, even if Q6 said no gate —create_flow_nodeswrites real nodes into the project; the validator refuses a path without a gate (node.flow_nodes_without_gate). Useconfig.approval_type: "approve_task_creation". create_flow_nodeshas no config. It takes the list from the output of the nearest preceding agent step (looking throughhuman_gate,conditionandloop). So thesystem_instructionsof the role runninganalyze specmust require it to finish withwork.complete({ …, output: { flow_nodes: [...] } }), each item:{ key, kind: "epic"|"feature"|"plan"|"task", title, content, parent, components, tags }—keyunique within the list,parentan existing node slug (the project slug for a root epic) or thekeyof an earlier item,componentscomponent slugs or IDs.- Nodes are created on behalf of the user who started the run (who must be a member of the
project's organization). A bad list fails the step with
flow_nodes.invalid_inputbefore anything is created;flow_nodes.permission_denied/flow_nodes.refusedname the item. A retried step skips items already created in this run.
5. Branching
Every completed step has an output; branching reads its outcome.
| Step | outcome it produces |
|---|---|
agent_task, review | whatever the agent passes to work.complete outcome (^[a-z0-9_]{1,32}$, e.g. approved, changes_requested) — state the allowed values in the role's instructions |
debate | settled, rounds_exhausted |
vote | winner, tied, no_quorum, no_consensus |
human_gate | approved, rejected (plus approved: true/false, selected_option) |
condition | true, false |
loop | continue, exit (plus exit_reason: condition / max_iterations) |
-
Conditional edges:
{ "field": "outcome", "op": "eq", "value": "approved" }. Every edge whose condition matches is taken. A node whose predecessors have all finished but none of whose incoming edges was taken is skipped, and so is everything downstream that depends only on it. -
conditionnode: no agent; evaluatesconfig.conditionon the output of its single predecessor (exactly one incoming edge) and outputsoutcometrue/false; branch on that. -
human_gaterejection branch: an edge without a condition out of ahuman_gateis taken only on approval. For every gate whose rejection must not silently end the run, add an edge with{ "field": "outcome", "op": "eq", "value": "rejected" }— back to rework, or to anend. The human decides in the Flownix web app; the agent can only read the open request withharness.list_pending_approvals. A gate withconfig.timeout_secondsthat expires fails the step (approval.timeout). -
loop— rework until approved, bounded:shell start → loop ─(outcome eq "continue")→ author(agent_task) → reviewer(review) ─┐ ↑ │ └───────────────────────────────────────────────────────────────────┘ loop ─(outcome eq "exit")→ end config: { max_iterations: 3, exit_condition: { field: "outcome", op: "eq", value: "approved" } }Required:
config.max_iterations1–100; an outgoing edge whose condition is exactly{ "field": "outcome", "op": "eq", "value": "continue" }into the body, one with"exit", and an edge from the last body node back into theloop.exit_conditionis evaluated on the output of the body step that returns into the loop; without it the loop runs untilmax_iterations. The loop may itself be the start node. Nested loops are not supported. Never model repetition as a plain back-edge — that isgraph.cycle. -
endcompletes the whole run at once (open work is cancelled, remaining steps skipped). It must have no outgoing edges.
6. Budget, policies, timeouts — only what is enforced
Budget (Q8) — per version, on a draft: default_budget in harness.create_version or
harness.set_budget({ harness_version_id, budget }) (the whole budget; omitted limits are
cleared). Limits: max_run_duration_seconds, max_work_items, max_tokens, max_cost_usd;
0/omitted = no limit. Each run snapshots it at start; harness.start_run budget can only
tighten it. Any exhausted limit fails the run with budget.exceeded. Tokens and cost count only
what agents report in work.complete / work.fail usage — tell the user that.
Policies (Q7) — the runtime enforces only category tools, and only on work.* and
council.* calls made by an agent holding a lease:
subject: { role_key }or{ role_keys: [...] }(omit for all roles);action: a tool name (council.cast_vote), a prefix (council.*) or omitted/*for all of them.- Precedence:
denybeatsrequire_approvalbeatsallow; no matching policy = allowed. Soallowon its own restricts nothing, and an allow-list cannot be built fromdeny *+allow X(the deny wins). Express "everything except this" as a narrowdeny. A denied call fails withpolicy.denied. require_approvalis accepted only withaction: "work.complete": the agent's result then waits for a human (runwaiting_for_user); approval completes the step, rejection fails it. Any otherrequire_approvalis refused withpolicy.unsupported.resource_patternandconditionare stored but not evaluated.- Categories
data_access,voting,budget,execution,approval,retention,privacyare stored and not enforced:harness.set_policyanswers with apolicy.not_enforcedwarning. Relay it to the user as "this is not a protection", and prefer not to add such policies at all. - Calls outside Flownix are never intercepted — shell, files, databases, other MCP servers.
A restriction like "the critic must not touch the prod database" goes into that role's
prohibited_actionsas an instruction (it reaches the agent viawork.claim/work.get_context), and you tell the user it is an instruction, not an enforced policy.
Timeouts and retries — per node:
config.lease_seconds(agent nodes only; 60–3600, default 300): how long a claim lasts betweenwork.heartbeats before the work returns to the queue. Set it to the realistic length of one unit of work.retry_policy: { max_attempts }(1–10, default 3): how many times failed work goes back to the queue before the step fails.timeout_seconds(top-level,0= none): wall time for an agent step from its first work item; when exceeded the open work is cancelled and the step fails withstep.timeout, without retries.config.timeout_secondsonvotecloses the ballot; onhuman_gateit expires the approval and fails the step.
7. Validate & publish
harness.validate({ harness_version_id }) // errors[] must be empty
harness.publish({ harness_version_id }) // only after a clean validateharness.validate is not optional and not advisory. errors block publishing — harness.publish
re-runs the same rules and refuses with version.invalid. Every warning is a decision you state
to the user out loud. node.unimplemented_type is always fixed; council.isolated_vote is fixed
in every council preset (the one tolerated case is the review-board judge described in step 4,
and only after telling the user).
Each finding carries code, a location (version, role:<key>, node:<name>,
edge:<from>-><to>) and a message.
Errors (block publishing):
| Code | What happened | How to fix |
|---|---|---|
graph.empty | the version has no nodes | add nodes with harness.add_workflow_node |
graph.no_start | every node has an incoming edge (loop back-edges excluded) | leave at least one node without incoming edges |
graph.cycle | a plain back-edge forms a cycle | remove the back-edge; model repetition with a loop node (step 5) |
graph.unreachable_node | a node cannot be reached from any start node | connect it with harness.connect_workflow_nodes, or build a new version without it |
node.name_duplicate | two nodes share a name | give every node a unique name |
node.unknown_type | type is not one of the twelve | use one of the listed types |
node.role_missing | an agent node has no config.role_key | set config.role_key to a role key of this version |
node.role_unknown | config.role_key matches no role | fix the key, or harness.add_role |
edge.condition_invalid | edge condition is not valid (bad JSON, empty field, unknown op, in without an array, mixed all/any/field) | rewrite it in the condition language from step 3 |
role.instances_invalid | not 1 ≤ min_instances ≤ max_instances | harness.update_role with valid counts |
limits.exceeded | over 200 nodes, 500 edges, 50 roles, or 20000 chars in a role's system_instructions / a node's config | split the process into several harnesses, merge roles, shorten text |
node.condition_invalid | condition node without a valid config.condition, or not exactly one incoming edge | set a valid config.condition; give it exactly one predecessor |
node.end_has_outgoing | an end node has outgoing edges | remove them — end finishes the run |
node.loop_invalid | loop lacks max_iterations 1–100, the continue edge, the exit edge, or a back-edge from the body | add the missing piece exactly as in step 5 |
node.config_invalid | config.lease_seconds outside 60–3600, retry_policy.max_attempts outside 1–10, or negative timeout_seconds | bring the value into range |
node.flow_nodes_without_gate | a path reaches create_flow_nodes without passing a human_gate | put a human_gate on every path into it |
Warnings (do not block):
| Code | What happened | How to fix |
|---|---|---|
graph.no_end | no path ends in an end node | add an end node at the end of each path |
role.unused | no node addresses the role | reference it in a config.role_key, or leave it out of the next version |
node.config_ignored | config keys the runtime does not read for this type | remove them or move the text into the role's instructions (table in step 3) |
node.unimplemented_type | parallel_group or transform — completes immediately, does nothing | replace with outgoing edges / an agent node |
node.field_ignored | input_bindings / output_bindings set | remove them; data flows via result_artifact_ids |
edge.field_ignored | edge mapping or non-zero priority set | remove them; only the condition selects a branch |
council.isolated_vote | vote/judge without a deliberation_key shared with a debate | give it the same deliberation_key as the council's debate nodes |
policy.not_enforced is not a validator code: it comes back from harness.set_policy (step 6).
Policies and input/output schemas are not checked by harness.validate.
Publishing makes the version the harness's current one. Never edit a published version: to
change anything, harness.create_version (empty draft — rebuild it), validate, publish; or
harness.clone. version.published_immutable on any write means you targeted a published one.
Share it (optional)
If the user wants the harness reusable by others, offer to publish the published version to the
library. Run harness.library_preview({ harness_version_id }) and show the user what will become
public: role instructions and step configs are copied as is, including internal paths, task slugs
and project data — suggest removing them first. Publish only on an explicit yes, with
harness.library_publish({ harness_version_id, title, summary, confirm_public: true }).
8. Run & link back
harness.start_run({ workspace_id, harness_id, harness_version_id,
title, input, source_node_ids: ["<node the run is about>"] })Drafts are refused (version.not_published). start_run advances the graph immediately: start
nodes get steps, agent nodes get work items visible through work.list_available(run_id, role_key).
Then link the run on the ticket — leave a comment on the source node:
add_comment({ slug: "<source node>", body: "[progress] Harness run started: run_id=<…>, harness_id=<…>, version=<…>" })Runs about a node are found with
harness.list_runs({ workspace_id, source_node: "<slug or UUID>" }); one run's steps and outputs
with harness.get_run({ run_id }); what it waits for from a human with
harness.list_pending_approvals({ run_id }).
Your procedure ends here. Taking work in the run is flownix-harness-participant, judging is
flownix-harness-moderator — name them to the user; do not call work.claim or council.*
yourself.
Rules
workspace_idis alwaysproject_id. No validation, no error, no second guess.harness.listbeforeharness.create. A second harness for the same purpose is a decision the user makes, not a default.- Never invent tools or fields. Only the
harness.*calls and parameters above exist for building;work.*andcouncil.*belong to participants and the judge. - Never promise what the runtime does not do.
parallel_group,transform, bindings, edgemapping/priority, non-toolspolicies and calls outside Flownix are not enforced — say so. - One
deliberation_keyper council, vote rules in thevotenode'sconfig. - A gate before
create_flow_nodes, always, and the predecessor's instructions demandflow_nodesin itswork.completeoutput. - Publish only what validated clean. Errors are blocking; warnings are a decision to state out loud, not to skip.
- Never edit a published version. Changes go into a new draft via
harness.create_versionor a clone. - Ask Q1, derive the rest. Anything you could have read from the spec, the docs or the repo is not a question.
- One profile block, all roles. Divergent role instructions produce a council arguing about the system rather than about the decision.