Skip to Content
DocumentationBuilding with Loopstack️ WorkflowsCreating Workflows

Creating Workflows

Workflows are the core building blocks of automation in Loopstack. They define a sequence of states and transitions that execute tools to accomplish specific tasks, from simple data processing to complex multi-step business processes.

Creating a Workflow

A workflow is created by decorating a class with @Workflow. Input is defined using the @Input decorator, state using the @State decorator, tools are injected using @InjectTool, and documents using @InjectDocument. The workflow logic is defined in a separate YAML configuration file.

Basic Workflow Definition

import { z } from 'zod'; import { AiGenerateDocument } from '@loopstack/ai-module'; import { InjectDocument, InjectTool, Input, Runtime, State, Workflow } from '@loopstack/common'; import { CreateDocument } from '@loopstack/core-ui-module'; import { MeetingNotesDocument, MeetingNotesDocumentSchema } from './documents/meeting-notes-document'; @Workflow({ configFile: __dirname + '/meeting-notes.workflow.yaml', }) export class MeetingNotesWorkflow { @InjectTool() aiGenerateDocument: AiGenerateDocument; @InjectTool() createDocument: CreateDocument; @InjectDocument() meetingNotesDocument: MeetingNotesDocument; @Input({ schema: z.object({ inputText: z.string().default('Some default input text'), }), }) args: { inputText: string; }; @State({ schema: z.object({ meetingNotes: MeetingNotesDocumentSchema.optional(), }), }) state: { meetingNotes?: z.infer<typeof MeetingNotesDocumentSchema>; }; @Runtime() runtime: any; }

Key Components

@Workflow Decorator

  • Marks a class as a Loopstack workflow
  • Links the workflow class to its YAML configuration via configFile

@Input Decorator

  • Defines the workflow’s input schema using Zod
  • Input properties are accessible in YAML templates via {{ args.propertyName }} or ${{ args.propertyName }}

@State Decorator

  • Defines the workflow’s mutable state schema using Zod
  • State properties are accessible in YAML templates via {{ state.propertyName }} or ${{ state.propertyName }}
  • Populated via assign in YAML transitions to store tool results

@InjectTool Decorator

  • Injects tool dependencies into the workflow
  • Tools decorated with @InjectTool() become available for use in YAML transitions
  • Tool names in YAML correspond to the property names in the class

@InjectDocument Decorator

  • Injects document class dependencies into the workflow
  • Document names in YAML correspond to the property names in the class

@Runtime Decorator

  • Provides access to runtime context such as runtime.transition.payload for manual trigger data

YAML Configuration

  • title and description: Metadata for the workflow
  • ui.form: Defines a form UI for workflow input arguments
  • transitions: Define the flow of states and the tools executed at each step
  • call: Specifies tool calls triggered in a transition
  • assign: Assigns the result to a workflow state property for later use

Defining Input

Input is defined using the @Input decorator with a Zod schema. This defines arguments that are provided when the workflow is executed.

@Input({ schema: z.object({ inputText: z.string().default('default value'), count: z.number().default(1), }), }) args: { inputText: string; count: number; };

Input properties can be accessed in YAML using template syntax:

  • {{ args.inputText }} - Inline template interpolation (within strings)
  • ${{ args.inputText }} - Expression reference (as a standalone value)

You can configure a UI form for the input in the YAML ui.form section:

ui: form: properties: inputText: title: 'Text' widget: 'textarea'

Defining State

State is defined using the @State decorator with a Zod schema. This provides type safety and validation for the workflow’s mutable state.

@State({ schema: z.object({ meetingNotes: MeetingNotesDocumentSchema.optional(), processedData: z.any().optional(), }), }) state: { meetingNotes?: z.infer<typeof MeetingNotesDocumentSchema>; processedData?: any; };

State properties can be accessed in YAML using template syntax:

  • {{ state.meetingNotes.text }} - Inline template interpolation
  • ${{ state.meetingNotes }} - Expression reference
  • ${{ result.data.content }} - Access the result of the current tool call (used in assign)

Injecting Tools

Tools are injected using the @InjectTool decorator. Each tool must be a valid Loopstack tool class.

export class MyWorkflow { @InjectTool() createDocument: CreateDocument; @InjectTool() aiGenerateDocument: AiGenerateDocument; }

The property name becomes the tool identifier used in YAML:

call: - tool: createDocument # Matches @InjectTool() createDocument args: # ...

Injecting Documents

Documents are injected using the @InjectDocument decorator. This makes document classes available for use in YAML transitions, for example when creating or updating documents via the createDocument tool.

export class MyWorkflow { @InjectDocument() meetingNotesDocument: MeetingNotesDocument; @InjectDocument() optimizedNotesDocument: OptimizedNotesDocument; }

The property name becomes the document identifier used in YAML:

call: - tool: createDocument args: document: meetingNotesDocument # Matches @InjectDocument() meetingNotesDocument update: content: text: "Some content"

Template Syntax

Loopstack YAML uses two template syntaxes:

  • {{ expression }} - Jinja-style inline interpolation, used within strings (e.g., in prompt or text fields)
  • ${{ expression }} - Expression references, used as standalone values (e.g., for assign or content)

Available context variables:

  • args - Workflow input arguments
  • state - Workflow mutable state
  • result - Result of the current tool call (used in assign)
  • runtime - Runtime Data including user defined payloads, intermediate tool call results and many more

Transition Configuration

Basic Transition

transitions: - id: my_transition from: state_a to: state_b call: - tool: myTool args: param1: value1 param2: ${{ state.someProperty }}

Manual Triggers

Transitions can be triggered manually by users through UI actions (e.g., buttons on documents):

- id: user_response from: waiting_for_response to: response_received trigger: manual call: - tool: createDocument args: document: meetingNotesDocument update: content: ${{ runtime.transition.payload }} assign: meetingNotes: ${{ result.data.content }}

The runtime.transition.payload provides access to data passed from the UI action.

Assigning Results

Use assign to store tool results in workflow state:

- id: optimize_notes from: response_received to: notes_optimized call: - id: prompt tool: aiGenerateDocument args: llm: provider: openai model: gpt-4o response: id: final document: optimizedNotesDocument prompt: | Extract all information from the provided notes. {{ state.meetingNotes.text }}

Complete Example: Meeting Notes Optimizer

Here’s a complete example of a human-in-the-loop workflow that takes unstructured meeting notes, lets the user review them, then uses AI to produce a structured document.

import { z } from 'zod'; import { AiGenerateDocument } from '@loopstack/ai-module'; import { InjectDocument, InjectTool, Input, Runtime, State, Workflow } from '@loopstack/common'; import { CreateDocument } from '@loopstack/core-ui-module'; import { MeetingNotesDocument, MeetingNotesDocumentSchema } from './documents/meeting-notes-document'; import { OptimizedMeetingNotesDocumentSchema, OptimizedNotesDocument } from './documents/optimized-notes-document'; @Workflow({ configFile: __dirname + '/meeting-notes.workflow.yaml', }) export class MeetingNotesWorkflow { @InjectTool() aiGenerateDocument: AiGenerateDocument; @InjectTool() createDocument: CreateDocument; @InjectDocument() meetingNotesDocument: MeetingNotesDocument; @InjectDocument() optimizedNotesDocument: OptimizedNotesDocument; @Input({ schema: z.object({ inputText: z .string() .default( '- meeting 1.1.2025\n- budget: need 2 cut costs sarah said\n- hire new person?? --> marketing\n- vendor pricing - follow up needed by anna', ), }), }) args: { inputText: string; }; @State({ schema: z.object({ meetingNotes: MeetingNotesDocumentSchema.optional(), optimizedNotes: OptimizedMeetingNotesDocumentSchema.optional(), }), }) state: { meetingNotes?: z.infer<typeof MeetingNotesDocumentSchema>; optimizedNotes?: z.infer<typeof OptimizedMeetingNotesDocumentSchema>; }; @Runtime() runtime: any; }

Registering the Workflow

Add your workflow as a module provider and import it in your workspace to make it available for execution.

@Module({ imports: [LoopCoreModule], providers: [ MeetingNotesWorkflow, MeetingNotesDocument, // ... other providers ], }) export class MyModule {}

Using Your Workflow

Once registered in a workspace, your workflow can be manually executed:

  1. Navigate to your workspace in the Loopstack Studio
  2. Click Run to create a new execution
  3. Select the workflow from the available workflows
Last updated on