Author a durable workflow
Long-running orchestration that survives process restarts, network partitions, and provider outages.
Workflows are the right tool when a single business operation spans multiple external calls, waits, retries, or compensations — and you need it to complete even if the process restarts halfway through. Vadyl's workflow engine journals every step, so on replay it skips work already completed and resumes from the next unfinished step.
Defining a workflow
// src/workflows/fulfillOrder.ts
import { workflow } from "@vadyl/sdk";
export default workflow.define("fulfillOrder", async (ctx, input: { orderId: string }) => {
const order = await ctx.step("load-order", () =>
ctx.entities.Order.read(input.orderId)
);
// Charge the customer
const charge = await ctx.step("charge", async () => {
return ctx.connections.stripe.createCharge({
amount: order.total, currency: order.currency, customer: order.customerId,
});
});
ctx.compensate(() => ctx.connections.stripe.refund({ id: charge.id }));
// Reserve inventory
await ctx.step("reserve", () =>
ctx.connections.inventory.reserve({ orderId: order.id, items: order.items })
);
ctx.compensate(() =>
ctx.connections.inventory.release({ orderId: order.id })
);
// Wait for the warehouse signal (durable wait — survives restart)
const shipped = await ctx.waitForSignal<{ trackingNumber: string }>("shipped", {
timeout: "72h",
});
// Update the order
await ctx.step("mark-fulfilled", () =>
ctx.entities.Order.update(order.id, {
status: "fulfilled",
trackingNumber: shipped.trackingNumber,
})
);
// Notify the customer
await ctx.step("notify", () =>
ctx.connections.email.send({
to: order.customerEmail, template: "shipped", data: { order, shipped },
})
);
return { ok: true };
});The three primitives
ctx.step(name, fn)— journaled, at-most-once per logical step. The first successful run is recorded; replays skip it and return the recorded result.ctx.waitForSignal(name, opts)— durably suspends the workflow until an external signal arrives. The process can restart, scale down, or die — the wait survives.ctx.compensate(fn)— registers a saga compensation. If the workflow fails after this point, registered compensations run in LIFO order to undo the prior side effects.
Determinism
Workflow code must be deterministic so replay can reproduce the history. Vadyl provides journaled accessors:
ctx.now()— replay-stable timestamp (records on first call, replays the same value)ctx.random()— replay-stable randomctx.uuid()— replay-stable UUID
Calling Date.now(), Math.random(), orcrypto.randomUUID() directly from inside a workflow is rejected at compile time.
Sending a signal
// From a webhook handler when the warehouse calls back:
await ctx.workflows.fulfillOrder.signal(workflowId, "shipped", {
trackingNumber: req.body.tracking,
});Pinning to a publication
Each workflow instance is pinned to the runtime publication version it started under. If you deploy a new version of the workflow code while an instance is running, the in-flight instance continues on the old version — guaranteeing replay determinism. New instances start under the new version. You can opt-in to migrate in-flight instances with an explicit migration step.
Failure and retry
Steps that throw are retried with exponential backoff per the workflow's retry policy. After exhausting retries, registered compensations run in reverse order (LIFO within priority). Each compensation is itself idempotent and journaled, so partial-failure recovery is safe.
Inspecting runs
vadyl runs list --workflow fulfillOrder
vadyl runs show <runId> # full step-by-step history
vadyl runs replay <runId> --dry # see what would happen on replay