Rotifer genes are powerful on their own, but the real magic happens when you compose them. The gene algebra — Seq, Par, Cond, Try, and Transform — lets you wire simple genes into complex agent pipelines that are type-safe, verifiable, and automatically optimizable.
In this tutorial, you'll build a real-world pipeline: search the web → summarize results → format output — then extend it with parallel execution, conditional branching, and error recovery.
Prerequisites
- Rotifer CLI installed (
npm i -g @rotifer/playground) - A project initialized (
rotifer init my-pipeline && cd my-pipeline) - Familiarity with basic gene concepts (see Your First Gene in 5 Minutes)
Step 1: Understand the Building Blocks
Your project already includes genesis genes. Let's check what we have:
rotifer arena list
┌──────┬─────────────────────┬────────────┬────────┬──────────┐
│ # │ Name │ Domain │ F(g) │ Fidelity │
├──────┼─────────────────────┼────────────┼────────┼──────────┤
│ 1 │ genesis-web-search │ search.web │ 0.9200 │ Native │
│ 2 │ genesis-search-lite │ search.web │ 0.7100 │ Native │
│ 3 │ genesis-code-format │ code.format│ 0.8800 │ Native │
└──────┴─────────────────────┴────────────┴────────┴──────────┘
We'll use genesis-web-search as the first stage. Now we need a summarizer gene.
Step 2: Create a Summarizer Gene
mkdir -p genes/summarizer
Write genes/summarizer/index.ts:
export async function express(input: {
text: string;
maxLength?: number;
}) {
const maxLen = input.maxLength || 200;
const sentences = input.text.split(/[.!?]+/).filter(Boolean);
let summary = "";
for (const sentence of sentences) {
if ((summary + sentence).length > maxLen) break;
summary += sentence.trim() + ". ";
}
return {
summary: summary.trim(),
wordCount: summary.split(/\s+/).length,
};
}
Wrap and submit it:
rotifer wrap summarizer --domain text.summarize
rotifer compile summarizer
rotifer arena submit summarizer
Step 3: Your First Composition — Seq
Seq executes genes sequentially, piping each output as input to the next:
Seq(A, B, C) = A → B → C
But wait — the output of genesis-web-search is { results: [...] }, while summarizer expects { text: string }. We need a Transform to bridge the schemas:
import { Seq, Transform } from "@rotifer/algebra";
const searchAndSummarize = Seq(
"genesis-web-search",
Transform((searchResult) => ({
text: searchResult.results.map(r => r.snippet).join(" "),
maxLength: 200,
})),
"summarizer"
);
Create an agent from this composition:
rotifer agent create researcher --genes genesis-web-search summarizer
Run it:
rotifer agent run researcher --input '{"query": "quantum computing 2026"}' --verbose
The --verbose flag shows intermediate inputs and outputs at each stage.
Step 4: Add Parallel Execution — Par
What if you want to search multiple sources simultaneously? Par executes genes concurrently:
Par(A, B, C) = A ‖ B ‖ C → [resultA, resultB, resultC]
import { Par, Seq, Transform } from "@rotifer/algebra";
const multiSourceSearch = Seq(
Par(
"genesis-web-search",
"genesis-web-search-lite"
),
Transform((results) => ({
text: results.flat().map(r => r.results?.map(x => x.snippet)).flat().join(" "),
maxLength: 300,
})),
"summarizer"
);
Both searches run in parallel using the thread pool. The results are collected into an array, then merged by the Transform and fed to the summarizer.
Step 5: Add Conditional Branching — Cond
Cond lets you route execution based on a runtime predicate:
import { Cond, Seq, Transform } from "@rotifer/algebra";
const adaptivePipeline = Seq(
"genesis-web-search",
Cond(
(result) => result.results.length > 10,
Seq(
Transform((r) => ({
text: r.results.map(x => x.snippet).join(" "),
maxLength: 500,
})),
"summarizer"
),
Transform((r) => ({
summary: r.results[0]?.snippet || "No results found.",
wordCount: 0,
}))
)
);
If the search returns more than 10 results, we summarize them. Otherwise, we just return the top snippet directly.
Step 6: Add Error Recovery — Try
Try attempts a primary gene and falls back to a secondary if it fails:
import { Try, Seq, Transform } from "@rotifer/algebra";
const resilientPipeline = Seq(
Try(
"genesis-web-search",
"genesis-web-search-lite"
),
Transform((r) => ({
text: r.results.map(x => x.snippet).join(" "),
maxLength: 200,
})),
"summarizer"
);
If the primary search fails (network error, rate limit, etc.), execution automatically falls back to the lite version. No manual error handling needed.
Step 7: The Complete Pipeline
Combining all operators into one production-grade pipeline:
import { Seq, Par, Cond, Try, Transform } from "@rotifer/algebra";
const productionPipeline = Seq(
// Stage 1: Resilient multi-source search
Try(
Par("genesis-web-search", "genesis-web-search-lite"),
Par("genesis-web-search-lite") // fallback: single source
),
// Stage 2: Merge parallel results
Transform((results) => ({
text: results.flat()
.map(r => r.results?.map(x => x.snippet))
.flat()
.filter(Boolean)
.join(" "),
resultCount: results.flat().reduce((n, r) => n + (r.results?.length || 0), 0),
})),
// Stage 3: Adaptive summarization
Cond(
(data) => data.resultCount > 5,
Seq(
Transform((d) => ({ text: d.text, maxLength: 400 })),
"summarizer"
),
Transform((d) => ({ summary: d.text.slice(0, 200), wordCount: 0 }))
),
// Stage 4: Format output
"genesis-code-format"
);
Create and run the agent:
rotifer agent create research-bot \
--genes genesis-web-search genesis-web-search-lite summarizer genesis-code-format
rotifer agent run research-bot \
--input '{"query": "rotifer protocol gene evolution"}' \
--verbose
Type Safety
The composition algebra enforces schema compatibility at composition time. If you wire two incompatible genes, you get a clear error:
Error[E0032]: Type mismatch in Seq composition
→ gene 'web-search' output: { results: SearchResult[] }
→ gene 'summarizer' input: { text: string, maxLength?: number }
help: Add a Transform between the genes to reshape the data
This catches data flow bugs before runtime.
Fitness of Composed Genes
Compositions have their own fitness scores:
| Operator | Fitness Formula |
|---|---|
Seq |
min(F(components)) × latency_penalty |
Par |
avg(F(components)) × parallelism_bonus |
Try |
F(primary) × success_rate + F(fallback) × (1 - success_rate) |
The Arena evaluates compositions as a whole, so there's selection pressure toward efficient structures.
What You've Learned
- Seq chains genes into sequential pipelines
- Par runs genes concurrently for speed and redundancy
- Cond routes execution based on runtime conditions
- Try provides automatic error recovery with fallbacks
- Transform bridges schema mismatches between genes
- Compositions are type-safe and have composite fitness scores
Deep Dive: See the full Composition Patterns guide for all operators, type constraints, and fitness formulas. For agent CLI commands, see the Agent Reference.
Top comments (0)