Examples
Self-contained workflows that show Skipper's features in context. Each focuses on a single idea; follow the linked guides for the full picture. Toggle Kotlin or Java on any snippet.
Peer-to-peer transfer
A money transfer that requires manual approval over a threshold, then debits and credits a ledger. The workflow hibernates while it waits for the approval signal.
class PeerToPeerTransfer : Workflow() {
private val ledger = actions<LedgerActions>()
@StateField var isApproved: Boolean? = null
@WorkflowMethod
suspend fun transfer(request: TransferRequest): Boolean {
if (request.amount > 1000) {
// Hibernate until approved — or up to a day — using zero resources.
val approved = waitUntil({ isApproved != null }, Duration.ofDays(1))
if (!approved || isApproved != true) return false
}
ledger.debit(request.from, request.amount)
ledger.credit(request.to, request.amount)
return true
}
@SignalMethod
fun approve(approved: Boolean) { this.isApproved = approved }
} public class PeerToPeerTransfer extends Workflow {
private final LedgerActions ledger = actions(LedgerActions.class);
@StateField Boolean isApproved;
@WorkflowMethod(returnType = Boolean.class)
public CompletableFuture<Boolean> transfer(TransferRequest request) {
if (request.getAmount() > 1000) {
// Hibernate until approved — or up to a day — using zero resources.
boolean approved = waitUntil(() -> isApproved != null, Duration.ofDays(1));
if (!approved || !isApproved) {
return CompletableFuture.completedFuture(false);
}
}
ledger.debit(request.getFrom(), request.getAmount());
ledger.credit(request.getTo(), request.getAmount());
return CompletableFuture.completedFuture(true);
}
@SignalMethod
public void approve(boolean approved) { this.isApproved = approved; }
} Key topics: Signals & Queries · Workflow API
Order processing with compensation
Charge, reserve inventory, and ship. If any step fails, Skipper automatically runs the compensation methods for completed actions in reverse order — the saga pattern.
class OrderWorkflow : Workflow() {
private val payments = actions<PaymentActions>()
private val inventory = actions<InventoryActions>()
private val shipping = actions<ShippingActions>()
@WorkflowMethod
suspend fun process(order: OrderRequest): OrderResult {
payments.charge(order) // compensated by refund if a later step fails
inventory.reserve(order) // compensated by release if a later step fails
shipping.schedule(order) // if this throws, the two above are undone
return OrderResult.completed(order.id)
}
}
class PaymentActions : Actions() {
@Execute
suspend fun charge(order: OrderRequest): String = gateway.charge(order)
@Compensate(forExecute = "charge")
suspend fun refund(order: OrderRequest, chargeId: String) = gateway.refund(chargeId)
} public class OrderWorkflow extends Workflow {
private final PaymentActions payments = actions(PaymentActions.class);
private final InventoryActions inventory = actions(InventoryActions.class);
private final ShippingActions shipping = actions(ShippingActions.class);
@WorkflowMethod(returnType = OrderResult.class)
public CompletableFuture<OrderResult> process(OrderRequest order) {
payments.charge(order); // compensated by refund if a later step fails
inventory.reserve(order); // compensated by release if a later step fails
shipping.schedule(order); // if this throws, the two above are undone
return CompletableFuture.completedFuture(OrderResult.completed(order.getId()));
}
}
public class PaymentActions extends Actions {
@Execute
public String charge(OrderRequest order) { return gateway.charge(order); }
@Compensate(forExecute = "charge")
public void refund(OrderRequest order, String chargeId) { gateway.refund(chargeId); }
} Key topics: Compensation (Saga)
Approval workflow
A document review that pauses for up to a week waiting on a human decision delivered by a signal, and exposes its pending state through a query.
class DocumentApproval : Workflow() {
@StateField var decision: Decision? = null
@WorkflowMethod
suspend fun review(doc: Document): Outcome {
// Wait up to a week for a human decision; no resources held while waiting.
val received = waitUntil({ decision != null }, Duration.ofDays(7))
return when {
!received -> Outcome.EXPIRED
decision == Decision.APPROVED -> Outcome.APPROVED
else -> Outcome.REJECTED
}
}
@SignalMethod fun submit(decision: Decision) { this.decision = decision }
@QueryMethod fun isPending(): Boolean = decision == null
} public class DocumentApproval extends Workflow {
@StateField Decision decision;
@WorkflowMethod(returnType = Outcome.class)
public CompletableFuture<Outcome> review(Document doc) {
// Wait up to a week for a human decision; no resources held while waiting.
boolean received = waitUntil(() -> decision != null, Duration.ofDays(7));
Outcome outcome;
if (!received) {
outcome = Outcome.EXPIRED;
} else if (decision == Decision.APPROVED) {
outcome = Outcome.APPROVED;
} else {
outcome = Outcome.REJECTED;
}
return CompletableFuture.completedFuture(outcome);
}
@SignalMethod
public void submit(Decision decision) { this.decision = decision; }
@QueryMethod
public boolean isPending() { return decision == null; }
} Key topics: Signals & Queries
Batch processing
Fan out over a list of items. Stable, per-item checkpoint names mean the loop resumes exactly where it left off after an interruption, without reprocessing finished items.
class BatchWorkflow : Workflow() {
private val processor = actions<ItemActions>()
@WorkflowMethod
suspend fun run(batch: Batch): BatchResult {
var processed = 0
batch.items.forEach { item ->
// A stable per-item checkpoint name keeps the loop safe across resumes.
processor.named("item-${item.id}").process(item)
processed++
}
return BatchResult(processed)
}
} public class BatchWorkflow extends Workflow {
private final ItemActions processor = actions(ItemActions.class);
@WorkflowMethod(returnType = BatchResult.class)
public CompletableFuture<BatchResult> run(Batch batch) {
int processed = 0;
for (Item item : batch.getItems()) {
// A stable per-item checkpoint name keeps the loop safe across resumes.
processor.named(ItemActions.class, "item-" + item.getId()).process(item);
processed++;
}
return CompletableFuture.completedFuture(new BatchResult(processed));
}
} Key topics: Versioning & Evolution