UserTaskExecutor API
Base class for user task executors. User tasks require human interaction and always wait for a signal.
UserTaskExecutor
dart
abstract class UserTaskExecutor implements SchemaItem {
/// Unique schema type identifier for registry lookup
/// Example: 'userTask.approval', 'userTask.review'
String get schemaType;
/// Human-readable name
String get name;
/// Optional description
String get description => '';
/// Execute - ALWAYS returns WaitForUserTaskResult
Future<WaitForUserTaskResult> execute(ExecutionContext context);
/// Process user's output before storing (optional override)
Map<String, dynamic> processOutput(Map<String, dynamic> userOutput) =>
userOutput;
/// Validate task configuration (optional)
List<String> validateConfig(Map<String, dynamic> config) => [];
}TypedUserTaskExecutor
Type-safe base class with compile-time safety for input:
dart
abstract class TypedUserTaskExecutor<TInput> extends UserTaskExecutor {
/// Deserialize input from workflow variables
TInput fromInput(Map<String, dynamic> input);
/// Type-safe execute method
Future<WaitForUserTaskResult> executeTyped(
TInput input,
ExecutionContext context,
);
}WaitForUserTaskResult
Result returned by user task executors:
dart
class WaitForUserTaskResult extends WaitForSignalResult {
const WaitForUserTaskResult({
required super.signalName,
required this.config,
super.timeout,
this.effects = const [], // NEW: Side effects before creating task
});
/// Configuration for the user task instance
final UserTaskConfiguration config;
/// Effects to apply before creating the user task
/// Common use: Cancel pending tasks before creating new ones
final List<WorkflowEffect> effects;
}UserTaskConfiguration
Specification for creating a user task instance:
dart
class UserTaskConfiguration {
const UserTaskConfiguration({
required this.title,
required this.schemaType,
this.description,
this.assignedToUserId,
this.assignedToRoleId,
this.assignedToGroupId,
this.priority = UserTaskPriority.normal,
this.dueDate,
this.input,
this.storeAs,
});
final String title;
final String schemaType;
final String? description;
final String? assignedToUserId;
final String? assignedToRoleId;
final String? assignedToGroupId;
final UserTaskPriority priority;
final DateTime? dueDate;
final Map<String, dynamic>? input;
final String? storeAs;
}UserTaskPriority
dart
enum UserTaskPriority {
low,
normal,
high,
urgent;
factory UserTaskPriority.fromString(String value);
}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 Output 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
Map<String, dynamic> processOutput(Map<String, dynamic> userOutput) {
// Transform or enrich user output before storing
return {
...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 = RegistryDeserializationContext(
descriptors: [DefaultWorkflowDescriptor(), descriptor],
);
// Create engine with context and storage
final engine = WorkflowEngine(
context: context,
storage: InMemoryStorage(context: context),
);
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