Skip to content

UserTaskExecutor API

Base class for user task executors. User tasks require human interaction and always wait for a signal.

UserTaskExecutor

abstract

Class Signature


TypedUserTaskExecutor

abstract

Class Signature

Type-safe base class with compile-time safety for input.


WaitForUserTaskResult

Class Signature

Properties

PropertyTypeDefaultDescription
signalNameString--Signal to wait for (inherited from WaitForSignalResult)
timeoutDuration?--Optional timeout (inherited)
configUserTaskConfiguration--Configuration for the user task instance
effectsList<WorkflowEffect>const []Effects to apply before creating the user task

Result returned by user task executors.


UserTaskConfiguration

Class Signature

Constructor Parameters

ParameterTypeRequiredDescription
titleStringYesTask title shown to user
schemaTypeStringYesExecutor type identifier
descriptionString?NoTask description
assignedToUserIdString?NoSpecific user assignment
assignedToRoleIdString?NoRole-based assignment
assignedToGroupIdString?NoGroup-based assignment
priorityUserTaskPriorityNoTask priority level
dueAtDateTime?NoOptional due date
inputMap<String, dynamic>?NoInput data passed to the task UI
storeAsString?NoOutput 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

FeatureUserTaskExecutorTypedUserTaskExecutor
Input accesscontext.get<T>('key')input.key
Return typeWaitForUserTaskResultWaitForUserTaskResult
Type safetyRuntimeCompile-time
SerializationManualVia fromInput
EffectsYes, via effects listSame

Best Practices

  1. Use namespaced schema types - userTask.domain.action format
  2. Prefer TypedUserTaskExecutor for complex input
  3. Set appropriate priority based on urgency
  4. Include context in input for UI rendering
  5. Use effects to cancel previous tasks - Before creating revision/resubmit tasks
  6. Use get<T> for previous node output - This is your primary data source
  7. Use getInitial<T> for original workflow input - For configuration that persists

See Also