Skip to content

Node Results

Result types returned by node executors during workflow execution.

Overview

Node executors return NodeResult to indicate execution outcome and control flow. The engine uses these results to determine the next steps in workflow execution.

NodeResult Hierarchy

NodeResult (sealed)
├── ContinueResult             → Move to target node(s)
├── WaitForSignalResult        → Pause for external signal
│   ├── WaitForUserTaskResult  → Create user task and wait
│   └── WaitForServiceTaskResult → Wait for service task routing
├── CompleteWorkflowResult     → Workflow finished successfully
├── FailWorkflowResult         → Workflow failed with error
├── WaitForJoinResult          → Wait for parallel branches
├── WaitForSubflowResult       → Start and wait for child workflow
├── FireAndForgetSubflowResult → Start async child workflow
└── WaitForTimerResult         → Wait for timer to fire

ContinueResult

Indicates successful execution with continuation to target node(s).

dart
class ContinueResult extends NodeResult {
  final List<String> targetNodeIds;
  final Map<String, dynamic> output;
  final List<WorkflowEffect> effects;  // NEW: Declarative side effects

  const ContinueResult({
    required this.targetNodeIds,
    this.output = const {},
    this.effects = const [],
  });

  // Factory for single target
  factory ContinueResult.single(
    String targetNodeId, {
    Map<String, dynamic> output = const {},
    List<WorkflowEffect> effects = const [],
  });

  // Factory for multiple targets (parallel gateways)
  factory ContinueResult.all(
    List<String> targetNodeIds, {
    Map<String, dynamic> output = const {},
    List<WorkflowEffect> effects = const [],
  });
}

Properties

PropertyTypeDescription
targetNodeIdsList<String>Target node IDs to continue to
outputMap<String, dynamic>Data to merge into workflow output
effectsList<WorkflowEffect>Side effects to process before continuing

Usage Examples

Single target:

dart
return ContinueResult.single('processData',
  output: {'validated': true},
);

Multiple targets (parallel execution):

dart
return ContinueResult.all(['sendEmail', 'sendSms'],
  output: {'notificationStarted': true},
);

With effects:

dart
return ContinueResult.single('nextNode',
  output: {'processed': true},
  effects: [
    const CancelUserTasksEffect(),
    RecordEventEffect(event: WorkflowEvent.custom(...)),
  ],
);

WaitForSignalResult

Pauses workflow execution until an external signal is received.

dart
class WaitForSignalResult extends NodeResult {
  final String signalName;
  final Duration? timeout;

  const WaitForSignalResult({
    required this.signalName,
    this.timeout,
  });
}

Properties

PropertyTypeDescription
signalNameStringName of signal to wait for
timeoutDuration?Optional timeout duration

Usage Examples

Wait for external event:

dart
return WaitForSignalResult(
  signalName: 'payment_confirmation',
);

Wait with timeout:

dart
return WaitForSignalResult(
  signalName: 'payment_confirmation',
  timeout: Duration(hours: 1),
);

WaitForUserTaskResult

Creates a user task and waits for completion. Extends WaitForSignalResult.

dart
class WaitForUserTaskResult extends WaitForSignalResult {
  final UserTaskConfiguration config;
  final List<WorkflowEffect> effects;  // NEW: Side effects before creating task

  const WaitForUserTaskResult({
    required super.signalName,
    super.timeout,
    required this.config,
    this.effects = const [],
  });
}

Properties

PropertyTypeDescription
signalNameStringSignal to wait for (inherited)
timeoutDuration?Optional timeout (inherited)
configUserTaskConfigurationUser task configuration
effectsList<WorkflowEffect>Effects to apply before creating the user task

UserTaskConfiguration

dart
class UserTaskConfiguration {
  const UserTaskConfiguration({
    required this.title,
    this.description,
    required this.schemaType,
    this.assignedToRoleId,
    this.assignedToUserId,
    this.assignedToGroupId,
    this.priority = UserTaskPriority.normal,
    this.dueAt,
    this.input = const {},
  });
}

Usage Example

dart
return WaitForUserTaskResult(
  signalName: 'approval_decision',
  config: UserTaskConfiguration(
    title: 'Review Expense Report',
    schemaType: 'approval',
    assignedToRoleId: 'managers',
    priority: UserTaskPriority.high,
    input: {'expenseId': expenseId},
  ),
);

With Effects (Cancel Previous Tasks)

dart
return WaitForUserTaskResult(
  signalName: 'revision_request',
  config: UserTaskConfiguration(
    title: 'Revise Document',
    schemaType: 'revision',
    assignedToUserId: submitterId,
  ),
  // Cancel any pending approval tasks before creating revision task
  effects: [
    const CancelUserTasksEffect(),
  ],
);

CompleteWorkflowResult

Indicates workflow has completed successfully.

dart
class CompleteWorkflowResult extends NodeResult {
  final Map<String, dynamic> output;

  const CompleteWorkflowResult({this.output = const {}});
}

Properties

PropertyTypeDescription
outputMap<String, dynamic>Final workflow output

Usage Example

dart
return CompleteWorkflowResult(
  output: {
    'result': 'approved',
    'completedAt': DateTime.now().toIso8601String(),
  },
);

FailWorkflowResult

Indicates workflow failed with an error.

dart
class FailWorkflowResult extends NodeResult {
  final ErrorType errorType;
  final String message;
  final bool isRetryable;
  final Map<String, dynamic>? details;

  const FailWorkflowResult({
    required this.errorType,
    required this.message,
    this.isRetryable = false,
    this.details,
  });

  // Factory constructors for common error types
  factory FailWorkflowResult.validation(String message, {Map<String, dynamic>? details});
  factory FailWorkflowResult.internal(String message, {Map<String, dynamic>? details});
  factory FailWorkflowResult.timeout(String message, {Map<String, dynamic>? details});
  factory FailWorkflowResult.custom({required ErrorType errorType, ...});
}

Properties

PropertyTypeDescription
errorTypeErrorTypeError category (enum)
messageStringHuman-readable message
isRetryableboolWhether retry might succeed
detailsMap<String, dynamic>?Additional error context

ErrorType Enum

dart
enum ErrorType {
  validation,  // Invalid input/configuration
  timeout,     // Operation timed out
  activity,    // Activity execution failed
  condition,   // Gateway condition evaluation failed
  internal,    // Internal engine error
  cancelled,   // Operation was cancelled
}

Usage Examples

Validation error:

dart
return FailWorkflowResult.validation(
  'Required field missing: entityId',
  details: {'field': 'entityId'},
);

Timeout error (retryable):

dart
return FailWorkflowResult.timeout(
  'Payment gateway timeout',
  details: {'gateway': 'stripe', 'attemptNumber': 2},
);

Custom error:

dart
return FailWorkflowResult.custom(
  errorType: ErrorType.activity,
  message: 'Task execution failed',
  isRetryable: true,
  details: {'taskId': taskId},
);

TaskResult

Result types returned specifically by task executors.

TaskResult Hierarchy

TaskResult (sealed)
├── TaskSuccess → Task completed successfully
└── TaskFailure → Task failed

TaskSuccess

dart
class TaskSuccess extends TaskResult {
  final Map<String, dynamic> output;
  final String? outputPortId;  // Optional port for routing
  final List<WorkflowEffect> effects;  // NEW: Declarative side effects

  const TaskSuccess({
    this.output = const {},
    this.outputPortId,
    this.effects = const [],
  });
}

Properties

PropertyTypeDescription
outputMap<String, dynamic>Output data merged into workflow variables
outputPortIdString?Optional port ID for multi-path routing
effectsList<WorkflowEffect>Side effects to apply after task completion

Usage

dart
Future<TaskResult> execute(ExecutionContext context) async {
  final result = await processData(context.input);

  return TaskSuccess(output: {
    'processedItems': result.items.length,
    'totalValue': result.totalValue,
    'processedAt': DateTime.now().toIso8601String(),
  });
}

With Effects

dart
Future<TaskResult> execute(ExecutionContext context) async {
  final result = await processData(context.input);

  return TaskSuccess(
    output: {'processed': true},
    effects: [
      // Cancel pending user tasks
      const CancelUserTasksEffect(),
      // Record audit event
      RecordEventEffect(
        event: WorkflowEvent.custom(
          instanceId: context.workflowInstanceId,
          nodeId: context.nodeId,
          eventType: 'data_processed',
          data: {'itemCount': result.items.length},
        ),
      ),
    ],
  );
}

TaskFailure

dart
class TaskFailure extends TaskResult {
  final ErrorType errorType;
  final String message;
  final Map<String, dynamic>? details;
  final bool isRetryable;

  const TaskFailure({
    required this.errorType,
    required this.message,
    this.details,
    this.isRetryable = true,  // Default is retryable
  });

  // Factory constructors
  factory TaskFailure.validation(String message, {Map<String, dynamic>? details});
  factory TaskFailure.internal(String message, {Map<String, dynamic>? details});
  factory TaskFailure.timeout(String message, {Map<String, dynamic>? details});
  factory TaskFailure.permanent({required ErrorType errorType, required String message, ...});
}

ErrorType Enum

dart
enum ErrorType {
  validation,  // Invalid input/configuration
  timeout,     // Operation timed out
  activity,    // Activity execution failed
  condition,   // Gateway condition evaluation failed
  internal,    // Internal engine error
  cancelled,   // Operation was cancelled
}

Usage

dart
Future<TaskResult> execute(ExecutionContext context) async {
  // Use get<T> for previous node output
  final userId = context.get<String>('userId');
  if (userId == null) {
    return TaskFailure(
      errorType: ErrorType.validation,
      message: 'userId is required',
    );
  }

  try {
    final user = await userService.findById(userId);
    if (user == null) {
      return TaskFailure(
        errorType: ErrorType.validation,
        message: 'User not found: $userId',
      );
    }

    return TaskSuccess(output: {'user': user.toJson()});
  } on TimeoutException {
    return TaskFailure(
      errorType: ErrorType.timeout,
      message: 'User service timeout',
      isRetryable: true,
    );
  } catch (e, st) {
    return TaskFailure(
      errorType: ErrorType.internal,
      message: 'Unexpected error: $e',
      details: {'stackTrace': st.toString()},
    );
  }
}

Conversion

Task executors return TaskResult, which the engine converts to NodeResult:

dart
// Conceptual - internal engine logic
if (taskResult is TaskSuccess) {
  // Success continues to next node(s) via edges
  return ContinueResult.single(nextNodeId, output: taskResult.output);
}

if (taskResult is TaskFailure) {
  // Failure fails the workflow
  return FailWorkflowResult(
    errorType: taskResult.errorType,
    message: taskResult.message,
    details: taskResult.details,
    isRetryable: taskResult.isRetryable,
  );
}

Best Practices

1. Return Specific Errors

dart
// Good: Specific error type
return TaskFailure(
  errorType: ErrorType.validation,
  message: 'Amount must be positive',
  code: 'INVALID_AMOUNT',
);

// Avoid: Generic error
return TaskFailure(
  errorType: ErrorType.internal,
  message: 'Error occurred',
);

2. Include Context

dart
return TaskFailure(
  errorType: ErrorType.activity,
  message: 'API call failed',
  details: {
    'endpoint': '/api/orders',
    'statusCode': response.statusCode,
    'responseBody': response.body,
  },
);

3. Mark Retryable Appropriately

dart
// Retryable: Transient failures
return TaskFailure(
  errorType: ErrorType.timeout,
  isRetryable: true,
);

// Not retryable: Permanent failures
return TaskFailure(
  errorType: ErrorType.validation,
  isRetryable: false,
);

4. Return Focused Output

dart
// Good: Only necessary data
return TaskSuccess(output: {
  'orderId': order.id,
  'status': order.status,
});

// Avoid: Dumping entire objects
return TaskSuccess(output: order.toJson()); // 50+ fields

WorkflowEffect

Effects allow executors to declaratively request side effects without directly calling mutation methods. This keeps executors pure and testable.

Common Effects

EffectDescription
SetOutputEffectSet or merge output data
CancelUserTasksEffectCancel pending user tasks
RecordEventEffectRecord workflow event
UpdateStatusEffectUpdate workflow status
CreateTokensEffectCreate new tokens for nodes

See Workflow Effects for the complete reference.

See Also