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:
@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:
// 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:
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:
@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
| Type | Description |
|---|---|
start | Entry point |
end | Terminal point |
task | Automated task |
userTask | Human task |
signalWait | Wait for external signal |
timerWait | Wait for duration or timestamp |
oneOf | Exclusive gateway (XOR) |
anyOf | Inclusive gateway (OR) |
allOf | Parallel gateway (AND) |
subflow | Nested workflow execution |
Type-Safe Configurations
Node configurations are deserialized to specific types based on the node type:
// 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:
{
"id": "validateOrder",
"type": "task",
"name": "Validate Order",
"config": {
"schemaType": "task.order.validate",
"storeAs": "validationResult",
"input": {
"strictMode": true
}
}
}User Task Node:
{
"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:
{
"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:
@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):
{
"id": "e1",
"sourceNodeId": "start",
"targetNodeId": "validateOrder"
}Edge with Expression Condition:
{
"id": "e2",
"sourceNodeId": "decisionGateway",
"targetNodeId": "approved",
"label": "Approved",
"condition": {
"schemaType": "condition.expression",
"expression": "output.decision == 'approved'"
}
}Edge with Custom Condition:
{
"id": "e3",
"sourceNodeId": "decisionGateway",
"targetNodeId": "nextLevel",
"label": "Needs More Approval",
"condition": {
"schemaType": "condition.approval.requiresNextLevel",
"maxLevels": 3
}
}Default Edge:
{
"id": "e4",
"sourceNodeId": "decisionGateway",
"targetNodeId": "fallback",
"isDefault": true
}Workflow Registration
Register workflows with the engine for execution:
// 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:
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:
// 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:
- With version specified: Looks up
code + versiondirectly - 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:
// 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:
// 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:
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
- Workflow Instance - Runtime state
- Tokens - Execution tracking
- Node Types - Available node types