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
TypeScript
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
assignin 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.payloadfor manual trigger data
YAML Configuration
titleanddescription: Metadata for the workflowui.form: Defines a form UI for workflow input argumentstransitions: Define the flow of states and the tools executed at each stepcall: Specifies tool calls triggered in a transitionassign: 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 inassign)
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., inpromptortextfields)${{ expression }}- Expression references, used as standalone values (e.g., forassignorcontent)
Available context variables:
args- Workflow input argumentsstate- Workflow mutable stateresult- Result of the current tool call (used inassign)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.
meeting-notes.workflow.ts
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.
my-module.ts
@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:
- Navigate to your workspace in the Loopstack Studio
- Click Run to create a new execution
- Select the workflow from the available workflows