CONCEPT Cited by 1 source
Workflow signal¶
Definition¶
A workflow signal is an externally-sent message that
mutates state fields inside a running or hibernating workflow,
unblocking waitUntil conditions and resuming workflow
execution. Signals decouple external events (a human
approval, a callback from another service, a timer firing, a
user submitting a form) from the workflow's forward progress,
letting the workflow method read naturally as a linear process
rather than a callback-driven queue consumer.
Skipper's @SignalMethod annotation
(Airbnb, 2026-04-28) is the canonical wiki instance. (Source:
sources/2026-04-28-airbnb-skipper-building-airbnbs-embedded-workflow-engine.)
The Skipper framing¶
From the ListingPublicationWorkflow example:
class ListingPublicationWorkflow : Workflow() {
@StateField val photosApproved: Boolean? = false
@WorkflowMethod
suspend fun publishListing(submission: ListingSubmission): PublicationResult {
val reviewId = actions.submitPhotosForReview(submission.getListingId())
val reviewTimedOut = waitUntil({ photosApproved != null }, Duration.ofHours(24))
...
}
@SignalMethod
fun completePhotoReview(approved: Boolean) {
photosApproved = approved
}
}
The post's framing:
"Signals (
@SignalMethod) let external events push data into a running workflow, updating@StateFieldfields that the workflow'swaitUntilconditions evaluate against." (Source: sources/2026-04-28-airbnb-skipper-building-airbnbs-embedded-workflow-engine.)
Three pieces interact:
@StateField— a persisted field the workflow method reads. Its current value survives replays.@SignalMethod— an externally-invoked method that mutates@StateFieldvalues.waitUntil { cond }— a durable hibernation primitive the workflow method calls; the workflow is suspended untilcondevaluates to true (or the optional timeout fires).
When completePhotoReview(true) is invoked externally, the
engine persists the state update, re-enters the workflow
method via replay, re-evaluates the waitUntil condition, and
continues past it.
Why signals exist¶
Without signals, an external event would need its own
orchestration logic: a queue consumer reads the event, looks
up which workflow is waiting for it, invokes something
workflow-specific, and coordinates a retry if the workflow
isn't ready. This is the "domain logic fragmentation"
Skipper's authors argue against. Signals collapse that
machinery into a single method on the workflow class, letting
the workflow author write waitUntil { photosApproved } as a
linear sequential statement.
Signals vs actions¶
Two ways a workflow can mutate its own state; they serve different purposes:
- Action — a side-effectful call the workflow makes outward (DB write, API call). The action's result is returned to the workflow and checkpointed.
- Signal — a message sent inward by external code.
The signal mutates a
@StateField; the workflow method reads it on replay.
An action is the workflow reaching out; a signal is the world reaching in. The two don't compete — a workflow that sends an email (action) and waits for the recipient to click an approval link (signal arrives via HTTP endpoint) uses both.
Durability property¶
Because signals mutate @StateField values (which are
persisted for replay),
signals survive workflow crashes and host-service restarts.
If a signal arrives while the host service is down, it's
either:
- Re-sent by the signal's origin (signal-producer retries), or
- Queued through a durable transport (message bus, HTTP retry) before reaching the engine.
Skipper's post doesn't deeply characterise signal delivery semantics; the embedded-library shape implies the host service's HTTP/RPC layer is where signal delivery is coordinated. Implicitly the contract is at-least-once signal delivery — the signal handler should tolerate duplicates.
Relationship to waitUntil hibernation¶
The waitUntil { cond } primitive is the pair to signals:
- On first entry to
waitUntil: the engine evaluatescond. If true, the workflow advances. If false, the engine persists current state and hibernates the workflow — it consumes no compute until a trigger event. - On signal arrival: the engine wakes the workflow,
replays the method from the start, and re-evaluates
waitUntil. Replay short-circuits already-completed actions via their checkpointed results; the new@StateFieldvalue (updated by the signal) is what makes the condition now true. - On timeout: the
waitUntil(cond, timeout)variant returnstruefor "timed out before cond became true" — the workflow can handle the timeout explicitly without needing its own timer machinery.
See the ListingPublicationWorkflow example for both arms (happy path: signal arrives with approval; timeout path: no signal within 24 h).
Test ergonomics¶
Because signals are plain methods on the workflow class,
testing a signal-driven workflow is straightforward — the
post shows the test directly invoking completePhotoReview
on the workflow under test:
workflow.publishListing(ListingSubmission("listing-123", "host-456", true))
helper.expectWorkflowToWait() // workflow waits for photo review
workflow.completePhotoReview(true) // photos approved
helper.waitForWorkflowToComplete()
(Source: sources/2026-04-28-airbnb-skipper-building-airbnbs-embedded-workflow-engine.)
No queue to stand up, no signal transport to mock — the signal is just a method call on the workflow object.
Seen in¶
- sources/2026-04-28-airbnb-skipper-building-airbnbs-embedded-workflow-engine
— canonical wiki disclosure.
@SignalMethodannotation as a first-class external-event primitive paired with@StateFieldpersistence and thewaitUntil { cond }durable hibernation primitive. Test ergonomics disclosure confirming the signal-as-plain-method shape.
Related¶
- systems/airbnb-skipper — canonical instance.
- systems/temporal — sibling system with a similar signal
primitive (Temporal's is called
SignalMethodtoo). - concepts/workflow-replay-from-checkpointed-actions — the replay mechanism that propagates signal-updated state into running workflows.
- concepts/workflow-determinism-requirement — why signals
mutate
@StateFieldfields rather than being read directly in the workflow method (determinism invariant). - concepts/durable-execution — parent property; signals are part of the primitive set durable workflow engines typically offer.
- concepts/embedded-workflow-engine — the shape Skipper instantiates; signals arrive via the host service's RPC/HTTP layer.
- patterns/workflow-primitives-as-annotated-classes — the programming-model pattern; signals are part of the annotation set that defines a workflow.