Durable execution (Temporal-style)
Wrap side-effectful steps so crashes, deploys, and retries replay from a step log instead of starting over.
When an agent crashes halfway through a 10-step workflow, the user
doesn't want you to start over β they want you to resume. Durable
execution gives you that with two primitives: a StepLogStore
(persistence) and a runner.step(id, fn) wrapper (short-circuits to
the recorded result if the id already exists in the log).
#Install
Ships with @agentskit/runtime.
#Wrap side effects in steps
import {
createDurableRunner,
createFileStepLog,
} from '@agentskit/runtime'
const store = await createFileStepLog('./runs/user-42.jsonl')
const runner = createDurableRunner({
store,
runId: 'user-42-onboard',
maxAttempts: 3,
retryDelayMs: 500,
})
await runner.step('create-account', async () => createAccount({ email }))
await runner.step('send-welcome', async () => sendEmail({ to: email, template: 'welcome' }))
await runner.step('charge-trial', async () => stripe.subscriptions.create({ ... }))Re-run the same code (same runId) after a crash β completed steps
short-circuit to their recorded values, only the remaining ones
execute.
#Stores
createInMemoryStepLog()β tests, single-process demos.createFileStepLog(path)β JSONL on disk, append-only, survives restarts.- Bring your own β anything implementing
{ append, get, list, clear? }works (Redis, Postgres, S3, etc).
#Step contract
A step is idempotent from the log's perspective: the fn does the
side effect, the recorded result captures everything downstream
steps need. Don't rely on global state outside the result.
const { userId } = await runner.step('create-account', async () => ({
userId: await createAccount(email),
}))
// Downstream steps use `userId` β NOT global `req.user.id`, which
// might not exist on a resumed run.
await runner.step('send-welcome', async () => sendEmail({ userId }))#Retries
maxAttempts: total attempts per step (default 1 β fail-fast).retryDelayMs: fixed backoff between attempts (default 0).- A step that fails all attempts is recorded with
status: 'failure'; replaying the samestepIdre-throws without running again (so you can diagnose without re-executing expensive failing work).
Call runner.reset() to wipe the log for a fresh retry.
#Observability
createDurableRunner({
store,
runId,
onEvent: e => logger.debug('durable', e),
})Events: step:replay, step:start, step:success, step:failure.
#See also
- HITL approvals β pause a step until a human approves.
- Background agents β run durable flows on a cron or webhook.
Explore nearby
- PeerRecipes
Copy-paste solutions grouped by theme. Every recipe end-to-end, runs as written.
- PeerCustom adapter
Wrap any LLM API as an AgentsKit adapter. Plug-and-play with the rest of the kit in 30 lines.
- PeerAdapter contract tests
Verify any adapter against the ADR 0001 invariants A1βA10 with the shared test harness.