Skip to content

Fluent API

This guide covers the Fluent API for programmatically building a workflow graph. For an alternative imperative approach, see the Flowcraft Compiler Guide.

Workflows can be defined programmatically using the fluent Flow builder API or declaratively using JSON, YAML, or in a database, before converting them to a WorkflowBlueprint for execution.

Defining Context

Defining a context provides a strongly-typed and intuitive way to construct your WorkflowBlueprint with compile-time type safety. Before creating workflows, define the shape of your context data using a TypeScript interface:

typescript
interface UserProcessingContext {
	user_data?: { id: number; name: string }
	validation_result?: boolean
	processing_status?: 'pending' | 'completed' | 'failed'
}

Using createFlow

The entry point to the builder is the createFlow function. It takes a unique ID for your workflow and is generic over your context type for full type safety.

typescript
import { createFlow } from 'flowcraft'

// Providing the context type is optional, but recommended
const flowBuilder = createFlow<UserProcessingContext>('my-first-workflow')

Adding Nodes

You can add tasks to your workflow using the .node() method. Node functions receive a strongly-typed NodeContext that provides access to the typed context.

typescript
const flowBuilder = createFlow<UserProcessingContext>('user-processing')
	// A simple function-based node with type safety
	.node('fetch-user', async ({ context }) => {
		const user = { id: 1, name: 'Alice' }
		await context.set('user_data', user)
		return { output: user }
	})
	// A node with type-safe input handling
	.node(
		'validate-user',
		async ({ context, input }) => {
			const userData = input as { id: number; name: string }
			const isValid = userData.name === 'Alice'

			await context.set('validation_result', isValid)
			return {
				output: isValid,
				action: isValid ? 'valid' : 'invalid',
			}
		},
		{
			// This tells the runtime to provide the output of 'fetch-user'
			// as the 'input' for this node.
			inputs: 'fetch-user',
		},
	)

Adding Edges

Edges define the dependencies and control flow between nodes. You can create them with the .edge() method, specifying the source and target node IDs.

typescript
const flowBuilder = createFlow<UserProcessingContext>('user-processing')
	.node('fetch-user' /* ... */)
	.node('validate-user' /* ... */)
	.node('process-valid', async ({ context }) => {
		// Type-safe context access in downstream nodes
		const userData = await context.get('user_data')
		const validation = await context.get('validation_result')

		await context.set('processing_status', 'completed')
		return { output: `Processed user ${userData?.name}` }
	})
	.node('handle-invalid', async ({ context }) => {
		await context.set('processing_status', 'failed')
		return { output: 'Invalid user data' }
	})

	// Basic edge: runs 'validate-user' after 'fetch-user'
	.edge('fetch-user', 'validate-user')

	// Conditional edges based on the 'action' returned by 'validate-user'
	.edge('validate-user', 'process-valid', { action: 'valid' })
	.edge('validate-user', 'handle-invalid', { action: 'invalid' })

Finalizing the Flow

Once your workflow is defined, call .toBlueprint() to get the serializable WorkflowBlueprint object.

The easiest way to run your flow is with flow.run(runtime), which automatically passes the node implementations to the runtime:

typescript
const runtime = new FlowRuntime()

// Recommended: convenience method
const result = await flow.run(runtime, { user_data: initialUser })

// Equivalent to:
// const blueprint = flowBuilder.toBlueprint()
// const functionRegistry = flowBuilder.getFunctionRegistry()
// const result = await runtime.run(blueprint, { user_data: initialUser }, { functionRegistry })

Demo

This workflow can be visualized and run in the demo below:

Released under the MIT License