Разделы
На этой странице

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

shell
cat .flownix 2>/dev/null || cat .ai-flow

Parse 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_id in every harness.* call is the project_id from the project config. harness-service stores workspace_id as 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

shell
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 itharness.create_version on that harness. The new draft starts empty: roles, nodes, edges and policies are not copied, so rebuild them (read the current one with harness.get / harness.export);
  • a cloneharness.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:

shell
harness.library_list({ query: "<purpose in a few words>", type: "council", sort: "rating" })
harness.library_get({ package_id })   // full composition, versions, rating, stats

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

#QuestionDefault if unansweredMaps to
1What 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
2Type: review / debate / design / audit?inferred from Q1, else councilharness.create type (single_agent, pipeline, council, review_board, debate, custom); picks the graph preset
3What goes in — a Flownix node, a diff, the spec, an external document?the node the request came from; nothing elseversion input_schema; input and source_node_ids on harness.start_run
4Who 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
5How is the winner picked — vote or judge?judge for review/audit, vote for debate/designa vote node with config.method/quorum/threshold/allow_abstain/timeout_seconds, or a judge node — both on the council's deliberation_key
6Is 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
7Any tool or data restrictions?none beyond the roles' prohibited_actionstools policies on work.* / council.*; everything else goes into prohibited_actions as an instruction (see step 6)
8Budget — how many rounds, what time/token/cost ceiling?1 debate round, no explicit ceilingversion 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:

  1. get_project_policy({ project_id }) — spec mode. In strict, every role's instructions must also say: the decision has to be expressible as a spec delta on an existing doc node.
  2. list_docs({ project_id }) plus rag_context / rag_query on "architecture", "stack", "conventions". If bootstrap-spec has been run on this project, the profile comes almost entirely from these doc nodes and the repo is only spot-checked.
  3. The repository, which wins on any disagreement with the docs — docs go stale, manifests do not:
    • manifests: go.work, each go.mod, each package.json;
    • service layout: services/**, top-level apps (web, admin, landing, …);
    • contracts: proto/** plus buf.yaml / buf.gen.yaml;
    • storage and schema history: migrations/**;
    • deploy: k3s/**, Dockerfile*, docker-compose.yml, CI workflows;
    • build and test commands: the root Makefile.

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.

shell
## 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:

shell
## 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:

shell
// 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 by role_id. agent_task, review, debate, vote and judge are executed by an agent and must have one (node.role_missing / node.role_unknown otherwise).

  • Node types accepted: agent_task, parallel_group, review, debate, vote, judge, human_gate, transform, create_flow_nodes, condition, loop, end. parallel_group and transform are 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).

  • review runs exactly like agent_task. No deliberation is opened for it.

  • config keys the runtime reads, per type (anything else, apart from title and description, is node.config_ignored):

    Typeconfig keys
    agent_task, reviewrole_key, lease_seconds
    debate, judgerole_key, subject, max_rounds, deliberation_key, lease_seconds
    votethe above + method, quorum, threshold, allow_abstain, timeout_seconds
    human_gateoptions, approval_type, timeout_seconds (+ title, description shown to the human)
    conditioncondition
    loopmax_iterations, exit_condition
    create_flow_nodes, endnone

    timeout_seconds and retry_policy are top-level parameters of harness.add_workflow_node, not config keys — except config.timeout_seconds on vote and human_gate, which is the ballot / approval deadline.

  • input_bindings, output_bindings, edge mapping and priority are not executed (node.field_ignored, edge.field_ignored). A step receives its predecessors' outputs and result_artifact_ids through work.get_context; that is the only data path.

  • Instances: for debate and vote the runtime creates min_instances work items of the node's role per round / ballot. max_instances is 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": … } with op one of eq, neq, in, not_in, gt, gte, lt, lte, contains, exists, not_exists (default eq; in/not_in need an array), or { "all": [...] } / { "any": [...] }. A missing field does not match. No condition = always taken (except from human_gate, see step 5).

  • output_schema is enforced. A role's output_schema (JSON Schema 2020-12) is checked on every work.complete of that role — output that does not match is refused with output.schema_violation and the agent retries; the version's output_schema (harness.set_output_schema) is checked on steps whose node has an edge into end. Give a schema to every role whose output something downstream reads: outcome-driven edges read the step output, create_flow_nodes reads output.flow_nodes, synthesis reads the named fields. A schema that is not valid JSON Schema is refused with output.schema_invalid and blocks publish. Checked before require_approval, so humans only see well-formed results.

  • Context policy (harness.set_context_policy) decides what work.get_context returns: include from run_input, role, predecessors, all_steps (every completed step, in order — for synthesis and triage nodes that are not directly wired to the research), exclude top-level output keys (e.g. flow_nodes, large blobs), max_tokens capping step outputs (JSON bytes / 4, earliest dropped first). Without a policy agents get run_input, role, predecessors. There is no retrieval strategy: agents search with rag_query themselves.

  • 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_policy or harness.set_output_schema on a published version is refused with version.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, vote and judge open a deliberation (proposals, critiques, ballots, decisions). Nodes with the same config.deliberation_key share one deliberation per run; a node without a key gets its own, empty one.
  • Give every debate, vote and judge node of one council the same deliberation_key, and put config.max_rounds on the debate nodes. A vote/judge whose key is missing or matches no debate produces council.isolated_vote: it has nothing to choose from. Fix it, never publish it as "accepted".
  • debate rounds: each round creates min_instances work items; when they are all closed and major/blocking critiques are still unanswered, the next round opens, until max_rounds. The step output outcome is settled or rounds_exhausted.
  • vote opens a ballot when the step starts: options are the deliberation's proposals that are not withdrawn or rejected (none → the step fails with ballot.no_options, so vote must come after the debate). It gives min_instances voters of its role a work item; they vote with council.cast_vote / council.abstain. The ballot closes on quorum, when all voter work is closed, or at config.timeout_seconds; the step outcome is winner, tied, no_quorum or no_consensus.
  • Vote rules live in the vote node's config — there is no separate "create ballot" step: method (single_choice default, approval, ranked_choice, score, consensus, veto), quorum (number of votes), threshold (minimum score for score), allow_abstain (default true), timeout_seconds.
  • judge gets one work item; the judge reads the deliberation and closes the step with council.submit_judgement (output decision_id, selected_proposal_ids).

Architecture decision (type: council / debate)

shell
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)

shell
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)

shell
agent_task(analyze spec) → human_gate → create_flow_nodes → end
                                └─(outcome eq "rejected")→ end
  • The human_gate is mandatory on every path to create_flow_nodes, even if Q6 said no gate — create_flow_nodes writes real nodes into the project; the validator refuses a path without a gate (node.flow_nodes_without_gate). Use config.approval_type: "approve_task_creation".
  • create_flow_nodes has no config. It takes the list from the output of the nearest preceding agent step (looking through human_gate, condition and loop). So the system_instructions of the role running analyze spec must require it to finish with work.complete({ …, output: { flow_nodes: [...] } }), each item: { key, kind: "epic"|"feature"|"plan"|"task", title, content, parent, components, tags }key unique within the list, parent an existing node slug (the project slug for a root epic) or the key of an earlier item, components component 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_input before anything is created; flow_nodes.permission_denied / flow_nodes.refused name the item. A retried step skips items already created in this run.

5. Branching

Every completed step has an output; branching reads its outcome.

Stepoutcome it produces
agent_task, reviewwhatever 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
debatesettled, rounds_exhausted
votewinner, tied, no_quorum, no_consensus
human_gateapproved, rejected (plus approved: true/false, selected_option)
conditiontrue, false
loopcontinue, 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.

  • condition node: no agent; evaluates config.condition on the output of its single predecessor (exactly one incoming edge) and outputs outcome true/false; branch on that.

  • human_gate rejection branch: an edge without a condition out of a human_gate is 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 an end. The human decides in the Flownix web app; the agent can only read the open request with harness.list_pending_approvals. A gate with config.timeout_seconds that 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_iterations 1–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 the loop. exit_condition is evaluated on the output of the body step that returns into the loop; without it the loop runs until max_iterations. The loop may itself be the start node. Nested loops are not supported. Never model repetition as a plain back-edge — that is graph.cycle.

  • end completes 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: deny beats require_approval beats allow; no matching policy = allowed. So allow on its own restricts nothing, and an allow-list cannot be built from deny * + allow X (the deny wins). Express "everything except this" as a narrow deny. A denied call fails with policy.denied.
  • require_approval is accepted only with action: "work.complete": the agent's result then waits for a human (run waiting_for_user); approval completes the step, rejection fails it. Any other require_approval is refused with policy.unsupported.
  • resource_pattern and condition are stored but not evaluated.
  • Categories data_access, voting, budget, execution, approval, retention, privacy are stored and not enforced: harness.set_policy answers with a policy.not_enforced warning. 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_actions as an instruction (it reaches the agent via work.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 between work.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 with step.timeout, without retries.
  • config.timeout_seconds on vote closes the ballot; on human_gate it expires the approval and fails the step.

7. Validate & publish

shell
harness.validate({ harness_version_id })   // errors[] must be empty
harness.publish({ harness_version_id })    // only after a clean validate

harness.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):

CodeWhat happenedHow to fix
graph.emptythe version has no nodesadd nodes with harness.add_workflow_node
graph.no_startevery node has an incoming edge (loop back-edges excluded)leave at least one node without incoming edges
graph.cyclea plain back-edge forms a cycleremove the back-edge; model repetition with a loop node (step 5)
graph.unreachable_nodea node cannot be reached from any start nodeconnect it with harness.connect_workflow_nodes, or build a new version without it
node.name_duplicatetwo nodes share a namegive every node a unique name
node.unknown_typetype is not one of the twelveuse one of the listed types
node.role_missingan agent node has no config.role_keyset config.role_key to a role key of this version
node.role_unknownconfig.role_key matches no rolefix the key, or harness.add_role
edge.condition_invalidedge 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_invalidnot 1 ≤ min_instances ≤ max_instancesharness.update_role with valid counts
limits.exceededover 200 nodes, 500 edges, 50 roles, or 20000 chars in a role's system_instructions / a node's configsplit the process into several harnesses, merge roles, shorten text
node.condition_invalidcondition node without a valid config.condition, or not exactly one incoming edgeset a valid config.condition; give it exactly one predecessor
node.end_has_outgoingan end node has outgoing edgesremove them — end finishes the run
node.loop_invalidloop lacks max_iterations 1–100, the continue edge, the exit edge, or a back-edge from the bodyadd the missing piece exactly as in step 5
node.config_invalidconfig.lease_seconds outside 60–3600, retry_policy.max_attempts outside 1–10, or negative timeout_secondsbring the value into range
node.flow_nodes_without_gatea path reaches create_flow_nodes without passing a human_gateput a human_gate on every path into it

Warnings (do not block):

CodeWhat happenedHow to fix
graph.no_endno path ends in an end nodeadd an end node at the end of each path
role.unusedno node addresses the rolereference it in a config.role_key, or leave it out of the next version
node.config_ignoredconfig keys the runtime does not read for this typeremove them or move the text into the role's instructions (table in step 3)
node.unimplemented_typeparallel_group or transform — completes immediately, does nothingreplace with outgoing edges / an agent node
node.field_ignoredinput_bindings / output_bindings setremove them; data flows via result_artifact_ids
edge.field_ignorededge mapping or non-zero priority setremove them; only the condition selects a branch
council.isolated_votevote/judge without a deliberation_key shared with a debategive 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 }).

shell
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:

shell
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_id is always project_id. No validation, no error, no second guess.
  • harness.list before harness.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.* and council.* belong to participants and the judge.
  • Never promise what the runtime does not do. parallel_group, transform, bindings, edge mapping/priority, non-tools policies and calls outside Flownix are not enforced — say so.
  • One deliberation_key per council, vote rules in the vote node's config.
  • A gate before create_flow_nodes, always, and the predecessor's instructions demand flow_nodes in its work.complete output.
  • 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_version or 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.