Workflows Overview
A first-party, serializable workflow engine for Cersei — author multi-step pipelines in Rust or draw them in a visual builder (React + xyflow). One IR, two front-ends.
Workflows
Workflows let you define a multi-step pipeline as an explicit graph of steps instead of leaning on a single agent's reasoning to hold the whole plan. You decide what runs, in what order, how data flows between steps, and what happens on a branch or a failure. A workflow can call plain Rust functions, agents, tools, or other workflows.
The defining property of cersei-workflows is that the entire workflow is serializable data. A workflow is a WorkflowDef — a flat list of nodes and edges — that round-trips losslessly to and from JSON. That is what lets a visual builder (React + xyflow / React Flow) draw a workflow, emit it as JSON, and have the Rust runtime execute it. It is the engine behind the Atlas workflow UI.
One IR, two front-ends. The programmatic WorkflowBuilder and the visual builder produce the same WorkflowDef. There is one compiler (Workflow::compile) and one executor. Anything you can draw, you can write — and vice versa — because they are the same data.
When to use a workflow
Use a workflow when the steps are known up front and the order matters — you want fine-grained control over data flow and which primitive runs at each stage. Use a plain Agent when the path is open-ended and you want the model to decide what to do next. The two compose: a workflow step can run an agent, and an agent can be handed a workflow as a tool.
The shape of a workflow
input ──▶ [validate] ──▶ ⟨branch⟩ ──when premium──▶ [enrich] ──▶ [summarize] ──▶ result
│
└──────else───────────▶ [summarize] ─────────────────▶ result
parallel block:
┌─▶ [fetch_a] ─┐
input ──▶ «par»┤ ├─▶ «join» ──▶ [merge] ──▶ result
└─▶ [fetch_b] ─┘Every box is a node in the WorkflowDef:
- Step — runs a registered
Stepby id (a function, agent, tool, or nested workflow). - Parallel / Join — fan out to several branches concurrently, then collect (
AllOrFailorAllSettled). - Branch — evaluate conditions in order; the first match wins.
- Loop —
dowhile/dountil/foreach. - Map — a pure JSON reshape between steps.
Core pieces
WorkflowDef (the IR)
A serializable { nodes, edges, entry } graph. The single source of truth, emitted identically by the builder and the UI. UiHints (x/y/label) ride along but are ignored at execution time, so the IR is lossless across the React Flow boundary.
Step trait
Shaped exactly like a Tool: an id, input/output JSON schemas, and an async execute. First-party impls: FnStep, AgentStep, ToolStep, WorkflowStep.
StepRegistry
Maps step-ids to executable implementations. UI JSON carries only references; the host supplies the code. catalog() exposes the palette of available steps to the builder.
WorkflowEvent / Stream
A Serialize-able event stream (StepStarted, StepCompleted, BranchTaken, StateUpdated, …) for lighting up the live graph over SSE or WebSocket.
A first workflow
Register the steps, author the graph, compile, run.
use cersei::workflows::prelude::*;
use serde_json::json;
use std::sync::Arc;
#[tokio::main]
async fn main() -> cersei::types::Result<()> {
// 1. Register executable steps by id.
let registry = StepRegistry::new();
registry.register(Arc::new(FnStep::new("upper", |input, _ctx| async move {
let s = input.get("message").and_then(|v| v.as_str()).unwrap_or("");
Ok(json!({ "message": s.to_uppercase() }))
})));
registry.register(Arc::new(FnStep::new("emphasize", |input, _ctx| async move {
let s = input.get("message").and_then(|v| v.as_str()).unwrap_or("");
Ok(json!({ "message": format!("{s}!!!") }))
})));
// 2. Author the workflow (the UI emits this very same WorkflowDef as JSON).
let def = WorkflowBuilder::new("greet")
.then("upper")
.then("emphasize")
.commit();
// 3. Compile against the registry and run.
let wf = Workflow::compile(def, ®istry)?;
let result = wf.start(json!({ "message": "hello world" })).await?;
assert_eq!(result.status, RunStatus::Success);
println!("{}", result.result.unwrap()); // {"message":"HELLO WORLD!!!"}
Ok(())
}Round-tripping through the UI
Because WorkflowDef is plain serde data, the same workflow is just JSON. The visual builder draws nodes/edges and serializes exactly this; the host deserializes it and calls Workflow::compile.
let def = WorkflowBuilder::new("greet").then("upper").then("emphasize").commit();
// Send to the UI / store it / receive it back — losslessly.
let wire = serde_json::to_string(&def)?;
let same: WorkflowDef = serde_json::from_str(&wire)?;
assert_eq!(def, same);{
"id": "greet",
"entry": "upper_1",
"nodes": [
{ "id": "upper_1", "kind": { "step": { "step_id": "upper", "config": null } } },
{ "id": "emphasize_2", "kind": { "step": { "step_id": "emphasize", "config": null } } }
],
"edges": [
{ "from": "upper_1", "to": "emphasize_2", "kind": "then" }
]
}The IR enums are externally tagged on purpose ({ "step": { … } }, "then"). Internally-tagged serde enums route through a content buffer that blows up serde_json serialization with an unbounded recursion_limit overflow — and that overflow surfaces in your crate, not ours. Keep these externally tagged.
Live status for the builder
stream() returns a WorkflowStream of WorkflowEvents. Every event is Serialize, so you can forward them straight to a browser over SSE/WebSocket to animate the graph.
let wf = Workflow::compile(def, ®istry)?; // wf: Arc<Workflow>
let mut stream = wf.stream(json!({ "message": "hi" }));
while let Some(event) = stream.next().await {
// Forward to the UI as JSON.
let frame = serde_json::to_string(&event)?;
// ws.send(frame).await?;
}Enable the feature
cersei-workflows is opt-in on the facade:
[dependencies]
cersei = { version = "0.2.1", features = ["workflows"] }Then use cersei::workflows::prelude::*; (the common types are also re-exported from cersei::prelude). Or depend on the crate directly:
cersei-workflows = "0.2.1"Where to go next
AgentRL Cookbook
Runnable AgentRL recipes — a self-improving coding agent, forcing the recovery loop, custom verifiers and runners, and authoring tools from AgentTemplate programs.
Workflows API
API reference for cersei-workflows — the Step trait, the WorkflowDef IR, conditions, the registry, the builder, execution, events, and results.