Gates & Route-Back#

Gates are where Flow earns its keep: they decide whether a run advances, blocks, routes back, or waits — from recorded evidence, never from narrative. This guide covers the evaluation rules, transition legality, route-back policy, and the adversarial-review pattern built on them.

Gate evaluation rules#

For the current step, flow evaluate applies the v0.1 rules in order:

Condition Outcome
an accepted exception exists on the gate pass
any attached gate evidence is marked failed route-back
any required typed expectation is missing or unsatisfied block
all required typed expectations are satisfied pass
no authored expectations and no decision wait

When a gate passes, Flow advances to the step's next value. When a gate blocks, Flow keeps enough state for another process or agent to resume without chat memory — the blocked expectation, its explore_hint, and the next action all land in the run state and reports.

How surface.claim expectations are matched (type, subject, status, freshness, producer trust, integrity) is covered in Evidence.

Exceptions#

An accepted exception lets a gate pass without its expected evidence — explicitly, and on the record:

flow accept-exception dev-1847 --gate verify-gate \
  --reason "browser evidence unavailable in CI; verified manually on staging" \
  --authority "brian@kontour.ai"

The exception is stored in run state with its reason and authority, counts as a gate pass, and appears in every report and console view. Exceptions are visible by design — the failure mode Flow prevents is the silent bypass, not the justified one.

Route-back#

A gate routes failed evidence back to a specific step via on_route_back:

{
  "step": "verify",
  "on_route_back": {
    "missing_evidence": "verify",
    "implementation_defect": "implement",
    "plan_gap": "plan",
    "decision_gap": "plan",
    "default": "implement"
  },
  "route_back_policy": {
    "max_attempts": 3,
    "on_exceeded": "block"
  }
}

Route reason ids are open strings. Flow documents four standard ids without enforcing a closed enum:

Reason id Meaning Inferred by Flow?
missing_evidence Required gate evidence is absent Yes — Flow infers this when evidence is missing
implementation_defect Work failed the gate; return to implementation No — producer or agent sets this
plan_gap Plan or acceptance shape is insufficient No — producer or agent sets this
decision_gap Work needs a decision or clarification No — producer or agent sets this
default Fallback when reason is absent or unmapped Special — not a reason id, used in on_route_back

Custom ids are allowed: add them to on_route_back when they should select a specific step, and include default for unknown or omitted reasons. If failed evidence has no route_reason, Flow uses default when present, otherwise the gate's own step.

Deterministic attempt counting#

Route-back attempts are derived from persisted state, not memory. Flow counts prior route_back transitions in state.transitions with the same gate id, route reason (or default), source step, and selected target step. Timestamps, classifier data, diagnostics, analytics metadata, and caller-supplied counters never affect routing or attempt counts — so neither an agent nor an adapter can fudge the loop budget.

When max_attempts is exceeded, on_exceeded decides the outcome:

  • a step id routes the run to that recovery step, recording both the selected route and the recovery step
  • block stops the run at the current step while recording the exceeded attempt

Flow validates route targets against defined step ids; block is special only inside route_back_policy.on_exceeded.

This is what an exhausted budget looks like in practice (from a real run):

route-back verify-gate: verify gate has failing evidence
current step: implement
next action: return to implement and replace failing evidence attempt 1/3

Recovering from a route-back means replacing the failing evidence: attach the new evidence with --supersede <failed-evidence-id>. The superseded entry stays in the manifest (reports still show the failed round happened) but stops driving the gate, so the next evaluation can pass on the replacement. The adversarial-survey scenario walks the full loop with real output.

Run state and reports expose the full route-back record for continuation and analysis: selected route, final target, reason, attempt, max attempts, exceeded state, evidence refs, expectation ids, and any recorded classifier/diagnostics/analytics metadata.

Transition validation#

Flow core owns provider-neutral transition legality. A runtime, adapter, or agent can propose a transition; Flow decides whether it matches the authored definition, current state, gate outcomes, route-back policy, and persisted history:

flow validate-transition ./transition-request.json

The request carries the definition, current state, evidence manifest, and the proposed transition. The result is machine-readable — here is a real rejection of a stale jump from plan to publish while the run was actually at verify:

{
  "valid": false,
  "status": "invalid",
  "diagnostics": [
    {
      "code": "transition.current_state.stale",
      "severity": "error",
      "path": "$.proposed_transition.from_step",
      "message": "proposed transition starts from plan, but current state is verify"
    },
    {
      "code": "transition.from_step.mismatch",
      "severity": "error",
      "path": "$.proposed_transition.from_step",
      "message": "transition from_step must match current step verify"
    }
  ]
}

flow validate-transition exits non-zero when the result status is invalid. Definitions that do not declare stricter policy keep permissive v0.1 behavior; a gate can close its reason vocabulary with route_back_policy.allow_unknown_reasons: false.

There is nothing special about step names. A Builder Kit-like path such as verify → evidence → publish-change → release-readiness → merge is just a Flow Definition — Flow rejects jumps across required gates because the proposed transition does not match the definition and evidence state, not because the names mean anything to Flow core.

Pattern: adversarial review with a defect budget#

examples/adversarial-pass-flow.json is a complete reference for a high-stakes review loop: produce → adversarial-review → resolve. The review gate expects two claims — adversarial.producer-output (the work being challenged) and adversarial.review (the per-round review result) — and routes defects deterministically:

Route reason Target Why
conclusion_defect produce the conclusion needs regeneration
framing_defect produce the task framing or assumptions need rework
completeness_defect produce missing coverage requires a new producer pass
citation_defect resolve repairable by fixing citations, no regeneration needed
missing_evidence adversarial-review required gate evidence is absent
default resolve unmapped or omitted reasons

max_attempts: 2 is the per-case adversarial budget; the third matching route-back exceeds it and on_exceeded: "block" stops the run with the exceeded state recorded. External systems own the actual review reasoning and may attach their records as per-round evidence — Kontour Survey's adversarial-pass records are built for exactly this slot — while Flow owns only the orchestration, route accounting, and the budget.

Validating definitions#

Catch shape and policy errors before a run exists:

flow validate-definition .flow/definitions/agent-dev-flow.json
flow validate-definition examples/invalid-claim-expectation-flow.json --json

--json emits a stable payload with valid, path, error_count, and diagnostics; the command exits non-zero for invalid definitions, so it slots directly into CI. Diagnostics cover shape errors, unknown gate step references, route-back targets, malformed expects entries, and invalid surface.claim fields.

Flow accepts two authoring shapes — the flat v0.1 shape (top-level id, version, steps, gates) and the Resource Contract shape (apiVersion, kind: "FlowDefinition", metadata, spec) shown in examples/flow-definition-resource-contract.json. Both map to the same runtime model (metadata.nameid, spec.versionversion, spec.stepssteps, spec.gatesgates), and existing flat definitions never need to migrate.