Skip to Content

API: @loopstack/common

Classes

BaseTool

Abstract base class for tools.

Tool authors extend this class and implement handle(args, ctx, options?). Consumers call tool.call(args, options?) — the base class routes through ToolPipelineService which handles validation, config merging, and interceptors.

import { BaseTool } from '@loopstack/common';
export abstract class BaseTool< TArgs extends object = object, TConfig extends object = object, TResult = unknown, TMeta = Record<string, unknown>, > { protected readonly documentStore: DocumentStore; protected readonly render: TemplateRenderFn; call(args?: TArgs, options?: ToolCallOptions<TConfig>): Promise<ToolResult<TResult, TMeta>>; protected abstract handle( args: TArgs, ctx: RunContext, options?: ToolCallOptions<TConfig>, ): Promise<ToolEnvelope<TResult, TMeta>>; complete(_result: Record<string, unknown>): Promise<ToolEnvelope>; }

BaseWorkflow

Abstract base class for workflows.

Generic parameters:

  • TArgs — the storage contract: what is validated, persisted, and arrives in ctx.args inside transitions (via ctx: RunContext<TArgs>).
  • TInput — the call-site contract: what callers pass to run(input). Defaults to TArgs, so workflows whose call shape equals their storage shape only need a single generic.

The two diverge when the workflow’s @Workflow({ schema }) is a transforming zod schema (schema.transform(input => args)). In that case, infer the types from the schema: BaseWorkflow<z.output<typeof Schema>, z.input<typeof Schema>>.

State and result are mutated via setters, never via the return value:

  • this.assignState(partial) — shallow-merge into state
  • this.setState(full) — replace state
  • this.assignResult(partial) — shallow-merge into result
  • this.setResult(full) — replace result

Transitions return nothing. Returning a non-undefined value throws at runtime. Use async only when the body awaits. Setters are immediately visible to subsequent code in the same transition; on error, the framework discards the draft as part of the transition rollback.

Workflows are singletons. State flows through the state parameter:

  • All transitions receive (state, ctx) and return nothing
  • Wait transitions receive (state, payload, ctx) and return nothing
  • ctx is optional (trailing param can be omitted)
  • Args are available via ctx.args — type with RunContext<TArgs> to drop the cast
  • Use from: 'start' (or omit from) for initial, to: 'end' for final

Launch sub-workflows by injecting the workflow class and calling run() with a callback.transition that resumes the parent when the child completes.

import { BaseWorkflow } from '@loopstack/common';
export abstract class BaseWorkflow<TArgs = Record<string, unknown>, TInput = TArgs> { protected readonly documentStore: DocumentStore; protected readonly render: TemplateRenderFn; run(input?: TInput, options?: RunOptions): Promise<QueueResult>; protected assignState(partial: Record<string, unknown>): void; protected setState(next: Record<string, unknown>): void; protected assignResult(partial: Record<string, unknown>): void; protected setResult(next: Record<string, unknown>): void; }

ErrorDocument

Document that renders an error message in Studio.

import { ErrorDocument } from '@loopstack/common';
export class ErrorDocument { error: string; }

MarkdownDocument

Document that renders Markdown content in Studio.

import { MarkdownDocument } from '@loopstack/common';
export class MarkdownDocument { markdown: string; }

MessageDocument

Document that renders a chat message (role plus optional text) in Studio.

import { MessageDocument } from '@loopstack/common';
export class MessageDocument { role: string; text?: string; }

PlainDocument

Document that renders plain text in Studio.

import { PlainDocument } from '@loopstack/common';
export class PlainDocument { text: string; }

ServerTool

Abstract base class for server-side tools — tools executed by the LLM provider rather than locally by the framework.

Server tools (e.g. Anthropic’s web_search, code_execution) are configured at API call time and the provider handles execution. The framework passes the validated config from toServerToolConfig directly to the provider.

Use the standard @Tool() decorator for metadata and constructor injection. Configuration from options.config is validated against the configSchema and passed as the config argument to toServerToolConfig.

import { ServerTool } from '@loopstack/common';
export abstract class ServerTool<TConfig extends object = object> { abstract toServerToolConfig(config?: TConfig): unknown; }

Interfaces

Clock

The framework’s time source — inject via the CLOCK token instead of calling Date.now() or setTimeout directly, so time-dependent logic stays deterministic under a test clock.

import { Clock } from '@loopstack/common';
export interface Clock { now(): number; schedule(fn: () => void, ms: number): () => void; }

DocumentOptions

Options for the @Document() decorator.

import { DocumentOptions } from '@loopstack/common';
export interface DocumentOptions { name?: string; title?: string; description?: string; widget?: WidgetRef | WidgetRef[]; schema?: z.ZodType; tags?: string[]; meta?: StaticDocumentMeta; internal?: boolean; }

DocumentStore

Document store for workflow and tool authors — create, save, and lookup of typed documents that render in Studio.

Available as this.documentStore on BaseWorkflow and BaseTool (no manual injection needed).

import { DocumentStore } from '@loopstack/common';
export interface DocumentStore { create<T extends object>(documentClass: DocumentClass<T>, data: T): T; save<T extends object>( classOrInstance: DocumentClass<T> | T, dataOrOptions?: T | DocumentSaveOptions, maybeOptions?: DocumentSaveOptions, ): Promise<DocumentEntity>; findAll<T extends object>(documentClass: DocumentClass<T>): T[]; findAllDocuments(): DocumentEntity[]; findByTag(tag: string): DocumentEntity[]; }

RunContext

Unified per-job framework context.

Used by both tools (passed as ctx parameter to handle()) and workflows (passed as the trailing parameter to transition methods).

  • args — validated workflow input args (frozen at job start)
  • execution — present in workflow transitions, absent in tools
import { RunContext } from '@loopstack/common';
export interface RunContext<TArgs = unknown> { userId: string; workspaceId: string; workflowId: string; args: TArgs; signal: AbortSignal; execution?: { place: string; retryCount: number; }; }

RunResult

Result of WorkflowRunner.run — a workflow enqueued via BullMQ.

import { RunResult } from '@loopstack/common';
export interface RunResult { workflowId: string; workspaceId: string; workerId: string; }

StatelessRunResult

Result of a stateless WorkflowRunner.runSync (no persistence) — the final status and published result, plus the in-memory run record: place, the run trace, produced documents, currently availableTransitions, and error info.

import { StatelessRunResult } from '@loopstack/common';
export interface StatelessRunResult { status: WorkflowState; result: unknown; place: string; trace: RunTraceEvent[]; documents: DocumentEntity[]; availableTransitions: WorkflowTransitionType[]; hasError: boolean; errorMessage?: string; }

StudioAppOptions

Options for the @StudioApp() decorator — the app identity, title, workflows, and UI config that make a module appear as a launchable app in Studio.

import { StudioAppOptions } from '@loopstack/common';
export interface StudioAppOptions { app?: string; title: string; description?: string; ui?: StudioUiConfig; workflows?: Type<BaseWorkflow<any>>[]; }

SyncRunResult

Result of WorkflowRunner.runSync with persistence — the final status and published result.

import { SyncRunResult } from '@loopstack/common';
export interface SyncRunResult extends RunResult { status: WorkflowState; result: unknown; }

ToolCallOptions

Options passed as the second argument to BaseTool.call().

import { ToolCallOptions } from '@loopstack/common';
export interface ToolCallOptions<TConfig = object> { callback?: { transition: string; metadata?: Record<string, unknown>; }; config?: TConfig; }

ToolDocumentDeclaration

A document a tool declares as part of its result envelope instead of writing it inside handle(). The tool pipeline applies declarations through the document store after the interceptor chain, so declared documents appear identically for live and replayed tool calls.

documentName is the document’s registered name (the @Document({ name }) option, or the kebab-case derivation of the class name). options mirror DocumentSaveOptions.

import { ToolDocumentDeclaration } from '@loopstack/common';
export interface ToolDocumentDeclaration { documentName: string; content: Record<string, unknown>; options?: { key?: string; meta?: Record<string, unknown>; validate?: 'strict' | 'safe' | 'skip'; }; }

ToolOptions

Options for the @Tool() decorator.

import { ToolOptions } from '@loopstack/common';
export interface ToolOptions { name?: string; description?: string; widget?: WidgetRef | WidgetRef[]; schema?: z.ZodType; configSchema?: z.ZodType; resultSchema?: z.ZodType; effects?: 'none' | 'external'; }

ToolResult

Narrowed success-path return of BaseTool.call().

data and metadata are non-optional — call() throws when the underlying envelope carries error or pending, so workflow authors never observe those states from this API.

import { ToolResult } from '@loopstack/common';
export interface ToolResult<TData = unknown, TMeta = Record<string, unknown>> { data: TData; metadata: TMeta; type?: 'text' | 'image' | 'file'; }

TransitionInput

Shape delivered to every @Transition({ wait: true }) method, regardless of trigger source (sub-workflow completion or frontend / API resume).

The schema option on @Transition validates data only — the framework constructs the surrounding envelope. meta carries whatever the parent passed to callback.metadata (used by FanOut/Sequence for per-child correlation, by the LLM tool loop for toolUseId, etc.); it is undefined for user-driven resumes.

hasError / errorMessage / status reflect the terminal state of the trigger so the receiver can branch on failure without separate lookups.

import { TransitionInput } from '@loopstack/common';
export interface TransitionInput<TData = unknown, TMeta = unknown> { workflowId: string; status: 'completed' | 'failed' | 'canceled'; hasError: boolean; errorMessage: string | null; data: TData; meta?: TMeta; }

TransitionOptions

Options for the @Transition() decorator — routing (from/to), wait, priority, the payload/args schema, and the retry / error-place / timeout controls.

import { TransitionOptions } from '@loopstack/common';
export interface TransitionOptions { from?: string; to: string; wait?: boolean; priority?: number; schema?: z.ZodType; retryAttempts?: number; retryDelay?: number; retryBackoff?: 'fixed' | 'exponential'; retryMaxDelay?: number; retryTarget?: string; errorPlace?: string; timeout?: number; }

WorkflowOptions

Options for the @Workflow() decorator.

import { WorkflowOptions } from '@loopstack/common';
export interface WorkflowOptions { name?: string; title?: string; description?: string; widget?: WidgetRef | WidgetRef[]; schema?: z.ZodType; configSchema?: z.ZodType; stateSchema?: z.ZodType; }

WorkflowRunnerOptions

Options for starting a workflow via WorkflowRunner (run / runSync) — userId, appName, and optional workspaceId / labels.

import { WorkflowRunnerOptions } from '@loopstack/common';
export interface WorkflowRunnerOptions { userId: string; appName: string; workspaceId?: string; labels?: string[]; }

WorkflowRunnerSyncOptions

Options for WorkflowRunner.runSync — extends WorkflowRunnerOptions with stateless to skip all DB persistence.

import { WorkflowRunnerSyncOptions } from '@loopstack/common';
export interface WorkflowRunnerSyncOptions extends WorkflowRunnerOptions { stateless?: boolean; }

Type Aliases

ToolEnvelope

Raw envelope returned by BaseTool.handle() and ToolPipeline.execute().

Models all three legitimate tool outcomes:

  • success: data (+ optional metadata, type)
  • recoverable failure: error (read by the LLM agent tool-call loop and packaged as is_error: true)
  • async pending: pending (the tool launched a sub-workflow; result arrives via callback)

Tool authors return this shape from handle(). Consumers go through BaseTool.call(), which returns the narrowed ToolResult and throws on error / pending.

import { ToolEnvelope } from '@loopstack/common';
export type ToolEnvelope<TData = unknown, TMeta = Record<string, unknown>> = { type?: 'text' | 'image' | 'file'; data?: TData; error?: string; metadata?: TMeta; pending?: { workflowId: string; }; documents?: ToolDocumentDeclaration[]; };

WorkflowArgs

Extracts the TArgs type from a BaseWorkflow subclass — the args shape you pass to WorkflowRunner.run(WorkflowClass, args, …).

import { WorkflowArgs } from '@loopstack/common';
export type WorkflowArgs<W> = W extends BaseWorkflow<infer A> ? A : never;

Functions

Document

Marks a class as a Document DTO.

Unlike @Tool and @Workflow, documents are NOT injectable NestJS providers — they are plain data classes. Their schema, widget, and meta are read from decorator metadata when documents are saved via documentStore.save(DocClass, …).

Identifier resolution: options.name ?? class name with Document suffix stripped, snake_cased (e.g. AskUserDocument → ask_user).

import { Document } from '@loopstack/common';
export function Document(options?: DocumentOptions): ClassDecorator;

Guard

Gates a transition behind a guard predicate method.

guardMethodName names a pure, side-effect-free method on the workflow that returns a boolean. When several transitions share a from place, the highest-priority transition whose guard returns true is the one that fires.

import { Guard } from '@loopstack/common';
export function Guard(guardMethodName: string): MethodDecorator;

StudioApp

Marks a NestJS module as a Loopstack App — the single source of truth for app identity, UI config, and developer-defined settings.

import { StudioApp } from '@loopstack/common';
export function StudioApp(options: StudioAppOptions): ClassDecorator;

Tool

Marks a class as a Loopstack tool. The class must extend BaseTool.

Also applies @Injectable() so the tool can be registered as a NestJS provider and constructor-injected into workflows and other tools.

Identifier resolution: options.name ?? class name (as-is). Always set options.name to a snake_case identifier — it appears in the LLM tool-calling wire format.

Relative widget: paths (./foo.ui.yaml, ../shared/foo.ui.yaml) are resolved against this file’s directory at decorator-evaluation time.

import { Tool } from '@loopstack/common';
export function Tool(options?: ToolOptions): ClassDecorator;

Transition

Marks a workflow method as a transition between two states (places).

The method fires when the workflow is in its from place and moves it to to. from defaults to 'start' (the initial transition); use to: 'end' for the final transition. Set wait: true to pause until an external trigger (user input or a sub-workflow callback) resumes the workflow. Transitions mutate state via setters and return nothing.

import { Transition } from '@loopstack/common';
export function Transition(options: TransitionOptions): MethodDecorator;

Workflow

Marks a class as a Loopstack workflow. The class must extend BaseWorkflow.

Also applies @Injectable() so the workflow can be registered as a NestJS provider and constructor-injected (tools, sub-workflows, services).

Identifier resolution: options.name ?? class name with Workflow suffix stripped, snake_cased (e.g. ChatWorkflow → chat).

Relative widget: paths (./foo.ui.yaml, ../shared/foo.ui.yaml) are resolved against this file’s directory at decorator-evaluation time.

import { Workflow } from '@loopstack/common';
export function Workflow(options?: WorkflowOptions): ClassDecorator;
Last updated on