Skip to content

Workflow Descriptor

A WorkflowDescriptor is a collection of executors that extend the workflow engine's capabilities.

Purpose

The workflow engine uses a registry-based architecture where executable components are registered by their schemaType. When a workflow references a task with schemaType: 'task.email.send', the engine looks up the corresponding executor from its registries.

WorkflowDescriptor is how you provide these registries with:

  • Task Executors - Automated work (API calls, computations, notifications)
  • User Task Executors - Human task specifications (approvals, reviews)
  • Condition Executors - Complex routing logic for gateways
  • Node Executors - Custom execution behavior for node types
  • Node Configurations - Type-safe config classes for nodes

Basic Usage

dart
// Define your executors
final myDescriptor = WorkflowDescriptor(
  title: 'My Application Executors',
  tasks: [
    SendEmailExecutor.typeDescriptor,
    ValidateDataExecutor.typeDescriptor,
  ],
  userTasks: [
    ApprovalTaskExecutor.typeDescriptor,
  ],
  conditions: [
    AmountThresholdCondition.typeDescriptor,
  ],
);

// Create deserialization context with all descriptors
final context = RegistryDeserializationContext(
  descriptors: [
    DefaultWorkflowDescriptor(),  // Built-in executors
    myDescriptor,                  // Your executors
  ],
);

// Create engine with context and storage
final engine = WorkflowEngine(
  context: context,
  storage: InMemoryStorage(context: context),
);

DefaultWorkflowDescriptor

The engine requires executors for all built-in node types. DefaultWorkflowDescriptor provides these:

Event Node Executors:

  • StartEventNodeExecutor - workflow entry points
  • EndEventNodeExecutor - workflow termination
  • SignalEventNodeExecutor - external signal handling
  • TimerEventNodeExecutor - timer-based waiting

Gateway Node Executors:

  • OneOfGatewayNodeExecutor - exclusive routing (XOR)
  • AnyOfGatewayNodeExecutor - inclusive routing (OR)
  • AllOfGatewayNodeExecutor - parallel routing (AND)

Task Node Executors:

  • TaskNodeExecutor - automated task execution
  • UserTaskNodeExecutor - human task handling
  • SubflowNodeExecutor - nested workflow execution

Node Configurations:

  • TaskNodeConfiguration - for task nodes
  • UserTaskNodeConfiguration - for user task nodes
  • SignalWaitNodeConfiguration - for signal wait nodes
  • TimerWaitNodeConfiguration - for timer wait nodes
  • GatewayNodeConfiguration - for gateway nodes
  • SubflowNodeConfiguration - for subflow nodes
  • EmptyNodeConfiguration - for start/end nodes

Built-in Conditions:

  • ExpressionCondition - expression-based routing
  • AlwaysTrueCondition - always matches
  • AlwaysFalseCondition - never matches

Always Include DefaultWorkflowDescriptor

Unless you're replacing all built-in executors, always include DefaultWorkflowDescriptor() first in your descriptors list.

Descriptor Properties

tasks

Task executor type descriptors for automated work:

dart
tasks: [
  TypeDescriptor<TaskExecutor>(
    schemaType: 'task.email.send',
    fromJson: SendEmailExecutor.fromJson,
    title: 'Send Email',
    description: 'Send an email notification',
    category: 'Notifications',
  ),
]

userTasks

User task executor type descriptors for human interactions:

dart
userTasks: [
  TypeDescriptor<UserTaskExecutor>(
    schemaType: 'userTask.approval',
    fromJson: ApprovalTaskExecutor.fromJson,
    title: 'Approval Task',
  ),
]

conditions

Condition executor type descriptors for complex routing:

dart
conditions: [
  TypeDescriptor<ConditionExecutor>(
    schemaType: 'condition.amount.threshold',
    fromJson: AmountThresholdCondition.fromJson,
    title: 'Amount Threshold',
  ),
]

nodeExecutors

Node executor instances (not type descriptors) for custom node execution:

dart
nodeExecutors: [
  MyCustomGatewayHandler(),
  MyCustomTaskExecutor(),
]

nodeConfigurations

Type descriptors for node configuration classes:

dart
nodeConfigurations: [
  TypeDescriptor<NodeConfiguration>(
    schemaType: 'config.myCustomNode',
    fromJson: MyCustomNodeConfiguration.fromJson,
  ),
]

TypeDescriptor

TypeDescriptor connects a schemaType string to a Dart class:

dart
class SendEmailExecutor extends TaskExecutor {
  static const schemaType = 'task.email.send';

  // Type descriptor for registration
  static final typeDescriptor = TypeDescriptor<TaskExecutor>(
    schemaType: schemaType,
    fromJson: (json) => SendEmailExecutor.fromJson(json),
    title: 'Send Email',
    description: 'Send an email notification to specified recipients',
    category: 'Notifications',
    iconName: 'email_outlined',
    tags: ['email', 'notification'],
  );

  // ... implementation
}

TypeDescriptor Properties

PropertyTypeDescription
schemaTypeStringUnique identifier (e.g., 'task.email.send')
fromJsonFunctionFactory to create instance from JSON
titleString?Human-readable name
descriptionString?What this executor does
categoryString?Grouping for UI (e.g., 'Notifications')
iconNameString?Icon identifier for UI
tagsList<String>?Search/filter tags
inputSchemaMap?JSON Schema for input validation
outputSchemaMap?JSON Schema for output

Descriptor Order Matters

Descriptors are processed in order. Later registrations override earlier ones for the same schemaType:

dart
final context = RegistryDeserializationContext(
  descriptors: [
    DefaultWorkflowDescriptor(),     // Built-in executors
    WorkflowDescriptor(
      nodeExecutors: [
        MyCustomTaskExecutor(),       // Overrides default TaskNodeExecutor
      ],
    ),
  ],
);

final engine = WorkflowEngine(
  context: context,
  storage: InMemoryStorage(context: context),
);

This enables:

  • Starting with sensible defaults
  • Overriding specific executors as needed
  • Layering application-specific executors

Merging Descriptors

Descriptors can be merged programmatically:

dart
final combined = baseDescriptor.merge(additionalDescriptor);

Organizing Executors

For large applications, organize executors into domain-specific descriptors:

dart
// notifications.dart
final notificationExecutors = WorkflowDescriptor(
  title: 'Notification Executors',
  tasks: [
    SendEmailExecutor.typeDescriptor,
    SendSmsExecutor.typeDescriptor,
    PushNotificationExecutor.typeDescriptor,
  ],
);

// payments.dart
final paymentExecutors = WorkflowDescriptor(
  title: 'Payment Executors',
  tasks: [
    ProcessPaymentExecutor.typeDescriptor,
    RefundPaymentExecutor.typeDescriptor,
  ],
  conditions: [
    PaymentThresholdCondition.typeDescriptor,
  ],
);

// main.dart
final context = RegistryDeserializationContext(
  descriptors: [
    DefaultWorkflowDescriptor(),
    notificationExecutors,
    paymentExecutors,
  ],
);

final engine = WorkflowEngine(
  context: context,
  storage: InMemoryStorage(context: context),
);

Next Steps