Skip to Content
DocumentationRegistryExamplesDynamic Routing Example

@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 priority option
  • 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, and MessageDocument

About

Author: Jakob Klippel 

License: MIT

Additional Resources

Last updated on