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
// 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 pointsEndEventNodeExecutor- workflow terminationSignalEventNodeExecutor- external signal handlingTimerEventNodeExecutor- timer-based waiting
Gateway Node Executors:
OneOfGatewayNodeExecutor- exclusive routing (XOR)AnyOfGatewayNodeExecutor- inclusive routing (OR)AllOfGatewayNodeExecutor- parallel routing (AND)
Task Node Executors:
TaskNodeExecutor- automated task executionUserTaskNodeExecutor- human task handlingSubflowNodeExecutor- nested workflow execution
Node Configurations:
TaskNodeConfiguration- for task nodesUserTaskNodeConfiguration- for user task nodesSignalWaitNodeConfiguration- for signal wait nodesTimerWaitNodeConfiguration- for timer wait nodesGatewayNodeConfiguration- for gateway nodesSubflowNodeConfiguration- for subflow nodesEmptyNodeConfiguration- for start/end nodes
Built-in Conditions:
ExpressionCondition- expression-based routingAlwaysTrueCondition- always matchesAlwaysFalseCondition- 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:
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:
userTasks: [
TypeDescriptor<UserTaskExecutor>(
schemaType: 'userTask.approval',
fromJson: ApprovalTaskExecutor.fromJson,
title: 'Approval Task',
),
]conditions
Condition executor type descriptors for complex routing:
conditions: [
TypeDescriptor<ConditionExecutor>(
schemaType: 'condition.amount.threshold',
fromJson: AmountThresholdCondition.fromJson,
title: 'Amount Threshold',
),
]nodeExecutors
Node executor instances (not type descriptors) for custom node execution:
nodeExecutors: [
MyCustomGatewayHandler(),
MyCustomTaskExecutor(),
]nodeConfigurations
Type descriptors for node configuration classes:
nodeConfigurations: [
TypeDescriptor<NodeConfiguration>(
schemaType: 'config.myCustomNode',
fromJson: MyCustomNodeConfiguration.fromJson,
),
]TypeDescriptor
TypeDescriptor connects a schemaType string to a Dart class:
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
| Property | Type | Description |
|---|---|---|
schemaType | String | Unique identifier (e.g., 'task.email.send') |
fromJson | Function | Factory to create instance from JSON |
title | String? | Human-readable name |
description | String? | What this executor does |
category | String? | Grouping for UI (e.g., 'Notifications') |
iconName | String? | Icon identifier for UI |
tags | List<String>? | Search/filter tags |
inputSchema | Map? | JSON Schema for input validation |
outputSchema | Map? | JSON Schema for output |
Descriptor Order Matters
Descriptors are processed in order. Later registrations override earlier ones for the same schemaType:
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:
final combined = baseDescriptor.merge(additionalDescriptor);Organizing Executors
For large applications, organize executors into domain-specific descriptors:
// 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
- Task Executors - Implement automated tasks
- User Task Executors - Implement human tasks
- Type Registries - How registries work
- WorkflowDescriptor API - Complete API reference