Defining Workflows
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. You might also need the function registry, which contains the node implementations.
// Continuing from above...
const blueprint = flowBuilder.toBlueprint()
const functionRegistry = flowBuilder.getFunctionRegistry()
// Now you can pass these to the FlowRuntime with type safety
// const runtime = new FlowRuntime({ registry: functionRegistry });
// const result = await runtime.run(blueprint, { user_data: initialUser });Demo
This workflow can be visualized and run in the demo below: