“Let’s split it into agents” has become the AI equivalent of “let’s make it a microservice.”
Sometimes the boundary is useful. Sometimes it only creates more state, more coordination, and a harder failure to explain.
The most dangerous assumption is that separate agents should run in parallel. Parallelism is safe only when the branches are genuinely independent. If one branch changes the world while another is evaluating it, both agents can make locally reasonable decisions that are unsafe together.
Google ADK 2.0 makes workflow topology explicit through graph-based Workflow objects. That is valuable because sequences, branches, and joins become part of the program instead of an agreement hidden in a supervisor prompt.
Series note: This is Part 5 of Reliable Google AI Agents in TypeScript. The examples were checked against
@google/adk2.0.0 in September 2026.
Start with the dependency, not the agent count
Imagine a system preparing a hotel recommendation.
It needs live inventory, company travel policy, and a final recommendation. Inventory lookup and policy evaluation can run concurrently because both observe the same request and neither changes shared state. The final decision must wait for both.
Now consider a different pair of operations:
- one agent changes the reservation;
- another calculates an upgrade using the current reservation.
Those branches are not independent. Running them concurrently can make the upgrade decision depend on state that no longer exists.
Before drawing a parallel branch, ask:
- Do both operations only read the same starting state?
- Can either operation change data the other consumes?
- Can either produce an irreversible side effect?
- Is there a deterministic way to combine their results?
- What happens when one succeeds and the other times out?
If those answers are unclear, parallel is an optimization you have not earned yet.
Encode safe parallelism as fan-out and join
ADK’s TypeScript Workflow graph can express two independent branches and a join barrier directly:
import { JoinNode, node, NodeContext, Workflow } from "@google/adk";
type Request = {
city: string;
maxNightlyPriceUsd: number;
};
const fetchInventory = node(
async (_ctx: NodeContext, request: Request) =>
searchHotels(request.city, request.maxNightlyPriceUsd),
{ name: "fetch_inventory" },
);
const evaluatePolicy = node(
async (_ctx: NodeContext, request: Request) =>
checkTravelPolicy(request),
{ name: "evaluate_policy" },
);
const evidenceReady = new JoinNode({ name: "evidence_ready" });
const chooseRecommendation = node(
(_ctx: NodeContext, results: Record<string, unknown>) => {
if (!("fetch_inventory" in results) || !("evaluate_policy" in results)) {
throw new Error("Recommendation requires both evidence branches");
}
return selectCompliantHotel(
results["fetch_inventory"],
results["evaluate_policy"],
);
},
{ name: "choose_recommendation" },
);
export const rootAgent = new Workflow({
name: "hotel_recommendation_workflow",
edges: [
["START", fetchInventory, evidenceReady],
["START", evaluatePolicy, evidenceReady],
[evidenceReady, chooseRecommendation],
],
});
The topology is the contract:
START ─┬─> fetch_inventory ──┐
└─> evaluate_policy ──┴─> evidence_ready ─> choose_recommendation
JoinNode waits for every predecessor and passes the next node a record keyed by predecessor name. Each predecessor must produce output. Validate that record at the join instead of allowing a missing branch to surface as an unrelated failure several nodes later.
Use sequences when state must move in order
Some work is naturally sequential:
normalize request
↓
search inventory
↓
request approval
↓
create booking
↓
send confirmation
The matching ADK graph is deliberately boring:
export const bookingWorkflow = new Workflow({
name: "booking_workflow",
edges: [[
"START",
normalizeRequest,
searchInventory,
requestApproval,
createBooking,
sendConfirmation,
]],
});
That sequence is safer than asking a supervisor model to remember the required order on every run. The model can still make bounded decisions inside individual nodes; the workflow owns the invariant.
This is a recurring production pattern:
Use probabilistic reasoning inside deterministic control flow.
Parallel branches should produce evidence, not mutations
Shared mutable state is where fan-out becomes dangerous.
If two branches write selected_hotel, the last writer wins. If one updates a booking while another reads it, behavior depends on timing. If both send a notification, the user receives duplicate side effects.
A safer ownership rule is:
| Stage | Responsibility |
|---|---|
| Parallel branches | Gather and normalize evidence |
| Join | Verify all required evidence arrived |
| Decision node | Select one outcome |
| Mutation node | Own the state transition or side effect |
When distributed mutation is unavoidable, use an idempotency key, resource-level concurrency control, and a durable result record. Do not rely on a model to notice that another branch is already acting.
Failure semantics belong in the graph
Parallelism introduces failure combinations that a happy-path diagram hides:
- inventory succeeds while policy times out;
- both branches succeed but one returns stale evidence;
- a retry produces the same side effect twice;
- the join receives structurally valid but semantically incompatible results.
Each branch needs a timeout and retry budget appropriate to its operation. The join needs a rule for partial failure: fail closed, use an explicitly degraded mode, or ask for human review. “Continue with whatever arrived” should be a named policy—not an accident.
A2A is a network boundary, not a style choice
ADK can expose and consume remote agents through the Agent2Agent protocol. That is useful when a capability belongs to another team, runtime, deployment, or trust domain.
But a remote agent is not a helper function. It introduces:
- network latency and partial failure;
- authentication and authorization;
- an independently versioned contract;
- deployment and ownership boundaries;
- a larger attack surface.
Keep tightly coupled work in one local workflow unless there is a real service boundary. Use A2A when that boundary already exists for organizational or platform reasons, not because a diagram looks more “agentic.”
Test the shape of the run
Output-only testing misses topology regressions.
A stable execution contract for the recommendation workflow might require:
-
fetch_inventoryandevaluate_policyboth complete; -
choose_recommendationoccurs afterevidence_ready; - no reservation mutation occurs in a parallel evidence branch;
- at most one booking side effect occurs;
- booking is forbidden before approval.
The evidence can be rendered as an execution tree:
hotel_recommendation_workflow
├─ fetch_inventory
├─ evaluate_policy
├─ evidence_ready
└─ choose_recommendation
A local evidence tool such as AgentInspect can check required, forbidden, and ordered operations after those ADK events have been mapped into its run format. That wording is deliberate: AgentInspect does not currently advertise a first-class ADK adapter, so the integration boundary should remain explicit until one ships.
The goal is not to make the model deterministic. It is to make the workflow contract deterministic.
The topology is part of correctness
Multi-agent architecture is not automatically better than one well-designed agent. Parallel execution is not automatically faster once retries, joins, and coordination are included. Remote delegation is not automatically modular once network contracts are involved.
Use parallel branches for independent evidence gathering. Use sequences for dependent work. Give one node ownership of each mutation. Use remote agents only at genuine service boundaries. Record the execution shape so the team can see what actually happened.
The production measure is not how many agents participated.
It is whether the system reached the right outcome without conflicting actions, hidden races, or an execution path nobody can explain.
References
- ADK graph-based workflows
- ADK graph routes, fan-out, and join
- ADK for TypeScript
- ADK guidance on local agents and A2A
Earlier in the series: Gemini Function Calling Is Not an Agent Runtime · Testing Google ADK TypeScript Agents Without Chasing Sentences · From Local Traces to Production Observability for Google AI Agents
Top comments (1)
Good post. I wanna discuss further about collaboration. How about you?