Lightweight, embeddable durable task execution for Go.
durable-go lets you define typed tasks, run memoized steps, and persist progress so work can resume safely after failures or restarts. Useful for any Go app that needs reliable, resumable workflows without a heavy orchestration framework.
Releases follow Semantic Versioning; see the latest release.
- Typed tasks — generic
Run/Taskwith input and output types. - Memoized steps — completed steps replay from the store; they are not run again.
- In-process — no cluster or workflow server; one process, one store.
- Pluggable persistence —
Storeinterface; journal-per-task filesystem store included. - Timeouts and retries —
WithTimeout,WithMaxRetrieson the task handle. - Panic recovery — task and step panics are recorded and returned as errors.
- Auto-purge — optional background cleanup of old completed and failed records.
- Flexible execution — tasks as
durable.Funcclosures or structs withExec.
Most durable-execution frameworks require external infrastructure—such as a dedicated workflow server or a Postgres database—and enforce strict code execution models like replay determinism.
durable-go takes a zero-infra, in-process approach: a single Go library with a filesystem-backed journal store running inside your application process. Instead of replaying entire function call graphs from an external orchestrator, durable-go memoizes individual step results in your store. On resume the task runs again from the top; completed steps return the cached result. There is no replay-determinism sandbox.
Single-process only. The built-in journal store is designed for use within one OS process. Do not share the store directory across multiple processes or pods — concurrent appends from separate processes corrupt the journal. For distributed workloads, implement the
Storeinterface backed by a server-mode database of your choice.
go get github.com/agenticenv/durable-go@latestGo 1.26.5+. No infrastructure required. No external dependencies — the included journal store writes to the local filesystem.
import (
"context"
durable "github.com/agenticenv/durable-go"
"github.com/agenticenv/durable-go/store/journal"
)
// errors omitted for brevity
store, _ := journal.NewJournalStore("./durable-data")
defer store.Close()
client, _ := durable.NewClient(context.Background(), store)
defer client.Close()
handle := client.NewTask("job-42", durable.WithName("Example job"))
out, _ := durable.Run(context.Background(), handle, "hello", durable.Func(
func(ctx context.Context, s *durable.StepRunner, in string) (string, error) {
greet, err := durable.Step(ctx, s, "greet", func(ctx context.Context) (string, error) {
return in + " world", nil
})
if err != nil {
return "", err
}
return durable.Step(ctx, s, "upper", func(ctx context.Context) (string, error) {
return greet, nil
})
},
))
_ = outFull example: examples/func-task/.
For services with injected dependencies, implement Exec on a struct and pass it to Run:
type Job struct {
DB *Database
Mail Mailer
}
func (j *Job) Exec(ctx context.Context, s *durable.StepRunner, id string) (string, error) {
return durable.Step(ctx, s, "notify", func(ctx context.Context) (string, error) {
return j.Mail.Send(ctx, id)
})
}
out, _ := durable.Run(ctx, handle, "42", &Job{DB: db, Mail: mailer})Full example: examples/struct-task/.
Call Run again with the same NewTask ID and the same input. Completed steps replay from the store. After a crash, leftover records show as StatusRunning in ListTasks; you still call Run — the library does not auto-resume.
To detect zombie running tasks from a previous crash, use durable.ListStaleTasks:
stale, err := durable.ListStaleTasks(ctx, store, 10*time.Minute)
// stale contains tasks with status=running not updated in the last 10 minutesFull example: examples/resume/.
Follow these when you write a task. On resume, the task runs again from the top; completed steps are reused, not re-executed.
- Side effects in
Step. Do not call an API, write to a database, or publish to a queue in the task body. Wrap that work indurable.Step. - Non-deterministic values in
Step. Do not usetime.Now(), UUIDs, or random values in the task body to choose a step ID or a branch. Generate them inside aStepso resume sees the same result. - Idempotent steps. A crash can re-run a step after the side effect already happened. Charging a card or sending mail must be safe to do twice (or no-op).
Also:
- Unique step IDs — one stable string per step (literals or deterministic keys). Reusing an ID returns the first completed result.
- JSON results — step outputs must be JSON-marshalable.
- Same task ID to resume — task inputs are not persisted; pass the same input to
Runwhen resuming. Do not callRunconcurrently for the same ID.
Runnable examples in examples/ — see examples/README.md for setup and run instructions.
| Example | What it shows |
|---|---|
examples/resume/ |
Crash after step 2, resume from cache |
examples/func-task/ |
Closure-style durable.Func |
examples/struct-task/ |
Struct task with injected deps, retries, timeout |
# from repo root
go run ./examples/resume/
go run ./examples/func-task/
go run ./examples/struct-task/See CONTRIBUTING.md for setup, workflow, and guidelines. Project policies: SECURITY.md · CODE_OF_CONDUCT.md
Quick commands (requires Task): task check | task test | task lint | task fmt | task tidy | task test-coverage
Coverage reports (PR and default branch) are on Codecov. Run task test-coverage locally to produce coverage.out and coverage.html.
This project is provided "as is" under the Apache License 2.0. You are responsible for how you persist and handle task data, including secrets and personally identifiable information in step outputs. For security issues, follow SECURITY.md.