Core concepts
Systems
Systems are async functions that form the building blocks of agent behavior.
A system is an async function that the framework runs. You write a regular async function, annotate it with #[system], and the framework handles parameter injection, execution, and output routing.
use polaris_ai::system::system;use polaris_ai::system::param::Res;use polaris_ai::system::resource::GlobalResource;
struct AgentConfig { model_id: String,}impl GlobalResource for AgentConfig {}
struct Greeting { text: String,}
#[system]async fn greet(config: Res<AgentConfig>) -> Greeting { Greeting { text: format!("Using model: {}", config.model_id), }}The greet function takes one parameter and returns a value. The framework resolves the parameter from context before calling the function, and stores the return value so downstream systems can access it.
The #[system] macro
Section titled “The #[system] macro”The #[system] attribute transforms an async function into a struct that implements the System trait. For a function named greet, the macro generates a struct called GreetSystem and a factory function greet() that returns an instance of it.
The generated struct handles three things: fetching each parameter from the SystemContext, calling your function body, and reporting the system’s name for tracing and debugging. You do not interact with the System trait directly in normal usage.
Parameter injection
Section titled “Parameter injection”Systems declare their dependencies as function parameters. The framework resolves each parameter from the system context before execution. You do not look up resources manually.
Three parameter types cover the most common cases:
Res<T>reads a resource. The lookup walks up the context hierarchy: local scope first, then parent contexts, then server globals.ResMut<T>gives mutable access to a resource in the current scope only.Out<T>reads the return value of a preceding system in the same graph.
A system can take any combination of these. The following system reads a global resource, mutates a local resource, and consumes the output of a previous system:
use polaris_ai::system::system;use polaris_ai::system::param::{Res, ResMut, Out};use polaris_ai::system::resource::{GlobalResource, LocalResource};
struct Config { verbose: bool,}impl GlobalResource for Config {}
struct Memory { messages: Vec<String>,}impl LocalResource for Memory {}
struct PreviousResult { summary: String,}
#[system]async fn process( config: Res<Config>, mut memory: ResMut<Memory>, previous: Out<PreviousResult>,) -> String { memory.messages.push(previous.summary.clone()); if config.verbose { format!("processed {} messages", memory.messages.len()) } else { String::new() }}If a parameter cannot be resolved (for example, the resource was never registered), the system returns a ParamError and the graph executor handles the failure.
System outputs
Section titled “System outputs”A system’s return value is stored in the context as an output. The next system in the graph can read it with Out<T>, where T is the return type of the preceding system.
use polaris_ai::system::system;use polaris_ai::system::param::Out;
struct AnalysisResult { score: f64,}
#[system]async fn analyze() -> AnalysisResult { AnalysisResult { score: 0.85 }}
#[system]async fn report(analysis: Out<AnalysisResult>) -> String { format!("Score: {:.0}%", analysis.score * 100.0)}The report system reads the AnalysisResult produced by analyze. This is how data flows between systems without shared mutable state.
Fallible systems
Section titled “Fallible systems”Systems can return Result<T, SystemError> to indicate they may fail. The #[system] macro detects this pattern automatically and marks the system as fallible.
use polaris_ai::system::system;use polaris_ai::system::param::Res;use polaris_ai::system::prelude::SystemError;use polaris_ai::system::resource::GlobalResource;use polaris_ai::models::llm::{Llm, LlmResponse};
struct AgentLlm(Llm);impl GlobalResource for AgentLlm {}
#[system]async fn act(llm: Res<AgentLlm>) -> Result<LlmResponse, SystemError> { let response = llm.0 .builder() .system("You are a helpful assistant.") .user("Say hello.") .generate() .await .map_err(|err| SystemError::ExecutionError(err.to_string()))?;
Ok(response)}On success, the T value is stored as the system’s output, available to downstream systems via Out<T>. On failure, the SystemError propagates to the graph executor, which routes execution to an error edge if one is defined, or halts the graph.
Systems as graph nodes
Section titled “Systems as graph nodes”Systems are the nodes in a directed graph that defines your agent’s behavior. You add them to a graph with add_system, and the graph determines execution order, branching, and looping.
graph.add_system(receive_input);graph.add_system(analyze);graph.add_system(report);This creates a sequential pipeline: receive_input runs first, then analyze, then report. Each system can read the output of the one before it. For conditional branching, loops, and parallel execution, see Graphs.
Copyright © 2026, Radiant AI Limited.