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:
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.
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.
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.
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:
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: