Repository navigation
Advanced Routing
This document serves as a cheat sheet and specification for the GraphCompose DSL flow: block, detailing all capabilities for routing, parallelism, and barrier synchronization.
The simplest scenario. Agents unconditionally pass execution to the next node in the chain.
flow: [
from(StartNode).next(AgentA),
from(AgentA).next(AgentB),
from(AgentB).next(FinishNode)
]Hardcoded branching. The graph unconditionally splits into a known number of paths and waits for all of them.
flow: [
from(StartNode).next(PreparationAgent),
// Branch into 3 parallel paths
// Note: you can use required() or background() to indicate if the main execution should wait for this branch
from(PreparationAgent).nextParallel({
awaiting: [AgentA, AgentB],
fireAndForget: [AgentC]
}),
// Barrier: waits for all 3 branches to finish
from(AgentA, AgentB, AgentC).join(AggregatorAgent),
from(AggregatorAgent).next(FinishNode)
]When splitting a process into multiple independent paths (not a map-reduce fork, but true structural branching), you use nextParallel. You can specify which branches the main execution should wait for (required) and which can be run asynchronously in a fire-and-forget manner (background).
flow: [
from(StartNode).next(OnboardingAgent),
// Branch the process into two independent tasks
from(OnboardingAgent).nextParallel({
awaiting: [MainAgent], // The user waits for this branch to finish
fireAndForget: [AnalyticsAgent] // Runs in the background, doesn't block the execution output
}),
// They don't need a join! They go to their own separate finishes:
from(MainAgent).next(SuccessFinish),
from(AnalyticsAgent).next(SilentlyFinish)
]Note: True early-return execution for background branches relies on engine-level support; otherwise it defaults to standard parallel execution.
The number of parallel branches is unknown until runtime (data-dependent). GraphCompose uses LangGraph's Send API under the hood to dynamically spawn N instances of an agent.
To preserve Dependency Injection and clean architecture, the extraction logic is placed in a dedicated @BatchStrategy class, rather than an inline lambda.
import { BatchStrategy, type BatchParallelStrategy } from "graphcompose/concurrency";
// The strategy decides WHICH elements are sent to the dynamic agents
@BatchStrategy({ name: "url_extractor" })
export class UrlExtractor implements BatchParallelStrategy<MyState, string> {
// Supports DI!
constructor(private readonly filterService: FilterService) {}
extract(state: MyState): string[] {
return this.filterService.filterUrls(state.urls);
}
}flow: [
from(WorkflowStart).next(DispatcherAgent),
// Extracts an array from the state via the DI Strategy class.
// Spawns a ScraperAgent for each element, limited to 5 concurrent instances.
// The join barrier automatically waits for all spawned instances to finish.
from(DispatcherAgent)
.batchParallel(ScraperAgent, UrlExtractor, { concurrency: 5 }),
from(ScraperAgent).join(SummaryAgent),
from(SummaryAgent).routes(WorkflowFinish)
]The router analyzes the payload and must choose strictly one of the provided paths.
flow: [
from(StartNode).next(ValidatorRouter),
// Either success or error (cannot run concurrently)
from(ValidatorRouter).routeOne(SuccessAgent, ErrorAgent),
// The barrier will wait for whichever path was chosen
from(SuccessAgent, ErrorAgent).join(Finalizer),
from(Finalizer).next(FinishNode)
]The classic "call an expert if needed, otherwise skip" scenario.
flow: [
from(StartNode).nextParallel(MainWorker, ComplianceRouter),
// The router either directs to LegalAgent, or cancels this branch (Skip)
from(ComplianceRouter).routeOneOrSkip(LegalAgent),
// Finalizer will wait for MainWorker and LegalAgent.
// If Legal was skipped, the barrier detects the 'aborted' status and proceeds without deadlocking.
from(MainWorker, LegalAgent).join(Finalizer),
from(Finalizer).next(FinishNode)
]A router can launch multiple branches in parallel. Any unselected branches are automatically skipped.
flow: [
from(StartNode).next(ExpertiseRouter),
// Choose any subset of the 3 paths. Provide constraints to the LLM router.
from(ExpertiseRouter).routeManyOrSkip(FinanceAgent, HrAgent, TechAgent, {
maxChoices: 2, // Cannot pick more than 2
required: [FinanceAgent] // Finance must always be included
}),
// Wait only for the branches the router actually launched (others are skipped)
from(FinanceAgent, HrAgent, TechAgent).join(Aggregator),
from(Aggregator).next(FinishNode)
]The @QuorumRouter is a specialized routing node in GraphCompose designed to act as an active synchronization barrier (fan-in) for parallel branches. Unlike a simple .join() which waits for all parallel agents to finish, .joinQuorum() actively monitors results as they come in, allowing for powerful short-circuiting, timeouts, and "Fail-Fast" behavior.
Configuration is supplied at the point of graph assembly to maximize router reusability:
from(Worker1, Worker2, Worker3)
.joinQuorum(SecurityQuorum, {
min: 2,
max: 3,
timeoutSeconds: 30
})Depending on how you combine min, max, and timeoutSeconds, the framework intelligently adapts its behavior. Here are the 4 primary scenarios:
(Under the hood, max automatically equals min)
-
Early Success: As soon as
minvotes are gathered, the remaining agents are aborted, and the router proceeds withhasQuorum = true. -
Fail-Fast: If too many agents vote "No" and it becomes mathematically impossible to reach
min, the remaining agents are aborted, and the router proceeds withhasQuorum = false.
Use case: "I want Y votes for a perfect consensus, but I'll settle for X if the others fail."
-
Keep Waiting: If we reach
min, we do not abort the others yet. We keep waiting in hopes of reachingmax. -
Early Success: As soon as we reach
max, we abort the remaining agents and proceed withhasQuorum = true. -
Natural Finish: If all agents finish and we gathered between
minandmaxvotes, we proceed withhasQuorum = true. If we gathered< min, we proceed withhasQuorum = false.
Use case: "Try to get a perfect Y consensus quickly, but if Z seconds pass, just check if we at least got X."
-
Early Success: If
maxvotes are gathered before the timeout, abort the rest and proceed withhasQuorum = true. -
Timeout Triggered: If
Zseconds pass, abort the remaining agents. Evaluate what was gathered: ifvotes >= min, proceed withhasQuorum = true. Otherwise,hasQuorum = false. -
Fail-Fast: If too many negative votes come in early, we don't even wait for the timeout. We abort immediately with
hasQuorum = false.
(Under the hood, max automatically equals min)
-
Early Success: As soon as
minvotes are gathered, abort the rest and proceed withhasQuorum = true. We do not wait for the timeout. -
Timeout Triggered: If
Zseconds pass, it means we haven't reachedminyet. Abort the remaining agents and proceed withhasQuorum = false.
import {
Agent, Workflow, QuorumRouter, type QuorumStrategy,
from, Append, Tool, type ToolHandler, WorkflowStart, WorkflowFinish
} from "graphcompose";
@QuorumRouter({ name: "security_quorum" })
class SecurityQuorum implements QuorumStrategy<CodeAuditState> {
// Evaluated every time ANY agent finishes
filterVote(state: CodeAuditState): boolean {
const latestReport = state.reports.at(-1);
return latestReport?.severity === "CRITICAL";
}
// Evaluated ONCE when the quorum terminates
route(state: CodeAuditState, hasQuorum: boolean) {
if (hasQuorum) {
console.warn("Quorum reached! Halting release.");
return EmergencyLockdown;
}
console.log("Code is safe (or quorum failed), proceeding.");
return FinishAudit;
}
}
@Workflow({
name: "security_audit",
version: "1.0.0",
flow: [
from(CodeAuditState).nextParallel(StaticAnalyzer, DynamicAnalyzer, DependencyChecker, PenTester),
// The active barrier
from(StaticAnalyzer, DynamicAnalyzer, DependencyChecker, PenTester)
.joinQuorum(SecurityQuorum, {
min: 2, // We need at least 2 critical reports
max: 3, // If we get 3, stop immediately
timeoutSeconds: 45 // Don't wait longer than 45s for the slow agents
})
.routes(EmergencyLockdown, FinishAudit),
from(EmergencyLockdown).next(FinishAudit)
]
})
class SecurityAuditWorkflow {}-
AgentOutput<T>: An interface for Agents to declare their output DTO shape, giving strict typing to.nextParallel(..., p => p.items). -
JoinArray<T>: A type alias (Array<ForkOutput<T>>) that hides generic complexity when writing aJoinHandlerfor dynamic parallelism. -
AgentState<T>: Wraps LangGraph's global state, providing strict type-safety over thepayloadproperty inside an agent'srunmethod. -
WorkflowContext: Replaces the genericFlowContext, providing access to infrastructure (likecontext.storefor large artifact saving without bloating the graph state).
AGBoss
- Home
- Delivery Pipeline
- Architecture
- Configuration
- Workflow — the graph
- Advanced Routing (DSL)
- Routers
- Guards
- CLI — agboss
- App — createApp
- Context and Authorization
- Testing
- Suites and Comparisons
- Components
- Standard DTOs
- Example: job-scout
- Agents — the loop
- Model providers
- Tools
- Knowledge bases (RAG)
- FinOps
- Tracing
- Observability
- Terns and evaluation
- Profiles and comparisons
- Pause seam
- Decisions (ADR)