Resonate basic debugging python
Skill resonatehq/resonate-skills/resonate-basic-debugging-python
Agent skills for building with Resonate — durable execution for long-running, crash-safe workflows.
npx -y skills add resonatehq/resonate-skills --skill resonate-basic-debugging-pythonAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 5 stars5 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Debug and troubleshoot Resonate applications using the Python SDK v0.7.0 and Resonate Server v0.9.x. Use when investigating stuck workflows, non-deterministic replays, registration errors, async/await mistakes, unexpected retries, or server connectivity issues.
The file declares its own license as Apache-2.0. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
9.4 KB, as published. Nobody here has run it
Resonate Debug and Troubleshoot — Python
Overview
Use this skill to diagnose Resonate runtime issues in Python. The Python SDK v0.7.0 uses async def + await (not generators); failure modes differ from TypeScript's generator-based v1 API and from the legacy v0.6.x Python SDK. Focus on reproducible symptoms, promise state, registration, and determinism.
For the language-agnostic replay + recovery mental model, read durable-execution first.
Triage flow
- Classify the failure: import/syntax error, runtime SDK error, server error code, workflow stuck/hanging, or unexpected replay behavior.
- Confirm Python is ≥ 3.12 and
resonate-sdk>=0.7.0is installed. - Confirm the Resonate server is reachable:
resonate devstarts it onlocalhost:8001(Rust v0.9.x, single port — no separate store port). - Confirm the worker process is running, connected to the right URL and group, and registered for the right functions.
- Inspect the promise state and call graph for the invocation ID.
- Validate determinism — no
time.time(), norandom.random(), no un-checkpointed side effects in the orchestrator body.
Version + compat pins
- Python ≥ 3.12 — the SDK uses recent typing syntax. 3.11 and below won't import.
resonate-sdk>=0.7.0— async/await rewrite; incompatible with v0.6.x API.- Resonate server v0.9.x (Rust, single port 8001) — run with
resonate devfor local development. resonate-sdkon PyPI (notresonate— the bare name is a different package).
If from resonate.resonate import Resonate fails, you either have the wrong package installed or you're on Python < 3.12.
Async/await pitfalls (Python-specific)
These are the most common Python-only footguns with the v0.7.0 SDK.
Forgot await before a Context call
async def foo(ctx: Context, arg: str) -> str:
ctx.run(bar, arg) # missing await; returns a future the SDK sees but you lose the result
return "done"
The future is tracked by structured concurrency (the runtime will still join it before settling foo), but the result is lost and your orchestrator continues without it. Fix:
result = await ctx.run(bar, arg)
Used yield instead of await
async def foo(ctx: Context, arg: str) -> str:
result = yield ctx.run(bar, arg) # wrong — this is generator syntax
The v0.7.0 SDK uses async def + await, not def + yield. Mixing them produces a TypeError or unexpected behavior. Use await.
Made the function a plain def instead of async def
def foo(ctx: Context, arg: str) -> str: # missing async
result = await ctx.run(bar, arg) # SyntaxError
All durable functions must be async def. The SDK registers and dispatches them as coroutines.
Not awaiting r.promises.*
r.promises.resolve(approval_id, Value(data={"ok": True})) # missing await
All r.promises.* methods are async. Missing await means the resolve is never sent. Fix:
await r.promises.resolve(approval_id, Value(data={"ok": True}))
Not calling await r.stop() on shutdown
The SDK needs to drain in-flight work cleanly. Always:
try:
result = await r.run(id, fn, arg).result()
finally:
await r.stop()
Registration errors
| Symptom | Likely cause | Fix |
|---|---|---|
Function 'X' is not registered | rpc target name doesn't match a registered name on any reachable worker | Check r.register(fn) is called; ensure the calling name and registered name match (if you used r.register(fn, name="custom") the caller must use "custom") |
Function version must be greater than zero | Passing version=0 explicitly | Default is 1; omit version or pass a positive integer |
Args unencodeable | Argument to run/rpc not JSON-serializable | Use dicts, lists, strings, numbers, bools, None, or types with custom encoders registered |
Promise-state errors
| Code | Meaning | Typical cause |
|---|---|---|
| 40900 | Promise already exists | Two calls used the same invocation ID; treat as idempotency — the second call returns the first's result. If you wanted a fresh run, change the ID |
| 40400 | Promise not found | Wrong ID, wrong server URL, or promise was purged |
| 40300 | Already resolved | Normal if you're checking idempotency; the value is the first resolution |
| 40301 | Already rejected | Normal; the reason is the first rejection |
| 40303 | Already timed out | Promise's deadline passed; re-create with a new ID if you want to retry |
Inspect from the CLI:
resonate promises get <promise-id>
resonate tree <invocation-id>
resonate promises search <id-prefix>
"My workflow hangs"
Three common causes, in order of likelihood:
- No worker for the target group. If your
rpctargets"backend"but no process has registered withgroup="backend", the invocation is queued forever. Check:resonate promises get <id>→ state stayspending; confirm your worker process is running and connected. - Forgot
await(see pitfalls above). The orchestrator advanced past a step without waiting for it. - Server not reachable. Confirm
resonate devis running on port 8001; checkRESONATE_URLenv var.
"My workflow replays things it shouldn't"
Replays are normal — Resonate re-executes the durable function from the top on every resumption, using stored promise values for the awaited checkpoints. Side effects outside the ctx.run envelope re-execute on every replay.
async def foo(ctx: Context, arg: str) -> str:
print(f"starting {arg}") # prints on every replay
log.info(f"starting {arg}") # logs on every replay
result = await ctx.run(work, arg)
return result
Fix: move any side effect into a helper called via ctx.run:
async def log_start(ctx: Context, arg: str) -> None:
log.info(f"starting {arg}")
async def foo(ctx: Context, arg: str) -> str:
await ctx.run(log_start, arg) # logged exactly once, persisted
result = await ctx.run(work, arg)
return result
Non-determinism regressions
Common non-deterministic code that breaks recovery:
async def bad(ctx: Context, x: str) -> str:
t = time.time() # changes between runs
r = random.random() # changes between runs
path = os.environ.get("FEATURE") # changes if env mutates
if t > 1700000000 or r > 0.5:
return await ctx.run(branch_a)
return await ctx.run(branch_b)
Each resumption may hit a different branch than the original run. Wrap non-deterministic values in leaf functions via ctx.run:
async def _get_time(ctx: Context) -> float:
return time.time()
async def _get_random(ctx: Context) -> float:
return random.random()
async def good(ctx: Context, x: str) -> str:
t = await ctx.run(_get_time)
r = await ctx.run(_get_random)
# Now t and r are stable across replays (stored at first execution)
Minimal repro templates
Worker:
# worker.py
from __future__ import annotations
import asyncio, os
from typing import TYPE_CHECKING
from resonate.resonate import Resonate
if TYPE_CHECKING:
from resonate.context import Context
r = Resonate(url=os.environ.get("RESONATE_URL", "http://localhost:8001"), group="workers")
async def ping(ctx: Context, name: str) -> str:
return f"pong {name}"
r.register(ping)
async def main() -> None:
try:
await asyncio.Event().wait() # block forever; SDK polls for work
finally:
await r.stop()
asyncio.run(main())
Client:
# client.py
from __future__ import annotations
import asyncio, os, time
from resonate.resonate import Resonate
async def main() -> None:
r = Resonate(url=os.environ.get("RESONATE_URL", "http://localhost:8001"))
try:
result = await r.options(target="workers").rpc(
f"ping-{time.time_ns()}", "ping", "alice"
).result()
print(result)
finally:
await r.stop()
asyncio.run(main())
If the client hangs, check: worker running? same URL? worker in the right group? resonate promises get <id> → what state is it in?
CLI one-liners
# local dev server — zero config, single port 8001
resonate dev
# inspect a workflow's tree of promises
resonate tree <invocation-id>
# get a specific promise's state
resonate promises get <id>
# resolve/reject external promises from the CLI (e.g., for HITL testing)
resonate promises resolve <id> --data '{"approve": true}'
resonate promises reject <id> --data '{"reason": "denied"}'
# search by prefix or tags
resonate promises search 'order:*'
Related skills
resonate-basic-ephemeral-world-usage-python— Client APIs; useful when the failure is at the process-entry layerresonate-basic-durable-world-usage-python— Context APIs; useful when the failure is inside a durable functiondurable-execution— foundational; the mental model for recovery + replayresonate-philosophy— the "do not build these things" list; many debug sessions end up being about patterns that were already warned against here