@foldedwave/flowologist
v0.0.2
Published
Type-safe workflow orchestration for TypeScript applications.
Readme
Flowologist
Type-safe workflow orchestration for TypeScript applications.
The Problem
Coordinating complex operations with dependencies between steps presents challenges:
- Dependency Management: How do you ensure operations happen in the correct order?
- Type Safety: How do you maintain type information between dependent steps?
- Parallelization: How do you maximize performance by running independent steps in parallel?
- Error Handling: How do you gracefully handle failures in multi-step processes?
- Reusability: How do you define reusable workflows with different execution paths?
Most solutions either sacrifice type safety for flexibility, or require verbose boilerplate to maintain types.
The Solution
Flowologist provides a fluent, type-safe API for defining and executing complex workflows:
import { WorkflowBuilder } from '@foldedwave/flowologist';
// Define a workflow with strongly-typed dependencies
const workflow = new WorkflowBuilder()
.addStepWithoutDependencies('userData')
.withImplementation(() => ({
id: 123,
name: 'John Doe'
}))
.addStep('userPreferences', ['userData'])
.withImplementation(({ userData }) => ({
userId: userData.id, // Fully typed!
theme: 'dark'
}))
.build();
// Access results with full type information
console.log(workflow.containers.userPreferences.theme); // 'dark'Key Benefits
- Type-Safe Dependencies: Compile-time validation of workflow dependencies
- Automatic Parallelization: Independent steps run concurrently
- Custom Flows: Define alternative execution paths through your workflow
- Circular Dependency Detection: Prevents deadlocks with automatic validation
- Async Support: Works with both synchronous and asynchronous operations
- Error Propagation: Clear error boundaries with detailed failure information
- Zero External Dependencies: Lightweight with no runtime dependencies
Installation
npm install @foldedwave/flowologist
# or
yarn add @foldedwave/flowologistBasic Usage
1. Define Steps and Dependencies
import { WorkflowBuilder } from '@foldedwave/flowologist';
const workflow = new WorkflowBuilder()
// Step with no dependencies
.addStepWithoutDependencies('config')
.withImplementation(() => ({
apiUrl: 'https://api.example.com'
}))
// Step that depends on config
.addStep('apiClient', ['config'])
.withImplementation(({ config }) => ({
fetch: (endpoint: string) =>
fetch(`${config.apiUrl}/${endpoint}`)
}))
.build();2. Access Results
// Access containers with full type information
const apiClient = workflow.containers.apiClient;
// Use it in your application
apiClient.fetch('users').then(users => {
console.log(users);
});3. Define and Execute Custom Flows
const workflow = new WorkflowBuilder()
.addStepWithoutDependencies('form')
.withImplementation(() => ({
data: { name: 'John' },
validate: () => true,
save: () => ({ success: true })
}))
// Define a save flow
.defineFlow('save')
.addFlowStep('form')
.withFlowAction((form) => {
if (!form.validate()) {
throw new Error('Validation failed');
}
return form.save();
})
.endFlow()
.build();
// Execute the flow
const result = workflow.execute('save');
console.log(result.results.form.success); // true4. Asynchronous Operations
// Define an async workflow
const workflow = await new WorkflowBuilder()
.addStepWithoutDependencies('user')
.withImplementation(async () => {
const response = await fetch('/api/user/1');
return response.json();
})
.addStep('permissions', ['user'])
.withImplementation(async ({ user }) => {
const response = await fetch(`/api/permissions/${user.id}`);
return response.json();
})
.buildAsync(); // Use buildAsync for async operationsCommon Use Cases
1. React Data Container Orchestration
function useDashboardData() {
return useEffect(() => {
const workflow = new WorkflowBuilder()
.addStepWithoutDependencies('apiClient')
.withImplementation(() => ({
fetchUser: () => fetch('/api/user').then(r => r.json())
}))
.addStep('userData', ['apiClient'])
.withImplementation(async ({ apiClient }) => {
return await apiClient.fetchUser();
})
.buildAsync();
// Set state with loaded data
setState({ user: workflow.containers.userData });
}, []);
}2. Business Process Orchestration
const orderWorkflow = new WorkflowBuilder()
.addStepWithoutDependencies('orderData')
.withImplementation(() => ({
items: [{ id: 1, qty: 2 }],
customer: { id: 42 }
}))
.addStep('inventoryCheck', ['orderData'])
.withImplementation(async ({ orderData }) => {
// Check inventory for all items
const results = await Promise.all(orderData.items.map(
item => checkInventory(item.id, item.qty)
));
return { allInStock: results.every(r => r.inStock) };
})
.addStep('orderCreation', ['orderData', 'inventoryCheck'])
.withImplementation(async ({ orderData, inventoryCheck }) => {
if (!inventoryCheck.allInStock) {
throw new Error('Some items are out of stock');
}
return await createOrder(orderData);
})
.buildAsync();3. Parallel Data Processing
const reportWorkflow = await new WorkflowBuilder()
// These steps run in parallel
.addStepWithoutDependencies('userStats')
.withImplementation(async () => {
const users = await fetchUsers();
return { total: users.length };
})
.addStepWithoutDependencies('salesStats')
.withImplementation(async () => {
const sales = await fetchSales();
return { revenue: calculateRevenue(sales) };
})
// This step depends on both parallel steps
.addStep('report', ['userStats', 'salesStats'])
.withImplementation(({ userStats, salesStats }) => ({
userCount: userStats.total,
revenue: salesStats.revenue,
revenuePerUser: salesStats.revenue / userStats.total
}))
.buildAsync();
console.log(reportWorkflow.containers.report);4. Form Submission with Validation
const formWorkflow = new WorkflowBuilder()
.addStepWithoutDependencies('form')
.withImplementation(() => ({
data: { name: 'John', age: 17 },
errors: {}
}))
.addStep('validator', ['form'])
.withImplementation(({ form }) => ({
validate: () => {
const errors = {};
if (form.data.age < 18) errors.age = 'Must be 18+';
return { isValid: Object.keys(errors).length === 0, errors };
}
}))
.defineFlow('submit')
.addFlowStep('validator')
.withFlowAction((validator) => validator.validate())
.addFlowStep('form', ['validator'])
.withFlowAction((form, context) => {
if (!context.validator.isValid) {
form.errors = context.validator.errors;
return { success: false, errors: context.validator.errors };
}
return { success: true };
})
.endFlow()
.build();
const result = formWorkflow.execute('submit');
console.log(result.results.form); // { success: false, errors: { age: 'Must be 18+' }}5. Data Pipeline with Custom Flow
const dataPipeline = new WorkflowBuilder()
.addStepWithoutDependencies('source')
.withImplementation(() => ({ data: ['10', '20', '30'] }))
.addStep('transform', ['source'])
.withImplementation(() => ({
process: (items: string[]) => items.map(Number)
}))
.addStep('output', ['transform'])
.withImplementation(() => ({
format: (values: number[]) => `Sum: ${values.reduce((a, b) => a + b, 0)}`
}))
// Define processing flow
.defineFlow('process')
.addFlowStep('source')
.withFlowAction((source) => source.data)
.addFlowStep('transform', ['source'])
.withFlowAction((transform, context) => transform.process(context.source))
.addFlowStep('output', ['transform'])
.withFlowAction((output, context) => output.format(context.transform))
.endFlow()
.build();
const result = dataPipeline.execute('process');
console.log(result.results.output); // "Sum: 60"Advanced Usage
Conditional Workflows
function createWorkflow(includeReporting: boolean) {
const builder = new WorkflowBuilder()
.addStepWithoutDependencies('base')
.withImplementation(() => ({ value: 42 }));
if (includeReporting) {
builder
.addStep('reporting', ['base'])
.withImplementation(({ base }) => ({
report: `Value is ${base.value}`
}));
}
return builder.build();
}Async Flow Execution
const workflow = new WorkflowBuilder()
.addStepWithoutDependencies('data')
.withImplementation(() => ({ id: 1 }))
.defineFlow('process')
.addFlowStep('data')
.withFlowAction(async (data) => {
// Async operation inside flow action
const result = await fetchDetails(data.id);
return result;
})
.endFlow()
.build();
// Execute async flow
const result = await workflow.executeAsync('process');How It Works
Flowologist combines compile-time type checking with runtime orchestration:
At Design Time:
- You define steps and their dependencies using a fluent API
- TypeScript validates that dependencies exist and have the correct types
- The builder pattern maintains type information through method chaining
At Runtime:
- The engine builds a dependency graph of all steps
- Independent steps are automatically executed in parallel
- Data flows between steps according to the dependency graph
- Custom flows can provide alternative execution paths through your steps
The workflow's dependency graph determines execution order - each step waits for all its dependencies before executing:
┌───────────┐
│ Step A │
└─────┬─────┘
│
┌───────────┴────────────┐
│ │
┌──────▼─────┐ ┌──────▼─────┐
│ Step B │ │ Step C │
└──────┬─────┘ └──────┬─────┘
│ │
└───────────┬────────────┘
│
┌─────▼─────┐
│ Step D │
└───────────┘API Reference
WorkflowBuilder<Steps>
Type Parameters:
Steps: Record type containing all registered steps and their result types.
Methods:
addStepWithoutDependencies<K>(name: K): Adds a step with no dependencies.addStep<K, Deps>(name: K, dependencies: [...Deps]): Adds a step with dependencies.defineFlow<F>(flowName: F): Defines a new flow for alternative execution paths.build(): Builds the workflow synchronously.buildAsync(): Builds the workflow asynchronously.
StepImplementer<Steps, CurrentStep, DepKeys>
Methods:
withImplementation<R>(execute: (input: { [P in DepKeys]: Steps[P] }) => R):
Defines a step implementation with strongly-typed dependencies.
FlowBuilder<Steps, FlowName, DefinedSteps>
Methods:
addFlowStep<K, Deps>(stepName: K, dependencies?: [...Deps]):
Adds a step to the flow, optionally with dependencies.endFlow(): Completes the flow definition.
Workflow<T>
Properties:
containers: { [K in keyof T]: T[K] }: Contains step results with type information.steps: Record<string, StepImplementation>: Step implementations.dependencies: Record<string, Array>: Dependency graph.flows: Record<string, FlowDefinition>: Defined flows.
Methods:
execute(flowName: string): Executes a flow synchronously.executeAsync(flowName: string): Executes a flow asynchronously.refresh(): Re-executes all steps of the workflow.
Features
- ✅ Type Safety: Full TypeScript support with strong typing for dependencies
- ✅ Dependency Validation: Automatic validation of the dependency graph
- ✅ Parallel Execution: Automatic parallelization of independent steps
- ✅ Custom Flows: Define and execute alternative paths through the workflow
- ✅ Error Handling: Detailed error information with original error preservation
- ✅ Sync & Async: Support for both synchronous and asynchronous execution
- ✅ Zero Dependencies: Lightweight with no external runtime dependencies
License
MIT
Inspired by declarative programming patterns and developed to solve real-world orchestration challenges while maintaining complete type safety.
