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 |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Operator Summary¶
Operator |
Meaning |
ADK Type |
|---|---|---|
|
Sequence |
|
|
Function step |
Zero-cost transform |
|
Parallel |
|
|
Loop (fixed) |
|
|
Loop (conditional) |
|
|
Typed output |
|
|
Fallback |
First-success chain |
|
Branch |
Deterministic routing |
|
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[">><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) |
|---|---|---|---|
|
Sequential agents |
Sequential activities |
Sequential coroutines |
|
Parallel agents |
|
|
|
Loop agent |
Checkpointed |
|
|
Fallback agent |
try/except over activities |
try/except |
|
output_schema |
Same (schema on activity) |
Same |
|
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
Execution Backends — backend selection and capability matrix
Temporal Guide — how operators map to Temporal concepts