Install
$ agentstack add skill-resonatehq-resonate-skills-resonate-basic-debugging-go ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Resonate Basic Debugging — Go
> Pre-release caveat. The Go SDK has no semver-tagged release yet — go get …@latest resolves to a pseudo-version; pin a commit for stability. APIs may change before the first tag is cut. Every code block here is verified against develop/go.mdx and the resonatehq-examples/*-go repos at SDK commit 22076134651f.
Overview
Go's type system catches some bugs at compile time, but several Go-specific traps only surface at workflow-execution time. The most dangerous ones are silent: wrong leaf signatures, bad promise encoding, and r.Stop() on a live worker all fail without a clear error at the point of mistake. This skill is a symptom-first guide to those failure modes.
For the language-agnostic replay and recovery mental model, read durable-execution first.
Triage flow
- Is the worker connected? Confirm
resonate.Newdid not return an error and that the correctURL/Networkis set. - Is the function registered?
resonate.Registerreturns(RegisteredFunc, error)— unwrap and log. - Is the promise stuck? Run
resonate promise getto check state (pending/resolved/rejected/timedout). - Is the workflow replaying but producing duplicates? An un-checkpointed side effect is re-running above a durable boundary.
- Does
Future.Awaitreturn a decode error after external settlement? Likely aresonate.NewValueencoding mismatch. - Is the worker up but not picking up work?
r.Stop()may have been called on a live worker.
Stuck / never-resuming workflows
Latent promise never settled
Symptom: Future.Await blocks indefinitely; resonate promise get shows state pending.
Causes:
- The external actor never called
PromiseSettle/resonate promise resolve. - The promise was settled but the value encoding is wrong —
Future.Awaitfails with a decode error and the workflow re-suspends.
The external settlement path via r.Sender().PromiseSettle requires the value to be encoded as JSON → base64 → quoted string stored in Value.Data. Using resonate.NewValue(x) stores raw JSON without the base64 layer, so Future.Await fails silently with a decode error (sdk-go issue #28):
// WRONG — NewValue stores raw JSON; Codec.Decode fails with a base64 error.
val, _ := resonate.NewValue(decision)
// CORRECT — JSON → base64 → JSON-quoted string.
rawJSON, _ := json.Marshal(decision)
b64 := base64.StdEncoding.EncodeToString(rawJSON)
quotedB64, _ := json.Marshal(b64)
val := resonate.Value{Data: json.RawMessage(quotedB64)}
settleReq := resonate.PromiseSettleReq{
ID: promiseID,
State: resonate.SettleStateResolved,
Value: val,
}
_, err := r.Sender().PromiseSettle(ctx, settleReq)
There is no compile-time or runtime warning at the settle call site. The failure only surfaces inside Future.Await on the workflow side. Until sdk-go issue #28 lands a higher-level Promises().Resolve API, replicate the encoding above exactly.
ctx.Run leaf blocks indefinitely
Symptom: the workflow task lease expires and the server reassigns the task; workflow appears to restart rather than resume; repeated attempts never complete.
Cause: the runtime joins every ctx.Run-spawned goroutine before it can suspend or fulfill the parent task. A function that does external I/O, waits on a channel, or sleeps for a long time inside ctx.Run holds the lease open until the TTL expires (default 60 s).
Fix: move long-running or external-blocking work into ctx.RPC (remote dispatch, workflow suspends cleanly) or ctx.Promise (latent promise settled by an external actor). Reserve ctx.Run for pure in-process computation that returns quickly.
Duplicated side effects (replay)
Symptom: emails, charges, log entries, or DB writes happen more than once per logical invocation.
Cause: the entire workflow body re-runs from the top on every resume. Durable child promises short-circuit work that already settled, but code that runs before reaching a durable boundary (ctx.Sleep, ctx.RPC, ctx.Promise) executes again on each replay pass.
// BAD — the log line re-executes on every replay pass.
func myWorkflow(ctx *resonate.Context, id string) (string, error) {
log.Printf("charging card for order %s", id) // runs on every replay
f, err := ctx.Run(chargeCard, id)
// ...
}
// GOOD — the side effect is inside a checkpointed ctx.Run; it runs once.
func myWorkflow(ctx *resonate.Context, id string) (string, error) {
f, err := ctx.Run(chargeCard, id) // result is checkpointed
if err != nil {
return "", err
}
var receipt string
if err := f.Await(&receipt); err != nil {
return "", err
}
return receipt, nil
}
Rule: any observable side effect (network call, write, notification) belongs inside its own ctx.Run or ctx.RPC so the durable promise records the result and short-circuits on replay.
Decode and error handling
Wrong leaf signature — silent runtime failure
Symptom: ctx.Run(myLeaf, arg) returns an error at execution time (not compile time): "unsupported function signature" or similar.
Cause: ctx.Run takes fn any and resolves the signature by reflection. The four valid shapes are:
| Signature | Notes | |---|---| | func(*resonate.Context, A) (R, error) | Full form | | func(*resonate.Context) (R, error) | No args | | func(A) (R, error) | Stateless leaf with args | | func() (R, error) | Stateless leaf |
Any other shape (wrong return arity, missing error, pointer-receiver method) compiles fine and only fails at execution time.
Mitigation: add a compile-time type guard in a test or init block:
// Fails at compile time if myLeaf's signature drifts.
var _ func(string) (string, error) = myLeaf
Also verify new leaf functions against a localnet run before shipping.
r.Get on a missing promise
Symptom: call returns an error; caller does not know whether the promise does not exist yet or whether the transport failed.
Fix: type-assert with errors.As to distinguish a 404 from a transport error:
h, err := r.Get(ctx, "order-123")
if err != nil {
var se *resonate.ServerError
if errors.As(err, &se) && se.Code == 404 {
// Promise does not exist yet — normal during startup races.
return
}
log.Fatalf("Get: %v", err) // unexpected transport or server error
}
Rejected promise surfaces as ApplicationError
Symptom: h.Result or f.Await returns a non-nil error even though no Go panic occurred.
Cause: the promise was rejected (either by a returned error from the registered function, or by an external resonate promise reject call). The error is deserialized as *resonate.ApplicationError.
var result string
if err := f.Await(&result); err != nil {
var ae *resonate.ApplicationError
if errors.As(err, &ae) {
log.Printf("workflow rejected: %s", ae.Message)
return
}
log.Fatalf("unexpected await error: %v", err)
}
Bounded DefaultRetryPolicy — not a bug
The Go SDK's DefaultRetryPolicy is ExponentialRetry{MaxAttempts: 3, Base: 100ms, Max: 30s, Jitter: true}. A workflow that "gives up too early" compared to TypeScript or Python expectations is hitting this 3-attempt ceiling, not a runtime defect. Override with a custom policy via RunOpts.RetryPolicy.
Setup footguns (localnet, Stop)
localnet without NoopHeartbeat{}
Symptom: heartbeat loop errors at startup; resonate.New or early task processing logs HTTP errors against an address that isn't serving.
Cause: the default AsyncHeartbeat issues HTTP keep-alive requests to refresh the task lease. localnet has no such endpoint.
Fix:
pid := "dev-worker"
r, err := resonate.New(resonate.Config{
Network: localnet.NewLocal("default", &pid),
Heartbeat: resonate.NoopHeartbeat{},
})
This is the only required deviation from the HTTP-server setup when using localnet.
r.Stop() on a long-running worker
Symptom: the worker process is running and healthy-looking, but it stops picking up new tasks.
Cause: r.Stop() closes the server connection, stops the heartbeat loop, and cancels the subscription-refresh goroutine. Any in-flight leased tasks have their TTL expire; the server reassigns them. The process keeps running, but the dispatch pipeline is dead.
Rule: call r.Stop() only in one-shot binaries, demos, and CI tasks that exit after their work finishes. Long-running workers should stay up; end the process lifecycle with SIGINT / SIGTERM.
// Correct for a one-shot job:
defer func() { _ = r.Stop() }()
// For a long-running worker — omit Stop and let the OS signal end the process.
Inspection tools
resonate dev # local dev server (in-process state)
resonate promise get # single promise state + value
resonate promise search 'order:*' # prefix search across promises
resonate promise resolve --data '"approved"' # settle a pending latent promise
resonate tree # call graph for an invocation
See the resonate-cli skill for the full command surface. The CLI is SDK-agnostic; the same commands work against any worker language.
Durable sleep tolerance: a 24 h ctx.Sleep firing in 23–25 h is within the server's timer tolerance window, not a bug.
Avoid
- Branching on
time.Now()orrand.Float64()directly inside a workflow body — non-deterministic values change between replay passes and cause divergent execution. Move them into a leaf so the result is checkpointed. - Using
time.Durationas a JSON-serializable arg type — it round-trips as a bare nanosecondint64, which is opaque in stored promise payloads. Prefer an explicit seconds or milliseconds field (e.g.Secs int64). - Passing unexported struct fields or non-serializable types (channels, functions,
sync.Mutex) as workflow args —ctx.Runandctx.RPCencode args into the durable promise via JSON; non-serializable types produce a silent zero value or a marshal error.
Related skills
resonate-basic-durable-world-usage-go— Context APIs (ctx.Run,ctx.RPC,ctx.Sleep,ctx.Promise)resonate-human-in-the-loop-pattern-go— latent promise settlement,PromiseSettleencoding detailresonate-cli— full CLI command surface for promise inspection and settlementresonate-defaults— default TTL, retry policy, and timeout values across all SDKsdurable-execution— foundational replay and recovery modelresonate-basic-debugging-typescript— TypeScript sibling (yield*, group routing, determinism helpers)resonate-basic-debugging-rust— Rust sibling (serde, tokio runtime,ctxvsinfo)
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: resonatehq
- Source: resonatehq/resonate-skills
- License: Apache-2.0
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.