Concepts

Durable execution

Durable execution means a function can crash, its host can be rebooted, and it resumes exactly where it left off, with the same local variables, the same call stack, the same place in a loop, as if nothing had happened. The function’s progress is durable: it survives the process that was running it.

The problem it solves

Take the order-processing function from Getting started: reserve inventory, charge the card, ship the order. If the process dies after charging but before shipping, a normal program has lost its state. You are left reconstructing “where was I?” from database rows and hoping you do not double-charge.

Durable execution makes the function itself the source of truth. Each step is recorded, and on resume the engine replays the recorded steps to rebuild the function’s state, then continues from the first unfinished step.

Workflows and activities

A workflow is the async function itself. It runs inside the deterministic sandbox, and every await is a checkpoint: the engine records enough state there to resume the function later, on any worker, after a crash or a restart.

An activity is a unit of side-effecting work: the HTTP request, the database write, the payment charge. There is no network inside the sandbox, so workflow code hands that work out by name:

export const orderProcessing = durable(async (order: Order) => {
  const reserved = await activity("reserve_inventory", order.sku);
  const charged = await activity("charge_card", order.total);
  return { reserved, charged };
});

Each activity() call dispatches to a worker registered for that activity’s task type (hopskip:activity.charge_card@1). The worker runs your activity function outside the sandbox and returns the result; the result is recorded in the workflow’s history, and on replay the recorded result is fed back at the await instead of running the activity again. Implementing the activity side, and controlling retries, deadlines, and failures, is covered in Activities & retries.

How resume works

When a worker picks up a suspended workflow, it does not re-run your side effects. Instead:

  1. Core sends the nearest memory snapshot plus the events since it.
  2. The worker restores that snapshot and replays the events, feeding each recorded activity result back at the await that requested it.
  3. Execution continues from the first await that has no recorded result yet.

Completed activities return their recorded results during replay, so a resumed charge_card does not charge the card again. This depends on determinism: replaying the same events must reproduce the same execution. See Determinism & the sandbox.

Suspension is cheap

A durable function can stay suspended for milliseconds or months. A workflow that waits three days for a human approval is not holding a thread; it is a snapshot on disk with a timer registered in Core. When the timer fires, or the approval signal arrives, Core dispatches the workflow to any available worker, which restores the snapshot and continues.

This is why while (true) workflows (perpetual entities, subscription managers, long-lived agents) are ordinary in Hopskip. There is no ContinueAsNew to manage, because memory snapshots bound the log automatically.

What the runtime owns

Retries, heartbeats, and cancellation are handled by the engine rather than by your workflow code:

  • Heartbeating: injected as host calls at loop boundaries. You never write heartbeat().
  • Idempotency keys: a first-class declarative primitive, not a convention.
  • Retries, timeouts, backoff: declarative policy Core evaluates, which can change without redeploying workflows.
  • Cancellation: delivered as a checked signal at await points.

The workflow reads like ordinary business logic, and the machinery is enforced in one place instead of reimplemented per workflow.