Skip to main content

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

Drafting Support

Awaiting signals come with drafting support.

val awaitApproval = WIO.draft.signal("Approval Required", error = "Rejected")
Rendering Outputs

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

  1. When a signal is first delivered, the workflow executes the side effects and stores the resulting event
  2. If the same signal type is delivered again to an already-processed signal handler, Workflows4s detects this by finding the stored event
  3. 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