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 fireContinueResult
Indicates successful execution with continuation to target node(s).
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
| Property | Type | Description |
|---|---|---|
targetNodeIds | List<String> | Target node IDs to continue to |
output | Map<String, dynamic> | Data to merge into workflow output |
effects | List<WorkflowEffect> | Side effects to process before continuing |
Usage Examples
Single target:
return ContinueResult.single('processData',
output: {'validated': true},
);Multiple targets (parallel execution):
return ContinueResult.all(['sendEmail', 'sendSms'],
output: {'notificationStarted': true},
);With effects:
return ContinueResult.single('nextNode',
output: {'processed': true},
effects: [
const CancelUserTasksEffect(),
RecordEventEffect(event: WorkflowEvent.custom(...)),
],
);WaitForSignalResult
Pauses workflow execution until an external signal is received.
class WaitForSignalResult extends NodeResult {
final String signalName;
final Duration? timeout;
const WaitForSignalResult({
required this.signalName,
this.timeout,
});
}Properties
| Property | Type | Description |
|---|---|---|
signalName | String | Name of signal to wait for |
timeout | Duration? | Optional timeout duration |
Usage Examples
Wait for external event:
return WaitForSignalResult(
signalName: 'payment_confirmation',
);Wait with timeout:
return WaitForSignalResult(
signalName: 'payment_confirmation',
timeout: Duration(hours: 1),
);WaitForUserTaskResult
Creates a user task and waits for completion. Extends WaitForSignalResult.
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
| Property | Type | Description |
|---|---|---|
signalName | String | Signal to wait for (inherited) |
timeout | Duration? | Optional timeout (inherited) |
config | UserTaskConfiguration | User task configuration |
effects | List<WorkflowEffect> | Effects to apply before creating the user task |
UserTaskConfiguration
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
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)
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.
class CompleteWorkflowResult extends NodeResult {
final Map<String, dynamic> output;
const CompleteWorkflowResult({this.output = const {}});
}Properties
| Property | Type | Description |
|---|---|---|
output | Map<String, dynamic> | Final workflow output |
Usage Example
return CompleteWorkflowResult(
output: {
'result': 'approved',
'completedAt': DateTime.now().toIso8601String(),
},
);FailWorkflowResult
Indicates workflow failed with an error.
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
| Property | Type | Description |
|---|---|---|
errorType | ErrorType | Error category (enum) |
message | String | Human-readable message |
isRetryable | bool | Whether retry might succeed |
details | Map<String, dynamic>? | Additional error context |
ErrorType Enum
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:
return FailWorkflowResult.validation(
'Required field missing: entityId',
details: {'field': 'entityId'},
);Timeout error (retryable):
return FailWorkflowResult.timeout(
'Payment gateway timeout',
details: {'gateway': 'stripe', 'attemptNumber': 2},
);Custom error:
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 failedTaskSuccess
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
| Property | Type | Description |
|---|---|---|
output | Map<String, dynamic> | Output data merged into workflow variables |
outputPortId | String? | Optional port ID for multi-path routing |
effects | List<WorkflowEffect> | Side effects to apply after task completion |
Usage
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
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
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
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
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:
// 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
// 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
return TaskFailure(
errorType: ErrorType.activity,
message: 'API call failed',
details: {
'endpoint': '/api/orders',
'statusCode': response.statusCode,
'responseBody': response.body,
},
);3. Mark Retryable Appropriately
// 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
// Good: Only necessary data
return TaskSuccess(output: {
'orderId': order.id,
'status': order.status,
});
// Avoid: Dumping entire objects
return TaskSuccess(output: order.toJson()); // 50+ fieldsWorkflowEffect
Effects allow executors to declaratively request side effects without directly calling mutation methods. This keeps executors pure and testable.
Common Effects
| Effect | Description |
|---|---|
SetOutputEffect | Set or merge output data |
CancelUserTasksEffect | Cancel pending user tasks |
RecordEventEffect | Record workflow event |
UpdateStatusEffect | Update workflow status |
CreateTokensEffect | Create new tokens for nodes |
See Workflow Effects for the complete reference.
See Also
- Task Executors - Executor implementation
- Workflow Effects - Effect types reference
- Error Handling - Error patterns
- Node Executors - Executor architecture