Condition Executors
Condition executors evaluate routing conditions for gateways. In the unified model, ConditionExecutor is both configuration and executor - it's deserialized directly from JSON on workflow edges.
ConditionExecutor Interface
dart
abstract class ConditionExecutor implements SchemaItem {
const ConditionExecutor({
required this.schemaType,
this.name,
});
/// Schema type identifier (e.g., 'condition.highValue')
final String schemaType;
/// Optional name for display
final String? name;
/// Evaluate the condition
Future<bool> execute(ExecutionContext context);
/// Serialize to JSON for storage
Map<String, dynamic> toJson();
}How Conditions Work
Conditions are stored on edges and deserialized directly to executable instances:
json
{
"id": "e1",
"sourceNodeId": "gateway",
"targetNodeId": "approved",
"label": "Approved",
"condition": {
"schemaType": "condition.approved",
"name": "Check if approved"
}
}When the workflow is loaded, condition.approved is looked up in the registry and the condition is deserialized to an ApprovedCondition instance that can be immediately executed.
Implementing Condition Executors
Simple Condition
dart
class ApprovedCondition extends ConditionExecutor {
static const _schemaType = 'condition.approved';
ApprovedCondition() : super(schemaType: _schemaType);
@override
Future<bool> execute(ExecutionContext context) async {
// Use get<T> for previous node output
final decision = context.get<String>('decision');
return decision == 'approved';
}
static final typeDescriptor = TypeDescriptor<ConditionExecutor>(
schemaType: _schemaType,
fromJson: (_) => ApprovedCondition(),
title: 'Approved Condition',
);
}Parameterized Condition
dart
class ThresholdCondition extends ConditionExecutor {
static const _schemaType = 'condition.threshold';
final String field;
final num threshold;
final String operator; // '>', '<', '>=', '<=', '=='
ThresholdCondition({
required this.field,
required this.threshold,
this.operator = '>',
}) : super(schemaType: _schemaType);
@override
Future<bool> execute(ExecutionContext context) async {
// Use get<T> with the parameterized field name
final value = context.get<num>(field);
if (value == null) return false;
switch (operator) {
case '>': return value > threshold;
case '<': return value < threshold;
case '>=': return value >= threshold;
case '<=': return value <= threshold;
case '==': return value == threshold;
default: return false;
}
}
static TypeDescriptor<ConditionExecutor> typeDescriptor() {
return TypeDescriptor<ConditionExecutor>(
schemaType: _schemaType,
fromJson: (json) => ThresholdCondition(
field: json['field'] as String,
threshold: json['threshold'] as num,
operator: json['operator'] as String? ?? '>',
),
title: 'Threshold Condition',
);
}
}Async Condition (with external lookup)
dart
class HasPermissionCondition extends ConditionExecutor {
static const _schemaType = 'condition.hasPermission';
final PermissionService permissionService;
final String permission;
HasPermissionCondition(this.permissionService, this.permission)
: super(schemaType: _schemaType);
@override
Future<bool> execute(ExecutionContext context) async {
// Use get<T> for previous node output
final userId = context.get<String>('userId');
if (userId == null) return false;
return await permissionService.hasPermission(userId, permission);
}
}Using Condition Executors
In Gateway Branches
dart
// Register via descriptor
final descriptor = WorkflowDescriptor(
title: 'Conditions',
conditions: [
ApprovedCondition.typeDescriptor,
ThresholdCondition.typeDescriptor(),
],
);
// Create context with descriptors
final context = RegistryDeserializationContext(
descriptors: [DefaultWorkflowDescriptor(), descriptor],
);
// Create engine with context
final engine = WorkflowEngine(
context: context,
storage: InMemoryStorage(context: context),
);
await engine.initialize();
// Use in workflow with function-based conditions (most common)
builder.oneOf('routeDecision', [
Branch.whenFn((o) => o['decision'] == 'approved', then: 'handleApproved', label: 'Approved'),
Branch.whenFn((o) => (o['amount'] ?? 0) > 10000, then: 'requireManagerApproval', label: 'High Value'),
Branch.otherwise(then: 'handleRejected'),
]);Function-Based Conditions
For simple conditions, use inline functions:
dart
builder.oneOf('routeByStatus', [
Branch.whenFn(
(output) => output['status'] == 'active',
then: 'processActive',
label: 'Active',
),
Branch.whenFn(
(output) => output['status'] == 'pending',
then: 'processPending',
label: 'Pending',
),
Branch.otherwise(then: 'processOther'),
]);Common Condition Patterns
Boolean Field Check
dart
class IsValidCondition extends ConditionExecutor {
static const _schemaType = 'condition.isValid';
IsValidCondition() : super(schemaType: _schemaType);
@override
Future<bool> execute(ExecutionContext context) async {
// Use get<T> for previous node output
return context.get<bool>('isValid') == true;
}
}String Comparison
dart
class StatusEqualsCondition extends ConditionExecutor {
static const _schemaType = 'condition.statusEquals';
final String expectedStatus;
StatusEqualsCondition(this.expectedStatus)
: super(schemaType: _schemaType);
@override
Future<bool> execute(ExecutionContext context) async {
// Use get<T> for previous node output
return context.get<String>('status') == expectedStatus;
}
}Nested Field Access
dart
class NestedFieldCondition extends ConditionExecutor {
static const _schemaType = 'condition.nestedField';
final String path; // Dot-notation path like 'level1Decision.decision'
final dynamic expectedValue;
NestedFieldCondition(this.path, this.expectedValue)
: super(schemaType: _schemaType);
@override
Future<bool> execute(ExecutionContext context) async {
// Use get<T> with dot-notation path for nested access
final value = context.get<dynamic>(path);
return value == expectedValue;
}
}
// More common: Use inline function or dot-notation path
Branch.whenFn(
(o) => o['level1Decision']?['decision'] == 'approved',
then: 'nextLevel',
),List Contains
dart
class ListContainsCondition extends ConditionExecutor {
static const _schemaType = 'condition.listContains';
final String listField;
final dynamic value;
ListContainsCondition(this.listField, this.value)
: super(schemaType: _schemaType);
@override
Future<bool> execute(ExecutionContext context) async {
// Use get<T> for previous node output
final list = context.get<List>(listField);
return list?.contains(value) ?? false;
}
}Condition Priority
Conditions are evaluated in priority order:
dart
builder.oneOf('decide', [
Branch.whenFn((o) => o['urgent'] == true, then: 'urgentPath', priority: 100),
Branch.whenFn((o) => o['important'] == true, then: 'importantPath', priority: 50),
Branch.whenFn((o) => true, then: 'normalPath', priority: 10),
Branch.otherwise(then: 'fallbackPath'), // priority: 0
]);Higher priority = evaluated first.
Best Practices
- Keep conditions simple - Single responsibility
- Use descriptive schemaType names -
condition.hasApprovalnotcond1 - Handle null values - Check before accessing
- Use function conditions for one-offs -
Branch.whenFn - Use executor conditions for reusable logic - Custom
ConditionExecutor - Always include default branch - Catch unexpected cases
Next Steps
- Signals - Signal processing
- Gateways - Gateway patterns
- Control Flow Patterns - Flow patterns