Skip to content

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:

LayerResponsibility
ApplicationWorkflow JSON, custom executors, UI integration
EngineOrchestration, token management, signal handling
ExecutorsTask/UserTask/Condition execution logic (via descriptors)
StoragePersistence, 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
dart
// 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:

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

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

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

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

dart
// 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 resolution
  • SimulationDeserializationContext - Editor/simulation, passthrough for unknown types

NodeExecutorRegistry

Maps node types to their executors (handled automatically by DefaultWorkflowDescriptor):

  • StartEventNodeExecutor - Entry point processing
  • EndEventNodeExecutor - Workflow completion
  • TaskNodeExecutor - Automated task execution
  • UserTaskNodeExecutor - Human task creation
  • SignalEventNodeExecutor - External signal waiting
  • OneOfGatewayNodeExecutor - Exclusive (XOR) routing
  • AnyOfGatewayNodeExecutor - Inclusive (OR) routing
  • AllOfGatewayNodeExecutor - 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.

dart
// 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 class

Next Steps