3 min readResonate HQ

Coming from Temporal: Hello World in Go

Temporal's helloworld maps to a single Go function — no workflow/activity types, no task queue, no activity options. Just `resonate.Register` and a promise ID.

Resonate brand card on a dark background with an ember spectrum wave at the lower left, the headline 'Hello World' in white Sansation, and the subtitle 'Coming from Temporal · Go SDK'.

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-48

greet 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

TemporalResonate
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 IDone promise ID (the idempotency key)

Porting it

  1. Collapse the two types into one function. Delete the separate Workflow and Activity. Write one func(_ *resonate.Context, args T) (R, error). (The context argument is optional — the SDK accepts no-args, ctx-only, args-only, or ctx+args.)
  2. Drop the activity options. Remove WithActivityOptions / ActivityOptions. There is no per-call timeout to declare at this level.
  3. Replace the task queue with a function name. resonate.Register(r, "greet", greet) is the routing key; there is no separate queue to create.
  4. Replace StartWorkflowOptions{ID} with a plain string ID passed to fn.Run(ctx, id, args). Call Run again with the same ID and you attach to the existing promise instead of starting a second run.
  5. Replace we.Get(ctx, &result) with h.Result(ctx) — the handle from Run carries the typed result.
  6. Point at the server. resonate.New(resonate.Config{URL: "http://localhost:8001"}) replaces client.Dial; run resonate dev to 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 greet and 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.Sleep boundary run again on every resume, so wrap external mutations (DB writes, emails, payments) in ctx.Run.
  • Pre-release SDK. resonate-sdk-go has no semver tag yet; the repo pins a specific commit. Expect API changes before v0.1.0.

Sources