Core Concepts
Loopstack is built on a small set of composable building blocks. Understanding these concepts will help you build any workflow.
Workflows
A workflow is a TypeScript state machine. It defines a sequence of states and the transitions between them. Each transition is a decorated method that runs when the workflow moves between states.
@Workflow({
uiConfig: __dirname + '/my.ui.yaml',
schema: z.object({ prompt: z.string() }),
})
export class MyWorkflow extends BaseWorkflow {
@Initial({ to: 'ready' })
async setup(args: { prompt: string }) {
// Entry point — runs when workflow starts
}
@Transition({ from: 'ready', to: 'done' })
async process() {
// Runs automatically after setup
}
@Final({ from: 'done' })
async finish() {
// Terminal state — workflow completes
}
}Key concepts:
@Initial— The entry point. Receives validated input args.@Transition— Moves between states. Can have guards for conditional routing.@Final— Marks completion. Its return value is the workflow output.wait: true— Pauses the workflow until triggered externally (user input, callback).- State — Plain instance properties, automatically persisted across transitions.
Tools
A tool is a reusable unit of logic. Tools extend BaseTool and implement a call() method. They can be injected into workflows or other tools.
@Tool({
uiConfig: { description: 'Fetches weather data.' },
schema: z.object({ city: z.string() }),
})
export class GetWeather extends BaseTool {
async call(args: { city: string }): Promise<ToolResult> {
return { data: { temp: 22, condition: 'sunny' } };
}
}Tools are called directly in workflow transition methods:
@InjectTool() getWeather: GetWeather;
@Transition({ from: 'ready', to: 'done' })
async process() {
const result = await this.getWeather.call({ city: 'Berlin' });
}Documents
A document is a typed data object displayed in the Loopstack Studio UI. Documents have a Zod schema for validation and a YAML config for UI rendering.
@Document({
schema: z.object({ text: z.string() }),
uiConfig: __dirname + '/notes.ui.yaml',
})
export class NotesDocument {
text: string;
}Documents are saved via this.repository.save() inside workflow methods:
await this.repository.save(NotesDocument, { text: 'Hello!' }, { id: 'notes' });Built-in document types include ClaudeMessageDocument, LinkDocument, MarkdownDocument, ErrorDocument, and PlainDocument.
Workspaces
A workspace groups related workflows together and makes them visible in the Studio UI.
@Workspace({
config: { title: 'My Workspace' },
})
export class DefaultWorkspace implements WorkspaceInterface {
@InjectWorkflow() myWorkflow: MyWorkflow;
@InjectWorkflow() chatWorkflow: ChatWorkflow;
}Modules
Loopstack uses NestJS modules to organize your application. Each module groups related workflows, tools, and services.
@Module({
imports: [LoopCoreModule, ClaudeModule],
providers: [DefaultWorkspace, MyWorkflow, GetWeather],
exports: [MyWorkflow, GetWeather],
})
export class MyFeatureModule {}Documents are not listed as providers — they are plain DTOs.
YAML Configuration
YAML files define the UI layout only. A workflow YAML file specifies widgets, form fields, and action buttons:
title: 'My Workflow'
ui:
widgets:
- widget: prompt-input
enabledWhen: [waiting_for_user]
options:
transition: userMessageThe transition value must match the method name of a wait: true transition in your workflow class.
How It All Fits Together
- Define a workflow class with transitions
- Inject tools to perform operations within transitions
- Save documents to display results in the UI
- Register everything in a module
- Add the workflow to a workspace to make it visible in Studio
- Configure YAML for UI layout (optional)
Module
├── Workspace
│ ├── WorkflowA (uses ToolX, ToolY)
│ └── WorkflowB (uses ToolZ)
├── ToolX
├── ToolY
└── ToolZ