Awaiting Signals
Signal handling is essential for workflows that need to pause and wait for external events before proceeding. This operation allows workflows to respond to user actions, system notifications, or other asynchronous events, making it ideal for human-in-the-loop processes or integration with external systems.
val MySignal = SignalDef[MyRequest, MyResponse]()
val doThings: WIO[MyState, Nothing, MyState] =
WIO
.handleSignal(MySignal)
.using[MyState]
.withSideEffects((state, request) => IO(MyEvent()))
.handleEvent((state, event) => state)
.produceResponse((state, event, request) => MyResponse())
.autoNamed
Rendering Outputs
- Flowchart
- BPMN
- Model
- Debug
{
"meta" : {
"signalName" : "My Request",
"operationName" : "Do Things",
"error" : null
},
"_type" : "HandleSignal"
}
[HandleSignal](Do Things) Signal: My Request
Drafting Support
Awaiting signals come with drafting support.
val awaitApproval = WIO.draft.signal("Approval Required", error = "Rejected")
Rendering Outputs
- Flowchart
- BPMN
- Model
- Debug
{
"meta" : {
"signalName" : "Approval Required",
"operationName" : null,
"error" : {
"name" : "Rejected"
}
},
"_type" : "HandleSignal"
}
[HandleSignal](no-name) Signal: Approval Required
Unhandled Signals
The Workflows4s API allows arbitrary signals to be sent to a workflow instance. While this provides flexibility, it also requires developers to be disciplined and diligent, with thorough tests to ensure only valid signals are passed.
Technically, it is possible to constrain signals to a specific ADT, similar to how state and events are managed. However, doing so would introduce significant complexity without fully eliminating unhandled signals. By design, signals are only expected at specific moments in a workflow's lifecycle, meaning unhandled signals can still occur outside those windows.
Signal Redelivery
In distributed systems, a caller may fail to receive a response even though the signal was successfully processed. For example, a network timeout might occur after the workflow handled the signal but before the response reached the caller. When the caller retries, Workflows4s automatically detects that the signal was already processed and returns the original response without re-executing the side effects.
How It Works
- When a signal is first delivered, the workflow executes the side effects and stores the resulting event
- If the same signal type is delivered again to an already-processed signal handler, Workflows4s detects this by finding the stored event
- The response is regenerated using the stored event (via
produceResponse), without running side effects again
Edge Cases
Multiple signal handlers of the same type: If your workflow has multiple handlers for the same signal type (e.g., in
a loop or sequential steps), redelivery matches the most recently executed handler. Use
the clashing-signals linter rule to detect potentially ambiguous signal configurations.
Validating request identity: The redelivery mechanism matches by signal type, not by request content. If you need to
verify that a redelivered request matches the original, include relevant data in your event and compare it in the
produceResponse function:
val handleWithRedeliveryValidation: WIO[MyState, Nothing, MyState] =
WIO
.handleSignal(MySignal)
.using[MyState]
.withSideEffects((state, request) => processRequest(request).map(r => MySignalEvent(request.id, r)))
.handleEvent((state, event) => state.copy(result = Some(event.result)))
.produceResponse { (state, event, currentRequest) =>
// Compare original request (stored in event) with current request
if event.originalRequestId != currentRequest.id then {
// Handle mismatch - could return error response or original result
RedeliveryMismatchResponse(expected = event.originalRequestId, received = currentRequest.id)
} else {
SuccessResponse(event.result)
}
}
.done