Signal Wait Nodes
Signal wait nodes pause workflow execution until an external signal is received. They're the foundation for async operations and external integrations.
Definition
dart
builder.signalWait(
'awaitPayment',
name: 'Await Payment Confirmation',
signal: 'payment_completed',
storeAs: 'paymentResult',
);Signal Wait Properties
| Property | Description |
|---|---|
signal | Name of signal to wait for |
storeAs | Key for storing signal payload in output |
name | Human-readable description |
How It Works
Step Details:
- Token arrives at SignalWait node
SignalWaitNodeExecutorreturnsWaitForSignalResultwith signalName and storeAs path- Engine updates status to
waitingForSignal, token stays at current node, instance is persisted - External system sends signal (seconds, minutes, or days later)
engine.sendSignal()finds instance, matches signal, callsexecutor.onSignalReceived()- Payload stored at
output[storeAs] - Executor returns
ContinueResult, workflow continues
Sending Signals
dart
await engine.sendSignal(
workflowInstanceId: instanceId,
node: 'awaitPayment', // The node ID waiting for the signal
payload: {
'transactionId': 'TXN-123456',
'amount': 99.99,
'currency': 'USD',
'paidAt': DateTime.now().toIso8601String(),
},
);Use Cases
External Webhooks
dart
// Workflow waits for payment
builder.signalWait('awaitPayment',
signal: 'stripe_payment_success',
storeAs: 'paymentData',
);
// POST /webhooks/stripe
Future<void> handleStripeWebhook(StripeEvent event) async {
if (event.type == 'payment_intent.succeeded') {
final instanceId = event.metadata['workflowInstanceId'];
await engine.sendSignal(
workflowInstanceId: instanceId,
node: 'awaitPayment', // The node ID
payload: event.data,
);
}
}Email Verification
dart
builder.signalWait('awaitEmailVerification',
signal: 'email_verified',
storeAs: 'verification',
);
// GET /verify/:token
Future<void> verifyEmail(String token) async {
final data = decodeVerificationToken(token);
await engine.sendSignal(
workflowInstanceId: data.workflowInstanceId,
node: 'awaitEmailVerification', // The node ID
payload: {'verifiedAt': DateTime.now().toIso8601String()},
);
}Document Upload
dart
builder.signalWait('awaitDocuments',
signal: 'documents_uploaded',
storeAs: 'documents',
);
// In file upload handler
Future<void> handleUpload(List<File> files, String instanceId) async {
final uploadedFiles = await fileService.upload(files);
await engine.sendSignal(
workflowInstanceId: instanceId,
node: 'awaitDocuments', // The node ID
payload: {
'files': uploadedFiles.map((f) => f.toJson()).toList(),
'uploadedAt': DateTime.now().toIso8601String(),
},
);
}Timer Events
dart
builder.signalWait('awaitDeadline',
signal: 'deadline_reached',
storeAs: 'deadlineResult',
);
// Scheduled job sends signal when deadline is reached
class DeadlineJob {
Future<void> checkDeadlines() async {
final expiredInstances = await findExpiredInstances();
for (final instance in expiredInstances) {
await engine.sendSignal(
workflowInstanceId: instance.id,
node: 'awaitDeadline', // The node ID
payload: {'expired': true, 'expiredAt': DateTime.now().toIso8601String()},
);
}
}
}Signal vs User Task
| Aspect | Signal Wait | User Task |
|---|---|---|
| Creates inbox item | No | Yes |
| Assignment | N/A | Role or user |
| UI integration | Custom | Standard inbox |
| Best for | External systems, async ops | Human decisions |
Routing After Signal
Often combined with gateways:
dart
builder
.signalWait('awaitDecision', signal: 'decision', storeAs: 'result')
.oneOf('routeDecision', [
Branch.whenFn(
(o) => o['result']?['approved'] == true,
then: 'processApproved',
),
Branch.otherwise(then: 'processRejected'),
])
.connect('awaitDecision', 'routeDecision');Best Practices
- Use descriptive signal names -
order_payment_receivednotsignal1 - Always use
storeAs- Namespace the payload - Include timestamps - In payload for audit
- Validate payload - In downstream tasks
- Handle missing signals - Timeout patterns