← Back to Blog

Compose Multi-Gene Agent Pipelines

Advanced tutorial: build a search → summarize → format pipeline using Seq, Par, Cond, and Try operators. Learn how gene algebra turns simple genes into powerful agent workflows.

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

Step 1: Understand the Building Blocks

Your project already includes genesis genes. Let’s check what we have:

Terminal window
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

Terminal window
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:

Terminal window
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:

Terminal window
rotifer agent create researcher --genes genesis-web-search summarizer

Run it:

Terminal window
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:

Terminal window
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:

OperatorFitness Formula
Seqmin(F(components)) × latency_penalty
Paravg(F(components)) × parallelism_bonus
TryF(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


Deep Dive: See the full Composition Patterns guide for all operators, type constraints, and fitness formulas. For agent CLI commands, see the Agent Reference.