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-workflowReceive 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
transitionvalue must match the method name of await: truetransition.