Architecture
The Vyuh Workflow Engine architecture is designed for simplicity, extensibility, and reliability.
Conceptual Overview
Unified Model Architecture
The engine uses a unified model where Workflow is both storable (JSON-serializable) and executable. There's no separate "definition" and "executable" representation - the same model serves both purposes.
Layered Architecture
The engine is organized into four distinct layers, each with clear responsibilities:
| Layer | Responsibility |
|---|---|
| Application | Workflow JSON, custom executors, UI integration |
| Engine | Orchestration, token management, signal handling |
| Executors | Task/UserTask/Condition execution logic (via descriptors) |
| Storage | Persistence, audit trail, repositories |
System Overview
Core Components
WorkflowEngine
The central orchestrator that:
- Loads and validates workflows from JSON using deserialization context
- Registers and manages workflows in memory
- Starts workflow instances from workflows
- Executes nodes via registered executors
- Routes tokens through the graph
- Processes signals for waiting workflows
- Coordinates with storage for persistence
// Create deserialization context with descriptors
final context = RegistryDeserializationContext(
descriptors: [
DefaultWorkflowDescriptor(), // Built-in node executors
myCustomDescriptor, // Your custom executors
],
);
// Create engine with context and storage
final engine = WorkflowEngine(
context: context,
storage: InMemoryStorage(context: context),
);
await engine.initialize();
// Load and register a workflow from JSON
final workflow = engine.loadAndRegisterWorkflow(workflowJson);
// Or load, then register separately
final workflow = engine.loadWorkflow(workflowJson);
engine.registerWorkflow(workflow);Workflow
The unified model that is both storable and executable:
class Workflow {
final String id;
final String code;
final String name;
final int version;
final bool isActive;
final List<WorkflowNode> nodes; // With typed configs
final List<WorkflowEdge> edges; // With condition executors
final WorkflowTimeouts timeouts;
final Map<String, dynamic>? inputSchema;
final Map<String, dynamic> metadata;
final List<String> tags;
// Query methods
List<WorkflowNode> get startNodes;
List<WorkflowNode> get endNodes;
WorkflowNode? getNode(String nodeId);
List<WorkflowEdge> getOutgoingEdges(String nodeId);
List<WorkflowEdge> getIncomingEdges(String nodeId);
// Lifecycle
void resolveExecutors(WorkflowRegistry registry);
List<String> validate();
}WorkflowNode
A node with type-safe configuration:
class WorkflowNode {
final String id;
final NodeType type;
final String name;
final NodeConfiguration config; // Type-safe!
final NodeUIConfig? ui;
// Resolved at runtime
NodeExecutor? executor;
// Convenience getters (from config)
String? get schemaType;
String? get signalName;
String? get storeAs;
}WorkflowEdge
An edge with executable condition:
class WorkflowEdge {
final String id;
final String sourceNodeId;
final String targetNodeId;
final String? sourcePortId;
final String? targetPortId;
final String? label;
final ConditionExecutor? condition; // Executable!
final bool isDefault;
final int priority;
}WorkflowStorage
Abstract interface for persistence:
abstract class WorkflowStorage {
// Workflow definitions (stores RawWorkflow)
WorkflowRepository get workflows;
// Running instances
WorkflowInstanceRepository get instances;
// Human tasks (inbox items)
UserTaskInstanceRepository get userTaskInstances;
// Audit trail
WorkflowEventRepository get events;
// Transactional operations
Future<T> transaction<T>(Future<T> Function() operation);
// Lifecycle
Future<void> initialize();
Future<void> dispose();
// Health monitoring
Future<bool> healthCheck();
Future<StorageStats> getStats();
}Implementations:
InMemoryStorage- Built-in for testing/development- PostgreSQL/MongoDB adapters - Implement yourself for production
WorkflowDeserializationContext
The context for type resolution during deserialization:
// Use RegistryDeserializationContext for production
final context = RegistryDeserializationContext(
descriptors: [
DefaultWorkflowDescriptor(), // Built-in node executors
WorkflowDescriptor(
title: 'My Executors',
tasks: [ValidateEntityTask.typeDescriptor],
userTasks: [ApprovalFormExecutor.typeDescriptor],
conditions: [AmountThresholdCondition.typeDescriptor],
),
],
);
// Create engine with context
final engine = WorkflowEngine(
context: context,
storage: InMemoryStorage(context: context),
);Available Contexts:
RegistryDeserializationContext- Production use, strict type resolutionSimulationDeserializationContext- Editor/simulation, passthrough for unknown types
NodeExecutorRegistry
Maps node types to their executors (handled automatically by DefaultWorkflowDescriptor):
StartEventNodeExecutor- Entry point processingEndEventNodeExecutor- Workflow completionTaskNodeExecutor- Automated task executionUserTaskNodeExecutor- Human task creationSignalEventNodeExecutor- External signal waitingOneOfGatewayNodeExecutor- Exclusive (XOR) routingAnyOfGatewayNodeExecutor- Inclusive (OR) routingAllOfGatewayNodeExecutor- Parallel (AND) fork/join
Deserialization Flow
The engine uses a context-based deserialization that resolves types automatically. The key insight is that nodeType (start, task, userTask, etc.) determines which NodeConfiguration subclass to use, while schemaType identifies the specific executor implementation.
// 1. Load workflow from JSON (uses engine's deserialization context)
final workflow = engine.loadWorkflow({
'id': 'wf-1',
'code': 'approval-workflow',
'name': 'Approval Workflow',
'nodes': [
{
'id': 'start',
'type': 'start',
'name': 'Start'
},
{
'id': 'validate',
'type': 'task', // nodeType → TaskNodeConfiguration
'name': 'Validate',
'config': {
'schemaType': 'task.entity.validate', // identifies executor
'storeAs': 'validationResult'
}
}
],
'edges': [
{
'id': 'e1',
'sourceNodeId': 'start',
'targetNodeId': 'validate'
}
]
});
// 2. Access typed configuration via pattern matching
for (final node in workflow.nodes) {
switch (node.config) {
case TaskNodeConfiguration config:
print('Task: ${config.schemaType}');
case UserTaskNodeConfiguration config:
print('User Task: ${config.title}');
case SignalWaitNodeConfiguration config:
print('Signal: ${config.signalName}');
case SplitJoinNodeConfiguration _:
print('Split/Join node');
case EmptyNodeConfiguration _:
print('Start/End node');
}
}Execution Flow
1. Start Workflow
2. Node Execution
3. Signal Processing
Package Structure
lib/
├── design/ # Design-time components
│ ├── definitions/ # NodeType, NodeUIConfig, Timeouts
│ ├── builders/ # WorkflowBuilder, Branch
│ └── registry/ # WorkflowRegistry, TypeRegistry
│
├── runtime/ # Runtime execution
│ ├── models/ # Workflow, WorkflowNode, WorkflowEdge
│ ├── configs/ # NodeConfiguration subclasses
│ ├── instances/ # WorkflowInstance, Token, UserTask
│ ├── executors/ # Node, Task, Condition executors
│ ├── contexts/ # WorkflowContext, ExecutionContext
│ ├── events/ # Event types and logging
│ └── errors/ # Error types
│
├── storage/ # Persistence
│ ├── memory/ # In-memory implementation
│ └── repositories/ # Repository interfaces
│
└── workflow_engine.dart # Main WorkflowEngine classNext Steps
- Workflow - Understanding the unified workflow model
- Workflow Instance - Runtime execution state
- Tokens - Execution position tracking