# Graphs

> Graphs connect systems into agent behavior with sequential, conditional, parallel, and looping control flow.

A graph connects systems into agent behavior — defining what runs, in what order, under what conditions.

```rust
use polaris_ai::graph::Graph;


async fn parse_input() -> String { todo!() }
async fn call_llm() -> String { todo!() }
async fn format_response() -> String { todo!() }


let mut graph = Graph::new();
graph
    .add_system(parse_input)
    .add_system(call_llm)
    .add_system(format_response);
```

`Graph::new()` creates an empty graph. The `add_system` method appends a system node and chains it sequentially to the previous one. The first system you add becomes the graph’s entry point. The result is a linear pipeline: `parse_input` runs, then `call_llm`, then `format_response`.

```plaintext
parse_input ──▶ call_llm ──▶ format_response
```

## Conditional branching

`add_conditional_branch` adds binary if/else routing based on a predicate over the previous system’s output. The predicate receives `&T` and returns `bool`. If true, the first branch runs; otherwise, the second branch runs.

```rust
use polaris_ai::graph::Graph;
use polaris_ai::models::llm::LlmResponse;


async fn act() -> LlmResponse { todo!() }
async fn execute_tools() { }
async fn respond() { }


let mut graph = Graph::new();
graph.add_system(act);


graph.add_conditional_branch::<LlmResponse, _, _, _>(
    "has_tool_calls",
    |response| response.has_tool_calls(),
    |tool_branch| {
        tool_branch.add_system(execute_tools);
    },
    |done_branch| {
        done_branch.add_system(respond);
    },
);
```

The type parameter `LlmResponse` tells the predicate what output type to read from the context. The two closures each receive a fresh `&mut Graph` for building their respective subgraphs.

```plaintext
         ┌─ true ──▶ execute_tools
act ──▶ has_tool_calls?
         └─ false ─▶ respond
```

## Loops

`add_loop` repeats a subgraph until a termination predicate returns `true`. The predicate evaluates an output type produced inside the loop body.

```rust
use polaris_ai::graph::Graph;


struct ReactState {
    is_complete: bool,
}


async fn init_loop() -> ReactState { todo!() }
async fn reason() -> ReactState { todo!() }
async fn act() -> ReactState { todo!() }


let mut graph = Graph::new();
graph.add_system(init_loop);


graph.add_loop::<ReactState, _, _>(
    "react_loop",
    |state| state.is_complete,
    |g| {
        g.add_system(reason)
         .add_system(act);
    },
);
```

The loop body executes repeatedly. After each iteration, the framework evaluates the termination predicate against the most recent `ReactState` output. When `is_complete` is true, execution continues past the loop.

```plaintext
              ┌─────────────────────┐
              ▼                     │
init_loop ──▶ reason ──▶ act ──────┘
              │ (exit when is_complete == true)
              ▼
           (continue)
```

For a simple bounded loop without a predicate, use `add_loop_n` with a maximum iteration count:

```rust
graph.add_loop_n("retry", 3, |g| { g.add_system(attempt); });
```

## Switch

`add_switch` provides multi-way routing based on a discriminator function. The discriminator returns a `&'static str` case key, and execution follows the matching branch.

```rust
use polaris_ai::graph::Graph;


struct RouterOutput { action: &'static str }


async fn route() -> RouterOutput { todo!() }
async fn use_tool() { }
async fn clarify() { }
async fn respond() { }


let mut graph = Graph::new();
graph.add_system(route);


graph.add_switch::<RouterOutput, _, _, _>(
    "route_action",
    |output| output.action,
    vec![
        ("tool", Box::new(|g: &mut Graph| { g.add_system(use_tool); })
            as Box<dyn FnOnce(&mut Graph)>),
        ("clarify", Box::new(|g: &mut Graph| { g.add_system(clarify); })
            as Box<dyn FnOnce(&mut Graph)>),
    ],
    Some(Box::new(|g: &mut Graph| { g.add_system(respond); })),
);
```

The last argument is an optional default branch, used when no case key matches. Switch is useful when the previous system produces more than two possible outcomes.

## Parallel

`add_parallel` runs multiple branches concurrently. All branches must complete before execution continues.

```rust
use polaris_ai::graph::Graph;


async fn fetch_context() { }
async fn fetch_tools() { }
async fn merge_results() { }


let mut graph = Graph::new();
graph.add_parallel("gather", vec![
    |g: &mut Graph| { g.add_system(fetch_context); },
    |g: &mut Graph| { g.add_system(fetch_tools); },
]);
graph.add_system(merge_results);
```

Each closure builds an independent subgraph. The branches run simultaneously, and execution resumes from the parallel node’s outgoing edge once all branches finish.

## Composing with `pipe`

`pipe` passes `&mut Graph` to a function, letting you compose reusable graph fragments inline without constructing a separate `Graph` object.

```rust
use polaris_ai::graph::Graph;


async fn log_metrics() { }
async fn check_health() { }
async fn fetch_data() { }
async fn respond() { }


fn monitoring(g: &mut Graph) {
    g.add_system(log_metrics)
     .add_system(check_health);
}


let mut graph = Graph::new();
graph
    .add_system(fetch_data)
    .pipe(monitoring)
    .add_system(respond);
```

The `monitoring` function adds its systems directly to the current graph. This is different from the branch closures in `add_loop` or `add_conditional_branch`, which build into isolated subgraphs.

## Scope and dynamic composition

Use `add_scope` to embed a subgraph with an explicit context policy. This is the boundary for reusing a sub-agent while controlling which resources it receives. Outputs merge back to the caller; the context policy does not filter those outputs.

```rust
use polaris_ai::graph::{Graph, node::ContextPolicy};


async fn summarize() -> String { "A summary".into() }
let mut research = Graph::new();
research.add_system(summarize);
let mut graph = Graph::new();
graph.add_scope("research", research, ContextPolicy::new().share_rest());
```

`add_dynamic` selects among named candidate graphs at runtime. A `DynamicSlot` declares the required graph signature and context policy; candidates must satisfy that contract. `add_dynamic_registry` instead reads candidates from a session-local `SubgraphRegistry`, allowing the candidate set to change between executions. These primitives support replaceable agent components; they do not implement an automatic improvement or training loop.

See the [public graph reference](https://github.com/RadiantAILabs/polaris/blob/main/docs/reference/graph.md/) for candidate registration and policy examples.

## Running a graph

You execute a graph with `GraphExecutor`. The executor traverses the graph from its entry point, running each system with a `SystemContext` that holds resources, outputs, and parent scopes.

```rust
use polaris_ai::graph::{Graph, GraphExecutor};
use polaris_ai::system::param::SystemContext;


async fn reason() -> i32 { 1 }
async fn act() -> i32 { 2 }


async fn run() -> Result<(), Box<dyn std::error::Error>> {
    let mut graph = Graph::new();
    graph.add_system(reason).add_system(act);


    let mut ctx = SystemContext::new();
    let executor = GraphExecutor::new();
    let result = executor.execute(&graph, &mut ctx, None, None).await?;
    Ok(())
}
```

The `execute` method returns an `ExecutionResult` with the number of nodes executed and the total duration. In practice, you rarely call `GraphExecutor` directly – the [Sessions](/docs/polaris/provided-plugins/sessions-and-persistence.md) plugin and the `Agent` trait handle execution for you.
