Skip to content
Color theme

Dynamic Routing

Route workflows conditionally using @Guard decorators and priority to control which transition fires when multiple transitions share the same source state.

Basic Guard

@Transition({ from: 'check', to: 'high', priority: 10 })
@Guard('isHigh')
routeHigh(state: MyState) {}
@Transition({ from: 'check', to: 'low' })
routeLow(state: MyState) {} // Fallback — no guard
isHigh(state: MyState): boolean {
return state.value > 100;
}

How it works:

  1. Transitions with higher priority are checked first
  2. The @Guard references a method that returns a boolean
  3. First transition whose guard returns true fires
  4. A transition without @Guard acts as the fallback

Multi-Level Routing

Chain routing decisions with cascading forks:

import { z } from 'zod';
import { BaseWorkflow, Guard, Transition, Workflow } from '@loopstack/common';
import type { RunContext } from '@loopstack/common';
import { MessageDocument } from '@loopstack/common';
interface RoutingState {
value?: number;
}
const RoutingSchema = z.object({ value: z.number().default(150) }).strict();
type RoutingArgs = z.infer<typeof RoutingSchema>;
@Workflow({
schema: RoutingSchema,
})
export class DynamicRoutingExampleWorkflow extends BaseWorkflow<RoutingArgs> {
@Transition({ to: 'prepared' })
async createMockData(state: RoutingState, ctx: RunContext<RoutingArgs>) {
await this.documentStore.save(MessageDocument, {
role: 'assistant',
text: `Analyzing value = ${ctx.args.value}`,
});
this.assignState({ value: ctx.args.value });
}
// First fork: value > 100?
@Transition({ from: 'prepared', to: 'placeA', priority: 10 })
@Guard('isAbove100')
routeToPlaceA(state: RoutingState) {}
@Transition({ from: 'prepared', to: 'placeB' })
routeToPlaceB(state: RoutingState) {} // Fallback: value <= 100
isAbove100(state: RoutingState): boolean {
return (state.value ?? 0) > 100;
}
// Second fork: value > 200?
@Transition({ from: 'placeA', to: 'placeC', priority: 10 })
@Guard('isAbove200')
routeToPlaceC(state: RoutingState) {}
@Transition({ from: 'placeA', to: 'placeD' })
routeToPlaceD(state: RoutingState) {} // Fallback: 100 < value <= 200
isAbove200(state: RoutingState): boolean {
return (state.value ?? 0) > 200;
}
// Terminal transitions
@Transition({ from: 'placeB', to: 'end' })
async showMessagePlaceB(state: RoutingState) {
await this.documentStore.save(MessageDocument, { role: 'assistant', text: 'Value is less or equal 100' });
}
@Transition({ from: 'placeC', to: 'end' })
async showMessagePlaceC(state: RoutingState) {
await this.documentStore.save(MessageDocument, { role: 'assistant', text: 'Value is greater than 200' });
}
@Transition({ from: 'placeD', to: 'end' })
async showMessagePlaceD(state: RoutingState) {
await this.documentStore.save(MessageDocument, {
role: 'assistant',
text: 'Value is less or equal 200, but greater than 100',
});
}
}

Routing Flow

prepared → [value > 100?]
├─ yes → placeA → [value > 200?]
│ ├─ yes → placeC (done)
│ └─ no → placeD (done)
└─ no → placeB (done)

Common Patterns

Tool Call Routing

Route based on LLM response (see AI Tool Calling):

@Transition({ from: 'prompt_executed', to: 'ready', priority: 10 })
@Guard('hasToolCalls')
async executeToolCalls(state: MyState) { ... } // Run tools, loop back
@Transition({ from: 'prompt_executed', to: 'end' })
async respond(state: MyState) { ... } // Fallback: no tool calls
hasToolCalls(state: MyState): boolean {
return state.llmResult?.message.stopReason === 'tool_use';
}

For async tools that complete via callback (sub-workflows, HITL), insert an awaiting_tools intermediate state with a wait: true re-entry transition — see Agent Workflows.

Error-Based Routing

Route based on a tool’s error response:

@Transition({ from: 'fetched', to: 'auth_needed', priority: 10 })
@Guard('needsAuth')
async startAuth(state: MyState) { ... }
@Transition({ from: 'fetched', to: 'end' })
async displayResults(state: MyState) { ... }
needsAuth(state: MyState): boolean {
return state.fetchResult?.error === 'unauthorized';
}

Guard Method Rules

  • Guard methods must return a boolean (or truthy/falsy value)
  • They receive state as their first parameter
  • They should be synchronous — no async guards
  • Use descriptive names: hasToolCalls, isAbove100, needsAuth

Registry References