ConditionExecutor API
Base class for condition executors. Conditions evaluate routing decisions at gateways.
ConditionExecutor
dart
abstract class ConditionExecutor {
const ConditionExecutor({
required this.schemaType,
this.name,
});
/// Schema type identifier for registry lookup
final String schemaType;
/// Optional human-readable name
final String? name;
/// Evaluate the condition - returns true or false
Future<bool> execute(ExecutionContext context);
/// Serialize to JSON
Map<String, dynamic> toJson();
/// Factory for built-in conditions
factory ConditionExecutor.fromJson(Map<String, dynamic> json);
}TypedConditionExecutor
Type-safe base class with compile-time safety for input:
dart
abstract class TypedConditionExecutor<TInput> extends ConditionExecutor {
const TypedConditionExecutor({
required super.schemaType,
super.name,
});
/// Deserialize input from workflow variables
TInput fromInput(Map<String, dynamic> input);
/// Type-safe evaluation method
Future<bool> evaluateTyped(TInput input, ExecutionContext context);
}Built-in Conditions
ExpressionCondition
Evaluates expression strings with variables:
dart
class ExpressionCondition extends ConditionExecutor {
static const schemaName = 'condition.expression';
const ExpressionCondition({
required this.expression,
super.name,
});
final String expression;
// Supports: ==, !=, <, >, <=, >=, &&, ||, !
// Variables: vars.path.to.value or just path.to.value
}Expression Syntax:
| Expression | Description |
|---|---|
decision == 'approved' | Equality check |
amount > 1000 | Numeric comparison |
vars.level1.approved | Nested variable (boolean) |
decision != null | Null check |
isValid && hasPermission | Logical AND |
isAdmin || isSupervisor | Logical OR |
!isBlocked | Logical NOT |
AlwaysTrueCondition
Default/fallback condition:
dart
class AlwaysTrueCondition extends ConditionExecutor {
static const schemaName = 'condition.always';
@override
Future<bool> execute(ExecutionContext context) async => true;
}AlwaysFalseCondition
Never-match condition:
dart
class AlwaysFalseCondition extends ConditionExecutor {
static const schemaName = 'condition.never';
@override
Future<bool> execute(ExecutionContext context) async => false;
}JSON Configuration
Expression-based
json
{
"schemaType": "condition.expression",
"expression": "vars.decision == 'approved'",
"name": "Is Approved"
}Custom executor
json
{
"schemaType": "condition.approval.requiresNextLevel",
"name": "Requires More Approvals",
"maxLevels": 3
}Examples
Custom ConditionExecutor
dart
class RequiresNextLevelCondition extends ConditionExecutor {
static const schemaName = 'condition.approval.requiresNextLevel';
static final typeDescriptor = TypeDescriptor<ConditionExecutor>(
schemaType: schemaName,
fromJson: RequiresNextLevelCondition.fromJson,
title: 'Requires Next Approval Level',
);
const RequiresNextLevelCondition({
this.maxLevels = 3,
super.name,
}) : super(schemaType: schemaName);
factory RequiresNextLevelCondition.fromJson(Map<String, dynamic> json) {
return RequiresNextLevelCondition(
maxLevels: json['maxLevels'] as int? ?? 3,
name: json['name'] as String?,
);
}
final int maxLevels;
@override
Future<bool> execute(ExecutionContext context) async {
// Use get<T> for previous node output
final currentLevel = context.get<int>('currentLevel') ?? 0;
return currentLevel < maxLevels;
}
@override
Map<String, dynamic> toJson() => {
'schemaType': schemaType,
'maxLevels': maxLevels,
if (name != null) 'name': name,
};
}Using Accumulated Output
Condition executors often need to access data from earlier nodes in the workflow:
dart
class AllLevelsApprovedCondition extends ConditionExecutor {
static const schemaName = 'condition.allLevelsApproved';
AllLevelsApprovedCondition() : super(schemaType: schemaName);
@override
Future<bool> execute(ExecutionContext context) async {
// Use getAny<T> to access accumulated output from any node
final level1 = context.getAny<Map>('level1Decision');
final level2 = context.getAny<Map>('level2Decision');
return level1?['decision'] == 'approved' &&
level2?['decision'] == 'approved';
}
@override
Map<String, dynamic> toJson() => {'schemaType': schemaType};
}TypedConditionExecutor
dart
@JsonSerializable()
class ApprovalState {
final int currentLevel;
final int totalLevels;
final bool wasRejected;
ApprovalState({
required this.currentLevel,
required this.totalLevels,
required this.wasRejected,
});
factory ApprovalState.fromJson(Map<String, dynamic> json) =>
_$ApprovalStateFromJson(json);
}
class HasMoreLevelsCondition
extends TypedConditionExecutor<ApprovalState> {
static const _schemaType = 'condition.hasMoreLevels';
static final typeDescriptor = TypeDescriptor<ConditionExecutor>(
schemaType: _schemaType,
fromJson: (json) => HasMoreLevelsCondition(),
title: 'Has More Approval Levels',
);
HasMoreLevelsCondition() : super(schemaType: _schemaType);
@override
ApprovalState fromInput(Map<String, dynamic> input) =>
ApprovalState.fromJson(input);
@override
Future<bool> evaluateTyped(
ApprovalState input,
ExecutionContext context,
) async {
// Fully typed access to input!
if (input.wasRejected) return false;
return input.currentLevel < input.totalLevels;
}
@override
Map<String, dynamic> toJson() => {'schemaType': schemaType};
}Amount-Based Routing
dart
class HighValueCondition extends ConditionExecutor {
static const schemaName = 'condition.highValue';
static final typeDescriptor = TypeDescriptor<ConditionExecutor>(
schemaType: schemaName,
fromJson: HighValueCondition.fromJson,
);
const HighValueCondition({
this.threshold = 10000,
super.name,
}) : super(schemaType: schemaName);
factory HighValueCondition.fromJson(Map<String, dynamic> json) {
return HighValueCondition(
threshold: json['threshold'] as num? ?? 10000,
name: json['name'] as String?,
);
}
final num threshold;
@override
Future<bool> execute(ExecutionContext context) async {
// Use get<T> for previous node output
final amount = context.get<num>('amount') ?? 0;
return amount >= threshold;
}
@override
Map<String, dynamic> toJson() => {
'schemaType': schemaType,
'threshold': threshold,
if (name != null) 'name': name,
};
}Usage in WorkflowBuilder
Expression-based (inline)
dart
builder.oneOf('routeDecision', [
Branch.when("decision == 'approved'", then: 'handleApproved'),
Branch.when("decision == 'revision'", then: 'requestRevision'),
Branch.otherwise(then: 'handleRejected'),
]);Function-based (inline)
dart
builder.oneOf('routeByAmount', [
Branch.whenFn(
(output) => (output['amount'] ?? 0) > 10000,
then: 'highValuePath',
),
Branch.otherwise(then: 'normalPath'),
]);With Custom Executor
dart
// In JSON definition or builder connect
edge.condition = RequiresNextLevelCondition(maxLevels: 3);Registration
dart
final descriptor = WorkflowDescriptor(
title: 'Custom Conditions',
conditions: [
RequiresNextLevelCondition.typeDescriptor,
HighValueCondition.typeDescriptor,
HasMoreLevelsCondition.typeDescriptor,
],
);
// Create context with descriptors
final context = RegistryDeserializationContext(
descriptors: [DefaultWorkflowDescriptor(), descriptor],
);
// Create engine with context and storage
final engine = WorkflowEngine(
context: context,
storage: InMemoryStorage(context: context),
);
await engine.initialize();ExecutionContext in Conditions
Condition executors have access to the same data as task executors:
dart
// From previous node output (most common)
final decision = context.get<String>('decision');
// From original workflow input
final tenantId = context.getInitial<String>('tenantId');
// From accumulated output (all previous nodes)
final level1Approved = context.getAny<bool>('level1Decision.approved');
// Edge being evaluated (for logging/debugging)
final edgeLabel = context.edge?.label;Comparison
| Feature | ConditionExecutor | TypedConditionExecutor |
|---|---|---|
| Input access | context.get<T>('key') | input.key |
| Return type | bool | bool |
| Type safety | Runtime | Compile-time |
| Serialization | Manual via toJson | Via fromInput |
Best Practices
- Use expressions for simple conditions -
Branch.when("x == 'y'") - Use custom executors for complex logic - Database lookups, external calls
- Use namespaced schema types -
condition.domain.rule - Always implement toJson for persistence
- Include fromJson factory for deserialization
- Use
get<T>for previous node output - Most common data source - Use
getAny<T>for accumulated output - When checking earlier decisions
See Also
- Condition Executors - Detailed guide
- Gateways - Gateway patterns
- Type Registries - Registration patterns