Build full-stack AI systems with NestJS
Loopstack is a TypeScript framework for stateful agents and AI workflows — engineered for reliability, scale, and human control.
@Module({
imports: [LoopstackModule.forRoot(options)],
providers: [MyAgent, SearchTool],
})
export class AppModule {}What You Can Build
Agent harnesses with tool calling, context and message history
Stateful automations that chain tools, transform data, and route dynamically
Compose complex systems by spawning nested agents and workflows
Run code and access files in isolated sandboxed environments
Pause workflows for human review, input, or confirmation
...built directly into your NestJS backends.
Getting Started
Drop this into Claude Code, Cursor, or any coding agent.
Set up a loopstack.ai project for me and show me how to build my first AI workflowHow It Works
Compose any AI agent from modular building blocks
@Workflow({ schema: TriageSchema })
export class TriageWorkflow extends BaseWorkflow {
// inject tools here
@Transition({ from: 'start', to: 'classified' })
async classify(state, ctx) {
const result = await this.llm.call({
outputSchema: ClassificationSchema,
prompt: ctx.args.ticket,
});
this.assignState({ classification: result.data });
}
@Transition({ from: 'classified', to: 'end' })
async notifyTeam(state) {
await this.notify.call(state.classification);
}
}Workflows
Workflows are TypeScript state machines with explicit states and transitions. Inject tools, call LLMs, save documents — each step is checkpointed. If something fails, resume from exactly where you left off.
Learn moreRun and Interact in the browser
Use the built-in ReactJS frontend for running, debugging and organizing automations.

Key Benefits
Drop an LLM call into any point in a deterministic workflow, or nest agents inside structured pipelines. One system, not two bolted together.
Approvals, forms, confirmations, and clarifications ship as framework primitives. Pause for human input for hours or days and resume cleanly.
Extend it like any NestJS app — add your own modules, providers, and entities. No vendor lock-in, no external execution engine. Free and open source under the MIT license.
Every state transition, tool call, and LLM decision is recorded. State is checkpointed so failures resume cleanly. Explicit state machines make workflows auditable and easy to reason about.
See It In Action
Agents With Full Control
Use a ready made agent or create your own flow. Same system, just the right level of abstraction.
Call an Agent
await this.agent.run(
{
system: 'Review the codebase and write a summary.',
tools: ['read', 'glob', 'grep'],
userMessage: 'Focus on the auth module.',
},
{ callback: { transition: 'agentDone' } },
);Build Your Own
// 1. LLM turn
@Transition({ from: 'ready', to: 'prompt_executed' })
async llmTurn(state) {
const result = await this.llmGenerateText.call({}, {
config: { system: '…', tools: ['read', 'grep'] },
});
this.assignState({ llmResult: result.data });
}
// 2. Dispatch tool calls, then loop back
@Transition({ from: 'prompt_executed', to: 'awaiting_tools' })
@Guard('hasToolCalls')
async dispatchTools(state) {
await this.llmDelegateToolCalls.call({
message: state.llmResult.message,
callback: { transition: 'toolResult' },
});
}
// 3. Resume on each tool result, then loop back to LLM
@Transition({ from: 'awaiting_tools', to: 'ready', wait: true })
toolResult(state, input) {}
// 4. LLM returned a final answer — done
@Transition({ from: 'prompt_executed', to: 'end' })
@Guard('isDone')
respond(state) {}Tool calling, error recovery, and cancellation built in. Need custom exit logic, setup phases, or human interaction? Copy the agent and make it yours.
Nested Agents and -Workflows
Let agents launch sub-agents: Just wrap them in tools.
1. Define a workflow
@Workflow({ title: 'Test Runner', schema: TestRunnerSchema })
export class TestRunnerWorkflow extends BaseWorkflow {
// runs tests, sets result via assignResult
}2. Wrap it as a tool
@Tool({ name: 'run_tests', schema: RunTestsSchema })
export class RunTestsTool extends BaseTool {
// inject workflow via constructor
async handle(args, ctx, options) {
const result = await this.testRunner.run(
args, { callback: options?.callback },
);
return {
data: { workflowId: result.workflowId },
pending: { workflowId: result.workflowId },
};
}
}3. Let the agent use it
await this.llmGenerateText.call({}, {
config: {
system: 'You are a build agent.',
tools: ['read', 'write', 'run_tests'],
},
});The LLM decides when to launch sub-workflows. Each one runs in the background and reports back. Nest workflows as you need.
Built-In Error Recovery
Auto-retry, timeout, and custom error states — for sync throws and sub-workflow failures alike.
Auto-Retry
@Transition({
from: 'fetching',
to: 'done',
retryAttempts: 3,
})
async fetchData(state) {
await this.http.call({ url });
}Retries 3 times with exponential backoff. State rolls back between attempts.
Timeout
@Transition({
from: 'analyzing',
to: 'done',
timeout: 5000,
})
async analyze(state) {
await this.analyzer.call({ data });
}Kills the transition after 5s. Combine with retry to auto-retry on timeout.
Error States
@Transition({
from: 'deploying',
to: 'deployed',
errorPlace: 'deploy_failed',
})
async deploy(state) {
await this.deployer.call({});
}Routes to a custom error state with recovery transitions. Also routes sub-workflow failure callbacks.
Documents and state roll back automatically on failure. Every error is recorded as an audit trail. Manual retry is always available as a fallback.
Human-in-the-Loop
Pause for human input. Resume hours or days later. State is always preserved.
Ask a Question
@Transition({ from: 'start', to: 'waiting' })
async showQuestion(state, ctx) {
await this.documentStore.save(
AskUserDocument,
{ question: ctx.args.question },
);
}
// pauses until user responds
@Transition({ from: 'waiting', to: 'end', wait: true })
userAnswered(state, input) {
this.assignResult({ answer: input.data.answer });
}Render the UI
widget: choices
options:
transition: userAnsweredDocuments define the data, YAML configures how it renders. The workflow sleeps until the user responds — no polling, no timeouts, no lost state.
Configurable UI
Documents define your data. A YAML config controls how they render - choices, forms, buttons, markdown. No frontend code needed.
The workflow renders the UI, pauses execution, the user responds, and the workflow continues.
widget: choices
options:
transition: userAnswered
Ready to Build?
Drop Loopstack into any NestJS app. Docker environment included.
npm install @loopstack/loopstack-moduleSupported by:
