Skip to Content

Features

Everything you need to build, operate, and trust durable AI workflows — from human-in-the-loop to testing, durability, and observability.

Human-in-the-Loop

Approvals, forms, and confirmations are first-class workflow steps. Your run pauses for a real person, then continues the moment they respond.

Waiting for approval

Escalate ticket #42 to on-call engineering? Severity was classified high.

Approvals, forms & confirmations Answer from Studio or the terminal Pause for minutes or days

Testing, Built In

Test workflows like any other code — unit tests for tools, end-to-end tests for whole runs. Recorded responses stand in for live LLM calls, simulated answers stand in for the human. Fast, deterministic, CI-ready.

triage-ticket.spec.ts
const run = await runWorkflow(TriageTicketWorkflow, { ticket: 1042 }, {
  fixture: 'triage.fixture.json', // recorded tool responses
  answers: { approve: { approved: true } }, // scripted human
});

expect(run.path).toEqual(['classify', 'report', 'approve']);
expect(run.result).toMatchObject({ severity: 'high' });
Unit & End-to-End Tests Record real runs, replay in CI Scripted humans for HITL flows

Never Lose a Run

Kill the process mid-run — the run survives. Every step is checkpointed to Postgres, so a restart reclaims the job and resumes from the last checkpoint. No lost work, no manual cleanup.

Server

~ backend
$ node dist/main
LOG Nest application started
DEBUG applying transition: analyze_ticket
LOG ⚒ fetch_ticket {"id":1042}
✗ process killed (SIGKILL)
$ node dist/main
LOG Nest application started
WARN job 1c766091 stalled — reclaiming
DEBUG applying transition: analyze_ticket
LOG ✓ job 1c766091 completed

Terminal

~ loopstack
$ loopstack run triage-ticket --arg ticket=1042
▸ run 9f2e41c8 started
▸ analyze_ticket
✓ analyze_ticket (0.3s)
■ run completed
Survives crashes & deploys Resumes from the last checkpoint HITL pauses last indefinitely

Inspect Every Run

Every run records what happened — each step with its duration, every tool call and document. Inspect runs live or afterwards, track token usage, and enforce quotas.

~ loopstack
$ loopstack runs 9f2e41c8
triage-ticket #42completed
started 8/6/2026, 9:14 AM 9f2e41c8-c3d4
analyze_ticket (2.4s)
waiting_for_approval (37m 12s)
notify_engineering (490ms)
assistant:
Auth latency spike after the 2.4.1 rollout —
affects EU tenants. Severity: high.
escalated:
true
Every transition, tool call & document traced Token usage & cost quotas Audit events & tool interceptors

Code Examples

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

orchestrator.workflow.ts
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

review-agent.workflow.ts
// 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

test-runner.workflow.ts
@Workflow({ title: 'Test Runner', schema: TestRunnerSchema })
export class TestRunnerWorkflow extends BaseWorkflow {
  // runs tests, sets result via assignResult
}

2. Wrap it as a tool

run-tests.tool.ts
@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

build-agent.workflow.ts
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

workflow.ts
@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

workflow.ts
@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

workflow.ts
@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.

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.

ask-user-options.ui.yaml
widget: choices
options:
  transition: userAnswered
Rendered choices widget in Loopstack Studio

Community Registry

Browse packages on npm — modules, tools, and workflow examples for your Loopstack app.

Terminal
$ npm install @loopstack/agent

Ready to Build?

Start a new app or drop Loopstack into your existing NestJS project.

Read loopstack.ai/llms.txt, then help me build a Loopstack project and my first AI workflow

Works in Claude Code, Cursor, or any coding agent.