← 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.

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:

// Composition semantics in TypeScript notation. On the CLI the equivalent is
// `rotifer agent create <name> --genes ... --composition <Seq|Par|Cond|Try|TryPool>`.

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 --composition Seq

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]
// Composition semantics in TypeScript notation. On the CLI the equivalent is
// `rotifer agent create <name> --genes ... --composition <Seq|Par|Cond|Try|TryPool>`.

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:

// Composition semantics in TypeScript notation. On the CLI the equivalent is
// `rotifer agent create <name> --genes ... --composition <Seq|Par|Cond|Try|TryPool>`.

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:

// Composition semantics in TypeScript notation. On the CLI the equivalent is
// `rotifer agent create <name> --genes ... --composition <Seq|Par|Cond|Try|TryPool>`.

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:

// Composition semantics in TypeScript notation. On the CLI the equivalent is
// `rotifer agent create <name> --genes ... --composition <Seq|Par|Cond|Try|TryPool>`.

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

One caveat on the CLI: --composition takes a single operator per agent, so the nested Try(Par(...)) structure above is the algebra's form, not one invocation. Build nested pipelines by composing agents, or edit the agent manifest directly.

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.