- Get Started

Decision & Routing

OpenFlows is a directed graph of decisions. Each agent node decides what to do next, and the PocketFlow engine routes that decision to the correct handler. The decision layer is the boundary between the unreliable world of LLM outputs and the deterministic world of state machines. It is also where most of the system's self-healing lives.

This page explains how agents make decisions, how malformed decisions are recovered, how the routing table maps a decision to the next action, and how the Node trait's prep/exec/post phases work together with the action dispatcher to advance the pipeline.

How Agents Make Decisions

An agent does not "run to completion." It runs in cycles. Each cycle, the agent's harness reads the current ticket state from the SharedStore, assembles a prompt with the right context, and asks the LLM to choose the next action. The LLM's answer is expected to be a structured decision object, not free-form code. That object is validated, dispatched, and the cycle repeats.

The decision object contains three things: the decision type (e.g. plan, implement, pr_open), the reasoning string, and a payload of parameters specific to the decision. The reasoning is stored for audit; the decision type and payload determine which action is dispatched. This separation makes the agent's intent explicit and auditable.

json
{
  "decision": "implement",
  "reasoning": "Segment 2 of the plan is to add Redis connection pooling. The current state shows the previous segment was approved, so I will implement this segment next.",
  "payload": {
    "segment_id": 2,
    "segment_name": "Add Redis connection pooling",
    "files": ["src/store/redis.rs", "src/config.rs"]
  }
}

The Five-Layer Crash Recovery Stack

LLMs are unreliable. They return malformed JSON, ignore instructions, hallucinate functions, or produce text that is almost right but not quite. OpenFlows does not expect perfect LLM behavior; it expects failure and has a five-layer recovery stack to handle it. Each layer is a fallback for the one above it.

LayerHow it works
1. Decision ToolPrimary path. The LLM calls submit_decision with structured arguments; reasoning is in the tool text, the decision is in the tool parameters. No parsing is required.
2. Enhanced ExtractionFallback path. Seven parsing strategies clean and extract a decision from a text-only response: clean JSON, fenced markdown blocks, last JSON object, truncated repair, line-by-line, loose key extraction, and pattern matching.
3. RetryIf extraction fails, recover_decision() sends a clarified prompt up to DECISION_RETRY_LIMIT=2 times.
4. Safe DefaultIf retry is exhausted, the harness returns a no_work AgentDecision with a [SELF-HEAL] prefix. The agent skips this cycle and lets NEXUS reconcile.
5. Flow RecoveryIf the node itself fails (panic, I/O error, unhandled exception), Flow::run() retries the node up to max_retries=3 and then routes to the node_error or awaiting_human handler.

Layer 1: Decision Tool

The primary path is for the LLM to call a tool named submit_decision. The tool schema is defined in the agent's prompt, and the harness registers the tool with the LLM provider. When the LLM calls the tool, the arguments are delivered in a structured format that the harness can validate directly. There is no regex parsing, no JSON extraction, and no guessing. This works across all providers that support function calling.

The tool schema includes an enum of allowed decision types and a typed payload object. The harness validates the payload against the schema before dispatching. If the LLM tries to call an unknown decision type or passes an invalid payload, the tool call is rejected and the fallback path begins.

Layer 2: Enhanced Extraction

Some providers or models do not reliably call tools, especially when context is long or the prompt is adversarial. If the LLM returns plain text instead of a tool call, the harness runs a sequence of seven fallback parsing strategies to extract a valid decision object.

  1. Clean JSON - try to parse the entire response as a JSON object.
  2. Markdown fenced block - extract JSON from ```json blocks.
  3. Last JSON object - find the last block in the response.
  4. Truncated repair - detect unterminated strings or missing braces and repair them heuristically.
  5. Line-by-line - parse each line as a key-value pair and assemble a decision object.
  6. Loose key extraction - search for known keys like decision, payload, reasoning anywhere in the text.
  7. Pattern matching - match against a set of known decision phrases and map them to safe decisions.

The strategies are ordered from most reliable to least reliable. If any strategy produces a structurally valid decision object, the harness uses it. If none do, the next layer is triggered.

Layer 3: Retry

When the decision cannot be extracted, the harness calls recover_decision(). This sends a clarified prompt to the LLM that includes the original request, the malformed response, and a reminder to use the submit_decision tool. The retry counter is DECISION_RETRY_LIMIT=2, meaning the harness tries the original call plus two retries before giving up.

The retry counter is stored in the SharedStore per ticket, so it survives NEXUS restarts. The clarified prompt is progressively more explicit: the first retry reminds the LLM of the tool schema, the second retry includes a worked example of a valid decision.

Layer 4: Safe Default

If the LLM still cannot produce a valid decision after all retries, the harness returns a safe default: no_work with a [SELF-HEAL] prefix in the reasoning. This is not a failure; it is a deliberate pause. The agent tells the flow, "I could not decide this cycle, so I am doing nothing and letting NEXUS reconcile."

The [SELF-HEAL] prefix is important because it signals to NEXUS that the agent is not blocked, it is just taking a break. NEXUS will try the same node again on a future cycle, and the global state will have a chance to change (for example, a SENTINEL review might complete and provide new context). This prevents a single bad LLM response from crashing the entire ticket.

Safe default is not silent failure

The no_work safe default is logged, counted in metrics, and surfaced in the TUI. It is a controlled pause, not a hidden failure. If a ticket emits too many self-heal events, NEXUS escalates it to awaiting_human because the LLM is clearly unable to make progress.

Layer 5: Flow Recovery

The final layer handles failures that are not about LLM output format. If a node panics, if a command fails, if a file cannot be written, or if the workspace loses connectivity, the PocketFlow engine catches the error and retries the node up to max_retries=3. After the retries are exhausted, the engine routes to the node_error handler, which writes the error to flow_recovery and either retries the whole flow at a coarser level or routes to the awaiting_human handler.

Decision Routing Table

The routing table is the mapping from a decision type to a pipeline action. It is implemented in the action dispatcher, which is shared by all agents. The dispatcher validates the decision, checks role-based permissions, and executes the action. Every decision type is handled, even if the handler is just to log and escalate.

DecisionRoute / handler
no_workAgent has no actionable task for this cycle. May carry a [SELF-HEAL] reason. Flow returns to NEXUS, which reconciles and retries.
planFORGE creates or updates PLAN.md with a segment breakdown. Advances ticket to in_progress.
implementFORGE implements the next segment from the active plan. Writes code, commits, and updates WORKLOG.md.
review_requestFORGE asks SENTINEL to evaluate the current segment or final plan. NEXUS provisions a SENTINEL workspace.
pr_openFORGE opens a pull request via GitHub MCP after final APPROVED. Writes STATUS.json and pending_prs.
mergeVESSEL polls CI and squash-merges the PR. Emits ticket_merged and moves ticket to merged.
documentLORE writes ADR and CHANGELOG entries after a merge event. Does not block the pipeline.
awaiting_humanEscalation decision. NEXUS suspends the worker and tears down the workspace.

The dispatcher is role-aware. FORGE is not allowed to emit MergePr; VESSEL is not allowed to emit plan. If a decision type violates the role contract, the dispatcher rejects it and triggers the recovery stack. This enforces the separation of responsibilities even if an LLM hallucinates a decision.

The Node Trait: Prep / Exec / Post

Every agent in the flow graph is implemented as a node that satisfies the Node trait. The trait has three methods: prep, exec, and post. This structure separates the concerns of context assembly, execution, and state validation, making nodes easy to test and easy to debug.

PhaseResponsibility
prepPrepare the node context. Reads the SharedStore, loads skills, and assembles the prompt.
execExecute the action. Makes LLM calls, runs commands, writes files, or opens PRs.
postPost-process the result. Validates the decision, writes state updates, and dispatches the next action.

Prep

prep is the context-building phase. It reads the SharedStore, loads the agent's skills and standards files, and assembles the prompt that will be sent to the LLM. It does not make any side effects. It returns a PrepResult that contains the prompt context and the expected decision schema. If prep fails, the node is not executed; the failure is usually due to missing state or a corrupted skill file.

Exec

exec is the execution phase. It sends the prompt to the LLM through the Coder AI Gateway, receives the response, and runs the decision recovery stack if needed. The result is a validated AgentDecision. If exec returns a no_work safe default, the post phase is still run so that the agent can write a status update and NEXUS can reconcile.

Post

post is the state-commit phase. It takes the validated decision and dispatches the corresponding action. This is where files are written, commands are run, PRs are opened, and SharedStore keys are updated. The post phase is also responsible for writing STATUS.json with the current ticket status. If post fails, the action may have partially applied; NEXUS reconcile detects this and heals the partial state on the next cycle.

rust
// Pseudocode of the Node trait
#[async_trait]
trait Node {
    async fn prep(&self, ctx: &NodeContext, store: &SharedStore) -> Result<PrepResult>;
    async fn exec(&self, ctx: &NodeContext, prep: PrepResult) -> Result<AgentDecision>;
    async fn post(&self, ctx: &NodeContext, store: &SharedStore, decision: AgentDecision) -> Result<()>;
}

impl Node for ForgeNode {
    async fn prep(&self, ctx, store) -> Result<PrepResult> {
        let ticket = store.get_ticket(&ctx.ticket_id).await?;
        let plan = read_file("PLAN.md")?;
        let skills = load_skills("forge")?;
        let prompt = build_prompt(ticket, plan, skills);
        Ok(PrepResult { prompt, schema: decision_schema_for(ticket.status) })
    }

    async fn exec(&self, ctx, prep) -> Result<AgentDecision> {
        let raw = llm_call(ctx.model, &prep.prompt, &prep.schema).await?;
        recover_decision(raw, &prep.schema).await
    }

    async fn post(&self, ctx, store, decision) -> Result<()> {
        dispatch_action(ctx.role, decision, store).await?;
        write_status_json(ctx.ticket_id, decision).await?;
        Ok(())
    }
}

Action Dispatch

The action dispatcher is the bridge between a decision and the real world. It takes a validated AgentDecision, looks up the action type, checks that the role is authorized to emit it, and then executes the action. The dispatcher is the single place where role permissions are enforced, which makes it easy to audit and hard to bypass.

ActionEffect
WriteFileWrite or overwrite a file in the workspace. Validates against path allow-list.
RunCommandExecute a bash command in the workspace. Routes through CommandGate for dangerous commands.
SubmitDecisionStructured tool call that returns the agent's next decision.
OpenPrCreate a GitHub pull request via MCP. Records the PR in pending_prs.
MergePrSquash-merge a PR. Only VESSEL is allowed to emit this action.
RequestReviewTrigger a SENTINEL evaluation for a plan or segment.
EscalateMove the ticket to awaiting_human with a reason.
NoOpDo nothing this cycle. Used for self-healing and idle states.

Dangerous actions are routed through the CommandGate. For example, a RunCommand that deletes files or modifies system state is paused and sent to NEXUS for approval before execution. Safe commands, such as running tests or linting, execute immediately. The allow-list is defined in the agent's registry configuration and can be customized per deployment.

Putting It Together: A Decision Cycle

A full decision cycle looks like this:

  1. NEXUS assigns a ticket to a FORGE worker and provisions a Coder workspace.
  2. The FORGE harness enters the first node and calls prep, which loads the issue and assembles the planning prompt.
  3. exec sends the prompt to the LLM and expects a submit_decision tool call with decision plan.
  4. If the LLM returns malformed JSON, enhanced extraction tries the seven strategies.
  5. If extraction fails, recover_decision() retries up to two times.
  6. If retry fails, the safe default no_work [SELF-HEAL] is returned.
  7. post dispatches the action. For a plan decision, it writes PLAN.md and updates the ticket state.
  8. If the node fails, Flow::run() retries up to three times before escalating.
  9. NEXUS reads the new state on the next reconcile cycle and routes the ticket to the next node.

Decisions are state transitions

Every decision is a request to change the SharedStore state. The action dispatcher applies the change atomically where possible, and NEXUS reconcile cleans up any partial changes. This is why the system is robust to LLM mistakes: a bad decision is caught before it can corrupt the state.

Testing and Observability

The decision layer is heavily tested. Unit tests cover each parsing strategy with a corpus of malformed LLM outputs. Integration tests simulate the full decision cycle against a fake LLM and a SharedStore. The most important property to test is that a bad LLM response never produces an invalid state transition.

In production, the decision layer emits metrics for each layer: tool-call success rate, extraction success rate by strategy, retry count distribution, and safe-default frequency. A spike in safe-default events is a leading indicator that the prompt, model, or provider needs attention.