Skip to content
Color theme

Skill: Use Core Tools

Overview

Core tools and documents ship with the Loopstack framework itself (via @loopstack/core and @loopstack/common). They are available globally through LoopstackModule.forRoot() without installing any registry packages — no extra imports needed in feature modules. Document classes are imported from @loopstack/common.

Sub-Workflow Execution

Inject sub-workflows via the constructor and use .run() to execute them asynchronously. The parent workflow pauses at a wait: true transition until the sub-workflow completes and triggers the callback.

Inject in workflow

import { QueueResult } from '@loopstack/common';
constructor(private readonly subWorkflow: SubWorkflow) { super(); }

Run the sub-workflow

const result: QueueResult = await this.subWorkflow.run(
{ prompt: 'Hello' }, // args passed to the sub-workflow
{
callback: { transition: 'onSubComplete' }, // method name of the wait transition
show: 'inline', // 'inline' (default) | 'link' | 'hidden' — how the child appears in the parent's view
label: 'Running sub-workflow', // optional override (defaults to the child workflow's name)
},
);
// result.workflowId — the ID of the spawned sub-workflow

Receive the callback

import type { TransitionInput } from '@loopstack/common';
const SubWorkflowMessageSchema = z.object({ message: z.string() });
@Transition({
from: 'sub_started',
to: 'sub_done',
wait: true,
schema: SubWorkflowMessageSchema,
})
onSubComplete(state: MyState, input: TransitionInput<{ message: string }>) {
// input.workflowId — the sub-workflow's ID
// input.status — 'completed' | 'failed' | 'canceled'
// input.hasError / input.errorMessage — populated if the child failed
// input.data — the sub-workflow's published result (validated against schema)
const message = input.data.message;
}

Sub-workflow output

The sub-workflow’s published result (written via this.assignResult(...) / this.setResult(...)) is passed as input.data in the parent’s callback:

// In the sub-workflow:
@Transition({ from: 'done', to: 'end' })
finish(state: SubState) {
this.setResult({ message: 'Hi mom!' });
}

In the parent, validate the data shape via the schema: option on the wait transition — it describes input.data only; the framework constructs the surrounding envelope.

Rendering in the Parent’s View

The show option on .run() controls how the child appears inside the parent’s run view:

  • 'inline' (default) — child is embedded as an inline iframe. Best for HITL / OAuth / interactive children.
  • 'link' — status link card, opens the child in a separate window. Best for autonomous children the parent just tracks.
  • 'hidden' — no card at all. Best for background fan-out.

The orchestrator auto-creates the corresponding LinkDocument for 'inline' and 'link'; the card’s status is derived live from the child workflow’s actual state — no manual updates needed.

Built-in Document Types

These document types are available from @loopstack/common without additional imports:

Document Description Key Fields
MessageDocument UI-only chat message role, text
MarkdownDocument Rendered markdown markdown
PlainDocument Plain text text
ErrorDocument Error message (red styling) error
LinkDocument Status card / iframe link to sub-workflows (auto-saved by run()) label, workflowId, embed, expanded

Usage

import { MessageDocument } from '@loopstack/common';
await this.documentStore.save(MessageDocument, {
role: 'assistant',
text: 'Hello! How can I help?',
});

Requirements

  • The parent workflow must be stateful (the default). Sub-workflow execution requires state persistence.
  • The target workflow must be registered as a provider in an imported module.
  • The callback transition value must match the method name of a wait: true transition.