Skill Guide | TypeScript SDK
Whether you are a human or an AI agent, this skill guide will help you develop applications using the Resonate TypeScript SDK. Though this is a skill guide, and not a detailed API reference and you will still likely need to refer to the detailed Resonate TypeScript SDK API reference for type definitions, defaults, and other technical details.
This skill sheet assumes that you already know Resonate is a good fit for your use case. If you are unsure, please refer to When to use Resonate to build apps.
It's important to think about the potential architecture of your application as it ties to your use case. This can determine which APIs and options you will use for function activations. If you have a use case in mind, but haven't built with Resonate before, we recommend reviewing some of the common patterns in our example applications.
Installation
How to install the Resonate Typescript SDK into your project.
To install the Resonate Typescript SDK, you can use any of your favorite package managers.
- Bun
- npm
- Yarn
bun add @resonatehq/sdk
npm install @resonatehq/sdk
yarn add @resonatehq/sdk
Once installed, you can import the Resonate SDK into your TypeScript files, and start using the Resonate Client.
Initialization
How to initialize a Resonate Client.
Initializing a Resonate Client gives you the APIs needed to register, activate, and await on functions.
import { Resonate } from "@resonatehq/sdk";
const resonate = new Resonate();
- A Resonate Client can connect to either an in-memory local store and message source (best for development), or a remote store and message sources (best for moving out of development).
- You can only have one Resonate Client per process.
- A Resonate Client can only be used in the Ephemeral World to register and activate functions, it cannot be used inside Durable Functions. Use the Context APIs for function activations inside Durable Functions.
Upon initialization, the Resonate Client will look for environment variables and/or use the options passed to it. For example, to specify a Resonate Server and process group, pass the url and group in the options.
import { Resonate } from "@resonatehq/sdk";
const resonate = new Resonate({
// ...
url: "https://localhost:8001", // Resonate Server URL
group: "worker-group-a", // Process group name
// ...
});
For a comprehensive list of Resonate Client defaults, see https://resonatehq.github.io/resonate-sdk-ts/classes/Resonate.html#constructor.
Zero-dependency development
Unlike other Durable Execution offerings, apart from installing the SDK itself, Resonate enables you to get started without any additional dependencies. This is because Resonate can run in a local development mode that uses in-memory storage for promises and tasks.
This is ideal for getting started quickly or for integrating Resonate into an existing application without relying on dependencies.
Resonate will automatically operate in "local development mode" when no URL is provided via constructor options or environment variables.
import { Resonate } from "@resonatehq/sdk";
const resonate = new Resonate();
Local development mode can be suitable for awhile. However, when you want to run multiple worker processes, persist state across restarts, or share work between machines, you will need to connect to a Resonate Server. See the Quickstart guide or How to run a Resonate Server for guidance on getting a server up and running.
Auth
When constructing a Resonate Client, you can provide authentication credentials for making requests to a Resonate Server.
import { Resonate } from "@resonatehq/sdk";
const resonate = new Resonate({
url: "https://localhost:8001",
auth: {
username: "my-username",
password: "my-password",
},
});
- This auth configuration only applies to reqests made to the Resonate Server API from this Resonate Client.
- The Client to Server request uses HTTP Basic Authentication for authenticating requests.
See How to customize Resonate Server configurations to learn how to set up authentication on the server side.
Message sources
A Resonate Client receives messages by making HTTP Long Polling requests to a Resonate Server. The messages tell the client/sdk what to work on next.
You can customize the transport by which the client receives messages using a transport plugin. For example, here is how to configure it with a Kafka message transport plugin.
import { type Context, Resonate } from "@resonatehq/sdk";
import { Kafka } from "@resonatehq/kafka";
const transport = new Kafka({ brokers: ["localhost:9092"] });
await transport.start();
const resonate = new Resonate({ transport });
The underlying protocols enable a wide range of potential message transports including SQS, Pub/Sub, RabbitMQ, NATS, and more. See Message transports for a complete list.
Environment Variables
The Resonate client constructor automatically inspects a handful of environment variables when instantiating the SDK.
Resolution order
When a Resonate Client is instantiated, explicit constructor arguments always win. If an argument is not provided, the client looks for related environment variables before falling back to built-in defaults.
The following order is used when determining the remote endpoint:
urlargument supplied to the constructor.RESONATE_URLenvironment variable.RESONATE_SCHEME,RESONATE_HOST, andRESONATE_PORTenvironment variables.- Built-in local development mode (no remote network).
Similarly, authentication credentials resolve in this order:
authargument supplied to the constructor.RESONATE_USERNAMEandRESONATE_PASSWORDenvironment variables.- No authentication (anonymous requests).
If neither an argument nor environment variable produces a URL, the client falls back to a local in-memory network. This is convenient for unit tests or quick experiments that do not require a Resonate server.
RESONATE_URL
-
Provides the full base URL (scheme, host, and port) for connecting to a remote Resonate server. Takes precedence over the individual scheme/host/port variables.
-
Default is unset
-
Example, compose from a single URL:
Shellexport RESONATE_URL="https://resonate.example.com"TypeScriptimport { Resonate } from "@resonatehq/sdk";
const resonate = new Resonate(); // picks up RESONATE_URL automatically -
Example, compose a URL from components:
Shellexport RESONATE_SCHEME=https
export RESONATE_HOST=resonate.example
export RESONATE_PORT=8443TypeScriptconst resonate = new Resonate();
// resolves to https://resonate.example:8443
RESONATE_SCHEME
- Scheme to use when
RESONATE_URLis unset and a host is supplied. Combine withRESONATE_HOST(required) andRESONATE_PORT(optional) to build the full URL. - Default is http.
RESONATE_HOST
- Hostname or IP address of the remote Resonate server. Must be present to use the scheme/host/port fallback path.
- Default is unset.
RESONATE_PORT
- Port number to use when building the URL from scheme/host/port.
Ignored if
RESONATE_URLis provided. - Default is
8001.
RESONATE_USERNAME
- Username for HTTP basic authentication. When set (even without a password) it enables authenticated requests.
- Default is unset.
RESONATE_PASSWORD
-
Password paired with
RESONATE_USERNAME. Defaults to an empty string when the username is set but no password is supplied. -
Default is an empty string.
-
Example:
Shellexport RESONATE_USERNAME=my-user
export RESONATE_PASSWORD=super-secretTypeScriptconst resonate = new Resonate();
// uses { username: "my-user", password: "super-secret" }
Troubleshooting tips
- Prefer passing constructor arguments when bootstrapping in application code that owns configuration loading. Environment variables are ideal for CLI usage, tests, or infrastructure-managed settings.
- Always set
RESONATE_HOSTif you intend to rely on the scheme/host/port combination. Without a host the client will treat the configuration as missing and default to local mode.
Client APIs
The following client APIs can only be used in the Ephemeral World, they can not be used inside Durable Functions.
.register()
Register a function to expose it to the Resonate system. A registered function can then be activated using by the Resonate Client, Resonate CLI, or from inside another Durable Function using the Context APIs.
function foo(ctx: Context, ...args: any[]) {
// ...
return result;
}
resonate.register(foo);
// or alternatively
resonate.register("foo", (ctx: Context, ...args: any[]) => {
// ...
return result;
});
You can specify a name for the function that is different from the function name by passing it as the first argument.
resonate.register("custom-foo", (ctx: Context, ...args: any[]) => {
// ...
return result;
});
Registration does provide a stub that can be used to activate the function locally, ranther than using the run or rpc methods on the Resonate Client.
const fooStub = resonate.register("foo", (ctx: Context, ...args: any[]) => {
// ...
return result;
});
fooStub.run(promiseId, args);
.setDependency()
Resonate's .setDependency() method enables you to set a dependency for other functions in the local call graph to use.
resonate.setDependency("dependency-name", dependency);
- Setting a dependency can only be done in the Ephemeral World.
- It is useful for sharing resources like database connections across functions.
- You can then access the dependency inside a Durable Function using Context's
.getDependency()method.
.run()
Resonate's .run() method invokes a function in the same process and returns the result.
You can think of it as a "run right here" invocation.
After invocation, the function is considered durable and will recover in another process if required.
const result = await resonate.run("invocation-id", foo, ...args);
.beginRun()
Similar to .run() but instead of returning the result, returns a handle so the result can be awaited later.
const handle = await resonate.beginRun("invocation-id", foo, ...args);
const result = await handle.result();
.rpc()
Resonate's .rpc() method (Remote Procedure Call) invokes a function in a remote process and returns the result.
You can think of it as a "run somewhere else" invocation (Asynchronous Remote Procedure Call).
After invocation, the function is considered durable and will recover in another process if required.
// worker.ts
resonate.register("foo", (ctx: Context, ...args: any[]) => {
// ...
return result;
});
// client.ts
const result = await resonate.rpc(
"invocation-id",
"foo",
...args,
resonate.options({
target: "poll://any@workers",
})
);
.beginRpc()
Similar to .rpc() but instead of returning the result, returns a handle so the result can be awaited later.
const handle = await resonate.beginRpc(
"invocation-id",
"foo",
...args,
resonate.options({
target: "poll://any@workers",
})
);
const result = await handle.result();
.schedule()
Resonate's .schedule() method allows you to schedule a function to be invoked on a specified cron schedule.
The scheduled function will be invoked until the schedule is deleted.
const schedule = await resonate.schedule(
"scheduled-foo",
"0 * * * *", // every hour
foo,
...args
);
.options()
Options can be used with .run(), .beginRun(), .rpc(), .beginRpc(), and .schedule() as the final argument.
await resonate.run(
"invocation-id",
foo,
...args,
resonate.options({
timeout: 60_000, // 1 minute in ms
target: "poll://any@workers",
tags: { key: "value" },
})
);
.get()
Resonate's .get() method allows you to subscribe to a function invocation.
If the function invocation does not exist, an error will be thrown.
const handle = await resonate.get("invocation-id");
const result = await handle.result();
.promises.get()
Resonate's .promises.get() method allows you to get a promise by ID.
const p = await resonate.promises.get("promise-id");
.promises.create()
Resonate's .promises.create() method allows you to create a promise.
await resonate.promises.create(
"promise-id",
Date.now() + 30000 // 30 seconds in the future
);
.promises.get()
Resonate's .promises.get() method allows you to get a promise by ID.
const p = await resonate.promises.get("promise-id");
.promises.resolve()
Resonate's .promises.resolve() method allows you to resolve a promise by ID.
This is useful for HITL use cases where you want to wait for a human to approve or reject a function execution.
It works well in conjunction with the .promise() method.
await resonate.promises.resolve("promise-id");
.promises.reject()
Resonate's .promises.reject() method allows you to reject a promise by ID.
await resonate.promises.reject("promise-id");
Context APIs
How to use the Resonate Context object in the TypeScript SDK.
Resonate's Context object enables you to invoke functions from inside a Durable Function.
This is how you extend the Call Graph and create a world of Durable Functions.
Inside a Durable Function you use the yield* keyword to interact with the Context object.
.getDependency()
Context's .getDependency() method allows you to get a dependency that was set in the ephemeral world using the .setDependency() method and use it the Durable World.
resonate.register("foo", function* (ctx: Context, ...args: any[]) {
// ...
const dependency = ctx.getDependency("dependency-name");
// do something with the dependency
// ...
});
.run()
Context's .run() method invokes a function in the same process in a synchronous manner.
That is — the calling function blocks until the invoked function returns.
resonate.register("foo", function* (ctx: Context, ...args: any[]) {
// ...
const result = yield* ctx.run(bar, ...args);
// do more stuff
// ...
});
function bar(ctx: Context, ...args: any[]) {
// ...
return;
}
.beginRun()
Context's .beginRun() method invokes a function in the same process in an asynchronous manner.
That is — the invocation returns a promise which can be awaited later.
resonate.register("foo", function* (ctx: Context, ...args: any[]) {
// ...
const promise = yield* ctx.beginRun(bar, ...args);
// do more stuff
const result = yield* promise;
// ...
});
function bar(ctx: Context, ...args: any[]) {
// ...
return;
}
.rpc()
Context's .rpc() method invokes a function in a remote process in a synchronous manner.
That is — the calling function blocks until the invoked function returns.
// process a
resonate.register("foo", function* (ctx: Context, ...args: any[]) {
// ...
const result = yield* ctx.rpc(
"bar",
...args,
ctx.options({ target: "poll://any@workers" })
);
// do more stuff
// ...
});
// process b
resonate.register("bar", function (ctx: Context, ...args: any[]) {
// ...
return;
});
.beginRpc()
Context's .beginRpc() method invokes a function in a remote process in an asynchronous manner.
That is — the invocation returns a promise which can be awaited on later.
// process a
resonate.register("foo", function* (ctx: Context, ...args: any[]) {
// ...
const promise = yield* ctx.beginRpc(
"bar",
...args,
ctx.options({ target: "poll://any@workers" })
);
// do more stuff
const result = yield* promise;
// ...
});
// process b
resonate.register("bar", function (ctx: Context, ...args: any[]) {
// ...
return;
});
.detached()
Context's .detached() method invokes a function in a remote process in an asynchronous manner
but unlike .beginRpc(), the promise is not implictly awaited.
Use .detached() when you want to fire-and-forget a function invocation.
resonate.register("foo", function* (ctx: Context, ...args: any[]) {
// ...
yield* ctx.detached("bar", ...args);
// do more stuff
});
.options()
Options can be used with .run(), .beginRun(), .rpc(), .beginRpc(), and .detached() as the final argument.
resonate.register("foo", function* (ctx: Context, ...args: any[]) {
// ...
yield* ctx.run(
bar,
...args,
ctx.options({
id: "custom-id",
timeout: 60_000, // 1 minute in ms
target: "poll://any@workers",
tags: { key: "value" },
})
);
});
.promise()
Context's .promise() method allows you to get or create a promise that can be awaited on.
If no ID is provided, one is generated and a new promise is created. If an ID is provided and a promise already exists with that ID, then the existing promise is returned.
This is very useful for HITL (Human-In-The-Loop) use cases where you want to block progress until a human has taken an action or provided data.
It works well in conjunction with the .promises.resolve() method.
resonate.register("foo", function* (ctx: Context, ...args: any[]) {
// ...
const promise = yield* ctx.promise({ id: "promise-id" });
// do more stuff
const result = yield* promise;
// ...
});
You can also pass custom data into the promise.
resonate.register("foo", function* (ctx: Context, ...args: any[]) {
// ...
const promise = yield* ctx.promise({ data: { key: "value" } });
// do more stuff
const result = yield* promise;
// ...
});
.sleep()
There is no limit to how long the function can sleep.
This API accepts either a millisecond duration or an options object.
The options object can specify a duration with the for property or an absolute wake-up time with the until property.
ctx.sleep(5_000); // wait 5 seconds
ctx.sleep({ for: 5_000 }); // identical to passing the number
ctx.sleep({ until: new Date() });
Passing a number or for value always measures the delay in milliseconds from "now".
until expects a JavaScript Date instance representing the exact time when the workflow should resume.
The options object is useful when you want to build the wake-up time conditionally before calling sleep.
Sleep for a fixed duration.
import { Context, Resonate } from "@resonatehq/sdk";
const resonate = new Resonate();
resonate.register("send-reminder", function* (ctx: Context, userId: string) {
// Pause for five seconds without blocking the worker
yield* ctx.sleep(5_000);
yield* ctx.rfc("notify-user", userId);
});
Sleep until a calendar time.
import { Context, Resonate } from "@resonatehq/sdk";
const resonate = new Resonate();
resonate.register("schedule-digest", function* (ctx: Context, userId: string) {
const nextEightAm = new Date();
nextEightAm.setHours(8, 0, 0, 0);
// If it is already past 8am today, schedule for tomorrow
if (nextEightAm.getTime() <= Date.now()) {
nextEightAm.setDate(nextEightAm.getDate() + 1);
}
// Resume exactly at 8am
yield* ctx.sleep({ until: nextEightAm });
yield* ctx.rfc("send-digest", userId);
});
.date.now()
Context's .date.now() method allows you to deterministically get the time in milliseconds since the epoch.
If your function execution is recovered after the time has been retrieved, the same time will be returned.
This is helpful for ensuring the same code path is taken in the event of a recovery.
resonate.register("foo", function* (ctx: Context, ...args: any[]) {
// ...
const time = yield* ctx.date.now();
// do something with time
// ...
});
.math.random()
Context's .math.random() method allows you to generate a deterministic random number.
If your function execution is recovered after the random number has been generated, the same number will be returned.
This is helpful for ensuring the same code path is taken in the event of a recovery.
resonate.register("foo", function* (ctx: Context, ...args: any[]) {
// ...
const rand = yield* ctx.math.random();
// do something with rand
// ...
});
.panic() and .assert()
Context's .panic() and .assert() aborts the top level durable execution based on a condition.
.panic() will abort if the condition is true. .assert() will abort if the condition is false.
These methods are intended to be used for invariant violations, for normal operation errors it is recommended
to throw an Exception.
resonate.register("foo", function* (ctx: Context) {
// ...
yield* ctx.panic(true, "Invariant violation");
// Code after this call will not be executed
});
resonate.register("foo", function* (ctx: Context) {
// ...
yield* ctx.panic(false, "Invariant violation");
// Code after this call will be executed as normal
});
resonate.register("foo", function* (ctx: Context) {
// ...
yield* ctx.assert(false, "Invariant violation");
// Code after this call will not be executed
});
resonate.register("foo", function* (ctx: Context) {
// ...
yield* ctx.assert(true, "Invariant violation");
// Code after this call will be executed as normal
});