Skip to content

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 @StateField fields that the workflow's waitUntil conditions 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 @StateField values.
  • waitUntil { cond } — a durable hibernation primitive the workflow method calls; the workflow is suspended until cond evaluates 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 evaluates cond. 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 @StateField value (updated by the signal) is what makes the condition now true.
  • On timeout: the waitUntil(cond, timeout) variant returns true for "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

Last updated · 433 distilled / 1,256 read