JavaScript SDK for Actors
Author, host, and call virtual actors with the Dapr JavaScript SDK
The Dapr JavaScript SDK lets Node.js applications host virtual actors, call actors through proxies or by method name, manage actor state, and schedule timers and durable reminders.
These guides target @dapr/dapr 3.19.0 or later. Use a supported Node.js LTS release and a Dapr runtime that supports the APIs you use. The how-to guide uses HTTP actor hosting and a local Dapr environment.
Choose a guide
| Guide | Learn how to |
|---|
| Run and use virtual actors | Create a contract, implement an actor, configure a state store, and run a host and client |
| Author and host actors | Register types, use lifecycle hooks, and configure the actor runtime |
| Call actors | Choose actor identities, build proxies, invoke methods dynamically, and understand serialization |
| Manage actor state | Read, update, remove, cache, and explicitly save actor state |
| Use timers and reminders | Schedule callbacks, retrieve and list reminders, and perform scoped or type-wide cleanup |
Protocol and runtime support
The application protocol and the protocol used to call the sidecar are separate choices.
| Capability | HTTP | gRPC |
|---|
Host JavaScript actors with DaprServer | Supported | Not implemented |
| Invoke actor methods through a proxy or the lower-level actor client | Supported | Supported |
| Read and transact actor state; register and unregister timers and individual reminders | Supported | Supported |
Retrieve an individual reminder with getActorReminder | Supported | Supported |
List reminders for one actor with listActorReminders | Dapr 1.19+ | Dapr 1.17+ |
| List reminders for every instance of an actor type | Not supported | Dapr 1.17+ |
Delete reminders in bulk with unregisterActorRemindersByType | Not supported | Dapr 1.17+ |
A gRPC client can call actors hosted by an HTTP application. It does not require a gRPC actor host. JavaScript actor hosting does not implement the runtime’s app-initiated actor streams.
The SDK does not expose every feature of the runtime’s Actors API. For example, this SDK version has no high-level timer retrieval/listing methods or reminder overwrite and failurePolicy options. See the individual guides for serialization and reentrancy limitations.
Registering actors
Call await server.actor.init() and await server.actor.registerActor(YourActor) before await server.start(). Registration advertises an actor type; it does not create every actor instance. Dapr activates instances on demand.
See author and host actors for registration and configuration, or follow the complete example.
1 - How to: Run and use virtual actors with the JavaScript SDK
Create an actor contract, host a stateful actor, and call it from a Node.js client
This guide creates a counter actor whose state survives deactivation. The host and client share a TypeScript contract and use @dapr/dapr 3.19.0 or later.
Prerequisites
Step 1: Prepare the project
In an empty directory, initialize a Node.js project and install the SDK and TypeScript tooling:
npm init -y
npm install @dapr/dapr@3.19.0
npm install --save-dev typescript tsx @types/node
The command pins the 3.19.0 release used by this guide. You can instead select a later published package version explicitly.
Create tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
Step 2: Define the contract
Create CounterActorContract.ts:
export interface CounterState {
value: number;
}
export interface CounterActorContract {
add(amount: number): Promise<CounterState>;
get(): Promise<CounterState>;
}
An interface supplies compile-time checking; it is not a runtime registration or access-control boundary. JavaScript applications can omit TypeScript interfaces.
Step 3: Implement the actor
Create CounterActor.ts:
import { AbstractActor } from "@dapr/dapr";
import type { CounterActorContract, CounterState } from "./CounterActorContract";
export class CounterActor extends AbstractActor implements CounterActorContract {
async add(amount: number): Promise<CounterState> {
if (!Number.isFinite(amount)) {
throw new TypeError("amount must be a finite number");
}
const manager = this.getStateManager<CounterState>();
const current = await manager.getOrAddState("counter", { value: 0 });
const next = { value: (current?.value ?? 0) + amount };
await manager.setState("counter", next);
return next;
}
async get(): Promise<CounterState> {
const state = await this.getStateManager<CounterState>().getOrAddState(
"counter",
{ value: 0 },
);
return state ?? { value: 0 };
}
}
The runtime saves tracked state changes after a successful method and post-method hook. You do not need to call saveState() in every method. See manage actor state for explicit saves and failure handling.
Create a components directory and save the following as components\statestore.yaml:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
spec:
type: state.redis
version: v1
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: ""
- name: actorStateStore
value: "true"
This uses Redis from the local Dapr initialization. In production, configure an actor-compatible state store with the required transactional support. Only one component per application can be marked as the actor state store.
Step 5: Register and start the host
Create server.ts:
import { CommunicationProtocolEnum, DaprServer } from "@dapr/dapr";
import { CounterActor } from "./CounterActor";
async function main(): Promise<void> {
const server = new DaprServer({
serverHost: "127.0.0.1",
serverPort: "3001",
communicationProtocol: CommunicationProtocolEnum.HTTP,
clientOptions: {
daprHost: "127.0.0.1",
daprPort: "3500",
communicationProtocol: CommunicationProtocolEnum.HTTP,
},
});
await server.actor.init();
await server.actor.registerActor(CounterActor);
await server.start();
console.log("Registered actor types:", await server.actor.getRegisteredActors());
}
main().catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});
Start the application and its sidecar:
dapr run --app-id counter-service --app-port 3001 --app-protocol http --dapr-http-port 3500 --resources-path components -- npx tsx server.ts
The application listens on port 3001. The sidecar’s HTTP API listens on port 3500. Wait for the host to report that CounterActor is registered.
Step 6: Call the actor
Create client.ts:
import { ActorId, ActorProxyBuilder, DaprClient } from "@dapr/dapr";
import { CounterActor } from "./CounterActor";
import type { CounterActorContract } from "./CounterActorContract";
async function main(): Promise<void> {
const client = new DaprClient({ daprHost: "127.0.0.1", daprPort: "3500" });
try {
const builder = new ActorProxyBuilder<CounterActorContract>(CounterActor, client);
const counter = builder.build(new ActorId("counter-1"));
console.log(await counter.add(2));
console.log(await counter.get());
} finally {
await client.stop();
}
}
main().catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});
In a second terminal, run:
For a new actor, both calls print { value: 2 }. Run the client again to increment the same actor to 4. The client uses the host’s sidecar for this local example; a separate application can use its own sidecar.
The actor’s identity is the pair CounterActor and counter-1. Creating a proxy does not activate the actor; invoking a method does.
2 - Author and host actors with the JavaScript SDK
Register actor types, use lifecycle hooks, and configure the JavaScript actor host
JavaScript actors derive from AbstractActor. The SDK constructs an instance with a DaprClient and an ActorId when Dapr activates it. See the how-to guide for a complete application using @dapr/dapr 3.19.0 or later.
Actor construction and identity
If you define a constructor, accept the two runtime arguments and pass them to the base constructor:
import { AbstractActor, ActorId, DaprClient } from "@dapr/dapr";
export class DeviceActor extends AbstractActor {
constructor(client: DaprClient, id: ActorId) {
super(client, id);
}
async identify(): Promise<{ type: string; id: string }> {
return {
type: this.getActorType(),
id: this.getActorId().getId(),
};
}
}
You can omit the constructor when no additional initialization is needed. The built-in registration API accepts a class, not a dependency-injection factory, and supplies only those two arguments. It does not provide the .NET SDK’s constructor dependency injection or actor-scoped service container.
The registered actor type is the implementation class’s name, such as DeviceActor. There is no separate type-name option on registerActor. Keep class names stable across deployments and configure bundlers/minifiers to preserve them. Renaming a class changes the actor type and therefore its identity and state namespace.
Lifecycle and method hooks
Override these async methods as needed:
| Hook | When it runs |
|---|
onActivate() | Once per activation, before dispatching the first method or callback |
onDeactivate() | When Dapr requests deactivation, for example after an idle timeout |
onActorMethodPre() | Before each dispatched actor method, timer callback, or reminder callback |
onActorMethodPost() | After a dispatched call completes successfully, before saving tracked state |
For a successful call, the order is activation if needed, pre-method hook, method, post-method hook, and state save. Activation and deactivation hooks also have their tracked changes saved. The SDK clears the state cache before the activation and deactivation hooks.
import { AbstractActor } from "@dapr/dapr";
export class LifecycleActor extends AbstractActor {
async onActivate(): Promise<void> {
console.log("Activating", this.getActorId().getId());
}
async onDeactivate(): Promise<void> {
console.log("Deactivating", this.getActorId().getId());
}
async onActorMethodPre(): Promise<void> {
console.log("Starting a call");
}
async onActorMethodPost(): Promise<void> {
console.log("Completed a call");
}
}
Do not depend on deactivation for guaranteed cleanup after a process crash. A thrown method skips the post-method hook and automatic state save; see state failure handling.
Use getDaprClient() inside an actor to access other Dapr building blocks. Await the resulting operations. External side effects, such as publishing messages, are not part of the actor state transaction.
The actor host must use HTTP. Initialize the callback routes and await all actor registrations before starting the server:
import { CommunicationProtocolEnum, DaprServer } from "@dapr/dapr";
import { CounterActor } from "./CounterActor";
const server = new DaprServer({
serverHost: "127.0.0.1",
serverPort: "3001",
communicationProtocol: CommunicationProtocolEnum.HTTP,
clientOptions: {
daprHost: "127.0.0.1",
daprPort: "3500",
communicationProtocol: CommunicationProtocolEnum.HTTP,
actor: {
actorIdleTimeout: "1h",
actorScanInterval: "30s",
drainOngoingCallTimeout: "5s",
drainRebalancedActors: true,
remindersStoragePartitions: 0,
},
},
});
await server.actor.init();
await server.actor.registerActor(CounterActor);
await server.start();
console.log(await server.actor.getRegisteredActors());
This fragment uses the CounterActor from the how-to guide and runs inside an async function. Set options on DaprServer.clientOptions.actor, because its embedded client supplies the actor runtime configuration. Setting actor options on an unrelated client does not configure the host.
The host exposes /healthz, /dapr/config, and the actor invocation, deactivation, timer, and reminder routes. getRegisteredActors() returns registered type names, not active instance counts. Actor runtime registration is process-wide in this SDK; do not treat multiple DaprServer instances in one process as independent actor runtimes.
Timeouts and scan intervals use Go duration strings, such as "30s" and "1h", rather than Temporal.Duration. Omitted settings are left to the runtime. See actor runtime configuration for defaults and runtime-version-specific behavior. remindersStoragePartitions concerns legacy state-store reminder storage, not Scheduler partitioning.
Reentrancy limitation
The configuration type accepts reentrancy: { enabled: true, maxStackDepth: 32 } and advertises it to Dapr. However, the JavaScript actor dispatcher and proxy do not implement reentrancy-context propagation. Do not assume that enabling the option makes actor call cycles, including an actor calling itself, safe. Prefer acyclic actor-to-actor calls and leave reentrancy disabled unless you provide and verify the required integration.Application boundaries and shutdown
Keep the HTTP callback routes reachable by the sidecar. In container deployments, configure the sidecar’s application-channel address and container networking so that callbacks reach the application. Loopback addresses refer to the sidecar’s own network namespace unless it shares one with the application.
The TypeScript contract does not restrict method dispatch: the dispatcher looks up a callable property on the actor instance by name. Do not rely on omitted interface members or TypeScript private modifiers to protect sensitive operations. Keep internal operations outside remotely callable actor methods and use appropriate application and sidecar access controls.
On orderly shutdown, stop accepting new work and await in-flight operations. await server.stop() closes the application listener; it does not stop its embedded client. Stop separately owned clients with await client.stop() and the embedded client with await server.client.stop() when they are no longer needed. Do not assume shutdown invokes every actor’s deactivation hook.
3 - Call actors with the JavaScript SDK
Use actor identities, typed proxies, dynamic invocations, and the lower-level actor client
Use a long-lived DaprClient and reuse an ActorProxyBuilder to call actors. These examples target @dapr/dapr 3.19.0 or later and use the counter contract and implementation from the how-to guide.
Build a proxy for a known identity
import {
ActorId,
ActorProxyBuilder,
CommunicationProtocolEnum,
DaprClient,
} from "@dapr/dapr";
import { CounterActor } from "./CounterActor";
import type { CounterActorContract } from "./CounterActorContract";
const client = new DaprClient({
daprHost: "127.0.0.1",
daprPort: "3500",
communicationProtocol: CommunicationProtocolEnum.HTTP,
});
const builder = new ActorProxyBuilder<CounterActorContract>(CounterActor, client);
const counter = builder.build(new ActorId("counter-1"));
console.log(await counter.add(5));
console.log(await counter.get());
Run the statements inside an async function. Await every remote call and handle failures at your application’s error boundary.
ActorId requires a non-empty string. Use a stable ID, such as an order or device ID, to return to the same actor. ActorId.createRandomId() generates a new identity. getId() and toString() return the raw ID; getURLSafeId() returns its URI-encoded form. Pass ActorId objects to the SDK rather than pre-encoding their contents. Runtime APIs can impose additional identifier restrictions.
The builder uses the class’s runtime name as the actor type. It does not instantiate that class locally. A separate client does not have to import the host implementation: it can use a client-side contract class with the same runtime name and method shape. Alternatively, use a dynamic invocation with the exact registered type name.
The generic interface checks calls at compile time only. The SDK creates a JavaScript Proxy that forwards property calls to the sidecar; it does not validate the remote contract or hydrate a local actor instance. Do not await the proxy itself or treat it as a local data object; await its method calls.
Create a proxy with a random identity
For cases where you do not need a predetermined ID:
const counter = client.actor.create<CounterActorContract>(CounterActor);
console.log(await counter.add(1));
Each create() call selects a new random actor ID. Use builder.build(actorId) when callers must reconnect to the same actor.
Choose HTTP or gRPC
The caller can use either sidecar API, regardless of the actor host’s HTTP application protocol. For gRPC, create the client using the sidecar’s gRPC port:
const grpcClient = new DaprClient({
daprHost: "127.0.0.1",
daprPort: "50001",
communicationProtocol: CommunicationProtocolEnum.GRPC,
});
const grpcBuilder = new ActorProxyBuilder<CounterActorContract>(CounterActor, grpcClient);
const counter = grpcBuilder.build(new ActorId("counter-1"));
console.log(await counter.get());
Use string port values. Configure daprApiToken or DAPR_API_TOKEN when the sidecar requires API-token authentication. Proxy builders inherit the client’s resolved options.
Access the lower-level actor API
DaprClient.actor exposes create(), not the individual invocation, state, and reminder methods. Those methods are available through the builder’s actor client:
const actors = builder.actorClient.actor;
const result = await actors.invoke(
"CounterActor",
new ActorId("counter-1"),
"add",
[3],
);
console.log(result);
This is the same lower-level API used by the proxy. It exposes:
| Methods | Purpose |
|---|
invoke(type, id, method, body?) | Invoke a named method |
stateGet(type, id, key), stateTransaction(type, id, operations) | Read actor state and execute state transactions |
registerActorTimer(type, id, name, timer), unregisterActorTimer(type, id, name) | Manage timers |
registerActorReminder(type, id, name, reminder), unregisterActorReminder(type, id, name) | Manage individual reminders |
getActorReminder(type, id, name) | Retrieve normalized reminder configuration |
listActorReminders(type, id?) | List reminders, subject to protocol and runtime support |
unregisterActorRemindersByType(type, id?) | Delete matching reminders in bulk over gRPC |
See manage actor state and use timers and reminders for details.
The lower-level interface also declares getActors(), but it is not a portable discovery API: gRPC returns active actor counts from sidecar metadata, while the HTTP implementation does not target a valid discovery endpoint in this release. Use server.actor.getRegisteredActors() for local registration information and client.metadata.get() for sidecar metadata instead.
Arguments and serialization
Proxies send supplied method arguments as a JSON array; a call without arguments sends null. The host spreads a top-level array into positional arguments, so JavaScript actor methods can accept multiple arguments:
// A proxy call:
await counter.add(3);
// The equivalent lower-level invocation body:
await actors.invoke("CounterActor", new ActorId("counter-1"), "add", [3]);
For a method accepting one array argument, send an outer argument array: [[1, 2, 3]]. A non-array object body is passed as one argument. For cross-SDK calls, agree on the actor type, method name, JSON shape, and argument convention explicitly; the JavaScript positional-array convention is not a universal actor contract.
Use argument arrays for scalar values as well: pass [false], [0], or [""], rather than the scalar itself. The lower-level invocation paths can omit falsy scalar bodies.
Use JSON-compatible data-transfer objects. TypeScript types do not restore class prototypes or Date instances after deserialization. Avoid BigInt, circular references, functions, and relying on undefined surviving a JSON round trip.
The actor host uses its built-in BufferSerializer and HTTP response handling. gRPC invocations encode arguments with JSON.stringify and parse response bytes as JSON when possible, otherwise returning text. HTTP responses depend on the response content type. In particular, primitive and void responses need not have identical JavaScript types over both transports. Prefer object-shaped responses, such as { value: 3 }, when callers need a predictable contract.
Setting DaprClientOptions.serializer does not replace the actor host serializer or supply a custom actor serialization pipeline. Actor paths do not wire this option through as the general-purpose application payload APIs do.
Errors and client lifetime
Transport failures, missing actor types or methods, and actor exceptions reject the awaited call. Do not assume a rejected call means no side effect occurred; design commands and retries to be idempotent where needed.
HTTP runtime failures are Error instances whose messages include a JSON envelope with status, error, and error_msg; network failures can have other shapes. gRPC failures preserve Connect RPC error codes. Do not parse every error message as JSON or compare gRPC codes to HTTP status numbers.
After outstanding calls finish, call await client.stop() for clients your application owns. Do not stop a shared client after each actor method.
4 - Manage actor state with the JavaScript SDK
Use the actor state manager, understand automatic persistence, and handle cache consistency
Actors access named state through this.getStateManager<T>(). State is scoped to an actor’s type and ID, and is persisted through the configured actor state store. Ordinary fields on the actor instance are not durable. These examples target @dapr/dapr 3.19.0 or later.
Read and update state
Call setState to mark a value as changed, even when you have modified an object returned from the cache:
import { AbstractActor } from "@dapr/dapr";
interface DeviceState {
reading: number;
}
export class DeviceStateActor extends AbstractActor {
async update(reading: number): Promise<DeviceState> {
const manager = this.getStateManager<DeviceState>();
const next = { reading };
await manager.setState("device", next);
return next;
}
async read(): Promise<DeviceState | null> {
const [found, state] = await this.getStateManager<DeviceState>().tryGetState("device");
return found ? state : null;
}
async remove(): Promise<void> {
const manager = this.getStateManager<DeviceState>();
const [found] = await manager.tryGetState("device");
if (found) {
await manager.removeState("device");
}
}
}
The generic parameter describes the values managed through that reference; it is not a runtime schema or serializer. Use separate typed references for differently shaped state values and validate data at application boundaries. The removal example loads the value into the cache first to avoid the uncached object-existence limitation described below.
Choose a state operation
| Method | Behavior |
|---|
setState(name, value) | Track an upsert |
getState(name) | Read cached or persisted state; throws when the state is not found |
tryGetState(name) | Return [found, value]; check the flag before using the value |
getOrAddState(name, initialValue) | Return existing state or track the supplied initial value |
addOrUpdateState(name, initialValue, updateValueFactory) | Add an initial value or compute a replacement from (name, currentValue) |
containsState(name) | Check the cache or state provider for existence, subject to the limitations below |
removeState(name) | Track a deletion; throws if no state is found |
tryRemoveState(name) | Track a deletion and report whether state was found |
saveState() | Submit tracked changes to the actor state provider |
clearCache() | Discard cached values and pending changes without deleting persisted state |
Although getState() is typed as Promise<T | null>, absence throws rather than returning null. With tryGetState(), check the found flag rather than treating the value as a presence indicator. The provider can return undefined for an absent value even though the declared tuple uses T | null.
State-manager limitations in this release
The state provider uses truthiness and a value’s length to infer existence. Persisted falsy values, empty strings/arrays, and object values do not behave consistently across existence checks. Prefer object-shaped state with getOrAddState or tryGetState for reads, and verify absence/deletion behavior for your data shape.
addState and tryAddState are present, but the uncached existence check in tryAddState is inverted; do not rely on them as create-only operations. getStateNames() only examines selected entries in the local change tracker, including pending deletions; it does not enumerate persisted keys. Use known state keys rather than treating it as a state-store query.
Automatic and explicit persistence
After a dispatched actor method succeeds, the SDK runs onActorMethodPost() and then saves tracked changes. The same path is used for timer and reminder callbacks. Activation and deactivation hooks also save their tracked changes on successful completion.
saveState() batches tracked additions, updates, and removals into an actor state transaction. Configure a store with the required actor and transaction support. This transaction does not include external side effects.
Save explicitly when an intermediate persistence point is required:
const manager = this.getStateManager<{ status: string }>();
await manager.setState("operation", { status: "accepted" });
await manager.saveState();
This fragment belongs inside an actor method. A later exception does not undo an explicit save.
The state manager caches values across calls on an active actor. Calling clearCache() forces subsequent reads to consult persisted state, but also discards unsaved changes. It is not a deletion API.
Failures and cache consistency
A thrown method or pre/post hook prevents the normal end-of-call save. The current dispatcher does not automatically roll back or clear cached changes after a failed call, so a later successful call can save changes left by the failed one. The state manager also marks changes as unmodified before awaiting the provider’s transaction; do not assume calling saveState() again will replay a failed write.
Treat state-save errors as operation failures and reconcile with persisted state before continuing. Where appropriate, use clearCache() to discard uncommitted local changes and reload, accounting for the fact that a timed-out transaction might already have committed. Avoid promising application-level rollback or exactly-once effects.
Lower-level state operations
The lower-level actor client supports stateGet(type, id, key) and stateTransaction(type, id, operations). They address the actor state store, not an arbitrary named state component.
For example, with the HTTP actors client from that guide:
await actors.stateTransaction("CounterActor", new ActorId("counter-1"), [
{ operation: "upsert", request: { key: "maintenance-note", value: "reviewed" } },
{ operation: "delete", request: { key: "obsolete-note" } },
]);
Keep writes inside actor methods whenever possible. Direct transactions do not update an active instance’s state-manager cache or provide an actor method’s serialized turn. They can race with actor calls or be overwritten by later cached writes. The gRPC transaction implementation stringifies values rather than applying the HTTP JSON encoding; do not assume structured state has transport parity.
5 - Use actor timers and reminders with the JavaScript SDK
Schedule actor callbacks, inspect reminders, and safely remove reminders in bulk
Timers schedule work during an actor’s activation. Reminders are durable and can reactivate an actor after deactivation or failover. Dapr runs the scheduling; the SDK registers callbacks and dispatches them to your actor. These examples target @dapr/dapr 3.19.0 or later.
See actor timers and reminders for runtime scheduling and retry semantics. Modern Dapr runtimes persist reminders in the Scheduler service, not in the actor’s in-memory fields.
Register callbacks inside an actor
Use the AbstractActor helpers from an application method. Import Temporal from @dapr/dapr:
import { AbstractActor, Temporal } from "@dapr/dapr";
export interface ScheduledActorContract {
start(): Promise<void>;
cancel(): Promise<void>;
}
export class ScheduledActor extends AbstractActor implements ScheduledActorContract {
async start(): Promise<void> {
await this.registerActorTimer(
"heartbeat",
"onHeartbeat",
Temporal.Duration.from({ seconds: 2 }),
Temporal.Duration.from({ seconds: 30 }),
Temporal.Duration.from({ minutes: 5 }),
"heartbeat",
);
await this.registerActorReminder(
"follow-up",
Temporal.Duration.from({ minutes: 1 }),
Temporal.Duration.from({ minutes: 5 }),
Temporal.Duration.from({ hours: 1 }),
"follow-up",
);
}
async onHeartbeat(data: unknown): Promise<void> {
console.log("Timer:", data);
}
async receiveReminder(data: unknown): Promise<void> {
console.log("Reminder:", data);
}
async cancel(): Promise<void> {
await this.unregisterActorTimer("heartbeat");
await this.unregisterActorReminder("follow-up");
}
}
Register ScheduledActor on the HTTP host just like the actor in the how-to guide, then call start() through a proxy. Both callbacks participate in the usual pre/post method hooks and successful state-save path.
The helper signatures are:
registerActorTimer(name, callback, dueTime, period?, ttl?, state?)
registerActorReminder(name, dueTime, period?, ttl?, state?)
unregisterActorTimer(name)
unregisterActorReminder(name)
The required helper dueTime is a Temporal.Duration. period and ttl are optional durations. Omit period for a one-shot callback; use undefined placeholders when supplying later optional arguments. For example:
await this.registerActorReminder(
"once",
Temporal.Duration.from({ seconds: 10 }),
undefined,
undefined,
"one-shot",
);
Keep the TTL long enough for the first due time and any desired repetitions. The runtime validates scheduling values. Prefer time-based durations such as seconds, minutes, and hours: the HTTP registration implementation converts the Temporal string to a Go-style duration and does not correctly encode calendar/day duration strings. These helpers do not accept absolute timestamps, cron expressions, or repetition-count strings.
Timer callbacks use the method name supplied as callback. Every reminder calls receiveReminder(data). The JavaScript callback does not receive the reminder name, due time, or period as separate arguments. Include a discriminator in the data when sharing one callback between several reminders. The inherited receiveReminder only logs a warning; override it to perform work.
The callback data is decoded by the built-in buffer serializer. Prefer simple string payloads or explicitly serialized JSON that you validate in the callback. Do not assume arbitrary objects and arrays round-trip unchanged through every timer/reminder registration path; arrays are also spread into positional arguments by the dispatcher. Make durable callback processing idempotent because retries can cause repeated delivery.
Manage schedules from a client
Use the lower-level actor API. The following setup uses a name-only class: it selects the registered actor type without importing its server implementation.
import { ActorId, ActorProxyBuilder, DaprClient, Temporal } from "@dapr/dapr";
class ScheduledActor {}
const client = new DaprClient({ daprHost: "127.0.0.1", daprPort: "3500" });
const actors = new ActorProxyBuilder(ScheduledActor, client).actorClient.actor;
const actorId = new ActorId("device-1");
Run this and subsequent client fragments inside an async function. This example assumes ScheduledActor is registered on the host. DaprClient.actor itself only exposes create(); calls such as client.actor.getActorReminder() are not supported.
To register a reminder over HTTP:
await actors.registerActorReminder("ScheduledActor", actorId, "follow-up", {
dueTime: Temporal.Duration.from({ minutes: 1 }),
period: Temporal.Duration.from({ minutes: 5 }),
ttl: Temporal.Duration.from({ hours: 1 }),
data: "follow-up",
});
Timer registration has the same options plus a required callback:
await actors.registerActorTimer("ScheduledActor", actorId, "heartbeat", {
callback: "onHeartbeat",
dueTime: Temporal.Duration.from({ seconds: 2 }),
period: Temporal.Duration.from({ seconds: 30 }),
data: "heartbeat",
});
Ensure the actor is active before registering a timer externally, and send timer operations to the sidecar that owns that actor. Registering timers inside an actor method avoids selecting the wrong host. Timers disappear on deactivation; re-register them during a later activation if the application requires them.
The HTTP registration path sends data as JSON. gRPC reminder registration uses data.toString(), while gRPC timer registration expects data accepted by Buffer.from; neither JSON-serializes arbitrary objects. Do not rely on falsy gRPC payloads being transmitted.
Registration and retrieval are not symmetric across protocols. A runtime can store the bytes sent by gRPC registration as a base64-encoded JSON string, and the SDK’s reminder queries preserve that string without automatically decoding it. For example, a gRPC registration with data: "hello" can be retrieved as "aGVsbG8=". Calling JSON.stringify before gRPC registration does not by itself restore a JSON round trip. Prefer HTTP registration for JSON payloads, even when querying over gRPC. If you must register over gRPC, define and verify an explicit encoding contract with your runtime and consumers; do not guess whether arbitrary returned strings are base64.
Retrieve a reminder
getActorReminder is available over HTTP and gRPC:
const reminder = await actors.getActorReminder("ScheduledActor", actorId, "follow-up");
console.log({
data: reminder.data,
dueTime: reminder.dueTime?.toString(),
period: reminder.period?.toString(),
remainingTtl: reminder.ttl?.toString(),
});
The returned configuration has optional dueTime, period, ttl, and data fields. Timing fields are normalized to Temporal.Duration, not raw runtime strings.
- ISO 8601 and Go-style durations, including subsecond units, are normalized. A period beginning with
@every is also accepted. - Absolute due-time and expiration timestamps become durations relative to retrieval time.
ttl is the remaining lifetime, not necessarily the originally registered TTL. - Missing or empty timing fields remain
undefined. Some Scheduler-backed runtimes omit ttl even for expiring reminders. Do not interpret an absent field as proof that no expiration was configured. - A cron expression or repetition-count period such as
R3/PT1M cannot be represented by Temporal.Duration; retrieval rejects rather than returning a lossy schedule.
HTTP retrieval preserves the runtime’s JSON data. gRPC retrieval unwraps a protobuf BytesValue and parses its bytes as JSON; unsupported envelopes or invalid JSON reject the operation. A missing reminder rejects rather than returning an empty configuration.
List reminders
For a single actor:
import type { NamedActorReminderType } from "@dapr/dapr";
const reminders: NamedActorReminderType[] =
await actors.listActorReminders("ScheduledActor", actorId);
for (const reminder of reminders) {
console.log(reminder.actorType, reminder.actorId, reminder.name, reminder.data);
}
Keep the type import at module scope. NamedActorReminderType contains name, actorType, and the raw string actorId, together with the normalized fields returned by getActorReminder. It does not expose HTTP’s actorID spelling or a nested protobuf reminder.
| Operation | Runtime requirement |
|---|
| HTTP listing with an exact actor ID | Dapr 1.19+ |
| gRPC listing with an exact actor ID | Dapr 1.17+ |
| gRPC listing without an actor ID, across the actor type | Dapr 1.17+ |
The actor ID is an exact filter, not a prefix. HTTP requires it and rejects an omitted ID before sending a request. With a gRPC-configured lower-level client, omit it to list every instance of that type:
const allReminders = await grpcActors.listActorReminders("ScheduledActor");
Here grpcActors is configured as shown in the next section. Ordering is not guaranteed; an empty successful result is []. Malformed entries, unrepresentable timing values, unsupported runtime APIs, and transport errors reject the list operation rather than returning a partial or empty success.
Unregister reminders and timers
Individual removals work over HTTP and gRPC:
await actors.unregisterActorReminder("ScheduledActor", actorId, "follow-up");
await actors.unregisterActorTimer("ScheduledActor", actorId, "heartbeat");
Bulk reminder deletion requires gRPC and Dapr 1.17 or later. For example, configure a separate administrative client:
import { CommunicationProtocolEnum } from "@dapr/dapr";
const grpcClient = new DaprClient({
daprHost: "127.0.0.1",
daprPort: "50001",
communicationProtocol: CommunicationProtocolEnum.GRPC,
});
const grpcActors = new ActorProxyBuilder(ScheduledActor, grpcClient).actorClient.actor;
await grpcActors.unregisterActorRemindersByType("ScheduledActor", actorId);
Keep the import at module scope. Passing an actor ID deletes reminders only for that exact instance and leaves other instances and actor types unaffected.
Type-wide deletion is destructive
Omitting the second argument to unregisterActorRemindersByType("ScheduledActor") deletes all reminders for all instances of that actor type in the sidecar’s application scope. Deleted reminders cannot be recovered by this operation. Do not turn a missing user-supplied actor ID into undefined unless type-wide deletion is explicitly intended.The HTTP implementation throws HTTPNotSupportedError for bulk deletion without sending a request. There is no automatic per-reminder HTTP fallback. Both instance-scoped and type-wide gRPC deletion return Promise<void>; RPC errors propagate.
This release does not expose timer get/list operations or reminder overwrite and failurePolicy registration options, even when the runtime supports them. Use the Actors API reference for those runtime capabilities rather than adding unsupported properties to SDK calls.
Do not assume registering an existing reminder name replaces its configuration. Runtimes with create-only reminder registration reject duplicates unless overwrite is requested, which these SDK helpers cannot express. The start() example therefore is not an idempotent scheduling operation. Use the runtime API when replacement is required, or explicitly unregister before recreating the reminder, accounting for the gap and reset schedule.
After administrative work completes, stop clients your application owns with await client.stop() and await grpcClient.stop().