@loopstack/dynamic-routing-example-workflow
A module for the Loopstack AI automation framework.
This module provides an example workflow demonstrating how to implement conditional routing based on runtime values using guards and transition priorities.
Overview
The Dynamic Routing Example Workflow shows how to create branching logic in workflows using @Guard decorators and @Transition priorities. It demonstrates how to route execution through different paths based on input values.
By using this workflow as a reference, you’ll learn how to:
- Define a workflow schema with
z.object()for typed input arguments - Store input in typed workflow state for use in guards
- Use
@Guard('methodName')to conditionally gate transitions - Control transition evaluation order with the
priorityoption - Build multi-level branching structures with fallback routes
This example is useful for developers building workflows that require decision trees, validation flows, or any logic that branches based on data.
Installation
See SETUP.md for installation and setup instructions.
How It Works
Module Setup
Register the workflow as a NestJS provider:
@Module({
providers: [DynamicRoutingExampleWorkflow],
exports: [DynamicRoutingExampleWorkflow],
})
export class DynamicRoutingExampleModule {}Workflow State
State is defined as a TypeScript interface and passed to each transition method. Return updated state from transitions to persist values for guards and later steps:
interface DynamicRoutingState {
value: number;
}
@Workflow({
uiConfig: __dirname + '/dynamic-routing-example.ui.yaml',
schema: z
.object({
value: z.number().default(150),
})
.strict(),
})
export class DynamicRoutingExampleWorkflow extends BaseWorkflow<{ value: number }, DynamicRoutingState> {
constructor(@Inject(DOCUMENT_STORE) private readonly documentStore: DocumentStore) {
super();
}
}Key Concepts
1. Receiving Input Arguments
The first @Transition receives validated input as args. Store the value in workflow state for guards and routing:
@Transition({ to: 'prepared' })
async createMockData(
ctx: WorkflowContext,
args: { value: number },
state: DynamicRoutingState,
): Promise<DynamicRoutingState> {
await this.documentStore.save(MessageDocument, {
role: 'assistant',
content: `Analysing value = ${args.value}`,
});
return { ...state, value: args.value };
}2. Guard Methods
A guard is a method that returns a boolean. It receives workflow state as its first argument. Reference it by name in the @Guard decorator:
@Transition({ from: 'prepared', to: 'placeA', priority: 10 })
@Guard('isAbove100')
async routeToPlaceA(ctx: WorkflowContext, state: DynamicRoutingState): Promise<DynamicRoutingState> {
return state;
}
isAbove100(state: DynamicRoutingState): boolean {
return state.value > 100;
}When the workflow reaches the prepared state, it evaluates guards on all auto-transitions from that state (highest priority first). If isAbove100(state) returns true, the workflow moves to placeA.
Routing transitions like routeToPlaceA only pass state through. The guard makes the decision; the transition method moves the state machine to the next place.
3. Transition Priority
When multiple transitions share the same from state, priority controls evaluation order (higher priority is evaluated first). The first transition whose guard passes (or that has no guard) is taken:
// Evaluated first (priority: 10). Taken if value > 100.
@Transition({ from: 'prepared', to: 'placeA', priority: 10 })
@Guard('isAbove100')
async routeToPlaceA(ctx: WorkflowContext, state: DynamicRoutingState): Promise<DynamicRoutingState> {
return state;
}
// Evaluated last (no priority). Fallback when value <= 100.
@Transition({ from: 'prepared', to: 'placeB' })
async routeToPlaceB(ctx: WorkflowContext, state: DynamicRoutingState): Promise<DynamicRoutingState> {
return state;
}A transition without a @Guard always matches once reached, acting as a fallback.
4. Multi-Level Branching
Chain conditional transitions to create decision trees. After reaching placeA, a second level of guards routes further:
@Transition({ from: 'placeA', to: 'placeC', priority: 10 })
@Guard('isAbove200')
async routeToPlaceC(ctx: WorkflowContext, state: DynamicRoutingState): Promise<DynamicRoutingState> {
return state;
}
isAbove200(state: DynamicRoutingState): boolean {
return state.value > 200;
}
@Transition({ from: 'placeA', to: 'placeD' })
async routeToPlaceD(ctx: WorkflowContext, state: DynamicRoutingState): Promise<DynamicRoutingState> {
return state;
}5. Complete Routing Flow
The workflow routes through different states based on the input value:
- value <= 100 -> placeB -> “Value is less or equal 100”
- 100 < value <= 200 -> placeA -> placeD -> “Value is less or equal 200, but greater than 100”
- value > 200 -> placeA -> placeC -> “Value is greater than 200”
Terminal terminal @Transitions save the result message:
@Transition({ from: 'placeB', to: 'end' })
async showMessagePlaceB(ctx: WorkflowContext, state: DynamicRoutingState): Promise<unknown> {
await this.documentStore.save(MessageDocument, {
role: 'assistant',
content: 'Value is less or equal 100',
});
return {};
}Complete Workflow
import { Inject } from '@nestjs/common';
import { z } from 'zod';
import {
BaseWorkflow,
DOCUMENT_STORE,
Final,
Guard,
Initial,
MessageDocument,
Transition,
Workflow,
} from '@loopstack/common';
import type { DocumentStore, WorkflowContext } from '@loopstack/common';
interface DynamicRoutingState {
value: number;
}
@Workflow({
uiConfig: __dirname + '/dynamic-routing-example.ui.yaml',
schema: z
.object({
value: z.number().default(150),
})
.strict(),
})
export class DynamicRoutingExampleWorkflow extends BaseWorkflow<{ value: number }, DynamicRoutingState> {
constructor(@Inject(DOCUMENT_STORE) private readonly documentStore: DocumentStore) {
super();
}
@Transition({ to: 'prepared' })
async createMockData(
ctx: WorkflowContext,
args: { value: number },
state: DynamicRoutingState,
): Promise<DynamicRoutingState> {
await this.documentStore.save(MessageDocument, {
role: 'assistant',
content: `Analysing value = ${args.value}`,
});
return { ...state, value: args.value };
}
@Transition({ from: 'prepared', to: 'placeA', priority: 10 })
@Guard('isAbove100')
async routeToPlaceA(ctx: WorkflowContext, state: DynamicRoutingState): Promise<DynamicRoutingState> {
return state;
}
isAbove100(state: DynamicRoutingState): boolean {
return state.value > 100;
}
@Transition({ from: 'prepared', to: 'placeB' })
async routeToPlaceB(ctx: WorkflowContext, state: DynamicRoutingState): Promise<DynamicRoutingState> {
return state;
}
@Transition({ from: 'placeA', to: 'placeC', priority: 10 })
@Guard('isAbove200')
async routeToPlaceC(ctx: WorkflowContext, state: DynamicRoutingState): Promise<DynamicRoutingState> {
return state;
}
isAbove200(state: DynamicRoutingState): boolean {
return state.value > 200;
}
@Transition({ from: 'placeA', to: 'placeD' })
async routeToPlaceD(ctx: WorkflowContext, state: DynamicRoutingState): Promise<DynamicRoutingState> {
return state;
}
@Transition({ from: 'placeB', to: 'end' })
async showMessagePlaceB(ctx: WorkflowContext, state: DynamicRoutingState): Promise<unknown> {
await this.documentStore.save(MessageDocument, {
role: 'assistant',
content: 'Value is less or equal 100',
});
return {};
}
@Transition({ from: 'placeC', to: 'end' })
async showMessagePlaceC(ctx: WorkflowContext, state: DynamicRoutingState): Promise<unknown> {
await this.documentStore.save(MessageDocument, {
role: 'assistant',
content: 'Value is greater than 200',
});
return {};
}
@Transition({ from: 'placeD', to: 'end' })
async showMessagePlaceD(ctx: WorkflowContext, state: DynamicRoutingState): Promise<unknown> {
await this.documentStore.save(MessageDocument, {
role: 'assistant',
content: 'Value is less or equal 200, but greater than 100',
});
return {};
}
}Dependencies
This workflow uses the following Loopstack modules:
@loopstack/common: Base classes, decorators, guards,DocumentStore, andMessageDocument
About
Author: Jakob Klippel
License: MIT
Additional Resources
- Loopstack Documentation
- Getting Started with Loopstack
- Find more Loopstack examples in the Loopstack Registry