Skip to content

PATTERN Cited by 1 source

Workflow primitives as annotated classes

Pattern

Expose durable-workflow primitives (workflow method, state fields, signals, actions, compensation, checkpointing) as annotations on plain Java / Kotlin classes, with no codegen client, no DSL, and no separate workflow-definition format. Developers write a class, tag methods and fields with a small set of annotations, and the workflow engine handles the orchestration / persistence / replay behaviour invisibly. The class is the workflow definition.

Skipper (Airbnb, 2026-04-28) is the canonical wiki instance; its minimal annotation set is @WorkflowMethod, @StateField (/@StateParam), @SignalMethod, @Execute(checkpoint = true), @Compensate. (Source: sources/2026-04-28-airbnb-skipper-building-airbnbs-embedded-workflow-engine.)

Canonical instance

From Skipper's post:

// Define workflow logic as normal-looking Kotlin
class ChargeAndAccept : Workflow() {
  private val billing = actions<BillingActions>()
  private val reservations = actions<ReservationActions>()
  @StateParam var paymentCaptured = false

  @WorkflowMethod
  suspend fun execute(r0: Reservation): Reservation {
    val r1 = billing.charge(r0)                 // durable side-effect boundary
    waitUntil { paymentCaptured }               // durable wait (resumes after restart)
    return reservations.markAccepted(r1)
  }
}

// Side effects live in Actions; one annotation makes it checkpointable
class BillingActions : Actions() {
  @Execute(checkpoint = true)
  suspend fun charge(r: Reservation): Reservation =
    billingApi.chargeAsync(r.id, r.amount).await()
}

// Invoke it like a normal typed call (no codegen client)
val out = workflow<ChargeAndAccept>("reservation:${req.id}").execute(req)

(Source: sources/2026-04-28-airbnb-skipper-building-airbnbs-embedded-workflow-engine.)

The annotation set

A minimal-but-complete workflow contract:

Annotation Target Purpose
@WorkflowMethod Method on Workflow subclass The top-level entry point; workflow orchestration logic goes here.
@StateField / @StateParam Field on Workflow subclass Persisted across replays; mutated by signals or action returns.
@SignalMethod Method on Workflow subclass Externally-invoked handler that mutates state (see concepts/workflow-signal).
@Execute(checkpoint = true) Method on Actions subclass The action's result is persisted on success; replay short-circuits it.
@Compensate Method on Actions subclass Undo pair for an action; engine invokes in reverse order on non-retryable failure (see concepts/workflow-compensation-action).

Skipper's pitch is that this is the whole contract: there is no workflow DSL, no XML, no codegen-generated client stub, no separate test harness to bring up. You write a class; you annotate it; you call it.

Why this shape works

The Skipper post's explicit design framing:

"By exposing workflows and actions as plain Java/Kotlin classes, with a minimal, annotation-based contract, Skipper enables developers to write business logic that looks like business logic, not framework boilerplate. Primitives such as conditional waits, durable retries, and signals are available, but they surface through the same straightforward class structure, rather than requiring developers to learn a separate execution model or wire up infrastructure abstractions. The contract stays out of the way: a workflow method reads like the process it represents, and an action method looks like the service call it wraps." (Source: sources/2026-04-28-airbnb-skipper-building-airbnbs-embedded-workflow-engine.)

And:

"Skipper isn't the first system to offer 'write normal code, get durable execution' — other workflow engines do as well — but Skipper's focus is on removing the adoption friction: fewer required constructs and less setup, so teams that use Java/Kotlin can get to a first durable workflow with minimal ceremony." (Source: sources/2026-04-28-airbnb-skipper-building-airbnbs-embedded-workflow-engine.)

Test ergonomics follow directly

A workflow that's just a class is testable as just a class. The post's test example:

class ListingPublicationTest : SkipperTest() {
    @Test
    fun testListingPublication() {
        val workflow = workflowBuilder(ListingPublicationWorkflow::class.java).build()
        workflow.publishListing(ListingSubmission("listing-123", "host-456", true))
        helper.expectWorkflowToWait()       // workflow waits for photo review
        workflow.completePhotoReview(true)  // photos approved
        helper.waitForWorkflowToComplete()
        val result = workflow.getResult()
        assertEquals(PublicationResult.Status.SUCCESS, result.getStatus())
    }
}

No queues to stand up, no infrastructure to mock. Signals are method calls; state is fields. (Source: sources/2026-04-28-airbnb-skipper-building-airbnbs-embedded-workflow-engine.)

Contrast with DSL-based / codegen-based engines

Other workflow engines require a separate definition surface between the business-logic code and the engine:

  • AWS Step Functions — Amazon States Language (ASL) JSON definition, external to the service code. Business logic lives in Lambdas invoked from states; the state machine is defined elsewhere.
  • Early Temporal versions / Cadence — interface-based contracts with codegen-generated clients, more ceremony than Skipper's typed workflow<T>(...) call.
  • Argo Workflows / Argo — YAML workflow definitions separate from step implementations.

Skipper's distinguishing property is the zero-ceremony contract: the class is the definition. The annotation set is small enough to remember without reference docs, and there's nothing else to configure.

Applicability

Works well when:

  • Single-runtime environments. One language (JVM), one service boundary, one annotation system to teach to developers.
  • Business logic dominates. The saving from collapsing orchestration plumbing into the workflow class is largest when the orchestration logic itself is non-trivial.
  • Fast onboarding matters. Teams without dedicated platform engineers can adopt the engine self-service because the contract is discoverable and the ergonomic cost of getting it wrong is visible.

Limits:

  • Cross-language requirements. An annotation-based contract is language-specific; polyglot workflows need a different contract surface (codegen, protobuf, or a cluster-native API).
  • Cross-team workflow definitions. If a workflow must be defined / viewed / reviewed outside the host service's codebase (product managers authoring workflows; ops reviewing without JVM access), a separate definition surface may still be needed.

Seen in

  • sources/2026-04-28-airbnb-skipper-building-airbnbs-embedded-workflow-engine — canonical wiki disclosure. Skipper's 5-annotation contract (@WorkflowMethod, @StateField/@StateParam, @SignalMethod, @Execute(checkpoint = true), @Compensate) as the full workflow surface. No codegen, no DSL, no separate config surface. Workflow invocation (workflow<T>("id").execute(req)) and test harness (workflowBuilder<T>::class.java).build()) both stay inside the Kotlin/Java type system. Explicit design goal: "removing the adoption friction: fewer required constructs and less setup."
Last updated · 433 distilled / 1,256 read