Quickstart | Get started with Resonate
Install the server and SDK and run your first Resonate workflow.
Install the server#
brew install resonatehq/tap/resonateInstall the SDK#
Requires Node.js 22 or later.
npm install @resonatehq/sdkThe Python SDK (v0.6.7) currently requires the legacy Resonate server (Go-based). It is not yet compatible with the current-generation Rust server that resonate dev starts. See the Python SDK guide for legacy server setup.
pip install resonate-sdk[dependencies]
resonate = { package = "resonate-sdk", git = "https://github.com/resonatehq/resonate-sdk-rs", branch = "main" }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }Requires Go 1.22 or later.
go mod init countdown
go get github.com/resonatehq/resonate-sdk-go@latestNo semver tag is published yet — @latest resolves to the latest main commit. See the Go SDK guide for details.
Write a function#
A countdown loop that survives restarts — minutes, hours, or days.
import { Resonate, type Context } from "@resonatehq/sdk";
function* countdown(context: Context, count: number, delay: number) {
for (let i = count; i > 0; i--) {
// Run a function, persist its result
yield* context.run((context: Context) => console.log(`Countdown: ${i}`));
// Sleep
yield* context.sleep(delay * 1000);
}
console.log("Done!");
}
// Instantiate Resonate
const resonate = new Resonate({ url: "http://localhost:8001" });
// Register the function
resonate.register(countdown);from resonate import Resonate, Context
from threading import Event
def countdown(ctx: Context, count: int, delay: int):
for i in range(count, 0, -1):
# Run a function, persist its result
yield ctx.run(ntfy, i)
# Sleep
yield ctx.sleep(delay)
print("Done!")
def ntfy(_: Context, i: int):
print(f"Countdown: {i}")
# Instantiate Resonate
resonate = Resonate.remote()
# Register the function
resonate.register(countdown)
resonate.start() # Start Resonate threads
Event().wait() # Keep the main thread aliveuse resonate::prelude::*;
use std::time::Duration;
#[resonate::function]
async fn countdown(ctx: &Context, count: i32, delay: u64) -> Result<()> {
for i in (1..=count).rev() {
ctx.run(notify, i).await?;
ctx.sleep(Duration::from_secs(delay)).await?;
}
println!("Done!");
Ok(())
}
#[resonate::function]
async fn notify(i: i32) -> Result<()> {
println!("Countdown: {i}");
Ok(())
}
#[tokio::main]
async fn main() {
let resonate = Resonate::new(ResonateConfig {
url: Some("http://localhost:8001".into()),
..Default::default()
});
resonate.register(countdown).unwrap();
resonate.register(notify).unwrap();
// Keep the process alive to receive work
tokio::signal::ctrl_c().await.unwrap();
}package main
import (
"fmt"
"log"
"os"
"os/signal"
"syscall"
"time"
resonate "github.com/resonatehq/resonate-sdk-go"
)
// CLI args arrive as a slice: `--arg 5 --arg 60` becomes args[0]=5, args[1]=60.
func countdown(ctx *resonate.Context, args []int) (struct{}, error) {
count, delay := args[0], args[1]
for i := count; i > 0; i-- {
// Run a function, persist its result
f, err := ctx.Run(notify, i)
if err != nil {
return struct{}{}, err
}
if err := f.Await(nil); err != nil {
return struct{}{}, err
}
// Sleep
s, err := ctx.Sleep(time.Duration(delay) * time.Second)
if err != nil {
return struct{}{}, err
}
if err := s.Await(nil); err != nil {
return struct{}{}, err
}
}
fmt.Println("Done!")
return struct{}{}, nil
}
func notify(i int) (struct{}, error) {
fmt.Printf("Countdown: %d\n", i)
return struct{}{}, nil
}
func main() {
// Instantiate Resonate
r, err := resonate.New(resonate.Config{URL: "http://localhost:8001"})
if err != nil {
log.Fatalf("resonate.New: %v", err)
}
// Register the function
if _, err := resonate.Register(r, "countdown", countdown); err != nil {
log.Fatalf("Register: %v", err)
}
// Keep the worker alive to receive work
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
<-sig
}Go's Register takes a single args type, so the CLI's --arg 5 --arg 60 arrives as []int{5, 60}. Most workflows use a named struct instead — see the Go SDK guide.
Start the server#
resonate devStart the worker#
npx ts-node countdown.tspython countdown.pycargo rungo run .Run the function#
Run the function with execution ID countdown.1:
resonate invoke countdown.1 --func countdown --arg 5 --arg 60Result#
npx ts-node countdown.ts
Countdown: 5
Countdown: 4
Countdown: 3
Countdown: 2
Countdown: 1
Done!python countdown.py
Countdown: 5
Countdown: 4
Countdown: 3
Countdown: 2
Countdown: 1
Done!cargo run
Countdown: 5
Countdown: 4
Countdown: 3
Countdown: 2
Countdown: 1
Done!go run .
Countdown: 5
Countdown: 4
Countdown: 3
Countdown: 2
Countdown: 1
Done!What to try#
Inspect the execution with resonate tree — it renders the call graph as durable promises.
resonate tree countdown.1Now kill the worker mid-countdown and restart it with the same start command (keep resonate dev running). Because the invocation countdown.1 already exists, the countdown resumes where it stopped instead of starting over. Use a delay long enough to interrupt it mid-countdown — for example --arg 5 --arg 60.