Skip to content

Subflows

As workflows grow in complexity, it becomes useful to break them down into smaller, reusable components. Flowcraft supports this through subflows.

A subflow is a standard WorkflowBlueprint that can be executed as a single node within another (parent) workflow. This allows you to encapsulate logic, promote reuse, and keep your main workflow graphs clean and organized.

The subflow Node

You can run a subflow by defining a node with uses: 'subflow'. This is a built-in node type that the FlowRuntime knows how to handle.

The params for a subflow node are critical:

  • blueprintId: The ID of the WorkflowBlueprint to execute. This blueprint must be available in the FlowRuntime's blueprints registry.
  • inputs (optional): An object mapping keys in the subflow's initial context to keys in the parent workflow's context. This is how you pass data into the subflow.
  • outputs (optional): An object mapping keys in the parent workflow's context to keys in the subflow's final context. This is how you get data out of the subflow.

Example: A Reusable Subflow

Let's create a subflow that adds two numbers and a parent workflow that uses it.

1. Define the Subflow

typescript
// subflow.ts
import { createFlow } from 'flowcraft'

export const mathSubflowBlueprint = createFlow('math-subflow')
	.node('add', async ({ context }) => {
		const a = await context.get('a')
		const b = await context.get('b')
		const sum = a + b
		// The result is stored in the subflow's context.
		return { output: sum }
	})
	.toBlueprint()

2. Define the Parent Workflow

typescript
// parent-flow.ts
import { createFlow } from 'flowcraft'

export const parentFlow = createFlow('parent-workflow')
	.node('prepare-data', async ({ context }) => {
		// Set up data in the parent context.
		await context.set('val1', 10)
		await context.set('val2', 20)
		return { output: 'Data ready' }
	})
	.node('run-math', {
		uses: 'subflow', // Use the built-in subflow runner
		params: {
			blueprintId: 'math-subflow',
			// Map parent context keys to subflow context keys
			inputs: {
				a: 'val1',
				b: 'val2',
			},
			// Map parent context key to a subflow result key
			outputs: {
				addition_result: 'add' // 'add' is the ID of the node in the subflow
			}
		}
	})
	.edge('prepare-data', 'run-math')

3. Set Up the Runtime

The key is to provide all necessary blueprints to the FlowRuntime constructor.

typescript
// main.ts
import { FlowRuntime } from 'flowcraft'
import { parentFlow } from './parent-flow'
import { mathSubflowBlueprint } from './subflow'

const runtime = new FlowRuntime({
	// The runtime needs access to all blueprints it might be asked to run.
	blueprints: {
		'math-subflow': mathSubflowBlueprint
	},
	// The registry only needs the implementations from the parent flow.
	registry: parentFlow.getFunctionRegistry()
})

const result = await runtime.run(parentFlow.toBlueprint(), {})
console.log(result.context)
// {
//   val1: 10,
//   val2: 20,
//   prepare_data: 'Data ready',
//   run_math: { a: 10, b: 20, add: 30 }, // Subflow's final context
//   addition_result: 30 // Mapped output
// }

This modular approach is invaluable for building large, maintainable workflow systems.

Error Handling in Subflows

When a subflow fails, the error is propagated to the parent workflow with details for better debugging. The FlowcraftError thrown by a failed subflow includes:

  • The original error message from the specific node that failed within the subflow
  • The node ID where the failure occurred
  • The stack trace from the subflow's execution

This allows you to trace failures back to their source, even in deeply nested subflow hierarchies.

typescript
// Example: Handling subflow errors
try {
  const result = await runtime.run(parentFlow.toBlueprint(), {})
} catch (error) {
  if (error instanceof FlowcraftError) {
    console.log(`Subflow failed: ${error.message}`)
    if (error.cause) {
      console.log(`Original error: ${error.cause.message}`)
      console.log(`Failed node in subflow: ${error.cause.nodeId}`)
    }
  }
}

If a subflow fails, it will prevent the parent workflow from continuing unless handled appropriately (e.g., via retries or fallbacks).

Awaiting Subflows

Subflows can contain wait nodes, causing the entire parent workflow to pause. When a subflow encounters a wait node, the parent workflow's status becomes 'awaiting', and the subflow's state is persisted in the parent context.

Example: Awaiting Subflow

typescript
// subflow-with-wait.ts
import { createFlow } from 'flowcraft'

export const approvalSubflow = createFlow('approval-subflow')
  .node('start', async ({ context }) => {
    await context.set('data', 'Request for approval')
    return { output: 'Started' }
  })
  .edge('start', 'wait-for-approval')
  .wait('wait-for-approval')  // Pauses here
  .edge('wait-for-approval', 'process')
  .node('process', async ({ input }) => {
    const approved = input?.approved
    return { output: approved ? 'Approved' : 'Rejected' }
  })
  .toBlueprint()

// parent-flow.ts
import { createFlow } from 'flowcraft'

export const parentFlow = createFlow('parent-workflow')
  .node('prepare', async ({ context }) => {
    await context.set('request', 'User request')
    return { output: 'Prepared' }
  })
  .edge('prepare', 'subflow-node')
  .node('subflow-node', {
    uses: 'subflow',
    params: {
      blueprintId: 'approval-subflow',
      inputs: { data: 'request' }
    }
  })
  .edge('subflow-node', 'finish')
  .node('finish', async ({ context }) => {
    const subflowResult = await context.get('_outputs.subflow-node')
    return { output: `Final result: ${subflowResult}` }
  })

// Execution
const runtime = new FlowRuntime({
  blueprints: { 'approval-subflow': approvalSubflow },
  registry: parentFlow.getFunctionRegistry()
})

const initialResult = await runtime.run(parentFlow.toBlueprint(), {})
// initialResult.status === 'awaiting'

const resumeResult = await runtime.resume(parentFlow.toBlueprint(), initialResult.serializedContext, {
  output: { approved: true }
})
// resumeResult.status === 'completed'

When resuming, the subflow's state is restored, and execution continues from the wait node.

Released under the MIT License.