Skip to Content
DocumentationGetting StartedCore Concepts

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: userMessage

The transition value must match the method name of a wait: true transition in your workflow class.

How It All Fits Together

  1. Define a workflow class with transitions
  2. Inject tools to perform operations within transitions
  3. Save documents to display results in the UI
  4. Register everything in a module
  5. Add the workflow to a workspace to make it visible in Studio
  6. Configure YAML for UI layout (optional)
Module ├── Workspace │ ├── WorkflowA (uses ToolX, ToolY) │ └── WorkflowB (uses ToolZ) ├── ToolX ├── ToolY └── ToolZ
Last updated on