User Tasks
User tasks represent human interaction points in a workflow. They create inbox items and wait for user completion.
Definition
dart
builder.userTask(
'approvalDecision',
name: 'Approval Decision',
signal: 'approval_signal', // Signal to wait for
title: 'Approve {{entityType}}: {{entityName}}',
description: 'Review and approve this submission',
assignToRole: 'approver_role',
storeAs: 'approvalResult',
);User Task Properties
| Property | Description |
|---|---|
signal | Signal name to wait for (auto-generated if omitted) |
schemaType | Type identifier (references UserTaskExecutor) |
title | Display title (supports template variables) |
description | Detailed description |
assignToRole | Role-based assignment |
assignToUser | Direct user assignment |
storeAs | Key for storing user's response |
priority | Task priority (low, normal, high, urgent) |
User Task Flow
Step Details:
- Token arrives at UserTask node
UserTaskNodeExecutor.execute()- usesUserTaskExecutorif provided, otherwiseDefaultUserTaskExecutor- Executor returns
WaitForUserTaskResultwith title, assignee, signal name - Engine creates
UserTaskInstance(idempotent - checks if active task exists) - Workflow status becomes
waitingForSignal - User completes task via UI
engine.sendSignal(node: nodeId)continues workflow
UserTaskInstance
Created when workflow reaches a user task:
dart
class UserTaskInstance {
final String id;
final String workflowInstanceId;
final String nodeId;
final String schemaType; // Executor type for deserialization
final String title;
final String? description;
final String? signalName;
final String? assignedToRoleId;
final String? assignedToUserId;
final String? assignedToGroupId;
final TaskStatus status;
final UserTaskPriority priority;
final Map<String, dynamic> input;
final Map<String, dynamic>? output;
final DateTime createdAt;
final DateTime updatedAt;
final DateTime? dueAt;
final DateTime? completedAt;
final String? completedBy;
}UserTaskExecutor
Implement custom executors for different task types:
dart
class ApprovalTaskExecutor extends UserTaskExecutor {
static const _schemaType = 'userTask.approval';
@override
String get schemaType => _schemaType;
@override
String get name => 'Approval Task';
@override
Future<WaitForUserTaskResult> execute(ExecutionContext context) async {
// Use get<T> for typed access to previous node output
final entityName = context.get<String>('entityName') ?? 'Item';
final approverRoleId = context.get<String>('approverRoleId');
final entityId = context.get<String>('entityId');
final entityType = context.get<String>('entityType');
final submittedBy = context.get<String>('submittedBy');
return WaitForUserTaskResult(
signalName: context.signalName!,
config: UserTaskConfiguration(
title: 'Approve: $entityName',
description: 'Please review and approve this request.',
schemaType: schemaType,
assignedToRoleId: approverRoleId,
priority: UserTaskPriority.high,
input: {
'entityId': entityId,
'entityType': entityType,
'submittedBy': submittedBy,
},
),
);
}
static final typeDescriptor = TypeDescriptor<UserTaskExecutor>(
schemaType: _schemaType,
fromJson: (_) => ApprovalTaskExecutor(),
title: 'Approval Task',
);
}
// Register via descriptor
final descriptor = WorkflowDescriptor(
title: 'Approval Tasks',
userTasks: [ApprovalTaskExecutor.typeDescriptor],
);Querying User Tasks
For Inbox UI
dart
// Get tasks assigned to a role
final tasks = await storage.userTaskInstances.findByRole('approver_role');
// Get tasks assigned to a user
final myTasks = await storage.userTaskInstances.findByUserId(currentUserId);
// Get pending tasks
final pending = await storage.userTaskInstances.findByStatus(
UserTaskStatus.pending,
);Task Details
dart
final task = await storage.userTaskInstances.getById(taskId);
// Display in UI
Text(task.title);
Text(task.description ?? '');
Text('Priority: ${task.priority}');
Text('Due: ${task.dueDate}');
// Show input data for user context
JsonView(task.input);Completing User Tasks
When a user completes a task, send a signal:
dart
// In your UI/API
Future<void> completeApproval(String taskId, String decision, String comments) async {
final task = await storage.userTaskInstances.getById(taskId);
await engine.sendSignal(
workflowInstanceId: task.workflowInstanceId,
node: task.nodeId, // The user task node ID
payload: {
'decision': decision,
'comments': comments,
'completedBy': currentUserId,
'completedAt': DateTime.now().toIso8601String(),
},
);
}User Task Status
| Status | Description |
|---|---|
pending | Created, waiting to be claimed |
claimed | User has taken ownership |
completed | User submitted response |
cancelled | Task was cancelled |
expired | Past due date |
Assignment Patterns
Role-Based
dart
builder.userTask('approval',
assignToRole: 'finance_approvers',
...
);Direct User
dart
builder.userTask('review',
assignToUser: specificUserId,
...
);Dynamic Assignment
dart
class DynamicAssignmentExecutor extends UserTaskExecutor {
static const _schemaType = 'userTask.dynamic';
@override
String get schemaType => _schemaType;
@override
String get name => 'Dynamic Assignment';
@override
Future<WaitForUserTaskResult> execute(ExecutionContext context) async {
// Determine assignee from workflow data using get<T>
final submitterId = context.get<String>('submitterId');
final managerId = await getManagerId(submitterId);
return WaitForUserTaskResult(
signalName: context.signalName!,
config: UserTaskConfiguration(
title: 'Review Required',
schemaType: schemaType,
assignedToUserId: managerId,
),
);
}
}Best Practices
- Use meaningful titles - Users scan inbox by title
- Include context in input - Help users make decisions
- Set appropriate priorities - Guide user attention
- Use due dates - Track SLAs
- Namespace with
storeAs- Keep responses organized
Next Steps
- Signal Wait - External triggers
- User Task Executors - Executor implementation
- Approval Workflows - Approval patterns