← Back to Blog

Compose Multi-Gene Agent Pipelines

Build a search → summarize → format pipeline using Seq, Par, Cond, and Try. Learn how gene algebra turns genes into agent workflows.

Compose Multi-Gene Agent Pipelines

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:

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) = ABC

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) = ABC → [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


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