resonate-human-in-the-loop-pattern-go — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited resonate-human-in-the-loop-pattern-go (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Version note. The Go SDK's first tagged release is0.1.0(go get github.com/resonatehq/[email protected]— the tag has novprefix, so@latestdoes not resolve to it).0.1.0shipped a top-levelpromisessub-client (r.Promises()), matching TypeScript'sresonate.promisesand Rust'sresonate.promises. Every code block here is verified against the0.1.0tag source andexample-human-in-the-loop-go.
For the language-agnostic mental model, start with resonate-human-in-the-loop-pattern-typescript. The idea is identical: create a latent durable promise, hand its ID to the external actor who will settle it, and await — the workflow goroutine parks until settlement arrives, surviving any number of crashes or restarts.
Resolution path. TypeScript has resonate.promises.settle(id, ...). Rust has resonate.promises.resolve(id, ...). Go's 0.1.0 tag has r.Promises().Resolve(id, v) / .Reject(id, v) / .Cancel(id, v) — the same shape, and the preferred way to settle a promise from Go code in another process. Three more mechanisms remain useful for settling from outside Go entirely (an ops runbook, curl, a non-Go service) — listed below in order of preference.
ctx.Promise → f.ID() → publish → Awaitimport (
"context"
"fmt"
"time"
resonate "github.com/resonatehq/resonate-sdk-go"
)
type ReviewRequest struct {
Item string `json:"item"`
Requester string `json:"requester"`
}
// approvalWorkflow parks until an external actor settles the latent promise.
// promiseIDs is a buffered channel (capacity 1) that hands the promise ID to
// whoever resolves it — swap this for a DB write, a notification queue, etc.
func approvalWorkflow(ctx *resonate.Context, req ReviewRequest) (string, error) {
// Create a latent durable promise. No registered function is behind it;
// it only settles when an external caller issues a promise-settle.
f, err := ctx.Promise(resonate.PromiseOpts{Timeout: 24 * time.Hour})
if err != nil {
return "", fmt.Errorf("ctx.Promise: %w", err)
}
promiseID := f.ID() // hand this to the external resolver
// Publish the promise ID inside a ctx.Run so the write is checkpointed.
// On replay, ctx.Run re-issues with the same child promise ID and
// short-circuits — the side-effect does not run twice.
// ctx.Run takes a single args value: ctx.Run(fn, args, opts...). Capture the
// values the leaf needs via the closure and pass struct{}{} as the (unused) arg.
_, err = ctx.Run(func(_ struct{}) (struct{}, error) {
// In production: write to DB, push to a notification queue, etc.
fmt.Printf(" [workflow] awaiting approval for %q — promise ID: %s\n", req.Item, promiseID)
promiseIDs <- promiseID // example: buffered channel to a local resolver
return struct{}{}, nil
}, struct{}{})
if err != nil {
return "", fmt.Errorf("publish promise ID: %w", err)
}
// Await parks the workflow until the promise settles. The decision value
// encoded by the settler is decoded here.
var decision string
if err := f.Await(&decision); err != nil {
return "", fmt.Errorf("await approval: %w", err)
}
return fmt.Sprintf("item %q approved: %s", req.Item, decision), nil
}
// promiseIDs is a buffered channel for single-process demos. Replace with a
// DB write or notification in production.
var promiseIDs = make(chan string, 1)Key points:
ctx.Promise() with no args is valid; opts are optional.f.ID() is available immediately after the call — the promise record is created synchronously.f.Await re-runs on every replay pass.ctx.Promise re-issues with the same SDK-generated ID (idempotent). f.Await short-circuits when the promise has already settled.f.Await(nil) is valid when you need to know the promise settled but don't need the payload.r.Promises() sub-client (preferred — resolving from Go code)Use this when resolving from Go code in another process (an HTTP webhook handler, an admin CLI). Resolve/Reject/Cancel route the value through the instance's Codec — the same encode path the workflow machinery uses — so you never hand-roll the base64 layer.
import (
"context"
"log"
resonate "github.com/resonatehq/resonate-sdk-go"
)
func resolveApproval(r *resonate.Resonate, promiseID string, decision string) error {
rec, err := r.Promises().Resolve(context.Background(), promiseID, decision)
// or: r.Promises().Reject(ctx, promiseID, err) / r.Promises().Cancel(ctx, promiseID, nil)
if err != nil {
return err
}
log.Printf("settled — state=%s", rec.State)
return nil
}Resolve/Reject/Cancel return *resonate.ServerError{Code: 404} when the promise does not exist. Pass a Go error to Reject/Cancel and it encodes as the standard error payload, so Future.Await on the workflow side deserializes it into an *ApplicationError — the same round-trip DeserializeError does elsewhere in the SDK.
# approve
resonate promise resolve <promiseID> --data '"approved"'
# reject
resonate promise reject <promiseID> --data '"rejected: budget exceeded"'The --data value is the JSON payload that will be decoded into the type you passed to f.Await. For a string awaiter, a quoted JSON string like '"approved"' is correct. See the resonate-cli skill for full flag reference.
POST directly to the Resonate Server's promise-settle endpoint with the promise ID and a JSON body. Use this from any language or tool that can make HTTP calls (curl, an admin script, a serverless function) that isn't going through an SDK's codec:
curl -s -X POST "http://localhost:8001/promises/${PROMISE_ID}/resolve" \
-H "Content-Type: application/json" \
-d '{"value":{"data":"ImFwcHJvdmVkIg=="}}'The data field must be the base64 encoding of the JSON-serialized payload — see the encoding note in mechanism 4 for why.
Sender().PromiseSettleThis is the transport-level primitive r.Promises().Resolve is itself built on. Reach for it directly only when you need to bypass the codec (e.g. a value that's already wire-encoded) — otherwise prefer mechanism 1.
import (
"context"
"encoding/base64"
"encoding/json"
"log"
resonate "github.com/resonatehq/resonate-sdk-go"
)
func resolveApprovalLowLevel(r *resonate.Resonate, promiseID string, decision string) error {
// The Codec stores values as: JSON bytes → base64 → JSON-quoted string
// stored in Value.Data. Replicate it manually when going around the Codec.
rawJSON, err := json.Marshal(decision) // "approved" → `"approved"`
if err != nil {
return err
}
b64 := base64.StdEncoding.EncodeToString(rawJSON) // → base64 string
quoted, err := json.Marshal(b64) // → JSON-quoted string
if err != nil {
return err
}
val := resonate.Value{Data: json.RawMessage(quoted)}
rec, err := r.Sender().PromiseSettle(context.Background(), resonate.PromiseSettleReq{
ID: promiseID,
State: resonate.SettleStateResolved, // or SettleStateRejected
Value: val,
})
if err != nil {
return err
}
log.Printf("settled — state=%s", rec.State)
return nil
}Known friction on this low-level path only: resonate.NewValue(decision) stores raw JSON bytes in Value.Data WITHOUT the base64 wrapper the Codec expects, so Future.Await on the workflow side fails with a decode error and no warning at the settle call site. r.Promises().Resolve (mechanism 1) does not have this trap — it goes through the Codec automatically. The manual JSON → base64 → quoted encoding above is only needed when you deliberately bypass the sub-client.
The resonatehq-examples/example-node-drain-orchestrator-go shows a net/http gateway process resolving promises owned by a separate worker process — a realistic two-process boundary. Prefer r.Promises().Resolve(...) (mechanism 1) for new code; the example may predate the 0.1.0 sub-client and use Sender().PromiseSettle directly (mechanism 4) — check which path it takes before copying it verbatim. Refer to it for the full plumbing (shared server URL, token config, error handling on 409 Already Settled). Do not reproduce the drain-orchestrator example inline; it is a large multi-file app.
For single-process demos and tests, use localnet. No external server is needed.
import (
resonate "github.com/resonatehq/resonate-sdk-go"
"github.com/resonatehq/resonate-sdk-go/localnet"
)
pid := "hitl-worker"
r, err := resonate.New(resonate.Config{
Network: localnet.NewLocal("default", &pid),
Heartbeat: resonate.NoopHeartbeat{}, // required — localnet has no heartbeat endpoint
TTL: 5 * time.Minute,
})r.Promises().Resolve and Sender().PromiseSettle both work against localnet exactly as they do against a real server. The -url flag in example-human-in-the-loop-go switches between the two modes at runtime.
ctx.Promise generates the ID internally — you cannot pass a deterministic application-layer ID. This is shared behavior, not a Go shortfall: TypeScript's ctx.promise() only accepts { timeout, data, tags } and generates the ID the same way. Fetch the ID via f.ID() after creation (TypeScript: promise.id) and publish it explicitly.select-style mechanism to await whichever of several latent promises settles first. Workaround: create one promise per participant, await each in sequence, and have a coordinator cancel the others once a winner is known; or use a helper workflow that calls r.Promises().Resolve on a shared promise from the first responder's branch.yield*). Wrap every observable side effect — DB writes, notifications, the channel send that hands off the promise ID — inside ctx.Run or ctx.RPC so the result is checkpointed and the action is not repeated on replay.ctx.Promise(resonate.PromiseOpts{Timeout: 24*time.Hour}). The struct is optional; ctx.Promise() is valid when defaults are acceptable.nil to Await. Saves a decode round-trip.Stop on a long-lived worker process — it tears down the task-receive channel and the heartbeat loop silently.ctx.Promise + f.Await instead.r.Promises().Resolve(...) (mechanism 1) instead — it encodes correctly — or the manual JSON → base64 → quoted encoding shown in mechanism 4 if you must stay on the low-level path.ctx.Run that publishes the ID has been checkpointed, a crash between those two lines can lose the ID. Always checkpoint the publication first (via ctx.Run), then await.ctx.Run goroutines must return promptly; a blocking call holds the task lease open until TTL expires. Long waits belong in ctx.Promise (latent, externally settled) or ctx.RPC (remote dispatch).resonate-basic-ephemeral-world-usage-go — r.Promises() / r.Schedules() sub-client referenceresonate-basic-durable-world-usage-go — ctx.Promise builder, opts, and Future.Await detailsresonate-cli — resonate promise resolve / reject / cancel flag reference (mechanism 2)durable-execution — foundational replay semantics; why f.Await survives crashesresonate-human-in-the-loop-pattern-typescript — mental model and resonate.promises.settle referenceresonate-human-in-the-loop-pattern-rust — ctx.promise::<T>() builder and resonate.promises.resolve reference~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.