Skip to Content
DocumentationFeaturesDynamic Routing

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:

  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:

@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

Last updated on