- name
- resonate-migrate-from-dbos
- description
- Port a DBOS application to Resonate, pattern by pattern. Use when migrating DBOS workflows/steps, durable queues, send/recv and setEvent/getEvent communication, durable sleep, scheduled (cron) workflows, child workflows, or saga-style compensation to the Resonate SDK. Maps canonical dbos-inc demo and SDK examples to their Resonate equivalents across TypeScript, Python, Rust, and Go, with per-SDK API notes and honest coverage gaps. Foundational skill — delegates to the per-SDK pattern skills for idiomatic target code.
- license
- Apache-2.0
# Migrate from DBOS to Resonate
A pattern-by-pattern playbook for porting a DBOS application to Resonate.
Identify which DBOS construct each piece of the source uses, apply the matching
transform, and reach for the linked per-SDK skill for idiomatic target code.
## Ground rules
- **Never invent DBOS code.** Quote it from the user's source or a named
`dbos-inc/dbos-demo-apps` / `dbos-transact-*` file. If you can't find it, say so.
- **DBOS has SDKs for TypeScript, Python, Go, and Java — but no Rust SDK.** Don't
emit DBOS Rust. When migrating to Resonate Rust, map from the DBOS Python or
TypeScript idiom.
- **Resonate has no `@DBOS.workflow`/`@DBOS.step` split.** A step is just a
function made durable by `ctx.run`. Don't invent decorators or a step type.
- **A DBOS workflow must be deterministic** — side effects live in steps. The same
discipline carries to Resonate: do side-effecting work inside `ctx.run` so it is
checkpointed, not re-executed on replay.
- **State coverage honestly.** A missing example ≠ impossible; it means no worked
reference exists yet.
- **Verify APIs against the pinned version before emitting** — both the DBOS and
Resonate SDK surfaces drift between versions.
## Pinned SDK versions (latest released at time of writing)
| SDK | Resonate (target) | DBOS (source) |
|---|---|---|
| TypeScript | `@resonatehq/sdk` v0.11.4 (npm) | `@dbos-inc/dbos-sdk` v4.19.8 (npm) |
| Python | `resonate-sdk` v0.7.4 (PyPI) | `dbos` v2.23.0 (PyPI) |
| Rust | `resonate-sdk` v0.6.0 (crates.io) | — (no DBOS Rust SDK) |
| Go | `0.1.0` (tag has no `v` prefix — `go get github.com/resonatehq/resonate-sdk-go@0.1.0`) | `dbos-transact-golang` v0.17.0 |
## Core mappings (apply everywhere)
| DBOS | Resonate |
|---|---|
| `@DBOS.workflow()` / `DBOS.registerWorkflow(fn)` / `dbos.RegisterWorkflow(ctx, fn)` | a registered function (`@resonate.register`, `resonate.register("name", fn)`, `#[resonate::function]`, `resonate.Register(r, "name", fn)`) |
| `@DBOS.step()` / `DBOS.runStep(fn)` / `dbos.RunAsStep(ctx, fn)` | a plain function invoked via `ctx.run(fn, args)` |
| `DBOS.start_workflow(fn, ...)` / `DBOS.startWorkflow(fn)(...)` / `dbos.RunWorkflow(ctx, fn, args)` (child workflow) | `ctx.rpc("name", args)` or `ctx.run(fn, args)` — invoke by registered name; recursion is trivial |
| durable queue: `DBOS.register_queue` + `DBOS.enqueue_workflow(...)` then `handle.get_result()` (fan-out) | start each non-blocking (`ctx.beginRun` / `ctx.rfi` / `.spawn()` / `ctx.RPC`), then await each |
| `DBOS.recv(topic, timeout)` (block for a message) | one latent durable promise: `p = ctx.promise()` then await `p` |
| `DBOS.send(destId, msg, topic)` (deliver from outside) | `resonate.promises.resolve(id, value)` (HTTP-addressable from anywhere) |
| `DBOS.set_event(key, value)` / `DBOS.get_event(wfId, key)` (publish/read status) | the promise's own resolved value is the status; read it by id |
| `DBOS.sleep(...)` | `ctx.sleep(duration)` |
| `@DBOS.scheduled(cron)` / `DBOS.create_schedule(...)` / `dbos.WithSchedule(cron)` | `resonate.schedule(id, cron, fn)` (ts/rust) or `resonate.schedules.create(...)` (py) |
| try/except + explicit undo step (no saga DSL) | inline `ctx.run(undo, …)` in the error branch, guarded by what completed |
| `systemDatabaseUrl` Postgres (or Python SQLite default) | nothing — the Worker runs in-memory until you connect a Resonate Server |
---
## Pattern: Workflow + step
- **DETECT:** `@DBOS.workflow()` + `@DBOS.step()` (py), `DBOS.registerWorkflow` +
`DBOS.runStep` / `@DBOS.workflow()`/`@DBOS.step()` static-method decorators (ts),
`dbos.RegisterWorkflow` + `dbos.RunAsStep` (go).
- **TRANSFORM:** Register one function. Turn each step into a plain function called
via `ctx.run`. Drop the decorator split and the `systemDatabaseUrl` config.
- **DBOS SOURCE:** `dbos-demo-apps/{python/dbos-app-starter,typescript/dbos-node-starter,golang/dbos-go-starter}`.
- **RESONATE TARGET:** `example-hello-world-{ts,py,rs,go}`.
- **RELATED SKILL:** `resonate-basic-durable-world-usage-{typescript,python,rust,go}`.
- **COVERAGE:** ts ✅ py ✅ rs ✅ go ✅ (DBOS has no Rust source — map from py/ts).
## Pattern: Child workflows (composition + recursion)
- **DETECT:** `DBOS.start_workflow(fn, ...)` (py), `DBOS.startWorkflow(fn)(...)` (ts),
`dbos.RunWorkflow(ctx, fn, args)` called inside a workflow (go).
- **TRANSFORM:** Replace the child-workflow call with a call to a registered
function by name (`ctx.rpc("name", args)` / `ctx.run(fn, args)`). A function can
recurse on itself — Resonate composes functions indefinitely. Optionally pin
child ids with `.options(id=…)`.
- **DBOS SOURCE:** `dbos-demo-apps/{python,golang}/widget-store` (`start_workflow`/`RunWorkflow`
of the dispatch workflow). DBOS does not ship a recursive example.
- **RESONATE TARGET:** `example-recursive-factorial-{ts,py,rs,go}`.
- **RELATED SKILL:** `resonate-recursive-fan-out-pattern-{typescript,python,rust,go}`.
- **COVERAGE:** ts ✅ py ✅ rs ✅ go ✅.
## Pattern: Fan-out / fan-in (durable queues → parallel + join)
- **DETECT:** `DBOS.register_queue(...)` + a loop of `DBOS.enqueue_workflow(queue, fn, x)`
then `handle.get_result()` (py), `DBOS.startWorkflow(fn, { queueName })(x)` then
`h.getResult()` (ts), `dbos.NewWorkflowQueue` + `dbos.RunWorkflow(..., dbos.WithQueue(q.Name))`
then `handle.GetResult()` (go).
- **TRANSFORM:** Start each unit non-blocking — `ctx.beginRun` (ts) / `ctx.rfi` (py)
/ `ctx.run(...).spawn()` (rs) / `ctx.RPC` (go); each returns a future immediately.
Then await each future. Start ALL before awaiting ANY, or the work serializes.
- **NOTE:** DBOS queues also provide concurrency limits, rate limits, priority, and
debouncing. Resonate's fan-out primitive is parallelism + join, not a managed
queue — if the source relies on per-queue flow control, plan how to reproduce it
(e.g. a worker `group` plus application-level limiting) and say so.
- **DBOS SOURCE:** DBOS docs Python queue tutorial + TypeScript programming guide
(the `process_tasks` enqueue-N-then-`get_result` fan-out), `dbos-transact-golang`
README (10-task fan-out). (`dbos-demo-apps/python/queue-patterns` shows fair-queue /
rate-limit / debounce, not a plain fan-out.)
- **RESONATE TARGET:** `example-fan-out-fan-in-{ts,py,rs,go}`.
- **RELATED SKILL:** `resonate-recursive-fan-out-pattern-{typescript,python,rust,go}`.
- **COVERAGE:** ts ✅ py ✅ rs ✅ go ✅.
## Pattern: Durable sleep
- **DETECT:** `DBOS.sleep(seconds)` (py), `DBOS.sleep(ms)` (ts), `dbos.Sleep(ctx, duration)` (go).
- **TRANSFORM:** Replace with `ctx.sleep(duration)`. **Mind the units — they match
DBOS almost exactly:**
- TypeScript: **milliseconds** (`ctx.sleep(ms)`). DBOS `DBOS.sleep` is also ms.
- Python: **seconds** as float (`ctx.sleep(secs)`). DBOS `DBOS.sleep` is also seconds.
- Go: `time.Duration` (`ctx.Sleep(d)` then `f.Await(nil)`). DBOS `dbos.Sleep(ctx, d)` is also `time.Duration`.
- Rust: `std::time::Duration` (`ctx.sleep(Duration::from_secs(n)).await?`).
- **DBOS SOURCE:** `dbos-demo-apps/{python,golang}/widget-store` (`DBOS.sleep(1)` /
`dbos.Sleep(ctx, time.Second)`), `dbos-demo-apps/typescript/widget-store/src/shop.ts`
(`DBOS.sleep(1000)`).
- **RESONATE TARGET:** `example-durable-sleep-{ts,py,rs,go}`.
- **RELATED SKILL:** `resonate-durable-sleep-scheduled-work-{typescript,rust,go}` (no
Python variant yet — for Python durable-sleep, delegate to `resonate-basic-durable-world-usage-python`).
- **COVERAGE:** ts ✅ py ✅ rs ✅ go ✅.
## Pattern: Scheduled (cron) workflows
- **DETECT:** `@DBOS.scheduled(cron)` decorator over `@DBOS.workflow()` with a
`(scheduled, actual)` datetime signature (py/ts), `DBOS.create_schedule(...)` /
`apply_schedules(...)` (py/ts), `dbos.WithSchedule(cron)` registration option (go).
- **TRANSFORM:** Register a schedule against the Resonate Server: `resonate.schedule(id, cron, fn, ...args)`
(ts/rust) or `resonate.schedules.create(id=…, cron=…, promise_id=…, …)` (py). The
server fires the promise on the cron; the worker runs the registered function.
- **MIND THE CRON FIELDS:** DBOS uses a **6-field** cron with a leading seconds component
(`* * * * * *` = every second). Resonate takes a **standard 5-field** cron (`* * * * *` =
every minute). Drop the leading seconds field when migrating, or a `* * * * * *` copied
verbatim silently fires far more often than intended.
- **DBOS SOURCE:** `dbos-transact-py/tests/test_scheduler_decorator.py`,
`dbos-transact-ts/tests/scheduler_decorator.test.ts`, `dbos-transact-golang` README.
- **RESONATE TARGET:** `example-schedule-{ts,py,rs}`.
- **RELATED SKILL:** `resonate-durable-sleep-scheduled-work-{typescript,rust,go}`.
- **COVERAGE:** ts ✅ py ✅ rs ✅ go ⚠️ (no `example-schedule-go` yet; Go's `0.1.0`
`Schedules().Create` covers the direct cron-fired-promise half, but there is no
top-level `resonate.Schedule(id, cron, fn, args)` convenience wrapper yet — map
by analogy, or set the `resonate:target` dispatch tag by hand, per
`resonate-durable-sleep-scheduled-work-go`).
## Pattern: Communication → durable promises (human-in-the-loop)
- **DETECT:** `DBOS.recv(topic, timeout)` + `DBOS.send(destId, msg, topic)` and/or
`DBOS.set_event(key, value)` + `DBOS.get_event(wfId, key)` (py/ts/go; Go uses
generic forms `dbos.Recv[T]` / `dbos.Send` / `dbos.SetEvent` / `dbos.GetEvent[T]`).
- **TRANSFORM:** Replace the `recv`/`set_event` pair with a single latent durable
promise (`p = ctx.promise()`; await `p`). Surface `p.id` to whoever will resolve it.
Replace the external `DBOS.send(...)` with `resonate.promises.resolve(id, value)`.
The promise's resolved value is the status — you don't need a separate
`get_event` channel.
- **RESOLVE API — verify against pinned version:**
- ts (0.11.4): the `data` field is base64-encoded JSON —
`const data = Buffer.from(JSON.stringify(v), "utf8").toString("base64"); resonate.promises.resolve(id, { data })`
(the codec base64-decodes `data`, so a raw `JSON.stringify(v)` round-trips to garbage)
- py (0.7.4): `await resonate.promises.resolve(id, value)` — positional `id`
and a `Value`, not `resolve(id=…, ikey=…)`; there is no `ikey` kwarg on
`Promises.resolve` in this release. Confirm the exact call shape against
`example-human-in-the-loop-py` before emitting.
- rs (0.6.0): `resonate.promises.resolve(&id, Value::from_serializable(v)?)`
(the example repo may use `json!(v)` — verify it compiles against your released crate version; use the `Value` form if not)
在 GitHub 查看