Pipelines

A pipeline is a directed graph of agent nodes. Output flows forward through the graph as each step reads, transforms, routes, or gates the artifact on the way to its destination.

Pipeline structure

A pipeline is a graph of nodes and edges. Each node has a UUID id, a node_type, and a canvas position. Each edge names its source_node_id, target_node_id, and edge_type.

Pipeline config
{
  "name": "pr-review-pipeline",
  "nodes": [
    {
      "id": "11111111-1111-4111-8111-111111111111",
      "node_type": "connector",
      "position": { "x": 80, "y": 200 },
      "connector_binding": {
        "type": "github",
        "instance_id": "22222222-2222-4222-8222-222222222222",
        "operation": "query",
        "resource": "pr_diff",
        "filters": { "repo": "my-org/my-repo", "pull_number": 42 }
      }
    },
    {
      "id": "33333333-3333-4333-8333-333333333333",
      "node_type": "agent",
      "position": { "x": 380, "y": 200 },
      "agent_id": "44444444-4444-4444-8444-444444444444"
    },
    {
      "id": "55555555-5555-4555-8555-555555555555",
      "node_type": "connector",
      "position": { "x": 680, "y": 200 },
      "connector_binding": {
        "type": "github",
        "instance_id": "22222222-2222-4222-8222-222222222222",
        "operation": "write",
        "resource": "pr_comment"
      }
    }
  ],
  "edges": [
    {
      "source_node_id": "11111111-1111-4111-8111-111111111111",
      "target_node_id": "33333333-3333-4333-8333-333333333333",
      "edge_type": "normal"
    },
    {
      "source_node_id": "33333333-3333-4333-8333-333333333333",
      "target_node_id": "55555555-5555-4555-8555-555555555555",
      "edge_type": "normal",
      "hitl_review_config": {
        "label": "Human review",
        "description": "A reviewer must approve the review comment before it is posted.",
        "claim_expiry_minutes": 15,
        "human_only": true
      }
    }
  ]
}

The prompt, model backend, and schemas live on the agent the node references, not on the node itself. Triggers are configured on the pipeline as separate resources, not as graph nodes. See Triggers.

Node types

Node types
node_type Purpose
agent Runs the stored agent referenced by agent_id
connector Runs a connector query, write, or dispatch through connector_binding
manual Placeholder for a step done outside Farna Control; the run pauses for human output
hitl Human review gate; requires a hitl_config
composite Runs a reusable subgraph referenced by composite_ref
sandbox_agent Runs an agent in a sandbox, in llm mode or script mode
router Pure decision node that passes state through so conditional edges can route
join Joins parallel branches
dispatch Dispatches work to a connector’s CI-runner operation

A manual node can be promoted to an agent node without rebuilding the pipeline (see Step replacement).

Edge routing

By default, a node runs after all its upstream nodes complete. Set edge_type to conditional and supply a JMESPath condition_expression to route on pipeline state:

Pipeline config
{
  "source_node_id": "33333333-3333-4333-8333-333333333333",
  "target_node_id": "55555555-5555-4555-8555-555555555555",
  "edge_type": "conditional",
  "condition_expression": "risk_score > `0.5`"
}

The other edge types are normal, reject, and loop. An edge can also carry its own hitl_review_config to place a review gate on the transition rather than on a node.

Snapshot management

Every pipeline run produces a snapshot, a complete record of the input, all intermediate outputs, and the final artifact. Snapshots can be:

  • Replayed - re-run a pipeline with the same inputs to debug agent behavior
  • Compared - diff two snapshots to see how changes in prompts or models affect output
  • Exported - download as JSON for offline analysis or compliance

Pipeline execution

When a pipeline run is triggered, the Farna Control engine compiles the pipeline config into a LangGraph state graph and executes nodes as their upstream dependencies complete.

Run lifecycle

A run is in one of these in-flight states:

  1. Pending - the run is queued, waiting for a capacity slot
  2. Running - ready nodes execute; checkpoints are persisted after each node
  3. Awaiting human - the run paused at a human-in-the-loop (HITL) gate; a human must claim and decide (approve, reject, or modify output). A claimed gate with no decision yet shows as Claimed, an expired unanswered gate shows as Parked, and a run whose outcome cannot yet be determined shows as Unknown, which recovery can still resolve.

It ends in one of these terminal states:

  • Complete - all nodes exhausted without error; eval suite thresholds checked
  • Failed - an unhandled exception or an output rejection occurred
  • Eval Failed - an eval blocked the output
  • Stalled - a sandbox agent went silent past the idle watchdog, so its sandbox was killed
  • Budget Exceeded - an agent’s token budget was breached
  • Cost Ceiling Exceeded - a per-run or organization spend ceiling was breached
  • Router No Match - a router node found no matching rule and had no default
  • Compensation Failed - a watched node and its compensation path both failed
  • Rejected - the run ended without being a failure. Usually a reviewer rejected it at a human review gate that had no reject route. The same status also covers two supersession cases: a newer version replaced the waiting review (hitl.superseded), or the run waited in the capacity queue past the pipeline’s queue_timeout_seconds (queue.expired).
  • Cancelled - cancellation was requested via the run detail

Capacity and concurrency

Each pipeline has a max_concurrent_runs setting (default 5) that caps how many of its runs execute at once. Farna Control enforces it with count-based checks: a run over the cap is queued, meaning it stays pending, holds no slot, and is re-checked for capacity every minute until it starts. Runs waiting for a human decision hold no pipeline slot either. A capacity-queued run waits indefinitely by default.

Each pipeline can also set queue_timeout_seconds, which bounds that wait. It is nullable, and empty means never (the default). The window is anchored on when the run was queued and applies only to capacity-queued runs. On expiry the run is superseded - status rejected, error code queue.expired - so it is visible in the runs list as expired while queued, but it is not a failure and pages nobody. Set it in the pipeline editor toolbar (Queue timeout (s)), or through PATCH /api/v1/pipelines/{pipeline_id}. A cron pipeline should set it to its schedule’s period (an hourly cron sets 3600) so a stale fire drops instead of running hours late; webhook, manual and Model Context Protocol (MCP) triggered pipelines should leave it empty.

This cap works together with the organization-level limits and the per-pipeline trigger backpressure guard, so the full model, the defaults, and the sizing guidance live on Concurrency and capacity sizing.

Checkpointing

After every node execution, the LangGraph state is checkpointed to PostgreSQL using the ModuloPostgresSaver (tenant-isolated via organization_id column and encrypted at rest via Fernet). Runs can be resumed from the last checkpoint after a server restart.

Human-in-the-Loop (HITL) gates

A HITL gate pauses execution at a node or an edge. A gate checks:

  1. Conditional gating - an optional JMESPath condition in the gate’s hitl_config; if falsy the gate is skipped
  2. Eval-before-interrupt - node-scoped eval definitions run against state; blocking failures raise EvalBlockedError
  3. Autonomy level - manual_approval (interrupt), notify_on_complete (auto-approve), or fully_autonomous (skip)
  4. human_only flag - defaults to true and overrides autonomy; a human-only gate always interrupts

Claims are managed via HITLManager with short-lived JSON Web Token (JWT) credentials and a configurable claim expiry (the default claim time-to-live is 15 minutes). See Human-in-the-Loop for the full claim workflow and rejection handling.

Error recovery

Failed or awaiting-human runs can recover individual manual nodes via POST /api/v1/runs/{run_id}/nodes/{node_id}/recover, either replaying the node with new input or skipping it. Runs paused at a HITL gate resume when a reviewer claims and approves (or rejects) the gate; see Human-in-the-Loop.

Runaway protection

Three independent guards prevent runaway runs:

  • Max duration - wall-clock timeout
  • Max steps - node completion count limit
  • Token budget - cumulative token usage limit

Any guard violation terminates the run with error_code="runaway".

Events & observability

The executor emits events (node_started, node_completed, node_failed, run_completed, hitl_awaiting, run_cancelled) through a per-run RunEventBroker. WebSocket subscribers receive live events; a 100-event ring buffer supports reconnection replay. OpenTelemetry spans are emitted via the LangGraphOtelBridge callback handler.

Manual (placeholder) nodes

A manual node represents a step performed by a human outside Farna Control. When a run reaches a manual node, it pauses (status: awaiting_human) and waits for a human to submit the output through the HITL review UI. The output is validated against the node’s schema before the run continues.

Manual nodes are the primary tool for Software Development Lifecycle (SDLC) onboarding: teams map their existing process, including hand-operated steps, into Farna Control and get a governed, observable pipeline immediately, even before any AI agents are added.

Step replacement

Manual nodes can be promoted to AI agent nodes without rebuilding the pipeline:

  • Convert to Agent (POST /api/v1/pipelines/{pipeline_id}/nodes/{node_id}/convert-to-agent) - replaces a manual node with an agent node. Requires an agent_id, model_backend_id, and connector_binding. The node type changes to agent and the output schema reference is removed (the agent produces its own output).
  • Revert to Manual (POST /api/v1/pipelines/{pipeline_id}/nodes/{node_id}/revert-to-manual) - restores an agent node back to manual using a snapshot that captures the original manual configuration. Requires a snapshot_id query parameter pointing to a snapshot where the target node had type manual with an output_schema_id.

Both operations are available from the pipeline editor UI via the node property panel.

Visual pipeline builder

The Pipeline Editor (/pipelines/:id/editor) provides a drag-and-drop canvas for building pipelines. Nodes snap to a grid, edges are drawn by clicking between ports. The canvas serializes to the JSON format above.