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() {}
@Transition({ from: 'check', to: 'low' })
routeLow() {} // Fallback — no guard
isHigh() {
const args = this.ctx.args as { value: number };
return args.value > 100;
}How it works:
- Transitions with higher
priorityare checked first - The
@Guardreferences a method that returns a boolean - First transition whose guard returns
truefires - A transition without
@Guardacts as the fallback
Multi-Level Routing
Chain routing decisions with cascading forks:
@Workflow({
uiConfig: __dirname + '/dynamic-routing-example.ui.yaml',
schema: z.object({ value: z.number().default(150) }).strict(),
})
export class DynamicRoutingExampleWorkflow extends BaseWorkflow {
@InjectTool() createChatMessage: CreateChatMessage;
@Initial({ to: 'prepared' })
async createMockData() {
const args = this.ctx.args as { value: number };
await this.createChatMessage.call({
role: 'assistant',
content: `Analysing value = ${args.value}`,
});
}
// First fork: value > 100?
@Transition({ from: 'prepared', to: 'placeA', priority: 10 })
@Guard('isAbove100')
routeToPlaceA() {}
@Transition({ from: 'prepared', to: 'placeB' })
routeToPlaceB() {} // Fallback: value <= 100
isAbove100() {
return (this.ctx.args as { value: number }).value > 100;
}
// Second fork: value > 200?
@Transition({ from: 'placeA', to: 'placeC', priority: 10 })
@Guard('isAbove200')
routeToPlaceC() {}
@Transition({ from: 'placeA', to: 'placeD' })
routeToPlaceD() {} // Fallback: 100 < value <= 200
isAbove200() {
return (this.ctx.args as { value: number }).value > 200;
}
// Terminal transitions
@Final({ from: 'placeB' })
async showMessagePlaceB() {
await this.createChatMessage.call({ role: 'assistant', content: 'Value is less or equal 100' });
}
@Final({ from: 'placeC' })
async showMessagePlaceC() {
await this.createChatMessage.call({ role: 'assistant', content: 'Value is greater than 200' });
}
@Final({ from: 'placeD' })
async showMessagePlaceD() {
await this.createChatMessage.call({
role: 'assistant',
content: '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: 'awaiting_tools', priority: 10 })
@Guard('hasToolCalls')
async executeToolCalls() { ... }
@Final({ from: 'prompt_executed' })
async respond() { ... } // Fallback: no tool calls
hasToolCalls() {
return this.llmResult?.stop_reason === 'tool_use';
}Error-Based Routing
Route based on a tool’s error response:
@Transition({ from: 'fetched', to: 'auth_needed', priority: 10 })
@Guard('needsAuth')
async startAuth() { ... }
@Final({ from: 'fetched' })
async displayResults() { ... }
needsAuth() {
return this.fetchResult?.error === 'unauthorized';
}Guard Method Rules
- Guard methods must return a boolean (or truthy/falsy value)
- They can access any workflow state (
this.ctx.args, instance properties, etc.) - They should be synchronous — no async guards
- Use descriptive names:
hasToolCalls,isAbove100,needsAuth
Registry References
- dynamic-routing-example-workflow — Multi-level guard-based routing with cascading forks
- tool-call-example-workflow — Guard-based routing for LLM tool call detection
Last updated on