@loopstack/run-sub-workflow-example
v0.24.0
Published
Execute child workflows from within a parent workflow for hierarchical workflow composition
Maintainers
Readme
title: Sub-Workflow Example description: Example executing child workflows from a parent workflow — workflow.run(), hierarchical workflow composition, callback transitions, the show option for parent-view rendering (inline embed, link, hidden), and graceful handling of failed sub-workflows
@loopstack/run-sub-workflow-example
A module for the Loopstack AI automation framework.
This module provides an example demonstrating how to execute child workflows from within a parent workflow for hierarchical workflow composition.
Overview
The Run Sub Workflow Example shows how to build workflows that spawn and manage child workflows asynchronously. It demonstrates a parent workflow that starts sub-workflows, tracks their completion through a callback mechanism, and receives result data from the child workflows.
By using this example as a reference, you'll learn how to:
- Use constructor injection and
.run()to start child workflows asynchronously - Set up callback transitions to handle sub-workflow completion
- Define callback schemas with
CallbackSchema.extend()to type callback payloads - Return structured data from sub-workflows via the return value of the final
@Transitionmethod - Control how a child appears in the parent's run view via the
showoption ('inline','link','hidden') - Handle failed sub-workflows in the parent via the
statusfield on the callback payload - Compose complex workflows from smaller, reusable workflow components
- Chain multiple sequential sub-workflow executions
- Run sub-workflows in parallel (
FanOutWorkflow) or in series (SequenceWorkflow)
This example is essential for developers building workflows that need to orchestrate multiple workflow executions or break down complex processes into manageable sub-workflows.
Installation
npm install @loopstack/run-sub-workflow-exampleThen register the module in your app:
import { StudioApp } from '@loopstack/common';
import { RunSubWorkflowExampleModule, RunSubWorkflowExampleParentWorkflow } from '@loopstack/run-sub-workflow-example';
@StudioApp({
title: 'Sub-Workflow Example',
workflows: [
RunSubWorkflowExampleParentWorkflow,
RunSubWorkflowExampleFanOutWorkflow,
RunSubWorkflowExampleSequenceWorkflow,
RunSubWorkflowExampleShowModesWorkflow,
RunSubWorkflowExampleErrorHandlingWorkflow,
],
})
@Module({
imports: [RunSubWorkflowExampleModule],
})
export class MyAppModule {}How It Works
Workflows
Parent Workflow
The parent workflow injects the sub-workflow class and calls .run() to start it:
@Workflow({ uiConfig: __dirname + '/run-sub-workflow-example-parent.ui.yaml' })
export class RunSubWorkflowExampleParentWorkflow extends BaseWorkflow {
constructor(private readonly subWorkflow: RunSubWorkflowExampleSubWorkflow) {
super();
}
}Sub-Workflow
The sub-workflow returns structured data from its final @Transition method. This data is delivered to the parent workflow via the callback payload:
@Workflow({ uiConfig: __dirname + '/run-sub-workflow-example-sub.ui.yaml' })
export class RunSubWorkflowExampleSubWorkflow extends BaseWorkflow {
@Transition({ to: 'end' })
async message(): Promise<{ message: string }> {
await this.documentStore.save(MessageDocument, {
role: 'assistant',
text: 'Sub workflow completed.',
});
return { message: 'Hi mom!' };
}
}Key Concepts
1. Running a Sub-Workflow with Callbacks
Use .run() on the injected workflow to start it asynchronously. Pass a callback option specifying which transition to trigger when the sub-workflow completes, and a show option to control how the child appears in the parent's run view:
@Transition({ to: 'sub_workflow_started' })
async runWorkflow() {
await this.subWorkflow.run(
{},
{ callback: { transition: 'subWorkflowCallback' }, show: 'link', label: 'Sub-Workflow' },
);
}2. The show Option
show controls how the child sub-workflow is rendered inside the parent's run view:
'inline'(default) — embed the child as an inline iframe in the parent's view. Best for HITL/OAuth/interactive children.'link'— show a status link card; clicking opens the child in a separate window. Best for autonomous children the parent just tracks.'hidden'— no UI at all. Best for background fan-out.
This example uses show: 'link' because the sub-workflows run autonomously without needing user interaction.
3. Defining Callback Schemas
Extend CallbackSchema to type the callback payload. The base schema includes workflowId, and you add your custom data shape:
const SubWorkflowCallbackSchema = CallbackSchema.extend({
data: z.object({ message: z.string() }),
});
type SubWorkflowCallback = z.infer<typeof SubWorkflowCallbackSchema>;4. Handling the Callback
Define a transition with wait: true and the callback schema. The payload contains both the workflowId and the data returned by the sub-workflow:
@Transition({ from: 'sub_workflow_started', to: 'sub_workflow_ended', wait: true, schema: SubWorkflowCallbackSchema })
async subWorkflowCallback(payload: SubWorkflowCallback) {
await this.documentStore.save(MessageDocument, {
role: 'assistant',
text: `A message from sub workflow 1: ${payload.data.message}`,
});
}5. Demonstrating Every show Mode (RunSubWorkflowExampleShowModesWorkflow)
This workflow chains the same sub-workflow three times — once per show mode — so you can see (or not see) each mode side-by-side in a single run:
await this.sub.run({}, { callback: { transition: 'onInlineDone' }, show: 'inline', label: 'Inline embed (iframe)' });
// ...later...
await this.sub.run({}, { callback: { transition: 'onLinkDone' }, show: 'link', label: 'Status link card' });
// ...later...
await this.sub.run({}, { callback: { transition: 'onHiddenDone' }, show: 'hidden', label: 'Background child' });show: 'inline'— Studio renders the child as an embedded iframe inside the parent's stream and auto-collapses it once the child reaches a terminal state.show: 'link'— Studio renders a status link card; clicking opens the child in a separate window.show: 'hidden'— no UI is rendered for the child but the callback still fires normally. Useful when surfacing the child would just be noise.
6. Handling Failed Sub-Workflows (RunSubWorkflowExampleErrorHandlingWorkflow)
When a child throws, the parent's callback is still triggered — with payload.status === 'failed'. The parent decides whether to recover, retry, or terminate:
@Transition({ to: 'awaiting' })
async launch() {
await this.failingSub.run(
{},
{ callback: { transition: 'onFinished' }, show: 'inline', label: 'Failing sub-workflow' },
);
}
@Transition({ from: 'awaiting', to: 'end', wait: true, schema: CallbackSchema })
async onFinished(state, payload) {
await this.documentStore.save(MessageDocument, {
role: 'assistant',
text: `Child finished with status="${payload.status}". The parent recovered gracefully.`,
});
return { childStatus: payload.status };
}In Studio, the failed child renders with a red link icon and an inline error message under the link card, while the embedded iframe collapses automatically.
7. Running Sub-Workflows in Parallel or Series
For dynamic fan-out and ordered sequencing, use the dedicated helpers from @loopstack/core:
FanOutWorkflow(seeRunSubWorkflowExampleFanOutWorkflow) — launches a map of children concurrently and aggregates their results into a single callback.SequenceWorkflow(seeRunSubWorkflowExampleSequenceWorkflow) — runs children one at a time and aggregates their results.
Both surface a single callback payload with hasErrors / errorCount plus per-child results, so the parent doesn't need a transition per child.
8. Chaining Multiple Sub-Workflows
The parent workflow demonstrates running two sub-workflows sequentially. After the first callback completes, a second sub-workflow is started with its own callback. The second callback uses a terminal @Transition to end the parent workflow:
@Transition({ from: 'sub_workflow_ended', to: 'sub_workflow2_started' })
async runWorkflow2() {
await this.subWorkflow.run(
{},
{ callback: { transition: 'subWorkflow2Callback' }, show: 'link', label: 'Sub-Workflow 2' },
);
}
@Transition({ from: 'sub_workflow2_started', to: 'end', wait: true, schema: SubWorkflowCallbackSchema })
async subWorkflow2Callback(payload: SubWorkflowCallback) {
await this.documentStore.save(MessageDocument, {
role: 'assistant',
text: `A message from sub workflow 2: ${payload.data.message}`,
});
}Dependencies
This workflow uses the following Loopstack modules:
@loopstack/common- Core workflow/runtime APIs (BaseWorkflow,@Workflow,@Transition,CallbackSchema,MessageDocument)
About
Author: Jakob Klippel
License: MIT
Additional Resources
- Loopstack Documentation
- Getting Started with Loopstack
- Find more Loopstack examples in the Loopstack Registry
