Skip to content

Workflow

A Workflow is the unified model that describes the structure and behavior of a business process. It is both JSON-serializable for storage and executable at runtime.

Unified Model

The Workflow class serves as both the storable representation and the executable workflow:

dart
@JsonSerializable()
class Workflow {
  final String id;
  final String code;
  final String name;
  final String? description;
  final int version;
  final bool isActive;
  final List<WorkflowNode> nodes;    // Typed configurations
  final List<WorkflowEdge> edges;    // Executable conditions
  final WorkflowTimeouts timeouts;
  final Map<String, dynamic>? inputSchema;
  final Map<String, dynamic> metadata;
  final List<String> tags;
  final DateTime? createdAt;
  final DateTime? updatedAt;
}

Loading Workflows

The engine provides methods to load workflows from JSON with automatic type resolution:

dart
// Load from JSON with full type resolution
final workflow = engine.loadWorkflow({
  'id': 'wf-order-processing',
  'code': 'ORDER',
  'name': 'Order Processing Workflow',
  'version': 1,
  'isActive': true,
  'nodes': [...],
  'edges': [...],
});

// Load and register in one step
final workflow = engine.loadAndRegisterWorkflow(jsonData);

Using WorkflowBuilder

The WorkflowBuilder provides a fluent API for creating workflows programmatically:

dart
final workflow = WorkflowBuilder('ORDER', 'Order Processing Workflow')
    // Add nodes
    .start('begin')
    .task('validateOrder', name: 'Validate Order', executor: validateExecutor)
    .task('processPayment', name: 'Process Payment', executor: paymentExecutor)
    .task('shipOrder', name: 'Ship Order', executor: shipExecutor)
    .end('completed')

    // Define the flow
    .connect('begin', 'validateOrder')
    .connect('validateOrder', 'processPayment')
    .connect('processPayment', 'shipOrder')
    .connect('shipOrder', 'completed')

    .build();

WorkflowNode Structure

Each node has a type-safe configuration:

dart
@JsonSerializable()
class WorkflowNode {
  final String id;                       // Unique identifier
  final NodeType type;                   // start, end, task, userTask, etc.
  final String name;                     // Human-readable name
  final String? description;             // Optional description
  final NodeConfiguration config;        // Type-safe configuration
  final NodeUIConfig? ui;                // Visual editor configuration
  final List<String> tags;               // Categorization tags
  final Map<String, dynamic> metadata;   // Additional metadata

  // Runtime-only (resolved after loading)
  NodeExecutor? executor;

  // Convenience getters from config
  String? get schemaType;
  String? get signalName;
  String? get storeAs;
}

Node Types

TypeDescription
startEntry point
endTerminal point
taskAutomated task
userTaskHuman task
signalWaitWait for external signal
timerWaitWait for duration or timestamp
oneOfExclusive gateway (XOR)
anyOfInclusive gateway (OR)
allOfParallel gateway (AND)
subflowNested workflow execution

Type-Safe Configurations

Node configurations are deserialized to specific types based on the node type:

dart
// Access configuration with pattern matching
switch (node.config) {
  case TaskNodeConfiguration config:
    print('Task: ${config.schemaType}');
    print('Store as: ${config.storeAs}');

  case UserTaskNodeConfiguration config:
    print('Title: ${config.title}');
    print('Assigned to: ${config.assignToRole}');

  case SignalWaitNodeConfiguration config:
    print('Waiting for: ${config.signalName}');
    print('Timeout: ${config.timeout}');

  case GatewayNodeConfiguration config:
    print('Output ports: ${config.outputPorts}');

  case TimerWaitNodeConfiguration config:
    print('Timer type: ${config.timerType}');
    print('Duration: ${config.duration}');

  case SubflowNodeConfiguration config:
    print('Subflow: ${config.workflowCode}');

  case EmptyNodeConfiguration _:
    print('Start/End node - no config needed');
}

JSON Configuration Examples

Task Node:

json
{
  "id": "validateOrder",
  "type": "task",
  "name": "Validate Order",
  "config": {
    "schemaType": "task.order.validate",
    "storeAs": "validationResult",
    "input": {
      "strictMode": true
    }
  }
}

User Task Node:

json
{
  "id": "approveOrder",
  "type": "userTask",
  "name": "Approve Order",
  "config": {
    "schemaType": "userTask.approval",
    "title": "Order Approval Required",
    "description": "Please review and approve this order",
    "assignToRole": "order-approvers",
    "priority": "high",
    "signalName": "approval_decision",
    "storeAs": "approvalResult"
  }
}

Signal Wait Node:

json
{
  "id": "awaitPayment",
  "type": "signalWait",
  "name": "Await Payment",
  "config": {
    "signalName": "payment_received",
    "storeAs": "paymentDetails",
    "timeout": "PT24H"
  }
}

WorkflowEdge Structure

Edges connect nodes and can have executable conditions:

dart
@JsonSerializable()
class WorkflowEdge {
  final String id;
  final String sourceNodeId;
  final String targetNodeId;
  final String? sourcePortId;           // For action-based routing
  final String? targetPortId;
  final String? label;
  final ConditionExecutor? condition;   // Executable condition!
  final bool isDefault;
  final int priority;
}

Edge JSON Examples

Simple Edge (no condition):

json
{
  "id": "e1",
  "sourceNodeId": "start",
  "targetNodeId": "validateOrder"
}

Edge with Expression Condition:

json
{
  "id": "e2",
  "sourceNodeId": "decisionGateway",
  "targetNodeId": "approved",
  "label": "Approved",
  "condition": {
    "schemaType": "condition.expression",
    "expression": "output.decision == 'approved'"
  }
}

Edge with Custom Condition:

json
{
  "id": "e3",
  "sourceNodeId": "decisionGateway",
  "targetNodeId": "nextLevel",
  "label": "Needs More Approval",
  "condition": {
    "schemaType": "condition.approval.requiresNextLevel",
    "maxLevels": 3
  }
}

Default Edge:

json
{
  "id": "e4",
  "sourceNodeId": "decisionGateway",
  "targetNodeId": "fallback",
  "isDefault": true
}

Workflow Registration

Register workflows with the engine for execution:

dart
// Load and register from JSON
final workflow = engine.loadAndRegisterWorkflow(jsonData);

// Or build and register
final workflow = WorkflowBuilder('APPROVAL', 'Approval Workflow')
    .start('begin')
    // ... add nodes and edges
    .build();

engine.registerWorkflow(workflow);

// Now you can start instances
await engine.startWorkflow(
  workflowId: workflow.id,  // or workflow.code
  input: {'orderId': '12345'},
);

Versioning

Workflows support versioning through the version field:

dart
final workflowV1 = WorkflowBuilder('APPROVAL', 'Approval Workflow')
    .version(1)
    // ... v1 implementation
    .build();

final workflowV2 = WorkflowBuilder('APPROVAL', 'Approval Workflow (Updated)')
    .version(2)
    // ... v2 implementation
    .build();

// Register both versions
engine.registerWorkflow(workflowV1);
engine.registerWorkflow(workflowV2);

Starting Specific Versions

The startWorkflow method accepts an optional version parameter:

dart
// Start the latest active version (default behavior)
await engine.startWorkflow(
  workflowId: 'APPROVAL',
  input: {...},
);

// Start a specific version
await engine.startWorkflow(
  workflowId: 'APPROVAL',
  version: 2,
  input: {...},
);

// Roll back to v1 if needed
await engine.startWorkflow(
  workflowId: 'APPROVAL',
  version: 1,
  input: {...},
);

Version Lookup Strategy

When startWorkflow is called:

  1. With version specified: Looks up code + version directly
  2. Without version: First tries to find the latest active version by code, then falls back to UUID lookup

Storage Repository Methods

The WorkflowRepository provides version-aware operations:

dart
// Get specific version
await storage.workflows.getByCodeAndVersion('APPROVAL', 2);

// Get latest version
await storage.workflows.getLatestByCode('APPROVAL');

// List all versions
await storage.workflows.getAllVersionsByCode('APPROVAL');

// Get next available version number
final nextVersion = await storage.workflows.getNextVersion('APPROVAL');

Running Instances

Changing a workflow does not affect already-running instances. Each instance stores the workflowVersion it was started with.

JSON Serialization

Workflows can be serialized for storage:

dart
// To JSON
final json = workflow.toJson();

// From JSON (use engine.loadWorkflow for proper type resolution)
final workflow = engine.loadWorkflow(json);

Deserialization Context

Always use engine.loadWorkflow() to deserialize workflows from JSON. This ensures the deserialization context is set up correctly for type-safe node configurations and condition executors. Direct Workflow.fromJson() calls won't have access to the registry for type resolution.

Validation

Workflows are validated when loaded or registered:

dart
final workflow = engine.loadWorkflow(json);  // Throws if invalid

// Or validate manually
final errors = workflow.validate();
if (errors.isNotEmpty) {
  print('Validation errors: $errors');
}

Validation checks include:

  • At least one start node
  • At least one end node
  • All edges reference valid nodes
  • No disconnected nodes (except start nodes can lack incoming edges, end nodes can lack outgoing edges)
  • Node configurations match their node types

Next Steps