# Sub-Workflows Execute workflows within other workflows using constructor injection and `.run()`. The parent workflow pauses at a `wait: true` transition until the sub-workflow completes. ## Injecting a Sub-Workflow ```typescript import { CallbackSchema, QueueResult } from '@loopstack/common'; constructor(private readonly subWorkflow: SubWorkflow) { super(); } ``` ## Running a Sub-Workflow ```typescript @Transition({ to: 'sub_started' }) async start(state: MyState): Promise { const result: QueueResult = await this.subWorkflow.run( { prompt: 'Hello' }, // Args passed to the sub-workflow { callback: { transition: 'onSubComplete' } }, // Method to call when done ); // Track with a link document await this.documentStore.save(LinkDocument, { label: 'Running sub-workflow...', workflowId: result.workflowId, }, { id: `link_${result.workflowId}` }); return state; } ``` ## Receiving the Callback The sub-workflow's final transition return value is passed as `payload.data`: ```typescript const SubWorkflowCallbackSchema = CallbackSchema.extend({ data: z.object({ message: z.string() }), }); @Transition({ from: 'sub_started', to: 'sub_done', wait: true, schema: SubWorkflowCallbackSchema, }) async onSubComplete(state: MyState, payload: { workflowId: string; status: string; data: { message: string } }): Promise { // Update the link document await this.documentStore.save(LinkDocument, { label: 'Sub-Workflow', status: 'success', workflowId: payload.workflowId, }, { id: `link_${payload.workflowId}` }); await this.documentStore.save(MessageDocument, { role: 'assistant', content: `Sub-workflow said: ${payload.data.message}`, }); return state; } ``` ## Sub-Workflow Output The sub-workflow defines its output as the return value of its final transition: ```typescript @Workflow({ widget: __dirname + '/sub.ui.yaml' }) export class SubWorkflow extends BaseWorkflow { @Transition({ to: 'end' }) async start(): Promise<{ message: string }> { return { message: 'Hi mom!' }; } } ``` ## Complete Example ```typescript @Workflow({ widget: __dirname + '/parent.ui.yaml' }) export class ParentWorkflow extends BaseWorkflow { constructor(private readonly subWorkflow: SubWorkflow) { super(); } @Transition({ to: 'sub_started' }) async runWorkflow(state: Record): Promise> { const result: QueueResult = await this.subWorkflow.run({}, { callback: { transition: 'subWorkflowCallback' } }); await this.documentStore.save( LinkDocument, { label: 'Executing Sub-Workflow...', workflowId: result.workflowId, }, { id: `link_${result.workflowId}` }, ); return state; } @Transition({ from: 'sub_started', to: 'end', wait: true, schema: CallbackSchema.extend({ data: z.object({ message: z.string() }) }), }) async subWorkflowCallback( state: Record, payload: { workflowId: string; status: string; data: { message: string } }, ): Promise { await this.documentStore.save( LinkDocument, { label: 'Sub-Workflow', status: 'success', workflowId: payload.workflowId, }, { id: `link_${payload.workflowId}` }, ); await this.documentStore.save(MessageDocument, { role: 'assistant', content: `Message from sub-workflow: ${payload.data.message}`, }); return {}; } } ``` ## Registering Sub-Workflows Both workflows must be registered in the module: ```typescript @Module({ providers: [ParentWorkflow, SubWorkflow], exports: [ParentWorkflow, SubWorkflow], }) export class MyModule {} ``` ## Wrapping as a Task Tool A task tool is a `BaseTool` that launches a sub-workflow and returns `pending`. The framework calls `complete()` when the sub-workflow finishes. This lets agents decide when to run sub-workflows. ```typescript @Tool({ name: 'run_tests', description: 'Run tests in the specified directory.', schema: z.object({ testDirectory: z.string().describe('Directory containing the test files to run.'), }), }) export class RunTestsTask extends BaseTool { constructor(private readonly testRunner: TestRunnerWorkflow) { super(); } protected async handle( args: { testDirectory: string }, ctx: LoopstackContext, options?: ToolCallOptions, ): Promise { const result = await this.testRunner.run({ testDirectory: args.testDirectory }, { callback: options?.callback }); await this.documentStore.save( LinkDocument, { status: 'pending', label: 'Running tests...', workflowId: result.workflowId, embed: true }, { id: `test_link_${result.workflowId}` }, ); return { data: { workflowId: result.workflowId }, pending: { workflowId: result.workflowId }, }; } async complete(result: Record): Promise { const data = result as { workflowId?: string; data?: { passed: boolean; output: string } }; await this.documentStore.save( LinkDocument, { status: data.data?.passed ? 'success' : 'failure', label: 'Tests complete', workflowId: data.workflowId! }, { id: `test_link_${data.workflowId}` }, ); return { data: data.data ?? result }; } } ``` Key parts: - **`pending: { workflowId }`** tells the framework this tool is async — the parent workflow waits for a callback - **`callback: options?.callback`** passes the parent's callback config to the sub-workflow - **`complete()`** is called when the sub-workflow finishes — transform results and update UI documents here - **`LinkDocument`** gives visual feedback while the sub-workflow runs ## Nested Agents The sub-workflow can be an `AgentWorkflow` itself, enabling multi-agent architectures. See [Agent Workflows](/docs/build/ai/agent-workflows) for the full pattern. ## Registry References - [run-sub-workflow-example](https://loopstack.ai/registry/loopstack-run-sub-workflow-example) — Parent workflow calling a sub-workflow with callbacks, LinkDocument tracking, and output passing - [@loopstack/code-agent](https://loopstack.ai/registry/loopstack-code-agent) — ExploreTask wrapping AgentWorkflow as a task tool