Skip to Content
DocumentationFeaturesSub-Workflow Tasks

Sub-Workflow Tasks

Launch workflows from other workflows. Wrap them as tools so agents can decide when to run them.

Define a Sub-Workflow

Any workflow can be launched as a sub-workflow. It just needs a schema for its input and a callback-compatible return value:

@Workflow({ uiConfig: __dirname + '/test-runner.ui.yaml', schema: z.object({ testDirectory: z.string(), }), }) export class TestRunnerWorkflow extends BaseWorkflow { @InjectTool() bash: BashTool; @InjectTool() read: ReadTool; @Initial({ to: 'running' }) async runTests(args: { testDirectory: string }) { await this.bash.call({ command: `npm test -- ${args.testDirectory}` }); } @Final({ from: 'running' }) async done(): Promise<{ passed: boolean; output: string }> { return { passed: true, output: 'All tests passed.' }; } }

Wrap It 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:

@Tool({ schema: z.object({ testDirectory: z.string().describe('Directory containing the test files to run.'), }), uiConfig: { description: 'Run tests in the specified directory. IMPORTANT: This must be the only tool call.', }, }) export class RunTestsTask extends BaseTool { @InjectWorkflow() private testRunner: TestRunnerWorkflow; async call(args: { testDirectory: string }, options?: ToolCallOptions): Promise<ToolResult> { const result = await this.testRunner.run( { testDirectory: args.testDirectory }, { alias: 'testRunner', callback: options?.callback }, ); await this.repository.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<string, unknown>): Promise<ToolResult> { const data = result as { workflowId?: string; data?: { passed: boolean; output: string } }; await this.repository.save( LinkDocument, { status: data.data?.passed ? 'success' : 'failure', label: 'Tests complete', workflowId: data.workflowId! }, { id: `test_link_${data.workflowId}` }, ); return { data: data.data ?? result }; } }

The 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, so the result routes back automatically
  • complete() is called when the sub-workflow reaches its @Final transition. Transform the result here and update UI documents.
  • LinkDocument gives visual feedback — shows a pending indicator while the sub-workflow runs, then updates with the result

Use It in an Agent

Register the task tool and its sub-workflow in your module, then add it to the agent’s tool list:

// In the parent agent workflow @Transition({ from: 'ready', to: 'prompt_executed' }) async llmTurn() { const result = await this.claudeGenerateText.call({ system: 'You are a build agent. Run tests when implementation is complete.', messagesSearchTag: 'message', tools: ['read', 'write', 'edit', 'runTests'], }); this.llmResult = result.data; }

The LLM sees runTests as a tool with the description from @Tool({ uiConfig }). When it calls the tool, the sub-workflow launches in the background. The parent agent pauses at awaiting_tools until the sub-workflow completes, then continues its loop with the test results.

Make sure the parent workflow has callback: { transition: 'toolResultReceived' } in its delegateToolCalls.call() so async results are routed back. See Agent Workflows for the full pattern.

Nested Agents

The sub-workflow doesn’t have to be a simple workflow — it can be an AgentWorkflow itself. This creates multi-agent architectures where a high-level orchestrator delegates to specialized agents:

@Tool({ schema: z.object({ instructions: z.string() }), uiConfig: { description: 'Launch an agent to explore the codebase.' }, }) export class ExploreTask extends BaseTool { @InjectWorkflow() private agent: AgentWorkflow; async call(args: { instructions: string }, options?: ToolCallOptions): Promise<ToolResult> { const result = await this.agent.run( { system: 'You are a codebase exploration agent.', tools: ['glob', 'grep', 'read'], userMessage: args.instructions, }, { alias: 'exploreAgent', callback: options?.callback }, ); return { data: { workflowId: result.workflowId }, pending: { workflowId: result.workflowId }, }; } }

An orchestrator agent can have multiple task tools — exploreTask, implementTask, runTestsTask — and decide which sub-agent to launch based on the conversation. Each sub-agent runs independently with its own tool loop and reports back when done.

Registry References

Last updated on