Skip to content

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:

ExpressionDescription
decision == 'approved'Equality check
amount > 1000Numeric comparison
vars.level1.approvedNested variable (boolean)
decision != nullNull check
isValid && hasPermissionLogical AND
isAdmin || isSupervisorLogical OR
!isBlockedLogical 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

FeatureConditionExecutorTypedConditionExecutor
Input accesscontext.get<T>('key')input.key
Return typeboolbool
Type safetyRuntimeCompile-time
SerializationManual via toJsonVia fromInput

Best Practices

  1. Use expressions for simple conditions - Branch.when("x == 'y'")
  2. Use custom executors for complex logic - Database lookups, external calls
  3. Use namespaced schema types - condition.domain.rule
  4. Always implement toJson for persistence
  5. Include fromJson factory for deserialization
  6. Use get<T> for previous node output - Most common data source
  7. Use getAny<T> for accumulated output - When checking earlier decisions

See Also