If you know how Temporal's helloworld Go sample works and you want to see what changes in Resonate, this is the shortest possible diff. The hello-world pattern is the simplest durable-execution shape: one function runs, produces a result, the caller reads it. Temporal models it as a Workflow that schedules an Activity across two processes. Resonate models it as one registered Go function invoked through a durable promise.
Temporal (samples-go/helloworld)
A workflow that wraps its context in activity options and schedules an activity, plus the activity itself:
func Workflow(ctx workflow.Context, name string) (string, error) {
ao := workflow.ActivityOptions{
StartToCloseTimeout: 10 * time.Second,
}
ctx = workflow.WithActivityOptions(ctx, ao)
var result string
err := workflow.ExecuteActivity(ctx, Activity, name).Get(ctx, &result)
if err != nil {
return "", err
}
return result, nil
}
func Activity(ctx context.Context, name string) (string, error) {
return "Hello " + name + "!", nil
}The worker registers both on a named task queue (worker.New(c, "hello-world", …) → RegisterWorkflow + RegisterActivity), and a separate starter process submits a run with client.StartWorkflowOptions{ID: …, TaskQueue: …} and reads the result with we.Get(ctx, &result).
Resonate (example-hello-world-go)
Register, run, and read the result in one program:
type GreetArgs struct {
Name string `json:"name"`
}
func greet(_ *resonate.Context, args GreetArgs) (string, error) {
return fmt.Sprintf("hello, %s!", args.Name), nil
}
func main() {
r, err := resonate.New(resonate.Config{URL: "http://localhost:8001"})
// ...
greetFn, err := resonate.Register(r, "greet", greet)
// ...
id := fmt.Sprintf("hello-%d", time.Now().UnixNano())
h, err := greetFn.Run(ctx, id, GreetArgs{Name: "world"})
// ...
out, err := h.Result(ctx)
fmt.Println(out)
}
// from example-hello-world-go/main.go:14-48greet is a plain function. resonate.Register ties it to the runtime under a dispatch name. greetFn.Run(ctx, id, args) creates a durable promise keyed on id; h.Result(ctx) blocks until that promise settles and returns the typed value.
What maps to what
| Temporal | Resonate |
|---|---|
workflow.Context (workflow) + context.Context (activity) | one *resonate.Context |
workflow.ActivityOptions + WithActivityOptions | (not required at this level) |
workflow.ExecuteActivity(ctx, Activity, name).Get(ctx, &result) | greetFn.Run(ctx, id, args) + h.Result(ctx) |
RegisterWorkflow + RegisterActivity (two types) | resonate.Register(r, "greet", greet) (one function) |
worker.New(c, "hello-world", …) (task queue) | resonate.New(resonate.Config{URL: …}) (no queue) |
StartWorkflowOptions{ID, TaskQueue} | a plain id string passed to fn.Run |
| Workflow ID + Run ID | one promise ID (the idempotency key) |
Porting it
- Collapse the two types into one function. Delete the separate
WorkflowandActivity. Write onefunc(_ *resonate.Context, args T) (R, error). (The context argument is optional — the SDK accepts no-args, ctx-only, args-only, or ctx+args.) - Drop the activity options. Remove
WithActivityOptions/ActivityOptions. There is no per-call timeout to declare at this level. - Replace the task queue with a function name.
resonate.Register(r, "greet", greet)is the routing key; there is no separate queue to create. - Replace
StartWorkflowOptions{ID}with a plain string ID passed tofn.Run(ctx, id, args). CallRunagain with the same ID and you attach to the existing promise instead of starting a second run. - Replace
we.Get(ctx, &result)withh.Result(ctx)— the handle fromRuncarries the typed result. - Point at the server.
resonate.New(resonate.Config{URL: "http://localhost:8001"})replacesclient.Dial; runresonate devto start a local server on that port.
What's actually different
No workflow/activity distinction. Temporal splits durable execution into a replay-safe coordinator (the workflow) and an exactly-once side-effect unit (the activity). Resonate collapses that: any registered function runs durably, and when one durable function needs to call another it uses ctx.Run inside the parent. The distinction doesn't matter for hello-world, but it's the thing that disappears, so it's worth seeing early.
The ID is the idempotency key. Temporal threads a Workflow ID (stable, caller-supplied) and a Run ID (per attempt), and routes by task queue. Resonate's single promise ID plays the role of the Workflow ID — it is the stable identity of the execution. This example mints a fresh ID per run with time.Now().UnixNano(); a real workflow would derive a stable, meaningful ID from the business domain so retries deduplicate.
One process here, not two — by choice. The Temporal sample separates worker and starter into two main packages. This example registers and invokes in one main for brevity. To split them, invoke from the client with r.RPC(ctx, id, "greet", args) instead of greetFn.Run; RPC returns an untyped *Handle, so you read with h.Result(ctx, &out) (or resonate.ResultOf[string](ctx, h)) rather than the typed h.Result(ctx).
Notes & coverage
- Routing is by function name, not queue. Run multiple workers registering
greetand the server load-balances across them. There is no queue to declare or tune. - Replay model is structurally the same as Temporal's. Resonate re-executes the function body from the top on resume; already-settled children short-circuit by promise ID (a durable promise cache, not an event log). The practical rule matches Temporal: side effects outside a
ctx.Run/ctx.RPC/ctx.Sleepboundary run again on every resume, so wrap external mutations (DB writes, emails, payments) inctx.Run. - Pre-release SDK.
resonate-sdk-gohas no semver tag yet; the repo pins a specific commit. Expect API changes beforev0.1.0.
Sources
- Example repo: github.com/resonatehq-examples/example-hello-world-go
- Temporal sample: github.com/temporalio/samples-go/helloworld
- Concept-level guide, all SDKs: docs.resonatehq.io/evaluate/coming-from/temporal
