Skip to main content

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

Pipeline config
{
  "name": "pr-review-pipeline",
  "nodes": [
    { "id": "fetch-diff", "type": "connector", "connector": "github", "action": "get_diff" },
    { "id": "analyze", "type": "llm", "model_backend": "anthropic", "prompt": "Review this diff..." },
    { "id": "human-review", "type": "gate", "human_only": true },
    { "id": "post-comment", "type": "connector", "connector": "github", "action": "create_comment" }
  ],
  "edges": [
    { "from": "fetch-diff", "to": "analyze" },
    { "from": "analyze", "to": "human-review" },
    { "from": "human-review", "to": "post-comment" }
  ]
}

Node types

Node types
Type Purpose
agent Delegates the step to an agent entity that carries the prompt, model backend, and connector binding
llm Calls a large language model (LLM) via the configured model backend
connector Reads/writes through a connector (GitHub, Notion, etc.)
gate Evaluates a condition and passes or blocks
manual Placeholder for a step done outside Modulo; run pauses for human output
trigger Starts a pipeline execution (manual or webhook)
transform Transforms the artifact without an LLM call

The llm, connector, gate, manual, trigger, and transform node types mirror the corresponding agent type, so a node can be written inline in the pipeline definition. The agent node type instead points at a reusable agent stored in the workspace; converting a manual node to an agent sets this type (see Step replacement).

Edge routing

By default, a node runs after all its upstream nodes complete. You can configure conditional routing:

Pipeline config
{
  "from": "check-quality",
  "to": "human-review",
  "condition": "{{ artifact.score < 0.8 }}"
}

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 Modulo engine compiles the pipeline config into a LangGraph state graph and executes nodes as their upstream dependencies complete.

Run lifecycle

  1. Pending - run is queued, waiting for a capacity slot
  2. Running - ready nodes execute; checkpoints are persisted after each node
  3. Awaiting human - run paused at a human-in-the-loop (HITL) gate; a human must claim and decide (approve, reject, or modify output)
  4. Completed - all nodes exhausted without error; eval suite thresholds checked
  5. Failed - an unhandled exception, eval block, or output rejection occurred
  6. 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).
  7. 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. Modulo 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

HITL gates pause execution at an edge boundary. The gate checks:

  1. Conditional gating - a JMESPath condition (JSON query language) on the gate 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 - overrides autonomy; always interrupts

Claims are managed via HITLManager with short-lived JSON Web Token (JWT) credentials and configurable time-to-live (TTL). 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 Modulo. 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 Modulo 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.