Expression Language

Nine operators compose any agent topology. All operators are immutable – sub-expressions can be safely reused across different pipelines.

Tip

Visual learner? Open the Operator Algebra Interactive Reference{target=”_blank”} for animated SVG flow diagrams, code examples, and composition rules for all 9 operators.

Note

Python uses operators, TypeScript uses method chains JavaScript has no operator overloading, so the TypeScript package replaces every operator with an explicit method call. Pick your language with the tabs on each example.

Python

TypeScript

Returns

a >> b

a.then(b)

Pipeline

a | b

a.parallel(b)

FanOut

a * 3

a.times(3)

Loop

a * until(pred, max=5)

a.timesUntil(pred, { max: 5 })

Loop

a // b

a.fallback(b)

Fallback

a @ Schema

a.outputAs(schema)

Agent

OPERATOR VISUAL REFERENCE >> Sequence a b c SequentialAgent | Parallel a b c ParallelAgent * Loop body n times LoopAgent @ Typed agent Schema{} output_schema // Fallback a b c first success Route Branch state[key] "a" → handler_a "b" → handler_b deterministic routing

Operator Summary

Operator

Meaning

ADK Type

a >> b

Sequence

SequentialAgent

a >> fn

Function step

Zero-cost transform

a | b

Parallel

ParallelAgent

a * 3

Loop (fixed)

LoopAgent

a * until(pred)

Loop (conditional)

LoopAgent + checkpoint

a @ Schema

Typed output

output_schema

a // b

Fallback

First-success chain

Route("key").eq(...)

Branch

Deterministic routing

S.pick(...), S.rename(...)

State transforms

Dict operations via >>

Reading an expression

Operators follow Python’s precedence, which does not match reading order. In a mixed expression, * binds tighter than @, which binds tighter than //, which binds tighter than |, which binds tighter than >>.

        flowchart LR
    T1[tightest] --> P1["*<br/>loop"]
    P1 --> P2["@<br/>typed output"]
    P2 --> P3["//<br/>fallback"]
    P3 --> P4["|<br/>parallel"]
    P4 --> P5["&gt;&gt;<br/>sequence"]
    P5 --> T2[loosest]

    classDef a fill:#e3f2fd,stroke:#1565c0,color:#0d47a1
    classDef op fill:#fff3e0,stroke:#e65100,color:#bf360c
    class T1,T2 a
    class P1,P2,P3,P4,P5 op
    

Practical consequence: a | b >> c is (a | b) >> c — the parallel block runs, then c. And a >> b @ Schema is a >> (b @ Schema) — only b is constrained. When in doubt, add parens; they are never wrong.

Tip

The “outer” operator is where to start reading Read expressions from the outermost operator inward. >> chains usually frame the whole pipeline, with |, //, *, and @ nested inside the steps.

Immutability

All operators produce new expression objects. Sub-expressions can be safely reused:

review = agent_a >> agent_b
pipeline_1 = review >> agent_c  # Independent
pipeline_2 = review >> agent_d  # Independent
const review = agentA.then(agentB);
const pipeline1 = review.then(agentC); // Independent
const pipeline2 = review.then(agentD); // Independent

>> – Pipeline (Sequential)

The >> operator (Python) / .then() method (TypeScript) chains agents into a sequential pipeline. Each agent runs after the previous one completes:

from adk_fluent import Agent

pipeline = (
    Agent("extractor", "gemini-2.5-flash").instruct("Extract entities.").writes("entities")
    >> Agent("enricher", "gemini-2.5-flash").instruct("Enrich {entities}.")
    >> Agent("formatter", "gemini-2.5-flash").instruct("Format output.")
).build()
import { Agent } from "adk-fluent-ts";

const pipeline = new Agent("extractor", "gemini-2.5-flash")
  .instruct("Extract entities.")
  .writes("entities")
  .then(new Agent("enricher", "gemini-2.5-flash").instruct("Enrich {entities}."))
  .then(new Agent("formatter", "gemini-2.5-flash").instruct("Format output."))
  .build();

This produces the same SequentialAgent as the builder-style Pipeline("name").step(...).step(...).build().

Function Steps

Plain functions compose as zero-cost workflow nodes (no LLM call):

def merge_research(state):
    return {"research": state["web"] + "\n" + state["papers"]}

pipeline = web_agent >> merge_research >> writer_agent
import { tap } from "adk-fluent-ts";

const mergeResearch = tap((state) => {
  state.research = `${state.web}\n${state.papers}`;
});

const pipeline = webAgent.then(mergeResearch).then(writerAgent);

Function steps receive the session state and can mutate it (TypeScript) or return a dict of updates (Python). They are useful for data transformations between agent steps.

| / .parallel() – Parallel (Fan-Out)

Run agents concurrently:

from adk_fluent import Agent

fanout = (
    Agent("web", "gemini-2.5-flash").instruct("Search web.").writes("web_results")
    | Agent("papers", "gemini-2.5-pro").instruct("Search papers.").writes("paper_results")
    | Agent("internal", "gemini-2.5-flash").instruct("Search internal docs.").writes("internal_results")
).build()
import { Agent } from "adk-fluent-ts";

const fanout = new Agent("web", "gemini-2.5-flash")
  .instruct("Search web.")
  .writes("web_results")
  .parallel(
    new Agent("papers", "gemini-2.5-pro").instruct("Search papers.").writes("paper_results"),
  )
  .parallel(
    new Agent("internal", "gemini-2.5-flash")
      .instruct("Search internal docs.")
      .writes("internal_results"),
  )
  .build();

This produces the same ParallelAgent as the builder-style FanOut("name").branch(...).branch(...).build().

* / .times() – Loop

Fixed Count

Repeat an expression a fixed number of times:

loop = (
    Agent("writer", "gemini-2.5-flash").instruct("Write draft.")
    >> Agent("reviewer", "gemini-2.5-flash").instruct("Review.")
) * 3
const loop = new Agent("writer", "gemini-2.5-flash")
  .instruct("Write draft.")
  .then(new Agent("reviewer", "gemini-2.5-flash").instruct("Review."))
  .times(3);

Conditional Loop with until()

Loop until a predicate on session state is satisfied:

from adk_fluent import until

loop = (
    Agent("writer").model("gemini-2.5-flash").instruct("Write.").writes("quality")
    >> Agent("reviewer").model("gemini-2.5-flash").instruct("Review.")
) * until(lambda s: s.get("quality") == "good", max=5)
const loop = new Agent("writer", "gemini-2.5-flash")
  .instruct("Write.")
  .writes("quality")
  .then(new Agent("reviewer", "gemini-2.5-flash").instruct("Review."))
  .timesUntil((s) => s.quality === "good", { max: 5 });

The max parameter sets a safety limit on the number of iterations.

@ / .outputAs() – Typed Output

Bind a structured-output schema as the agent’s response contract:

from pydantic import BaseModel

class Report(BaseModel):
    title: str
    body: str

agent = Agent("writer").model("gemini-2.5-flash").instruct("Write.") @ Report
// Use any opaque schema descriptor — Zod, JSON Schema, or a plain object.
const ReportSchema = {
  type: "object",
  properties: {
    title: { type: "string" },
    body: { type: "string" },
  },
  required: ["title", "body"],
} as const;

const agent = new Agent("writer", "gemini-2.5-flash")
  .instruct("Write.")
  .outputAs(ReportSchema);

The agent’s output is validated against the schema, ensuring structured, typed responses.

// / .fallback() – Fallback Chain

Try each agent in order. The first agent to succeed wins:

answer = (
    Agent("fast").model("gemini-2.0-flash").instruct("Quick answer.")
    // Agent("thorough").model("gemini-2.5-pro").instruct("Detailed answer.")
)
const answer = new Agent("fast", "gemini-2.0-flash")
  .instruct("Quick answer.")
  .fallback(new Agent("thorough", "gemini-2.5-pro").instruct("Detailed answer."));

This is useful for cost optimization: try a cheaper, faster model first and fall back to a more capable model only if needed.

Route("key").eq(...) – Deterministic Routing

Route on session state without LLM calls:

from adk_fluent import Agent, Route

classifier = Agent("classify").model("gemini-2.5-flash").instruct("Classify intent.").writes("intent")
booker = Agent("booker").model("gemini-2.5-flash").instruct("Book flights.")
info = Agent("info").model("gemini-2.5-flash").instruct("Provide info.")

# Route on exact match — zero LLM calls for routing
pipeline = classifier >> Route("intent").eq("booking", booker).eq("info", info)

# Dict shorthand
pipeline = classifier >> {"booking": booker, "info": info}
import { Agent, Route } from "adk-fluent-ts";

const classifier = new Agent("classify", "gemini-2.5-flash")
  .instruct("Classify intent.")
  .writes("intent");
const booker = new Agent("booker", "gemini-2.5-flash").instruct("Book flights.");
const info = new Agent("info", "gemini-2.5-flash").instruct("Provide info.");

const pipeline = classifier.then(
  new Route("intent").eq("booking", booker).eq("info", info),
);

Conditional Gating

.proceedIf() / .proceed_if() gates an agent’s execution based on a state predicate:

enricher = (
    Agent("enricher")
    .model("gemini-2.5-flash")
    .instruct("Enrich the data.")
    .proceed_if(lambda s: s.get("valid") == "yes")
)
import { Agent, gate } from "adk-fluent-ts";

const enricher = gate(
  (s) => s.valid === "yes",
  new Agent("enricher", "gemini-2.5-flash").instruct("Enrich the data."),
);

The agent only runs if the predicate returns a truthy value.

Full Composition

All operators compose into a single expression. The following example combines every operator:

Full composition topology:

  ┬─ web ────┐
  └─ scholar ┘  (|)
       │
  S.merge(into="research")
       │
  writer @ Report // writer_b @ Report  (//)
       │
  ┌──► critic ──► reviser ──┐
  └── until(confidence≥0.85) ┘  (*)
from pydantic import BaseModel
from adk_fluent import Agent, S, until

class Report(BaseModel):
    title: str
    body: str
    confidence: float

pipeline = (
    (   Agent("web").model("gemini-2.5-flash").instruct("Search web.")
      | Agent("scholar").model("gemini-2.5-flash").instruct("Search papers.")
    )
    >> S.merge("web", "scholar", into="research")
    >> Agent("writer").model("gemini-2.5-flash").instruct("Write.") @ Report
       // Agent("writer_b").model("gemini-2.5-pro").instruct("Write.") @ Report
    >> (
        Agent("critic").model("gemini-2.5-flash").instruct("Score.").writes("confidence")
        >> Agent("reviser").model("gemini-2.5-flash").instruct("Improve.")
    ) * until(lambda s: s.get("confidence", 0) >= 0.85, max=4)
)
import { Agent, S } from "adk-fluent-ts";

const ReportSchema = {
  type: "object",
  properties: {
    title: { type: "string" },
    body: { type: "string" },
    confidence: { type: "number" },
  },
  required: ["title", "body", "confidence"],
} as const;

const pipeline = new Agent("web", "gemini-2.5-flash")
  .instruct("Search web.")
  .parallel(new Agent("scholar", "gemini-2.5-flash").instruct("Search papers."))
  .then(S.merge_(["web", "scholar"], "research"))
  .then(
    new Agent("writer", "gemini-2.5-flash")
      .instruct("Write.")
      .outputAs(ReportSchema)
      .fallback(
        new Agent("writer_b", "gemini-2.5-pro").instruct("Write.").outputAs(ReportSchema),
      ),
  )
  .then(
    new Agent("critic", "gemini-2.5-flash")
      .instruct("Score.")
      .writes("confidence")
      .then(new Agent("reviser", "gemini-2.5-flash").instruct("Improve."))
      .timesUntil((s) => Number(s.confidence ?? 0) >= 0.85, { max: 4 }),
  );

This expression combines parallel fan-out (|), state transforms (S.merge), typed output (@ Report), fallback (//), and conditional loops (* until).

Backend Compatibility

All expression operators work identically across backends. The definition is the same — only execution semantics change:

Operator

ADK (default)

Temporal (in dev)

asyncio (in dev)

>> (sequence)

Sequential agents

Sequential activities

Sequential coroutines

| (parallel)

Parallel agents

asyncio.gather() over activities

asyncio.gather()

* (loop)

Loop agent

Checkpointed while loop

while loop

// (fallback)

Fallback agent

try/except over activities

try/except

@ Schema

output_schema

Same (schema on activity)

Same

Route(...)

Custom agent

Inline deterministic code

Inline

When using Temporal, deterministic operators (>>, Route, S.*) become replay-safe workflow code, while non-deterministic operators (anything involving an LLM call) become cached activities. See Temporal Guide for details.

See also