UserTaskExecutor API
Base class for user task executors. User tasks require human interaction and always wait for a signal.
UserTaskExecutor
abstractClass Signature
TypedUserTaskExecutor
abstractClass Signature
Type-safe base class with compile-time safety for input.
WaitForUserTaskResult
Class Signature
Properties
| Property | Type | Default | Description |
|---|---|---|---|
signalName | String | -- | Signal to wait for (inherited from WaitForSignalResult) |
timeout | Duration? | -- | Optional timeout (inherited) |
config | UserTaskConfiguration | -- | Configuration for the user task instance |
effects | List<WorkflowEffect> | const [] | Effects to apply before creating the user task |
Result returned by user task executors.
UserTaskConfiguration
Class Signature
Constructor Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
title | String | Yes | Task title shown to user |
schemaType | String | Yes | Executor type identifier |
description | String? | No | Task description |
assignedToUserId | String? | No | Specific user assignment |
assignedToRoleId | String? | No | Role-based assignment |
assignedToGroupId | String? | No | Group-based assignment |
priority | UserTaskPriority | No | Task priority level |
dueAt | DateTime? | No | Optional due date |
input | Map<String, dynamic>? | No | Input data passed to the task UI |
storeAs | String? | No | Output key for task response |
Specification for creating a user task instance.
UserTaskPriority
Class Signature
Key Difference from TaskExecutor
dart
// TaskExecutor - returns immediately with result
abstract class TaskExecutor {
Future<TaskResult> execute(ExecutionContext context);
// Returns: TaskSuccess or TaskFailure
}
// UserTaskExecutor - ALWAYS waits for human signal
abstract class UserTaskExecutor {
Future<WaitForUserTaskResult> execute(ExecutionContext context);
// Returns: ALWAYS WaitForUserTaskResult
}Examples
Basic UserTaskExecutor
dart
class ApprovalTaskExecutor extends UserTaskExecutor {
static const _schemaType = 'userTask.approval';
static final typeDescriptor = TypeDescriptor<UserTaskExecutor>(
schemaType: _schemaType,
fromJson: (json) => ApprovalTaskExecutor(),
title: 'Approval Task',
);
@override
String get schemaType => _schemaType;
@override
String get name => 'Approval Task';
@override
Future<WaitForUserTaskResult> execute(ExecutionContext context) async {
// Get data from previous node output
final entityType = context.getRequired<String>('entityType');
final roleId = context.getRequired<String>('approverRoleId');
return WaitForUserTaskResult(
signalName: context.signalName!,
config: UserTaskConfiguration(
title: 'Approve $entityType',
description: 'Please review and approve this ${entityType.toLowerCase()}',
schemaType: schemaType,
assignedToRoleId: roleId,
priority: UserTaskPriority.high,
input: context.input,
),
);
}
}With Effects (Cancel Previous Tasks)
dart
class RevisionTaskExecutor extends UserTaskExecutor {
@override
String get schemaType => 'userTask.revision';
@override
String get name => 'Revision Task';
@override
Future<WaitForUserTaskResult> execute(ExecutionContext context) async {
final submitterId = context.getRequired<String>('submitterId');
return WaitForUserTaskResult(
signalName: context.signalName!,
config: UserTaskConfiguration(
title: 'Revise Document',
schemaType: schemaType,
assignedToUserId: submitterId,
),
// Cancel any pending approval tasks before creating revision task
effects: [const CancelUserTasksEffect()],
);
}
}TypedUserTaskExecutor
dart
@JsonSerializable()
class ApprovalTaskInput {
final String entityId;
final String entityType;
final int currentLevel;
final String currentLevelRoleId;
ApprovalTaskInput({
required this.entityId,
required this.entityType,
required this.currentLevel,
required this.currentLevelRoleId,
});
factory ApprovalTaskInput.fromJson(Map<String, dynamic> json) =>
_$ApprovalTaskInputFromJson(json);
Map<String, dynamic> toJson() => _$ApprovalTaskInputToJson(this);
}
class ApprovalTaskExecutor
extends TypedUserTaskExecutor<ApprovalTaskInput> {
static const _schemaType = 'userTask.approval';
static final typeDescriptor = TypeDescriptor<UserTaskExecutor>(
schemaType: _schemaType,
fromJson: (json) => ApprovalTaskExecutor(),
title: 'Approval Task',
);
@override
String get schemaType => _schemaType;
@override
String get name => 'Approval Task';
@override
ApprovalTaskInput fromInput(Map<String, dynamic> input) =>
ApprovalTaskInput.fromJson(input);
@override
Future<WaitForUserTaskResult> executeTyped(
ApprovalTaskInput input,
ExecutionContext context,
) async {
// Fully typed access to input!
return WaitForUserTaskResult(
signalName: context.signalName!,
config: UserTaskConfiguration(
title: 'Approve ${input.entityType}: Level ${input.currentLevel}',
description: 'Review and approve this ${input.entityType}',
schemaType: schemaType,
assignedToRoleId: input.currentLevelRoleId,
priority: UserTaskPriority.high,
input: input.toJson(), // Pass typed data for UI
storeAs: 'approvalDecision',
),
);
}
}With Completion Processing
dart
class ReviewTaskExecutor extends UserTaskExecutor {
@override
String get schemaType => 'userTask.review';
@override
String get name => 'Review Task';
@override
Future<WaitForUserTaskResult> execute(ExecutionContext context) async {
return WaitForUserTaskResult(
signalName: context.signalName!,
config: UserTaskConfiguration(
title: 'Review Document',
schemaType: schemaType,
assignedToRoleId: 'reviewers',
storeAs: 'reviewResult',
),
);
}
@override
UserTaskCompletionResult processCompletion(
Map<String, dynamic> userOutput,
Map<String, dynamic> taskInput,
ExecutionContext context,
) {
// Transform or enrich user output before storing
return UserTaskCompletionResult.passThrough({
...userOutput,
'processedAt': DateTime.now().toIso8601String(),
'reviewComplete': true,
});
}
}DefaultUserTaskExecutor
Built-in executor that resolves template variables from node configuration:
dart
class DefaultUserTaskExecutor extends UserTaskExecutor {
static const _schemaType = 'userTask.default';
@override
String get schemaType => _schemaType;
@override
String get name => 'Default User Task';
@override
Future<WaitForUserTaskResult> execute(ExecutionContext context) async {
// Parses UserTaskNodeConfiguration and resolves {{variable}} templates
final nodeConfig = UserTaskNodeConfiguration.fromJson(context.config);
// Templates like {{entityType}} are resolved from workflow output
final title = _resolveTemplate(nodeConfig.title, context.input);
return WaitForUserTaskResult(
signalName: context.signalName!,
config: UserTaskConfiguration(
title: title ?? 'User Task',
schemaType: nodeConfig.schemaType,
assignedToRoleId: _resolveTemplate(nodeConfig.assignToRole, context.input),
// ... other resolved fields
),
);
}
}Registration
dart
final descriptor = WorkflowDescriptor(
title: 'User Tasks',
userTasks: [
ApprovalTaskExecutor.typeDescriptor,
ReviewTaskExecutor.typeDescriptor,
],
);
// Create context with descriptors
final context = RegistryTypeResolver(
descriptors: [DefaultWorkflowDescriptor(), descriptor],
);
// Create engine with context and storage
final engine = WorkflowEngine(
context: context,
storage: InMemoryStorage(),
executionMode: ExecutionMode.production,
);
await engine.initialize();Comparison
| Feature | UserTaskExecutor | TypedUserTaskExecutor |
|---|---|---|
| Input access | context.get<T>('key') | input.key |
| Return type | WaitForUserTaskResult | WaitForUserTaskResult |
| Type safety | Runtime | Compile-time |
| Serialization | Manual | Via fromInput |
| Effects | Yes, via effects list | Same |
Best Practices
- Use namespaced schema types -
userTask.domain.actionformat - Prefer TypedUserTaskExecutor for complex input
- Set appropriate priority based on urgency
- Include context in input for UI rendering
- Use effects to cancel previous tasks - Before creating revision/resubmit tasks
- Use
get<T>for previous node output - This is your primary data source - Use
getInitial<T>for original workflow input - For configuration that persists
See Also
- User Task Executors - Detailed guide
- Workflow Effects - Effect types reference
- Type Registries - Registration patterns
- WorkflowDescriptor - Registration API