1---2name: resonate-migrate-from-dbos3description: 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.4license: Apache-2.05---67# Migrate from DBOS to Resonate89A pattern-by-pattern playbook for porting a DBOS application to Resonate.10Identify which DBOS construct each piece of the source uses, apply the matching11transform, and reach for the linked per-SDK skill for idiomatic target code.1213## Ground rules1415- **Never invent DBOS code.** Quote it from the user's source or a named16 `dbos-inc/dbos-demo-apps` / `dbos-transact-*` file. If you can't find it, say so.17- **DBOS has SDKs for TypeScript, Python, Go, and Java — but no Rust SDK.** Don't18 emit DBOS Rust. When migrating to Resonate Rust, map from the DBOS Python or19 TypeScript idiom.20- **Resonate has no `@DBOS.workflow`/`@DBOS.step` split.** A step is just a21 function made durable by `ctx.run`. Don't invent decorators or a step type.22- **A DBOS workflow must be deterministic** — side effects live in steps. The same23 discipline carries to Resonate: do side-effecting work inside `ctx.run` so it is24 checkpointed, not re-executed on replay.25- **State coverage honestly.** A missing example ≠ impossible; it means no worked26 reference exists yet.27- **Verify APIs against the pinned version before emitting** — both the DBOS and28 Resonate SDK surfaces drift between versions.2930## Pinned SDK versions (latest released at time of writing)3132| SDK | Resonate (target) | DBOS (source) |33|---|---|---|34| TypeScript | `@resonatehq/sdk` v0.11.4 (npm) | `@dbos-inc/dbos-sdk` v4.19.8 (npm) |35| Python | `resonate-sdk` v0.7.4 (PyPI) | `dbos` v2.23.0 (PyPI) |36| Rust | `resonate-sdk` v0.6.0 (crates.io) | — (no DBOS Rust SDK) |37| 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 |3839## Core mappings (apply everywhere)4041| DBOS | Resonate |42|---|---|43| `@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)`) |44| `@DBOS.step()` / `DBOS.runStep(fn)` / `dbos.RunAsStep(ctx, fn)` | a plain function invoked via `ctx.run(fn, args)` |45| `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 |46| 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 |47| `DBOS.recv(topic, timeout)` (block for a message) | one latent durable promise: `p = ctx.promise()` then await `p` |48| `DBOS.send(destId, msg, topic)` (deliver from outside) | `resonate.promises.resolve(id, value)` (HTTP-addressable from anywhere) |49| `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 |50| `DBOS.sleep(...)` | `ctx.sleep(duration)` |51| `@DBOS.scheduled(cron)` / `DBOS.create_schedule(...)` / `dbos.WithSchedule(cron)` | `resonate.schedule(id, cron, fn)` (ts/rust) or `resonate.schedules.create(...)` (py) |52| try/except + explicit undo step (no saga DSL) | inline `ctx.run(undo, …)` in the error branch, guarded by what completed |53| `systemDatabaseUrl` Postgres (or Python SQLite default) | nothing — the Worker runs in-memory until you connect a Resonate Server |5455---5657## Pattern: Workflow + step5859- **DETECT:** `@DBOS.workflow()` + `@DBOS.step()` (py), `DBOS.registerWorkflow` +60 `DBOS.runStep` / `@DBOS.workflow()`/`@DBOS.step()` static-method decorators (ts),61 `dbos.RegisterWorkflow` + `dbos.RunAsStep` (go).62- **TRANSFORM:** Register one function. Turn each step into a plain function called63 via `ctx.run`. Drop the decorator split and the `systemDatabaseUrl` config.64- **DBOS SOURCE:** `dbos-demo-apps/{python/dbos-app-starter,typescript/dbos-node-starter,golang/dbos-go-starter}`.65- **RESONATE TARGET:** `example-hello-world-{ts,py,rs,go}`.66- **RELATED SKILL:** `resonate-basic-durable-world-usage-{typescript,python,rust,go}`.67- **COVERAGE:** ts ✅ py ✅ rs ✅ go ✅ (DBOS has no Rust source — map from py/ts).6869## Pattern: Child workflows (composition + recursion)7071- **DETECT:** `DBOS.start_workflow(fn, ...)` (py), `DBOS.startWorkflow(fn)(...)` (ts),72 `dbos.RunWorkflow(ctx, fn, args)` called inside a workflow (go).73- **TRANSFORM:** Replace the child-workflow call with a call to a registered74 function by name (`ctx.rpc("name", args)` / `ctx.run(fn, args)`). A function can75 recurse on itself — Resonate composes functions indefinitely. Optionally pin76 child ids with `.options(id=…)`.77- **DBOS SOURCE:** `dbos-demo-apps/{python,golang}/widget-store` (`start_workflow`/`RunWorkflow`78 of the dispatch workflow). DBOS does not ship a recursive example.79- **RESONATE TARGET:** `example-recursive-factorial-{ts,py,rs,go}`.80- **RELATED SKILL:** `resonate-recursive-fan-out-pattern-{typescript,python,rust,go}`.81- **COVERAGE:** ts ✅ py ✅ rs ✅ go ✅.8283## Pattern: Fan-out / fan-in (durable queues → parallel + join)8485- **DETECT:** `DBOS.register_queue(...)` + a loop of `DBOS.enqueue_workflow(queue, fn, x)`86 then `handle.get_result()` (py), `DBOS.startWorkflow(fn, { queueName })(x)` then87 `h.getResult()` (ts), `dbos.NewWorkflowQueue` + `dbos.RunWorkflow(..., dbos.WithQueue(q.Name))`88 then `handle.GetResult()` (go).89- **TRANSFORM:** Start each unit non-blocking — `ctx.beginRun` (ts) / `ctx.rfi` (py)90 / `ctx.run(...).spawn()` (rs) / `ctx.RPC` (go); each returns a future immediately.91 Then await each future. Start ALL before awaiting ANY, or the work serializes.92- **NOTE:** DBOS queues also provide concurrency limits, rate limits, priority, and93 debouncing. Resonate's fan-out primitive is parallelism + join, not a managed94 queue — if the source relies on per-queue flow control, plan how to reproduce it95 (e.g. a worker `group` plus application-level limiting) and say so.96- **DBOS SOURCE:** DBOS docs Python queue tutorial + TypeScript programming guide97 (the `process_tasks` enqueue-N-then-`get_result` fan-out), `dbos-transact-golang`98 README (10-task fan-out). (`dbos-demo-apps/python/queue-patterns` shows fair-queue /99 rate-limit / debounce, not a plain fan-out.)100- **RESONATE TARGET:** `example-fan-out-fan-in-{ts,py,rs,go}`.101- **RELATED SKILL:** `resonate-recursive-fan-out-pattern-{typescript,python,rust,go}`.102- **COVERAGE:** ts ✅ py ✅ rs ✅ go ✅.103104## Pattern: Durable sleep105106- **DETECT:** `DBOS.sleep(seconds)` (py), `DBOS.sleep(ms)` (ts), `dbos.Sleep(ctx, duration)` (go).107- **TRANSFORM:** Replace with `ctx.sleep(duration)`. **Mind the units — they match108 DBOS almost exactly:**109 - TypeScript: **milliseconds** (`ctx.sleep(ms)`). DBOS `DBOS.sleep` is also ms.110 - Python: **seconds** as float (`ctx.sleep(secs)`). DBOS `DBOS.sleep` is also seconds.111 - Go: `time.Duration` (`ctx.Sleep(d)` then `f.Await(nil)`). DBOS `dbos.Sleep(ctx, d)` is also `time.Duration`.112 - Rust: `std::time::Duration` (`ctx.sleep(Duration::from_secs(n)).await?`).113- **DBOS SOURCE:** `dbos-demo-apps/{python,golang}/widget-store` (`DBOS.sleep(1)` /114 `dbos.Sleep(ctx, time.Second)`), `dbos-demo-apps/typescript/widget-store/src/shop.ts`115 (`DBOS.sleep(1000)`).116- **RESONATE TARGET:** `example-durable-sleep-{ts,py,rs,go}`.117- **RELATED SKILL:** `resonate-durable-sleep-scheduled-work-{typescript,rust,go}` (no118 Python variant yet — for Python durable-sleep, delegate to `resonate-basic-durable-world-usage-python`).119- **COVERAGE:** ts ✅ py ✅ rs ✅ go ✅.120121## Pattern: Scheduled (cron) workflows122123- **DETECT:** `@DBOS.scheduled(cron)` decorator over `@DBOS.workflow()` with a124 `(scheduled, actual)` datetime signature (py/ts), `DBOS.create_schedule(...)` /125 `apply_schedules(...)` (py/ts), `dbos.WithSchedule(cron)` registration option (go).126- **TRANSFORM:** Register a schedule against the Resonate Server: `resonate.schedule(id, cron, fn, ...args)`127 (ts/rust) or `resonate.schedules.create(id=…, cron=…, promise_id=…, …)` (py). The128 server fires the promise on the cron; the worker runs the registered function.129- **MIND THE CRON FIELDS:** DBOS uses a **6-field** cron with a leading seconds component130 (`* * * * * *` = every second). Resonate takes a **standard 5-field** cron (`* * * * *` =131 every minute). Drop the leading seconds field when migrating, or a `* * * * * *` copied132 verbatim silently fires far more often than intended.133- **DBOS SOURCE:** `dbos-transact-py/tests/test_scheduler_decorator.py`,134 `dbos-transact-ts/tests/scheduler_decorator.test.ts`, `dbos-transact-golang` README.135- **RESONATE TARGET:** `example-schedule-{ts,py,rs}`.136- **RELATED SKILL:** `resonate-durable-sleep-scheduled-work-{typescript,rust,go}`.137- **COVERAGE:** ts ✅ py ✅ rs ✅ go ⚠️ (no `example-schedule-go` yet; Go's `0.1.0`138 `Schedules().Create` covers the direct cron-fired-promise half, but there is no139 top-level `resonate.Schedule(id, cron, fn, args)` convenience wrapper yet — map140 by analogy, or set the `resonate:target` dispatch tag by hand, per141 `resonate-durable-sleep-scheduled-work-go`).142143## Pattern: Communication → durable promises (human-in-the-loop)144145- **DETECT:** `DBOS.recv(topic, timeout)` + `DBOS.send(destId, msg, topic)` and/or146 `DBOS.set_event(key, value)` + `DBOS.get_event(wfId, key)` (py/ts/go; Go uses147 generic forms `dbos.Recv[T]` / `dbos.Send` / `dbos.SetEvent` / `dbos.GetEvent[T]`).148- **TRANSFORM:** Replace the `recv`/`set_event` pair with a single latent durable149 promise (`p = ctx.promise()`; await `p`). Surface `p.id` to whoever will resolve it.150 Replace the external `DBOS.send(...)` with `resonate.promises.resolve(id, value)`.151 The promise's resolved value is the status — you don't need a separate152 `get_event` channel.153- **RESOLVE API — verify against pinned version:**154 - ts (0.11.4): the `data` field is base64-encoded JSON —155 `const data = Buffer.from(JSON.stringify(v), "utf8").toString("base64"); resonate.promises.resolve(id, { data })`156 (the codec base64-decodes `data`, so a raw `JSON.stringify(v)` round-trips to garbage)157 - py (0.7.4): `await resonate.promises.resolve(id, value)` — positional `id`158 and a `Value`, not `resolve(id=…, ikey=…)`; there is no `ikey` kwarg on159 `Promises.resolve` in this release. Confirm the exact call shape against160 `example-human-in-the-loop-py` before emitting.161 - rs (0.6.0): `resonate.promises.resolve(&id, Value::from_serializable(v)?)`162 (the example repo may use `json!(v)` — verify it compiles against your released crate version; use the `Value` form if not)163 - go (0.1.0): `r.Promises().Resolve(ctx, id, v)` — the direct `Promises()`164 sub-client handles the codec encoding for you. The CLI165 (`resonate promises resolve <id> --value '{"headers":{},"data":"…"}'`) and the166 lower-level sender/promise-settle path remain available.167- **DBOS SOURCE:** `dbos-demo-apps/{python,typescript,golang}/widget-store`,168 `dbos-demo-apps/python/agent-inbox` (richer HITL with `recv` + `set_event`).169- **RESONATE TARGET:** `example-human-in-the-loop-{ts,py,rs,go}`.170- **RELATED SKILL:** `resonate-human-in-the-loop-pattern-{typescript,python,rust,go}`.171- **COVERAGE:** ts ✅ py ✅ rs ✅ go ✅ (`0.1.0` has a direct `Promises().Resolve`; the CLI and low-level `Sender().PromiseSettle` remain as alternates).172173## Pattern: Saga / compensation174175- **DETECT:** a DBOS workflow that, on a failed/declined step, calls an explicit176 undo step (e.g. `undo_reserve_inventory()` / `undoSubtractInventory()` /177 `undoReserveInventory`) in the failure branch. DBOS has no saga DSL.178- **TRANSFORM:** Run each step with `ctx.run`. On failure, run the undo inline in179 the `catch`/error branch, guarded by which steps actually completed. Make180 compensations idempotent. Same shape as DBOS — no compensation stack, no helper.181- **DBOS SOURCE:** `dbos-demo-apps/{python,typescript,golang}/widget-store`182 (checkout workflow), `dbos-demo-apps/python/reliable-refunds-langchain`.183- **RESONATE TARGET:** `example-saga-booking-ts`, `example-money-transfer-{py,rs}`.184- **RELATED SKILL:** `resonate-saga-pattern-{typescript,python,rust,go}`.185- **COVERAGE:** ts ✅ py ✅ rs ✅ go ⚠️ (no Go example yet — map by analogy).186187---188189## What does not map cleanly (do not fabricate)190191- **Exactly-once Kafka consumers** (`@DBOS.kafka_consumer`) are a DBOS built-in with192 no direct Resonate example. The closest Resonate shape is a worker that creates a193 promise keyed by the message's idempotency key, but there is no worked sample — say so.194- **Durable queue flow control** (concurrency/rate-limit/priority/debounce) is a DBOS195 queue feature. Resonate fan-out is parallelism + join only; reproduce limits at the196 application or worker-group level and flag the gap.197- **Built-in observability dashboard + workflow-management APIs** (`list_workflows`,198 `cancel`, `resume`, `fork`) are DBOS features without a 1:1 Resonate analog.199200## Coverage gaps (Resonate examples that don't exist yet)201202- Scheduled work: no `example-schedule-go`.203- Saga: no Go example (`example-money-transfer-go` / `example-saga-booking-go`).204- DBOS has no Rust SDK, so all Rust migrations map from the DBOS Python/TypeScript idiom.205206## Source of truth207208- Resonate examples: https://github.com/resonatehq-examples209- DBOS demos + SDKs: https://github.com/dbos-inc (`dbos-demo-apps`, `dbos-transact-{py,ts,golang}`)210- DBOS docs: https://docs.dbos.dev211- Side-by-side guide: https://docs.resonatehq.io/evaluate/coming-from/dbos212- Concepts first: see the `durable-execution` and `resonate-philosophy` skills.