This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

JavaScript SDK

JavaScript SDK packages for developing Dapr applications

A client library for building Dapr apps in JavaScript and TypeScript. This client abstracts the public Dapr APIs like service to service invocation, state management, pub/sub, secrets, and much more, and provides a simple, intuitive API for building applications.

Installation

To get started with the JavaScript SDK, install the Dapr JavaScript SDK package from NPM:

npm install --save @dapr/dapr

Conversation

Converse with LLMs using DaprClient.conversation over HTTP or gRPC, including tool calling, structured output, PII scrubbing, prompt cache retention, and token usage statistics. These guides target JavaScript SDK 3.19.0 or later.

Follow the Conversation how-to guide to get started. See client usage for runtime requirements and request options, and tool calling for a complete function-call round trip.

Jobs

Schedule one-time, interval-based, or cron-based jobs and receive their callbacks using DaprClient.jobs and DaprServer.jobs. Jobs requires JavaScript SDK 3.19.0 or later, Dapr runtime 1.16 or later, and a running Scheduler service.

Follow the Jobs how-to guide, read the client and callback usage guidance, or build calendar-based schedules with the cron builder.

Configuration

Read application settings and subscribe to changes with DaprClient.configuration. The configuration how-to guide and client usage guidance cover selected or all keys, store-specific metadata, response handling, and subscription cleanup using @dapr/dapr 3.19.0 or later. Configuration operations require gRPC in the JavaScript SDK.

State Management

Save, retrieve, and delete application state with DaprClient.state. The SDK supports bulk operations, ETags, consistency options, transactions, TTL metadata, custom serialization, and state queries over HTTP or gRPC.

Follow the state management how-to guide, read the client usage guidance for protocol-specific behavior, or learn how to query state. These guides target JavaScript SDK 3.19.0 or later.

Publish and subscribe

Publish single events or batches over HTTP or gRPC, receive programmatic server callbacks, or subscribe through a client-initiated gRPC stream without an application server.

The publish and subscribe guide covers JavaScript SDK 3.19.0 or later, including routing, acknowledgements, dead-letter topics, serialization, error handling, and subscription cleanup.

Service invocation

Call services over HTTP or gRPC and receive invocations through DaprServer or an existing HTTP application.

The service invocation guide covers JavaScript SDK 3.19.0 or later, including headers, query parameters, callback responses, serialization, error handling, and transport limitations.

Secrets

Retrieve individual and bulk secrets with DaprClient.secret. Follow the Secrets how-to guide for setup, or read the client usage guidance for metadata, response shapes, and HTTP/gRPC differences. The guides target JavaScript SDK 3.19.0 or later.

Binary Store

Store, retrieve, stream, and delete binary files with DaprClient.binaryStore over HTTP or gRPC. Binary Store requires Dapr runtime 1.19 or later and JavaScript SDK 3.19.0 or later.

Follow the Binary Store how-to guide to get started, or read the client usage guidance for configuration, streaming, cancellation, and error handling.

Cryptography

Encrypt and decrypt messages and files with DaprClient.crypto over HTTP or gRPC in @dapr/dapr 3.19.0 or later. Prefer gRPC for large payloads and production streaming.

Follow the Cryptography how-to guide to configure a component and encrypt data. Read the client usage guidance for supported inputs, ciphers, key references, streaming, and limitations.

Manage indexes and index, retrieve, delete, and query JSON documents with DaprClient.search over HTTP or gRPC. Search requires Dapr runtime 1.19 or later and JavaScript SDK 3.19.0 or later.

Follow the Search how-to guide to get started, or read the client usage guidance for query options, pagination, write acknowledgements, cancellation, and errors.

Vector

Store precomputed embeddings and run similarity searches with DaprClient.vector over HTTP or gRPC. Vector requires a Vector-capable Dapr runtime from the 1.19 release line or later and JavaScript SDK 3.19.0 or later.

Follow the Vector how-to guide to get started, or read the client usage guidance for configuration, indexing acknowledgements, cancellation, and error handling.

Structure

The Dapr JavaScript SDK contains two major components:

  • DaprServer: to manage all Dapr sidecar to application communication.
  • DaprClient: to manage all application to Dapr sidecar communication.

The above communication can be configured to use either of the gRPC or HTTP protocols.

Dapr ServerDapr Client

Getting Started

To help you get started, check out the resources below:

Client

Create a JavaScript client and interact with the Dapr sidecar and other Dapr applications (e.g., publishing events, output binding support, etc.).

Server

Create a JavaScript server and let the Dapr sidecar interact with your application (e.g., subscribing to events, input binding support, etc.).

Actors

Create virtual actors with state, reminders/timers, and methods.


Logging

Configure and customize the SDK logging.

Examples

Clone the JavaScript SDK source code and try out some of the examples to get started quickly.

1 - JavaScript Client SDK

JavaScript Client SDK for developing Dapr applications

Introduction

The Dapr Client allows you to communicate with the Dapr Sidecar and get access to its client facing features such as Publishing Events, Invoking Output Bindings, State Management, Secret Management, and much more.

For LLM interactions, use client.conversation.converse(request). The Conversation guides cover setup, message history, structured output, caching, token usage, and tool calling with JavaScript SDK 3.19.0 or later.

Pre-requisites

Installing and importing Dapr’s JS SDK

  1. Install the SDK with npm:
npm i @dapr/dapr --save
  1. Import the libraries:
import { DaprClient, DaprServer, HttpMethod, CommunicationProtocolEnum } from "@dapr/dapr";

const daprHost = "127.0.0.1"; // Dapr Sidecar Host
const daprPort = "3500"; // Dapr Sidecar Port of this Example Server
const serverHost = "127.0.0.1"; // App Host of this Example Server
const serverPort = "50051"; // App Port of this Example Server

// HTTP Example
const client = new DaprClient({ daprHost, daprPort });

// GRPC Example
const client = new DaprClient({ daprHost, daprPort, communicationProtocol: CommunicationProtocolEnum.GRPC });

Running

To run the examples, you can use two different protocols to interact with the Dapr sidecar: HTTP (default) or gRPC.

Using HTTP (default)

import { DaprClient } from "@dapr/dapr";
const client = new DaprClient({ daprHost, daprPort });
# Using dapr run
dapr run --app-id example-sdk --app-protocol http -- npm run start

# or, using npm script
npm run start:dapr-http

Using gRPC

Since HTTP is the default, you will have to adapt the communication protocol to use gRPC. You can do this by passing an extra argument to the client or server constructor.

import { DaprClient, CommunicationProtocolEnum } from "@dapr/dapr";
const client = new DaprClient({ daprHost, daprPort, communicationProtocol: CommunicationProtocolEnum.GRPC });
# Using dapr run
dapr run --app-id example-sdk --app-protocol grpc -- npm run start

# or, using npm script
npm run start:dapr-grpc

Environment Variables

Dapr Sidecar Endpoints

You can use the DAPR_HTTP_ENDPOINT and DAPR_GRPC_ENDPOINT environment variables to set the Dapr Sidecar’s HTTP and gRPC endpoints respectively. When these variables are set, the daprHost and daprPort don’t have to be set in the options argument of the constructor, the client will parse them automatically out of the provided endpoints.

import { DaprClient, CommunicationProtocolEnum } from "@dapr/dapr";

// Using HTTP, when DAPR_HTTP_ENDPOINT is set
const client = new DaprClient();

// Using gRPC, when DAPR_GRPC_ENDPOINT is set
const client = new DaprClient({ communicationProtocol: CommunicationProtocolEnum.GRPC });

If the environment variables are set, but daprHost and daprPort values are passed to the constructor, the latter will take precedence over the environment variables.

Dapr API Token

You can use the DAPR_API_TOKEN environment variable to set the Dapr API token. When this variable is set, the daprApiToken doesn’t have to be set in the options argument of the constructor, the client will get it automatically.

General

Increasing Body Size

You can increase the body size that is used by the application to communicate with the sidecar by using aDaprClient’s option.

import { DaprClient, CommunicationProtocolEnum } from "@dapr/dapr";

// Allow a body size of 10Mb to be used
// The default is 4Mb
const client = new DaprClient({
  daprHost,
  daprPort,
  communicationProtocol: CommunicationProtocolEnum.HTTP,
  maxBodySizeMb: 10,
});

Proxying Requests

In JavaScript SDK 3.19.0, client.proxy.create() is unsupported for both HTTP and the gRPC ConnectRPC transport and rejects when called. Use client.invoker.invoke() for Dapr service invocation.

For native protobuf services, use your generated gRPC client connected to the Dapr sidecar with dapr-app-id metadata. See native gRPC services and streaming and the runtime gRPC proxying guide.

Building blocks

The JavaScript Client SDK allows you to interface with all of the Dapr building blocks focusing on Client to Sidecar features.

Search API

Use client.search to manage indexes and index, retrieve, delete, and query JSON documents over HTTP or gRPC. Requires Dapr runtime 1.19 or later and JavaScript SDK 3.19.0 or later.

See the Search how-to guide for a complete example, and Search client usage for method signatures, query options, write acknowledgements, cancellation, and errors.

Jobs API

Use client.jobs to schedule, retrieve, and delete jobs with JavaScript SDK 3.19.0 or later. See the Jobs how-to guide and client usage guidance for scheduling options, payloads, runtime compatibility, and error handling.

Invocation API

For JavaScript SDK 3.19.0 or later, see the service invocation overview for HTTP and gRPC capabilities.

Invoke a Service

import { DaprClient, HttpMethod } from "@dapr/dapr";

const daprHost = "127.0.0.1";
const daprPort = "3500";

async function start() {
  const client = new DaprClient({ daprHost, daprPort });

  const serviceAppId = "my-app-id";
  const serviceMethod = "say-hello";

  try {
    const response = await client.invoker.invoke(
      serviceAppId,
      serviceMethod,
      HttpMethod.POST,
      { hello: "world" },
      { headers: { "X-User-ID": "123" } },
    );
    console.log(response);
  } finally {
    await client.stop();
  }
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

Follow calling services for a runnable client and server setup, other verbs, query parameters, namespaces, and non-Dapr endpoints. See configuration and usage for serialization and errors. Custom request headers are forwarded by the HTTP invoker, not the gRPC invoker.

State Management API

Use client.state to access configured state stores over HTTP or gRPC. The state management guides cover JavaScript SDK 3.19.0 or later, including bulk deletion, ETags, state options, TTL, transactions, serialization, and protocol-specific errors.

Save, Get and Delete application state

Follow How to: Manage state for a runnable example with component configuration, save/get/delete, bulk reads and deletes, conditional updates, and transactions.

HTTP save, delete, and deleteBulk can return { error } instead of rejecting. Check that field and also handle promise rejections; gRPC RPC failures reject. See state client usage for the complete method and response reference.

Query State API

The alpha query API supports filtering, sorting, and pagination on query-capable stores over HTTP and gRPC. Follow How to: Query state for query expressions, continuation tokens, and result decoding.

PubSub API

For JavaScript SDK 3.19.0 or later, see the publish and subscribe overview to choose a delivery mode.

Publish messages

import { DaprClient } from "@dapr/dapr";

const daprHost = "127.0.0.1";
const daprPort = "3500";

async function start() {
  const client = new DaprClient({ daprHost, daprPort });

  try {
    const response = await client.pubsub.publish("pubsub", "orders", { orderId: "101" });
    if (response.error) {
      throw response.error;
    }
  } finally {
    await client.stop();
  }
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

See publishing events for bulk operations, metadata, CloudEvents, raw payloads, and transport-specific error handling. See configuration and usage for client lifetime and serialization.

Streaming message subscriptions

With a gRPC client, client.pubsub.subscribeStream() receives messages without DaprServer or an application callback port. Follow the streaming subscription guide for a complete worker, runtime requirements, acknowledgements, cancellation, and failure supervision.

Bindings API

Invoke Output Binding

Output Bindings

import { DaprClient } from "@dapr/dapr";

const daprHost = "127.0.0.1";
const daprPort = "3500";

async function start() {
  const client = new DaprClient({ daprHost, daprPort });

  const bindingName = "my-binding-name";
  const bindingOperation = "create";
  const message = { hello: "world" };

  const response = await client.binding.send(bindingName, bindingOperation, message);
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

For a full guide on output bindings visit How-To: Use bindings.

Secret API

Retrieve secrets

import { DaprClient } from "@dapr/dapr";

const daprHost = "127.0.0.1";
const daprPort = "3500";

async function start() {
  const client = new DaprClient({ daprHost, daprPort });

  const secretStoreName = "my-secret-store";
  const secretKey = "secret-key";

  // Retrieve a single secret; the result is the secret's key-value data
  const secret = await client.secret.get(secretStoreName, secretKey);

  // Retrieve all accessible secrets, nested by secret name
  const secrets = await client.secret.getBulk(secretStoreName);
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

For JavaScript SDK 3.19.0 or later, follow the Secrets how-to guide for a runnable example, or read the Secrets client usage guidance for configuration, TypeScript response validation, and error handling.

In 3.19.0, HTTP get accepts metadata as a query-string string and preserves all values in a multi-valued secret. gRPC get ignores metadata and returns only the first key-value pair. Bulk retrieval preserves nested values with either protocol, but neither exposes bulk metadata.

For the underlying building block, see How to: Retrieve a secret.

Configuration API

Use client.configuration to read application settings and subscribe to changes. The JavaScript SDK requires gRPC for every configuration method; the HTTP client is not supported.

The configuration how-to guide provides runnable examples for @dapr/dapr 3.19.0 or later. The client usage guidance covers all methods, metadata, response shapes, and subscription error limitations.

Get Configuration Keys

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const daprHost = "127.0.0.1";

async function start() {
  const client = new DaprClient({
    daprHost,
    daprPort: process.env.DAPR_GRPC_PORT ?? "50001",
    communicationProtocol: CommunicationProtocolEnum.GRPC,
  });

  try {
    const config = await client.configuration.get("config-store", ["key1", "key2"]);
    console.log(config);
  } finally {
    await client.stop();
  }
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

Sample output:

{
   items: {
     key1: { key: 'key1', value: 'foo', version: '', metadata: {} },
     key2: { key: 'key2', value: 'bar2', version: '', metadata: {} }
   }
}

Subscribe to Configuration Updates

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const daprHost = "127.0.0.1";

async function start() {
  const client = new DaprClient({
    daprHost,
    daprPort: process.env.DAPR_GRPC_PORT ?? "50001",
    communicationProtocol: CommunicationProtocolEnum.GRPC,
  });

  try {
    const stream = await client.configuration.subscribeWithKeys("config-store", ["key1", "key2"], async (data) => {
      console.log("Subscribe received updates from config store: ", data);
    });

    try {
      await new Promise((resolve) => setTimeout(resolve, 60000));
    } finally {
      await stream.stop();
    }
  } finally {
    await client.stop();
  }
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

Sample output:

Subscribe received updates from config store:  {
  items: { key2: { key: 'key2', value: 'bar', version: '', metadata: {} } }
}
Subscribe received updates from config store:  {
  items: { key1: { key: 'key1', value: 'foobar', version: '', metadata: {} } }
}

To watch all keys, use subscribe(storeName, callback). To include store-specific metadata, use subscribeWithMetadata(storeName, keys, metadata, callback). All-key behavior depends on the component. See subscription lifetime and callbacks before relying on a subscription for configuration freshness.

Cryptography API

In @dapr/dapr 3.19.0 or later, client.crypto supports encryption and decryption over both HTTP and gRPC. Pass data and options to receive a Buffer, or pass only options to receive a Node.js Duplex stream.

Prefer gRPC for large payloads and production streaming. Dapr buffers HTTP cryptography output until the request is fully consumed.

import { DaprClient, CommunicationProtocolEnum } from "@dapr/dapr";

async function start() {
  const client = new DaprClient({
    daprHost: "127.0.0.1",
    daprPort: "50001",
    communicationProtocol: CommunicationProtocolEnum.GRPC,
  });

  try {
    const ciphertext = await client.crypto.encrypt("A message to encrypt", {
      componentName: "crypto-local",
      keyName: "symmetric256",
      keyWrapAlgorithm: "A256KW",
    });
    const plaintext = await client.crypto.decrypt(ciphertext, {
      componentName: "crypto-local",
    });
    console.log("Decrypted bytes:", plaintext.length);
  } finally {
    await client.stop();
  }
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

This example requires a configured crypto-local component and a compatible symmetric256 key. Follow the JavaScript Cryptography how-to guide for complete setup and file-stream examples. See the client usage guidance for ciphers, key references, errors, and runtime limitations.

Distributed Lock API

Try Lock and Unlock APIs

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";
import { LockStatus } from "@dapr/dapr/types/lock/UnlockResponse";

const daprHost = "127.0.0.1";
const daprPortDefault = "3500";

async function start() {
  const client = new DaprClient({ daprHost, daprPort });

  const storeName = "redislock";
  const resourceId = "resourceId";
  const lockOwner = "owner1";
  let expiryInSeconds = 1000;

  console.log(`Acquiring lock on ${storeName}, ${resourceId} as owner: ${lockOwner}`);
  const lockResponse = await client.lock.lock(storeName, resourceId, lockOwner, expiryInSeconds);
  console.log(lockResponse);

  console.log(`Unlocking on ${storeName}, ${resourceId} as owner: ${lockOwner}`);
  const unlockResponse = await client.lock.unlock(storeName, resourceId, lockOwner);
  console.log("Unlock API response: " + getResponseStatus(unlockResponse.status));
}

function getResponseStatus(status: LockStatus) {
  switch (status) {
    case LockStatus.Success:
      return "Success";
    case LockStatus.LockDoesNotExist:
      return "LockDoesNotExist";
    case LockStatus.LockBelongsToOthers:
      return "LockBelongsToOthers";
    default:
      return "InternalError";
  }
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

For a full guide on distributed locks visit How-To: Use Distributed Locks.

Workflow API

DaprClient.workflow exposes the basic workflow management API over HTTP or gRPC. For @dapr/dapr 3.19.0 or later, use scheduleNewWorkflow, getWorkflowState, and raiseEvent; the older start, get, and raise names are deprecated aliases.

For authoring workers, durable waits, listing, history retrieval, reruns, and binary-store payload references, use the separate workflow SDK guides. DaprWorkflowClient uses gRPC and returns WorkflowState, not the basic API’s response object shown below.

Workflow management

import { DaprClient } from "@dapr/dapr";

async function start() {
  const client = new DaprClient();

  // Start a new workflow instance
  const instanceId = await client.workflow.scheduleNewWorkflow("OrderProcessingWorkflow", {
    Name: "Paperclips",
    TotalCost: 99.95,
    Quantity: 4,
  });
  console.log(`Started workflow instance ${instanceId}`);

  // Get a workflow instance
  const workflow = await client.workflow.getWorkflowState(instanceId);
  console.log(
    `Workflow ${workflow.workflowName}, created at ${workflow.createdAt.toUTCString()}, has status ${
      workflow.runtimeStatus
    }`,
  );
  console.log(`Additional properties: ${JSON.stringify(workflow.properties)}`);

  // Pause a workflow instance
  await client.workflow.pause(instanceId);
  console.log(`Paused workflow instance ${instanceId}`);

  // Resume a workflow instance
  await client.workflow.resume(instanceId);
  console.log(`Resumed workflow instance ${instanceId}`);

  // Terminate a workflow instance
  await client.workflow.terminate(instanceId);
  console.log(`Terminated workflow instance ${instanceId}`);

  // Purge a workflow instance
  await client.workflow.purge(instanceId);
  console.log(`Purged workflow instance ${instanceId}`);
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

2 - State management with the Dapr JavaScript SDK

Save, retrieve, delete, transact, and query application state with the Dapr JavaScript SDK

Use DaprClient.state to access configured Dapr state store components from JavaScript or TypeScript. The client supports single-key reads and deletes, bulk saves, bulk reads and deletes, optimistic concurrency with ETags, multi-key transactions, and state queries over HTTP or gRPC.

These guides target @dapr/dapr 3.19.0 or later. State management is part of this package; there is no separate state management package or client registration.

GuideWhat you will learn
How to: Manage stateConfigure a store, save and read values, use ETags, perform bulk operations, and execute transactions.
State client usageConfigure the client, understand response shapes, metadata, serialization, error handling, and protocol differences.
How to: Query stateFilter, sort, and paginate state using the alpha query API.

Not every state store supports ETags, transactions, TTL, or queries. Check the supported state stores and your component’s reference before depending on these features. Bulk save and bulk delete are not substitutes for an atomic transaction.

This section covers application state, not the actor state manager. The JavaScript SDK does not provide the .NET SDK’s source-generated, store-scoped state interfaces; pass the component name to each client.state operation.

2.1 - How to: Manage state with the Dapr JavaScript SDK

Learn how to save, retrieve, and delete state, use ETags, and execute bulk operations and transactions

Use DaprClient.state to manage key/value application state without depending on a particular database client. This guide uses Redis and covers basic operations, optimistic concurrency, bulk operations, TTL, and transactions.

Prerequisites

In your application directory, initialize an npm project if needed and install the SDK:

npm init -y
npm install @dapr/dapr@3.19.0

The examples use JavaScript ES modules in an .mjs file. The same APIs are available in TypeScript.

Configure a state store

Create a components directory and save this component 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: ""

Adjust the connection settings for your Redis instance. See the Redis component reference for authentication and production configuration.

The component’s metadata.name, statestore, is the store name used in client calls. It is not the component type or the file name. Keep the same Dapr application ID between runs to read previously saved state; Dapr prefixes keys with the application ID by default.

Save, retrieve, and delete state

Save the following as app.mjs:

import { CommunicationProtocolEnum, DaprClient, JsonSerializer } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "3500",
  communicationProtocol: CommunicationProtocolEnum.HTTP,
  serializer: new JsonSerializer(),
});
const storeName = "statestore";

try {
  const saved = await client.state.save(storeName, [
    { key: "widget-a", value: { size: "small", color: "blue" } },
    { key: "widget-b", value: { size: "large", color: "white" } },
  ]);
  if (saved.error) {
    throw saved.error;
  }

  const widget = await client.state.get(storeName, "widget-a");
  console.log("Loaded widget:", widget);

  const deleted = await client.state.delete(storeName, "widget-b");
  if (deleted.error) {
    throw deleted.error;
  }
} catch (error) {
  console.error("State operation failed:", error);
  process.exitCode = 1;
} finally {
  await client.stop();
}

Start the application with a Dapr sidecar:

dapr run --app-id state-example --dapr-http-port 3500 --dapr-grpc-port 50001 --resources-path .\components -- node app.mjs

To use gRPC, change communicationProtocol to CommunicationProtocolEnum.GRPC and daprPort to "50001". The explicit JsonSerializer makes value serialization consistent across the examples, including object-valued transactions and query results. It is optional, but opting in can change how existing values are interpreted. Read the serialization guidance before changing an existing application’s configuration.

The remaining examples reuse client and storeName. Place them inside the try block before the client is stopped. They assume the initial save has completed.

Read multiple entries

save already accepts an array for bulk writes. Use getBulk to read multiple keys:

const items = await client.state.getBulk(
  storeName,
  ["widget-a", "widget-b"],
  { parallelism: 4 },
);

for (const item of items) {
  if (item.error) {
    throw new Error(`Unable to read ${item.key}: ${item.error}`);
  }
  console.log(item.key, item.data, item.etag);
}

Each result is an item with key, data, and an optional etag, not just the saved value. Match results by key rather than relying on array order. In this example, widget-b has already been deleted; missing values differ between protocols. See response shapes for details.

Optimistic concurrency with ETags

get returns only the value, not the response ETag. To read a value and its ETag, use getBulk with one key. Pass the returned ETag to a subsequent save or delete to detect concurrent changes.

import { StateConcurrencyEnum, StateConsistencyEnum } from "@dapr/dapr";

Keep this import at module scope. Inside the try block:

const itemsWithEtags = await client.state.getBulk(storeName, ["widget-a"]);
const current = itemsWithEtags.find((item) => item.key === "widget-a");
if (!current || current.error || !current.etag) {
  throw new Error("Could not read widget-a with an ETag");
}

const updated = await client.state.save(storeName, [
  {
    key: "widget-a",
    value: { ...current.data, color: "green" },
    etag: current.etag,
    options: {
      concurrency: StateConcurrencyEnum.CONCURRENCY_FIRST_WRITE,
      consistency: StateConsistencyEnum.CONSISTENCY_STRONG,
    },
  },
]);
if (updated.error) {
  throw updated.error;
}

The state store must support ETags. With first-write concurrency, an ETag mismatch indicates that another writer changed the value. Handle the conflict according to your application’s rules, for example by reading the latest value and recomputing the update. Do not blindly retry a stale ETag.

For a conditional delete, read a fresh ETag after any successful update:

const latestItems = await client.state.getBulk(storeName, ["widget-a"]);
const latest = latestItems.find((item) => item.key === "widget-a");
if (!latest || latest.error || !latest.etag) {
  throw new Error("Could not read widget-a with an ETag");
}

const removed = await client.state.delete(storeName, "widget-a", {
  etag: latest.etag,
  concurrency: StateConcurrencyEnum.CONCURRENCY_FIRST_WRITE,
  consistency: StateConsistencyEnum.CONSISTENCY_STRONG,
});
if (removed.error) {
  throw removed.error;
}

HTTP write failures can be returned in result.error, while gRPC failures reject the promise. The examples check the result and also have an outer error handler so neither path is ignored.

Delete multiple entries

deleteBulk accepts key-only items, with optional ETags, per-item metadata, and state options. It does not require or serialize values:

const bulkDeleted = await client.state.deleteBulk(storeName, [
  { key: "widget-a" },
  { key: "widget-b" },
]);
if (bulkDeleted.error) {
  throw bulkDeleted.error;
}

An empty array succeeds without contacting the sidecar. Over HTTP, the SDK issues individual deletes and waits for all of them; over gRPC, it sends one bulk-delete RPC. Neither is atomic, and a failure can leave some entries deleted. Use a transaction when all-or-nothing behavior is required.

Execute a state transaction

Use transaction to perform multiple upsert and delete operations atomically in a store that supports transactions:

await client.state.transaction(storeName, [
  {
    operation: "upsert",
    request: {
      key: "widget-a",
      value: { size: "medium", color: "green" },
    },
  },
  {
    operation: "delete",
    request: { key: "widget-b" },
  },
]);

This example uses the JsonSerializer configured above. Without an explicit serializer, the gRPC transaction implementation expects an already encoded string or bytes rather than serializing object values as save does.

The optional third argument is transaction-wide metadata:

await client.state.transaction(
  storeName,
  [
    {
      operation: "upsert",
      request: { key: "widget-c", value: { size: "small", color: "red" } },
    },
  ],
  { partitionKey: "widgets" },
);

Metadata and transaction constraints depend on the component. For example, a store may require all operations to use the same partition. See the client usage limitations before using per-operation metadata or ETags in a transaction.

Set a time to live

For a store that supports TTL, pass ttlInSeconds as metadata. This saves an entry that expires after 60 seconds:

const expiring = await client.state.save(
  storeName,
  [{ key: "temporary-widget", value: { color: "blue" } }],
  { metadata: { ttlInSeconds: "60" } },
);
if (expiring.error) {
  throw expiring.error;
}

Metadata can also be supplied on individual save items. Shared save metadata overrides matching per-item metadata keys. See state store TTL for component support and expiration semantics.

Next steps

  • State client usage: options, serialization, metadata, response shapes, and errors.
  • Query state: filtering, sorting, and pagination with a query-capable store.

2.2 - State client usage

Configuration, options, serialization, response shapes, and protocol-specific behavior of the JavaScript state client

State management is available through DaprClient.state. This page describes the implementation in 3.19.0 and the behavior to account for when using that package or a later compatible release.

Client lifetime and configuration

Reuse a long-lived DaprClient instead of constructing one per operation. Await outstanding requests before calling await client.stop() during shutdown.

HTTP is the default protocol. To use gRPC:

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "50001",
  communicationProtocol: CommunicationProtocolEnum.GRPC,
  daprApiToken: process.env.DAPR_API_TOKEN,
});

For HTTP, use CommunicationProtocolEnum.HTTP and the sidecar’s HTTP port, normally "3500". Port options are strings.

When explicit host and port options are omitted, the client can read DAPR_HTTP_ENDPOINT or DAPR_GRPC_ENDPOINT for the selected protocol. Otherwise it uses host/port options with defaults of 127.0.0.1 and DAPR_HTTP_PORT or DAPR_GRPC_PORT (normally 3500 and 50001). The API token defaults to DAPR_API_TOKEN.

Choose an operation

Every method takes a state store component name as its first argument.

MethodPurposeSuccessful result
save(storeName, items, options?)Save one or more key/value items.{}
get(storeName, key, options?)Read a single value.The value, not an item containing an ETag.
getBulk(storeName, keys, options?)Read values and ETags for multiple keys.Array of result items.
delete(storeName, key, options?)Delete one key, optionally checking an ETag.{}
deleteBulk(storeName, items, options?)Delete multiple keys without supplying values.{}
transaction(storeName, operations?, metadata?)Atomically execute upserts and deletes in a supporting store.undefined (Promise<void>).
query(storeName, query)Filter, sort, and paginate a supporting store.{ results, token? }

The SDK does not expose a separate single-item save, a getWithETag method, a boolean-returning conditional write, or a generic get<T> method. Use a one-element array for single saves and bulk reads that need an ETag. The broadly typed read results do not perform runtime schema validation.

State options

Import StateConsistencyEnum and StateConcurrencyEnum from @dapr/dapr rather than passing the HTTP wire-format strings:

OptionValuesMeaning
consistencyCONSISTENCY_UNSPECIFIED, CONSISTENCY_EVENTUAL, CONSISTENCY_STRONGSelect the store default, eventual consistency, or strong consistency.
concurrencyCONCURRENCY_UNSPECIFIED, CONCURRENCY_FIRST_WRITE, CONCURRENCY_LAST_WRITESelect the store default, detect conflicting writes with ETags, or use last-write-wins behavior.

These are members of the corresponding enum. Support depends on the state store. ETags are opaque version strings; do not invent or increment them.

Options have different locations depending on the operation:

OperationWhere to put options
saveEach item’s options: { concurrency, consistency } and optional string etag. The final argument accepts shared metadata only.
getFinal argument: { consistency, metadata }, with either field optional.
getBulkFinal argument: { parallelism, metadata }. There is no consistency option.
deleteFinal argument: { concurrency, consistency, etag, metadata }, with fields optional.
deleteBulkEach item’s options, etag, and metadata; final argument accepts shared metadata only.
transactionEach operation’s request.options; transaction-wide metadata is the third argument. See the limitations below.

When supplying an item’s options from TypeScript, its IStateOptions type requires both concurrency and consistency. Use the respective UNSPECIFIED enum member when you want the store default for one of them.

Metadata and TTL

Use string key/value metadata supported by the component, such as ttlInSeconds, partitionKey, or contentType. State metadata is not an arbitrary HTTP header collection.

OperationHTTP behaviorgRPC behavior
saveSends item metadata in the body and shared metadata as query parameters.Merges shared metadata into each item, overriding duplicate keys.
getForwards options.metadata.Does not forward options.metadata in this implementation.
getBulkForwards options.metadata.Forwards options.metadata.
deleteForwards options.metadata.Does not forward options.metadata in this implementation.
deleteBulkMerges shared metadata over item metadata, then forwards it in individual deletes.Merges shared metadata over item metadata in one bulk-delete RPC.
transactionForwards transaction-wide metadata and each request’s metadata.Forwards transaction-wide metadata, but not each request’s metadata.
queryNo separate metadata argument.No separate metadata argument.

For saves, shared metadata takes precedence over matching item metadata keys. TTL values belong in metadata, not in state options. See state store TTL.

If a component requires per-request metadata to locate an entry, do not assume single-key reads and deletes behave identically across protocols. Use HTTP for those calls, or use getBulk and deleteBulk with metadata where appropriate.

Response shapes

Single read

get returns the deserialized value, or text if the default JSON parse fails. It does not return the HTTP headers or the gRPC response envelope. A missing key returns an empty string in the standard no-data response, not a guaranteed undefined or null.

Do not use a truthiness check to distinguish a missing key from a legitimate stored value such as false, 0, or "". If your application needs an unambiguous presence check, store a document envelope and use a bulk read to examine the item and ETag, taking the component’s capabilities into account.

Bulk read

Items contain key, data, and an optional etag. HTTP returns the runtime’s item objects, which may also include error and metadata. A missing HTTP item’s data can be absent or null; gRPC maps empty data to "".

Without a configured serializer, gRPC bulk reads return only key, data, and etag; per-item error and metadata are not preserved by that mapping. With a serializer configured, gRPC preserves those fields as well. Where available, inspect each item’s error even when the overall request succeeded. Match items by key, not array position.

Set parallelism explicitly if your application needs a particular concurrency limit for bulk reads. When it is omitted, the protocols differ in this implementation: gRPC sends the SDK default of 10, while HTTP omits the field and the runtime default applies.

Writes and deletes

save, delete, and deleteBulk return a StateSaveResponseType: an empty object on success, with an optional error on HTTP failure. They do not return a new ETag. Read it again before the next conditional update.

Query

query returns results and an optional pagination token. Each result has key, data, and optional etag and error. Data decoding depends on protocol and serializer configuration; see query guidance.

Bulk deletion

deleteBulk accepts a readonly array of items shaped like save items without the required value. Existing key/value arrays are also accepted; their values are ignored and the serializer is not invoked.

HTTP sends individual deletes concurrently and waits for all requests to finish. It returns the first error in input order, not necessarily the first request to fail. gRPC sends one DeleteBulkState RPC and rejects on RPC failure. An empty batch returns {} without initializing the sidecar connection.

Bulk deletion is not atomic on either protocol. A reported failure does not mean no keys were deleted, and the response does not provide a complete per-key outcome list. Reconcile partial completion if your application needs to retry safely.

Transactions

Use upsert and delete operation names. The underlying component must support transactions; save and deleteBulk do not provide equivalent atomicity.

There are important differences in this implementation:

  • Without an explicit serializer, gRPC transaction upserts pass the supplied value to Buffer.from rather than applying save’s object serialization. Configure JsonSerializer for object-valued transactions, or supply appropriately encoded data for that protocol.
  • gRPC does not forward individual request.metadata fields. Use transaction-wide metadata where the component supports it.
  • The transaction request type declares etag as { value: string }, but the gRPC implementation calls toString() on that object rather than reading its value. Do not rely on transaction ETags for portable conditional writes in this release. Use the documented single-item save/delete pattern when atomic multi-key conditions are not required, or verify a later SDK fix before relying on conditional transactions.

Serialization

With no serializer configured, the SDK retains its legacy behavior:

OperationHTTPgRPC
SaveValues are part of a JSON request body.Objects are JSON-stringified; primitives are converted to strings.
GetParses JSON when possible, otherwise returns text.Parses JSON when possible, otherwise returns text.
Bulk getReturns the runtime’s JSON item data.Parses each item’s bytes as JSON when possible, otherwise returns text.
QueryReturns the runtime’s JSON item data.Returns raw Uint8Array item data.

For consistent JSON value encoding, explicitly configure the exported JsonSerializer:

import { DaprClient, JsonSerializer } from "@dapr/dapr";

const client = new DaprClient({
  serializer: new JsonSerializer(),
});

The serializer is a client-wide setting, not a per-state-call option. For state, it serializes saved values and transaction upsert values, and deserializes values returned by single reads, bulk reads, and queries. It does not serialize keys, ETags, metadata, delete values, or query filter expressions.

TypeScript applications can implement the exported IDaprSerializer interface, with synchronous serialize<T>(value): string and deserialize<T>(data: string): T methods and an optional contentType. A JSON media type (the default, application/json, or a +json media type) must produce valid JSON. A non-JSON serializer’s output is stored as a JSON string inside the Dapr envelope; encode binary formats as text, for example using base64. This does not turn state management into a streaming file API.

The serializer’s contentType controls SDK encoding; it does not automatically add state component contentType metadata. Supply that metadata explicitly when required by the store, such as when storing queryable JSON documents.

All readers and writers must agree on the stored representation. Opting in to JsonSerializer can change existing string behavior, especially gRPC values previously stored as unquoted text. Test old data and cross-protocol readers before migrating. Custom serializers can also change which document fields are available to state queries. Type parameters do not supply runtime schema validation or automatically restore class instances.

Error handling

Await operations and handle both returned write errors and promise rejections:

try {
  const result = await client.state.save("statestore", [
    { key: "widget-a", value: { color: "blue" } },
  ]);
  if (result.error) {
    throw result.error;
  }
} catch (error) {
  console.error("Could not save widget-a:", error);
  throw error;
}

This snippet assumes an existing client and an async function or ES module.

Failure pathBehavior
HTTP save and delete request failuresLogged by the SDK and returned as { error }.
HTTP deleteBulk request failuresIndividual deletes log failures; the bulk call returns the first error in input order after all deletes finish.
HTTP get, getBulk, transaction, and query request failuresReject the promise.
gRPC state RPC failuresReject the promise, preserving the transport error, normally a ConnectError.
Serialization or local request construction failuresCan reject before a request is sent, including on write operations.

HTTP runtime errors normally have a JSON-encoded Error.message with status, error, and error_msg. Transport errors can have other shapes; do not unconditionally parse every error message as JSON. Do not interpret gRPC error codes as HTTP status codes.

An ETag conflict is an error, not a false result. Read or query calls can also succeed at the request level while containing item-level errors. Avoid retrying a whole non-atomic batch without considering partial completion.

Cancellation

The state methods in this release do not accept an AbortSignal, cancellation token, or per-operation deadline. Do not copy the Binary Store client’s signal option into state calls. Racing a promise against a timer does not cancel the underlying state request. Use runtime resiliency policies where appropriate for component timeouts and retries, accounting for the semantics of the operation.

2.3 - How to: Query state with the Dapr JavaScript SDK

Filter, sort, and paginate state with the JavaScript SDK’s alpha state query API

Use client.state.query to query JSON documents in a state store that supports the state query API. Both HTTP and gRPC are implemented in 3.19.0. This is the alpha state query API, not a general-purpose SQL or native database query interface.

Prerequisites

Follow the state management how-to to install the SDK and run an application with a sidecar. For this guide, configure a query-capable component named state-mongodb, following the MongoDB state store reference. The basic Redis component in the how-to does not by itself configure query support.

Use a JSON-compatible stored document representation. Some components require additional query configuration or metadata; check the component reference.

Configure the client

Use the following client configuration inside an ES module, and wrap application operations in an error boundary with await client.stop() in finally, as shown in the state management how-to:

import { CommunicationProtocolEnum, DaprClient, JsonSerializer } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "3500",
  communicationProtocol: CommunicationProtocolEnum.HTTP,
  serializer: new JsonSerializer(),
});

For gRPC, change the protocol to CommunicationProtocolEnum.GRPC and the port to "50001". The explicit serializer deserializes query result values on either protocol. Without a serializer, gRPC query results contain raw Uint8Array data rather than parsed documents.

Save queryable documents

Supply contentType metadata when the store requires it to retain JSON documents:

const saved = await client.state.save(
  "state-mongodb",
  [
    {
      key: "widget-a",
      value: { size: "small", color: "blue", warehouse: { region: "west" } },
    },
    {
      key: "widget-b",
      value: { size: "large", color: "blue", warehouse: { region: "east" } },
    },
  ],
  { metadata: { contentType: "application/json" } },
);
if (saved.error) {
  throw saved.error;
}

Filter and sort

The SDK accepts an object with filter, sort, and page fields. The TypeScript StateQueryType requires all three. The following query finds blue widgets in either region and sorts by size:

const query = {
  filter: {
    AND: [
      { EQ: { color: "blue" } },
      { IN: { "warehouse.region": ["west", "east"] } },
    ],
  },
  sort: [{ key: "size", order: "ASC" }],
  page: { limit: 10 },
};

const response = await client.state.query("state-mongodb", query);
for (const item of response.results) {
  if (item.error) {
    throw new Error(`Unable to read query result ${item.key}: ${item.error}`);
  }
  console.log(item.key, item.data, item.etag);
}

Use field paths into the stored document. The SDK does not translate filter paths through a custom serializer. For example, a serializer that wraps each value in a payload property requires filter paths matching that representation.

The SDK’s declared filter surface includes EQ, IN, AND, and OR. Sort entries accept ASC or DESC, with ascending order as the runtime default when omitted. The runtime query API documents additional operators, but do not assume they are represented in this SDK version’s top-level filter type.

For an unfiltered, unsorted page, use filter: {}, sort: [], and page: { limit: 10 }. A SQL string or an object containing language and query is not the StateQueryType accepted by this API.

In TypeScript, preserve literal sort orders when extracting a query into a variable, for example with order: "ASC" as const, or pass the query object directly to client.state.query.

Retrieve another page

The response contains results and an optional token. Treat the token as opaque and pass it back in page.token, retaining the original filter, sort, and page size:

if (response.token) {
  const nextPage = await client.state.query("state-mongodb", {
    ...query,
    page: { ...query.page, token: response.token },
  });

  for (const item of nextPage.results) {
    if (item.error) {
      throw new Error(`Unable to read query result ${item.key}: ${item.error}`);
    }
    console.log(item.key, item.data);
  }
}

Continue until no continuation token is returned. There is no automatic paginator; an empty results array should not replace the token check.

Response and protocol considerations

Each query result has key, data, and optional etag and error. Check item errors as well as catching promise rejections.

With a configured serializer, the SDK deserializes successful, non-empty result values. Without one, HTTP returns the runtime’s JSON result data, while gRPC returns bytes. For known JSON data on gRPC without a serializer, explicitly decode a successful item’s data with JSON.parse(Buffer.from(item.data).toString("utf8")); do not apply that conversion to HTTP results that are already objects.

The declared response exposes results and token, not a portable response metadata map. query has no separate request metadata parameter. Store support, indexes, filter behavior, and pagination semantics remain component-dependent.

See state client usage for configuration, serialization compatibility, and error handling.

3 - JavaScript Server SDK

JavaScript Server SDK for developing Dapr applications

Introduction

The Dapr Server will allow you to receive communication from the Dapr Sidecar and get access to its server facing features such as: Subscribing to Events, Receiving Input Bindings, and much more.

With JavaScript SDK 3.19.0 or later, register scheduled job callbacks through server.jobs.listen(name, callback). See the Jobs how-to guide for a runnable example and the callback usage guidance for acknowledgments, retries, and protocol requirements.

Pre-requisites

Installing and importing Dapr’s JS SDK

  1. Install the SDK with npm:
npm i @dapr/dapr --save
  1. Import the libraries:
import { DaprServer, CommunicationProtocolEnum } from "@dapr/dapr";

const daprHost = "127.0.0.1"; // Dapr Sidecar Host
const daprPort = "3500"; // Dapr Sidecar Port of this Example Server
const serverHost = "127.0.0.1"; // App Host of this Example Server
const serverPort = "50051"; // App Port of this Example Server

// HTTP Example
const server = new DaprServer({
  serverHost,
  serverPort,
  communicationProtocol: CommunicationProtocolEnum.HTTP, // DaprClient to use same communication protocol as DaprServer, in case DaprClient protocol not mentioned explicitly
  clientOptions: {
    daprHost,
    daprPort,
  },
});

// GRPC Example
const server = new DaprServer({
  serverHost,
  serverPort,
  communicationProtocol: CommunicationProtocolEnum.GRPC,
  clientOptions: {
    daprHost,
    daprPort,
  },
});

Running

To run the examples, you can use two different protocols to interact with the Dapr sidecar: HTTP (default) or gRPC.

Using HTTP (built-in express webserver)

import { DaprServer } from "@dapr/dapr";

const server = new DaprServer({
  serverHost: appHost,
  serverPort: appPort,
  clientOptions: {
    daprHost,
    daprPort,
  },
});
// initialize subscribtions, ... before server start
// the dapr sidecar relies on these
await server.start();
# Using dapr run
dapr run --app-id example-sdk --app-port 50051 --app-protocol http -- npm run start

# or, using npm script
npm run start:dapr-http

ℹ️ Note: The app-port is required here, as this is where our server will need to bind to. Dapr will check for the application to bind to this port, before finishing start-up.

Using HTTP (bring your own express webserver)

Instead of using the built-in web server for Dapr sidecar to application communication, you can also bring your own instance. This is helpful in scenarios like when you are building a REST API back-end and want to integrate Dapr directly in it.

Note, this is currently available for express only.

💡 Note: when using a custom web-server, the SDK will configure server properties like max body size, and add new routes to it. The routes are unique on their own to avoid any collisions with your application, but it’s not guaranteed to not collide.

import { DaprServer, CommunicationProtocolEnum } from "@dapr/dapr";
import express from "express";

const myApp = express();

myApp.get("/my-custom-endpoint", (req, res) => {
  res.send({ msg: "My own express app!" });
});

const daprServer = new DaprServer({
      serverHost: "127.0.0.1", // App Host
      serverPort: "50002", // App Port
      serverHttp: myApp,
      clientOptions: {
        daprHost
        daprPort
      }
    });

// Initialize subscriptions before the server starts, the Dapr sidecar uses it.
// This will also initialize the app server itself (removing the need for `app.listen` to be called).
await daprServer.start();

After configuring the above, you can call your custom endpoint as you normally would:

const res = await fetch(`http://127.0.0.1:50002/my-custom-endpoint`);
const json = await res.json();

Using gRPC

Since HTTP is the default, you will have to adapt the communication protocol to use gRPC. You can do this by passing an extra argument to the client or server constructor.

import { DaprServer, CommunicationProtocolEnum } from "@dapr/dapr";

const server = new DaprServer({
  serverHost: appHost,
  serverPort: appPort,
  communicationProtocol: CommunicationProtocolEnum.GRPC,
  clientOptions: {
    daprHost,
    daprPort,
  },
});
// initialize subscribtions, ... before server start
// the dapr sidecar relies on these
await server.start();
# Using dapr run
dapr run --app-id example-sdk --app-port 50051 --app-protocol grpc -- npm run start

# or, using npm script
npm run start:dapr-grpc

ℹ️ Note: The app-port is required here, as this is where our server will need to bind to. Dapr will check for the application to bind to this port, before finishing start-up.

Building blocks

The JavaScript Server SDK allows you to interface with all of the Dapr building blocks focusing on Sidecar to App features.

Invocation API

For JavaScript SDK 3.19.0 or later, see the service invocation overview for HTTP and gRPC capabilities.

Listen to an Invocation

import { DaprServer, HttpMethod } from "@dapr/dapr";
import type { DaprInvokerCallbackContent } from "@dapr/dapr";

const daprHost = "127.0.0.1"; // Dapr Sidecar Host
const daprPort = "3500"; // Dapr Sidecar Port of this Example Server
const serverHost = "127.0.0.1"; // App Host of this Example Server
const serverPort = "50051"; // App Port of this Example Server "

async function start() {
  const server = new DaprServer({
    serverHost,
    serverPort,
    clientOptions: {
      daprHost,
      daprPort,
    },
  });

  const callbackFunction = async (data: DaprInvokerCallbackContent) => {
    console.log("Received body: ", data.body);
    console.log("Received metadata: ", data.metadata);
    console.log("Received query: ", data.query);
    console.log("Received headers: ", data.headers); // only available in HTTP
    return { received: true };
  };

  await server.invoker.listen("hello-world", callbackFunction, { method: HttpMethod.GET });

  // You can now invoke the service with your app id and method "hello-world"

  await server.start();
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

Follow receiving invocations for matching startup commands, callback field shapes, response behavior, and existing Express applications. See configuration and usage for custom serialization and failure handling.

PubSub API

For JavaScript SDK 3.19.0 or later, see the publish and subscribe overview to choose a delivery mode. Client-initiated streaming subscriptions do not require a DaprServer.

Subscribe to messages

Register each component/topic pair once before server.start(). This example uses an HTTP listener on port 50002:

import { DaprPubSubStatusEnum, DaprServer } from "@dapr/dapr";

async function start() {
  const server = new DaprServer({
    serverHost: "127.0.0.1",
    serverPort: "50002",
    clientOptions: { daprHost: "127.0.0.1", daprPort: "3500" },
  });

  await server.pubsub.subscribe("pubsub", "orders", async (data) => {
    console.log("Order received:", data);
    return DaprPubSubStatusEnum.SUCCESS;
  });

  await server.start();
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

Follow the server subscription guide for the matching dapr run command, gRPC configuration, registration methods, and callback data and headers. See configuration and usage for serialization and shutdown.

Subscribe with SUCCESS/RETRY/DROP status

Return DaprPubSubStatusEnum values rather than response objects. See acknowledgements and retries for status precedence, thrown errors, and an inbound resiliency policy.

Subscribe to messages rule based

Use subscribeWithOptions() to declare routes and subscribeToRoute() to attach handlers. See the content-based routing example.

Subscribe with Wildcards

See wildcard topics for MQTT-style # and + examples and component requirements.

Bulk Subscribe to messages

subscribeBulk() delivers one entry at a time to your callback and builds per-entry acknowledgements. See bulk subscriptions for batching options and HTTP payload limitations.

Dead Letter Topics

Set deadLetterTopic on the source subscription and register a separate consumer for failed messages. See dead-letter topics for the complete example and the limitations of deadLetterCallback.

Bindings API

Receive an Input Binding

import { DaprServer } from "@dapr/dapr";

const daprHost = "127.0.0.1";
const daprPort = "3500";
const serverHost = "127.0.0.1";
const serverPort = "5051";

async function start() {
  const server = new DaprServer({
    serverHost,
    serverPort,
    clientOptions: {
      daprHost,
      daprPort,
    },
  });

  const bindingName = "my-binding-name";

  const response = await server.binding.receive(bindingName, async (data: any) =>
    console.log(`Got Data: ${JSON.stringify(data)}`),
  );

  await server.start();
}

start().catch((e) => {
  console.error(e);
  process.exit(1);
});

For a full guide on output bindings visit How-To: Use bindings.

Configuration API

Configuration is a client-side building block, including in applications that also run a DaprServer. Use a gRPC DaprClient to read settings or subscribe to updates. A configuration subscription receives updates over the client’s gRPC stream; it does not require a server callback route.

Getting a configuration value

Use client.configuration.get(storeName, keys?, metadata?). Follow the configuration how-to guide for client setup and examples using @dapr/dapr 3.19.0 or later.

Subscribing to Key Changes

Use subscribe, subscribeWithKeys, or subscribeWithMetadata on client.configuration. Retain each returned handle and call await subscription.stop() during shutdown, not close(). See the subscription example and client usage guidance for callback behavior, cleanup, and stream failure limitations.

4 - Service invocation with the JavaScript SDK

Call services and receive invocations over HTTP or gRPC with the JavaScript SDK

Use DaprClient.invoker.invoke() to call another application through your local Dapr sidecar, and DaprServer.invoker.listen() to receive invocations. The target can also be an existing HTTP application without the JavaScript SDK.

These guides describe @dapr/dapr 3.19.0 or later, including API and transport limitations.

Prerequisites

  • Dapr CLI and an initialized Dapr environment.
  • A supported Node.js LTS release. The package requires Node.js 18 or later.
  • A target application running with a Dapr app ID and application port. No state store or pub/sub component is required.

Install the 3.19.0 release:

npm install @dapr/dapr@3.19.0

For a later release, substitute its version. Examples use ES module imports; run snippets containing await inside an async function or an ES module.

Choose the protocol

The client’s communicationProtocol selects application-to-sidecar communication. The receiving application’s --app-protocol selects sidecar-to-application communication. These are independent: a gRPC client can invoke an HTTP application and an HTTP client can invoke a DaprServer configured for gRPC.

CapabilityHTTP client or servergRPC client or server
client.invoker.invoke()Calls the HTTP invocation APICalls Dapr’s unary InvokeService API
server.invoker.listen()Registers an HTTP routeRegisters an AppCallback.OnInvoke handler
Request verbsGET, POST, PUT, PATCH, DELETESame verbs, carried in the HTTP extension
Custom request headersoptions.headers is forwardedNot forwarded as invocation metadata
Query parameters from the SDK clientAppend an encoded query string to methodNameNo separate query-string option
Default response decodingJSON when parseable; otherwise textJSON required; text or empty responses cause a parse error
Custom application serializerSupportedSupported
Response status and headers through invoke()Not exposedNot exposed
Streaming through invoke()Not supported; responses are bufferedNot supported; unary request/response
client.proxy.create()Not supportedNot supported with the SDK’s ConnectRPC transport

Selecting gRPC does not turn invoke() into a client for arbitrary protobuf services. For native gRPC services, use your generated client with Dapr’s gRPC proxying protocol.

Guides

GuideContents
Calling servicesHTTP and gRPC clients, verbs, headers, paths, query strings, namespaces, and non-Dapr endpoints.
Receiving invocationsA complete server, HTTP and gRPC startup commands, callback data, return values, and existing Express applications.
Configuration and usageClient lifetime, endpoints, authentication, serialization, errors, resiliency, and API limitations.

Implementation references

4.1 - How to: Call services with the JavaScript SDK

Invoke application methods with HTTP or gRPC clients, headers, and query parameters

DaprClient.invoker.invoke() sends a request through the caller’s sidecar to a target application. These examples use @dapr/dapr 3.19.0 or later. Follow the receiving invocations guide to start the invocation-service target used below.

Invoke an application over HTTP

Save this complete example as client.mjs:

import { DaprClient, HttpMethod } from "@dapr/dapr";

async function main() {
  const client = new DaprClient({
    daprHost: "127.0.0.1",
    daprPort: "3501",
  });

  try {
    const response = await client.invoker.invoke(
      "invocation-service",
      "orders",
      HttpMethod.POST,
      { orderId: "101", quantity: 2 },
      { headers: { "X-Correlation-ID": "order-101" } },
    );
    console.log(response);
  } finally {
    await client.stop();
  }
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

In another terminal, run the caller with its own sidecar:

dapr run --app-id invocation-caller --dapr-http-port 3501 --dapr-grpc-port 50002 -- node client.mjs

The caller does not need --app-port because it does not receive callbacks. Its sidecar ports differ from those of the target service.

Invocation arguments

The public signature is:

invoke(
  appId: string,
  methodName: string,
  method: HttpMethod,
  data?: object,
  options?: InvokerOptions,
): Promise<object>;
ArgumentMeaning
appIdTarget Dapr app ID, not the caller’s app ID or host address.
methodNameTarget method or relative HTTP path, for example orders or orders/101. Use no leading slash.
methodHttpMethod.GET, POST, PUT, PATCH, or DELETE. Supply it explicitly; the implementations default to GET, but the public TypeScript interface requires it.
dataOptional payload. Objects and arrays use JSON by default.
optionsAn InvokerOptions object containing optional headers. It has no per-call timeout, cancellation, or retry fields.

The result is the decoded response body, not a response envelope. Although the public return type is object, HTTP may return text and a serializer may return other JavaScript values. There is no invoke<T>() overload and TypeScript types do not validate the response at runtime.

Select a verb

These snippets assume an existing client and matching routes on the target:

const order = await client.invoker.invoke("invocation-service", "orders/101", HttpMethod.GET);

const replaced = await client.invoker.invoke(
  "invocation-service", "orders/101", HttpMethod.PUT, { quantity: 4 },
);

const updated = await client.invoker.invoke(
  "invocation-service", "orders/101", HttpMethod.PATCH, { quantity: 3 },
);

await client.invoker.invoke("invocation-service", "orders/101", HttpMethod.DELETE);

Do not provide a body with an HTTP GET request. A gRPC invocation with omitted data sends the default payload {}, rather than an absent body.

Pass headers and query parameters over HTTP

Use the fifth argument for headers. To omit the body, pass undefined as the fourth argument:

const query = new URLSearchParams({ status: "pending", customer: "Ada Lovelace" });
const response = await client.invoker.invoke(
  "invocation-service",
  `orders?${query.toString()}`,
  HttpMethod.GET,
  undefined,
  { headers: { "X-Correlation-ID": "query-101" } },
);

Encode individual dynamic path segments with encodeURIComponent() and query values with URLSearchParams. The SDK interpolates methodName into the HTTP invocation URL; it does not encode it for you.

Custom headers are supported by the HTTP implementation. The gRPC implementation does not forward options.headers as request metadata. With a configured serializer, it only uses headers["Content-Type"] to override the payload content type. It also does not populate the request’s separate gRPC HTTP-extension query-string field, so do not use the HTTP query-string example with a gRPC client.

Invoke through a gRPC sidecar endpoint

Replace the client construction in client.mjs with:

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "50002",
  communicationProtocol: CommunicationProtocolEnum.GRPC,
});

Keep the HttpMethod import and invoke() call from the original example, but omit the HTTP headers option. Use the same caller startup command: it exposes both sidecar endpoints. The receiving application can remain HTTP.

By default, the gRPC client decodes the response bytes as UTF-8 and parses them as JSON. An empty body or plain-text response rejects with COULD_NOT_PARSE_RESULT, even if the remote operation succeeded. Return a JSON value from the target, or configure a compatible serializer.

Call across namespaces

Where supported by Dapr’s name-resolution configuration, qualify the target app ID with its namespace:

const response = await client.invoker.invoke(
  "invocation-service.production", "orders", HttpMethod.POST, { orderId: "101" },
);

Namespace routing is a runtime capability, not a separate SDK option. See service invocation across namespaces.

Call a non-Dapr HTTP endpoint

With an HTTP client, the same API can target an HTTPEndpoint resource by name or a fully qualified URL, when supported by the runtime:

const configuredEndpoint = await client.invoker.invoke(
  "external-orders", "orders/101", HttpMethod.GET,
);

const urlEndpoint = await client.invoker.invoke(
  "https://api.example.com", "orders/101", HttpMethod.GET,
);

These are alternative examples: configure external-orders or replace the illustrative URL before running them. The target does not need a Dapr sidecar. See invoking non-Dapr endpoints for resource configuration, authentication, and runtime requirements.

4.2 - How to: Receive service invocations with the JavaScript SDK

Register HTTP or gRPC invocation handlers and understand callback requests and responses

Use DaprServer.invoker.listen(methodName, callback, options?) to register an invocation handler. These examples use @dapr/dapr 3.19.0 or later.

Start an HTTP service

Save this complete example as server.mjs:

import { DaprServer, HttpMethod } from "@dapr/dapr";

async function main() {
  const server = new DaprServer({
    serverHost: "127.0.0.1",
    serverPort: "3000",
    clientOptions: {
      daprHost: "127.0.0.1",
      daprPort: "3500",
    },
  });

  await server.invoker.listen(
    "orders",
    async (request) => {
      if (!request.body) {
        throw new Error("An order request body is required");
      }
      const order = JSON.parse(request.body);
      console.log("Received order:", order);
      console.log("Correlation ID:", request.headers?.["x-correlation-id"]);
      return { orderId: order.orderId, status: "accepted" };
    },
    { method: HttpMethod.POST },
  );

  await server.invoker.listen("health", async () => ({ healthy: true }));
  await server.start();

  async function stop() {
    await server.stop();
    await server.client.stop();
  }

  process.once("SIGINT", () => {
    stop().catch((error) => {
      console.error(error);
      process.exitCode = 1;
    });
  });
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Run the service and its sidecar:

dapr run --app-id invocation-service --app-port 3000 --app-protocol http --dapr-http-port 3500 --dapr-grpc-port 50001 -- node server.mjs

Register handlers before server.start(). The listen() method defaults to GET; explicitly set { method: HttpMethod.POST } for the orders handler. The caller must use the same method name and verb. The health route is a regular example invocation route, not a configuration of Dapr’s application health checks.

Use the client example to call the service, or invoke it through the CLI:

dapr invoke --app-id invocation-service --method health --verb GET

Receive over gRPC

To use Dapr’s gRPC application callback API, add CommunicationProtocolEnum to the imports and replace the server construction with:

const server = new DaprServer({
  serverHost: "127.0.0.1",
  serverPort: "3000",
  communicationProtocol: CommunicationProtocolEnum.GRPC,
  clientOptions: {
    daprHost: "127.0.0.1",
    daprPort: "50001",
  },
});

Keep the same handler registrations and startup code. Restart the target using:

dapr run --app-id invocation-service --app-port 3000 --app-protocol grpc --dapr-http-port 3500 --dapr-grpc-port 50001 -- node server.mjs

Unless overridden in clientOptions, the embedded client’s protocol follows the server protocol. serverPort is the application listener, while clientOptions.daprPort is the sidecar endpoint; do not interchange them. An HTTP caller can still invoke this gRPC callback service.

Understand the callback request

TypeScript handlers receive DaprInvokerCallbackContent, exported from @dapr/dapr. The callback is asynchronous and returns a promise for the response value.

FieldHTTP callbackgRPC callback
bodyNormally JSON.stringify(req.body) after the HTTP body parser; undefined when no body was parsedUTF-8 decoding of the request data bytes; "" when no data was sent
metadata.contentTypeIncoming content-type headerInvocation request’s content type
queryThe original URL, including path and query stringThe HTTP extension’s query-string field
headersIncoming HTTP headers; names are normally lowercaseNot populated

For a JSON request, explicitly parse request.body as in the example. Invocation handlers do not receive an automatically deserialized domain object, even when a custom serializer is configured.

For an HTTP callback, parse query parameters as a URL, not as a query string alone:

await server.invoker.listen("orders", async (request) => {
  const url = new URL(request.query ?? "/orders", "http://localhost");
  const status = url.searchParams.get("status");
  return { status };
}, { method: HttpMethod.GET });

For gRPC callbacks, query contains only the extension’s query string and can be read with URLSearchParams. The JavaScript gRPC invoker does not itself populate that field.

Return a response

Return the application response value directly:

await server.invoker.listen("version", async () => ({ version: "1.0" }));

By default, the SDK JSON-encodes truthy return values. There is no special { status, headers, body } response envelope: returning such an object sends those properties as application data. The callback API does not expose an HTTP response object or gRPC response metadata.

Without a custom serializer, undefined, null, false, 0, and "" do not produce their corresponding JSON values. An HTTP handler instead responds with {"closed":true}; a gRPC handler returns no data. A default gRPC client cannot parse that empty response. Prefer an object response such as { accepted: false }.

With a custom serializer, every return value except undefined is serialized, including falsy values. For undefined, HTTP still returns the closure object and gRPC still returns no data. See serialization guidance.

Thrown handler errors propagate to the underlying HTTP error middleware or gRPC transport; they are not successful application response objects. If callers need specific HTTP status codes, headers, or streaming responses, use a regular HTTP framework route instead.

Use an existing HTTP application

Dapr can invoke existing HTTP routes without DaprServer. Run that application with --app-port and --app-protocol http, and use its route path as methodName from a Dapr client.

To share an Express application with SDK callbacks, pass the Express instance as serverHttp:

import express from "express";
import { DaprServer } from "@dapr/dapr";

const app = express();
app.get("/orders/:id", (request, response) => {
  response.status(200).json({ orderId: request.params.id });
});

const server = new DaprServer({
  serverHost: "127.0.0.1",
  serverPort: "3000",
  serverHttp: app,
  clientOptions: { daprHost: "127.0.0.1", daprPort: "3500" },
});

await server.start();

Install express as an application dependency for this alternative example. Use the HTTP startup command above. server.start() starts the listener, so do not also call app.listen(). The SDK adds middleware and routes to the supplied app; avoid conflicting route names. Use your application’s shutdown handling to call server.stop() and then server.client.stop().

4.3 - Service invocation configuration and usage

Configure clients, serialization, authentication, and failure handling for JavaScript service invocation

This guidance applies to @dapr/dapr 3.19.0 or later. See calling services and receiving invocations for complete application examples.

Client lifetime

Reuse a long-lived DaprClient instead of creating one per invocation. Await outstanding work before calling await client.stop() during application shutdown. If using DaprServer, call await server.stop() to stop its listener, then await server.client.stop() to close its embedded client’s resources. Stopping the server alone does not stop that client.

The client initializes its connection and waits for the sidecar when first used. You can call await client.start() to check readiness before accepting application traffic. This connects to an existing sidecar; it does not launch the Dapr runtime process.

Endpoints and authentication

HTTP is the default protocol. Use the sidecar’s HTTP port for HTTP clients and its gRPC port for gRPC clients. Port options are strings:

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "50001",
  communicationProtocol: CommunicationProtocolEnum.GRPC,
  daprApiToken: process.env.DAPR_API_TOKEN,
});

When neither host nor port is supplied, the SDK uses DAPR_HTTP_ENDPOINT or DAPR_GRPC_ENDPOINT for the selected protocol if set. Otherwise it uses the configured host and port, falling back to 127.0.0.1 and DAPR_HTTP_PORT or DAPR_GRPC_PORT (defaults: 3500 and 50001). Supplying either host or port bypasses the endpoint environment variable.

daprApiToken defaults to DAPR_API_TOKEN. This authenticates the caller to its sidecar; it is not an application authorization header for the destination service. For HTTP destination authorization, supply the appropriate application header through InvokerOptions.headers. See Dapr API token authentication and service invocation access control.

Both transports propagate baggage from an active OpenTelemetry context to the sidecar. This built-in propagation does not mean the gRPC invoker supports arbitrary options.headers metadata.

Payload limits

The gRPC client uses maxBodySizeMb for its transport’s send and receive limits (default: 4 MiB). The HTTP client accepts the same option but does not currently enforce it; HTTP requests are limited by the sidecar and the target application. DaprServer.maxBodySizeMb controls the HTTP listener’s body-parser limit (4 MiB by default). These are separate settings; the client’s option is not an HTTP upload-size limit.

Configure the Dapr sidecar’s request body limit and the target application’s own limits as needed. Increasing a limit does not enable streaming or remove the invoker’s response buffering.

Serialization

Default behavior

Objects and arrays use JSON. The underlying serialization helpers also support text and binary request data, including Buffer, with inferred content types. The public invoke() TypeScript signature accepts data?: object, so primitive payloads are not part of that typed contract.

For HTTP, options.headers["Content-Type"] overrides the inferred request content type. Use that exact casing for the SDK’s serializer selection. Successful HTTP responses are read completely as text and parsed as JSON when possible, regardless of response content type; otherwise the text is returned.

For gRPC, the default request serializer infers the content type and does not use the headers option. Responses are decoded as UTF-8 and must contain JSON. Sending binary data does not make this a binary response API: neither invoker exposes response bytes or a response stream.

Configure an application serializer

Set serializer on DaprClient and clientOptions.serializer on DaprServer. IDaprSerializer is exported as a type from @dapr/dapr and defines:

interface IDaprSerializer {
  readonly contentType?: string;
  serialize<T>(value: T): string;
  deserialize<T = unknown>(data: string): T;
}

The SDK also exports JsonSerializer. For example, this opt-in JSON configuration ensures that falsy server return values other than undefined are serialized:

import { DaprClient, DaprServer, JsonSerializer } from "@dapr/dapr";

const serializer = new JsonSerializer();
const client = new DaprClient({ daprPort: "3501", serializer });
const server = new DaprServer({
  serverPort: "3000",
  clientOptions: { daprPort: "3500", serializer },
});

This configuration snippet does not start the server. For a custom encoding, implement IDaprSerializer and configure compatible serializers on both ends. Its wire representation is a string; binary formats must use a string-safe representation such as base64. Serialization errors reject the operation.

On invocation, the client serializes the request and deserializes the response. HTTP skips custom response deserialization for an empty response; gRPC passes even an empty response string to the deserializer.

Server invocation callbacks still receive a string body. Deserialize it explicitly:

import { HttpMethod } from "@dapr/dapr";

await server.invoker.listen("echo", async (request) => {
  // HTTP passes undefined for a missing body; gRPC passes an empty string.
  if (!request.body) {
    throw new Error("An invocation body is required");
  }
  return serializer.deserialize(request.body);
}, { method: HttpMethod.POST });

With a custom serializer, HTTP preserves non-JSON text or buffer request bodies as strings before invoking the handler. JSON bodies still pass through the HTTP JSON parser and are re-stringified. The server serializes the callback’s returned value using its configured serializer.

The serializer’s contentType defaults to application/json. Both clients can override it with headers["Content-Type"] when using a custom serializer, but the gRPC client still does not forward other custom headers. Changing the content type does not change the serializer implementation.

Errors and response information

Await invocations and handle failures at your application’s error boundary. Do not treat a rejected promise as proof that the target did not execute the operation.

FailureBehavior
HTTP sidecar or application error responseRejects with an Error whose message is a JSON string containing status, error (status text), and error_msg (response body). The HTTP helper accepts statuses 200 through 399; redirect handling follows its underlying HTTP client.
HTTP connection or serialization errorRejects with the underlying error, which need not have a JSON message.
gRPC transport or runtime failureRejects with the transport error; ConnectRPC failures use ConnectError codes, not HTTP status numbers.
gRPC non-JSON or empty response without a custom serializerRejects with an Error whose JSON message contains error: "COULD_NOT_PARSE_RESULT" and error_msg.
Custom response deserialization failureRejects with the serializer error.

invoke() returns only the decoded response body. It does not expose successful status codes, response headers, trailers, or structured per-call response metadata.

A missing gRPC callback registration currently logs a warning and returns an empty invocation response rather than a not-found application payload. The default gRPC client then fails to parse it. Check both the target’s logs and the caller’s error when diagnosing method or verb mismatches.

Timeouts, cancellation, and resiliency

InvokerOptions only contains headers. invoke() does not expose an AbortSignal, timeout, deadline, retry policy, or arbitrary gRPC call options. Do not copy timeout options from other SDKs or rely on illustrative API comments that show unsupported options.

Configure runtime timeouts, retries, and circuit breakers through Dapr resiliency policies. Policies apply in the runtime and do not add client-side cancellation to the JavaScript method. A local Promise.race() timeout only stops the caller waiting; it does not cancel the request or remote processing.

Design state-changing handlers to tolerate retries, for example by using an application request ID and deduplication. A timeout or response decoding failure can occur after the target has committed its work.

Native gRPC services and streaming

client.proxy.create() is not a working alternative in this version. HTTP rejects with HTTPNotSupportedError, and gRPC rejects because GRPCClientProxy is not supported with the ConnectRPC transport.

For a native protobuf service, create a generated gRPC client connected to the caller’s sidecar and attach dapr-app-id metadata as described in invoking services using gRPC. That is runtime gRPC proxying, separate from the SDK’s unary invoke() and DaprServer callback APIs.

For large HTTP requests or responses requiring streaming, use the runtime’s HTTP streaming support with an HTTP client and server that expose streams. The JavaScript invoker buffers the complete response and has no streaming overload.

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

GuideLearn how to
Run and use virtual actorsCreate a contract, implement an actor, configure a state store, and run a host and client
Author and host actorsRegister types, use lifecycle hooks, and configure the actor runtime
Call actorsChoose actor identities, build proxies, invoke methods dynamically, and understand serialization
Manage actor stateRead, update, remove, cache, and explicitly save actor state
Use timers and remindersSchedule 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.

CapabilityHTTPgRPC
Host JavaScript actors with DaprServerSupportedNot implemented
Invoke actor methods through a proxy or the lower-level actor clientSupportedSupported
Read and transact actor state; register and unregister timers and individual remindersSupportedSupported
Retrieve an individual reminder with getActorReminderSupportedSupported
List reminders for one actor with listActorRemindersDapr 1.19+Dapr 1.17+
List reminders for every instance of an actor typeNot supportedDapr 1.17+
Delete reminders in bulk with unregisterActorRemindersByTypeNot supportedDapr 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.

5.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.

Step 4: Configure the state store

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:

npx tsx client.ts

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.

5.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:

HookWhen 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.

Register actors and configure the runtime

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.

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.

5.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:

MethodsPurpose
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.

5.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

MethodBehavior
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.

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

OperationRuntime requirement
HTTP listing with an exact actor IDDapr 1.19+
gRPC listing with an exact actor IDDapr 1.17+
gRPC listing without an actor ID, across the actor typeDapr 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.

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().

6 - Publish and subscribe with the JavaScript SDK

Choose a pub/sub delivery mode and get started with publishing, server subscriptions, or gRPC streaming

The JavaScript SDK supports publishing events over HTTP or gRPC, programmatic subscriptions through DaprServer, and client-initiated streaming subscriptions through DaprClient. These guides describe @dapr/dapr 3.19.0 or later.

Prerequisites

Installation

Install the 3.19.0 release:

npm install @dapr/dapr@3.19.0

For a later release, substitute its version.

Code snippets use ES module imports. Run statements containing await in an async function or an ES module. Each guide identifies the client or server configuration used by its examples.

Choose a delivery mode

CapabilityHTTPgRPC
client.pubsub.publish()SupportedSupported
client.pubsub.publishBulk()SupportedSupported
server.pubsub.subscribe() and subscribeWithOptions()Inbound HTTP callbacksInbound gRPC callbacks
Content-based routing and subscribeToRoute()SupportedSupported
server.pubsub.subscribeBulk()Per-message callback from each batchPer-message callback from each batch
client.pubsub.subscribeStream()Not supportedClient-initiated stream; no application server

Choose server subscriptions when Dapr should deliver events to your application’s HTTP or gRPC listener. Subscriptions are discovered at application startup. Choose streaming subscriptions for workers that initiate the connection to Dapr and do not need an application callback listener. Streams can be created and closed at runtime, but do not support routing rules or bulk subscription options.

Runtime compatibility

Bulk publishing uses the stable API on Dapr 1.17 or later, with automatic fallback to the deprecated alpha1 API on older runtimes.

Streaming subscriptions use SubscribeTopicEventsAlpha1, an alpha gRPC API. Use a runtime that implements this API; installing the JavaScript package alone does not enable it. API availability and broker-specific features depend on the Dapr runtime and component.

Guides

GuideContents
Publishing eventsSingle and bulk publishing, metadata, CloudEvents, raw payloads, and publication failures.
Server subscriptionsRegistration, callback data, acknowledgements, routing, wildcards, bulk delivery, and dead-letter topics.
Streaming subscriptionsA complete worker, streaming options, cancellation, failure supervision, and cleanup.
Configuration and usageClient lifetime, endpoint and authentication settings, serialization, and reliable processing.

Implementation references

For implementation details, see the published 3.19.0 package and the upstream SDK implementation:

6.1 - Publish events with the JavaScript SDK

Publish single events and batches, configure metadata and CloudEvents, and handle publication failures

Use DaprClient.pubsub to publish events over HTTP or gRPC with @dapr/dapr 3.19.0 or later. Complete the pub/sub prerequisites before following this guide.

Configure a publisher

The examples use this HTTP client, component pubsub, and topic orders:

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "3500",
  communicationProtocol: CommunicationProtocolEnum.HTTP,
});

For gRPC, select CommunicationProtocolEnum.GRPC and port "50001". Run statements containing await in an async function or an ES module. Reuse the client and stop it during application shutdown; see configuration and usage.

Single messages and metadata

const response = await client.pubsub.publish(
  "pubsub",
  "orders",
  { orderId: "101", total: 49.99 },
  {
    contentType: "application/json",
    metadata: { ttlInSeconds: "60" },
  },
);

if (response.error) {
  throw response.error;
}

Without a custom serializer or explicit contentType, the SDK infers application/json for objects and arrays, text/plain for strings, and application/cloudevents+json for recognized CloudEvents.

metadata carries Dapr publish options and component-specific settings, such as message TTL or partition keys. It is not a general-purpose mechanism for adding CloudEvent attributes or arbitrary subscriber headers. Supported keys depend on the component.

Publish CloudEvents

Dapr normally wraps published data in a CloudEvent. To supply your own event attributes, publish a CloudEvent explicitly:

const response = await client.pubsub.publish("pubsub", "orders", {
  specversion: "1.0",
  id: "order-101-created",
  source: "/checkout",
  type: "com.example.order.created",
  datacontenttype: "application/json",
  data: { orderId: "101", total: 49.99 },
});

if (response.error) {
  throw response.error;
}

See CloudEvents for envelope attributes and extensions.

Raw payloads

To publish without a CloudEvent envelope:

const response = await client.pubsub.publish("pubsub", "orders-raw", "order-101", {
  contentType: "text/plain",
  metadata: { rawPayload: "true" },
});

if (response.error) {
  throw response.error;
}

Configure the receiving subscription with metadata: { rawPayload: true } for server subscriptions, or metadata: { rawPayload: "true" } for streaming subscriptions. Server subscription metadata values are JSON-stringified before discovery; streaming metadata is sent directly as a string map. This distinction matters for boolean options such as rawPayload.

Raw payloads do not provide the same CloudEvent attributes as enveloped events. See raw payloads.

Bulk publishing

publishBulk(pubSubName, topic, messages, metadata?) accepts strings, objects, or explicit entries with event, optional entryID, and optional contentType. Missing entry IDs are generated as UUIDs:

const response = await client.pubsub.publishBulk(
  "pubsub",
  "orders",
  [
    { entryID: "order-101", event: { orderId: "101" }, contentType: "application/json" },
    { entryID: "order-102", event: { orderId: "102" }, contentType: "application/json" },
  ],
  { ttlInSeconds: "60" },
);

for (const failure of response.failedMessages) {
  console.error("Publish failed:", failure.message.entryID, failure.error);
}

if (response.failedMessages.length > 0) {
  throw new Error(`Failed to publish ${response.failedMessages.length} orders`);
}

Bulk publishing is not transactional. A resolved promise can contain failures; always inspect failedMessages. Each failure contains the original normalized message and an error. Request-level failures are generally represented as failures for every entry, while argument processing, serialization, or client initialization can still reject.

Bulk publishing uses the stable API on Dapr 1.17 or later, with automatic fallback to the deprecated alpha1 API on older runtimes. HTTP selects the API using sidecar metadata; gRPC falls back when the stable RPC is unsupported.

Use the fourth argument for request metadata on either transport. Although the explicit entry type exposes metadata, this SDK version does not forward per-entry metadata in the gRPC bulk request. Also, without a custom serializer, the gRPC bulk path infers content types from each event rather than honoring explicit entry content types. Avoid relying on those fields for cross-protocol behavior.

See bulk publish and subscribe.

Publish error handling

Single-message publishing has different failure behavior by transport in this version:

OperationFailure behavior
HTTP publish()Normally resolves with { error } for request failures.
gRPC publish()Rejects with an object containing error for RPC failures.
publishBulk()Normally resolves with failedMessages; preprocessing/setup failures can reject.

Await calls inside your application’s error boundary and inspect their response fields. Do not assume that resolution means publication succeeded, or that every rejection is an Error instance. Publisher success means Dapr accepted the publish request, not that all subscribers processed the event.

6.2 - Receive events with a JavaScript application server

Register HTTP or gRPC subscriptions, handle acknowledgements, route events, and configure bulk and dead-letter delivery

Use DaprServer.pubsub to receive events through HTTP or gRPC callbacks with @dapr/dapr 3.19.0 or later. Complete the pub/sub prerequisites before following this guide. For workers without an application callback listener, use streaming subscriptions instead.

Register subscriptions before startup

Save the following as subscriber.js in an ES module application:

import { CommunicationProtocolEnum, DaprPubSubStatusEnum, DaprServer } from "@dapr/dapr";

const server = new DaprServer({
  serverHost: "127.0.0.1",
  serverPort: "50002",
  communicationProtocol: CommunicationProtocolEnum.HTTP,
  clientOptions: { daprHost: "127.0.0.1", daprPort: "3500" },
});

await server.pubsub.subscribe("pubsub", "orders", async (data, headers) => {
  console.log("Order received:", data, headers);
  return DaprPubSubStatusEnum.SUCCESS;
});

await server.start();

Run the subscriber with an application port matching serverPort:

dapr run --app-id orders-subscriber --app-port 50002 --app-protocol http --dapr-http-port 3500 --resources-path ./components -- node subscriber.js

For a gRPC server, select CommunicationProtocolEnum.GRPC, set the client port to "50001", and run Dapr with --app-protocol grpc --dapr-grpc-port 50001.

The remaining examples assume this server and enum import. Add their subscription registrations before server.start().

HTTP subscriptions are exposed through /dapr/subscribe; gRPC subscriptions use ListTopicSubscriptions. Configure each component/topic pair once. Repeating subscribe() or subscribeWithOptions() for an already registered pair throws. Use subscribeToRoute() to add another handler to an existing route instead.

MethodPurpose
subscribe(pubSubName, topic, callback, route?, metadata?)Register a subscription and a handler.
subscribeWithOptions(pubSubName, topic, options)Configure callbacks, metadata, routes, dead-letter topic, and bulk delivery.
subscribeToRoute(pubSubName, topic, route, callback)Add a handler to an already registered route; does not create a runtime subscription.
subscribeBulk(pubSubName, topic, callback, options?)Register bulk delivery with a callback invoked once per entry.
getSubscriptions()Inspect local registrations grouped by component and topic, including routes and Dapr discovery configuration.

Handlers can be attached to existing routes after startup, but adding a handler does not cause Dapr to rediscover new topics or routes. There is no server-side unsubscribe method in this interface. Stop the application listener with await server.stop() during shutdown. See configuration and usage for client lifetime and serialization.

Callback data and headers

Callbacks receive (data, headers) and should be async. data is normally the deserialized event payload, not the full CloudEvent envelope.

DeliveryCallback headers
HTTP server, single messageIncoming HTTP request headers.
HTTP server, bulkPer-entry metadata.
gRPC server, single or bulkIncoming gRPC request metadata converted to an object.

Do not assume that server callbacks expose CloudEvent attributes in headers. If your application requires these attributes, the streaming callback explicitly provides them.

To consume raw payloads, use subscribeWithOptions() with metadata: { rawPayload: true }. Server subscription metadata values are JSON-stringified before discovery. See publishing raw payloads for publisher settings and the different streaming metadata representation.

Acknowledge, retry, or drop

Handler outcomeAcknowledgement
Return DaprPubSubStatusEnum.SUCCESS, or complete without a valueSuccessfully processed.
Return DaprPubSubStatusEnum.RETRYRequest redelivery.
Return DaprPubSubStatusEnum.DROPDiscard the message.
Throw or rejectLog the error and request redelivery.

Return the enum value, not an object such as { status: "RETRY" }. When multiple server handlers share a route, all run sequentially and their combined status has priority RETRY > DROP > SUCCESS.

Retries and dead-letter forwarding are controlled by Dapr resiliency policies and the pub/sub component, not by an SDK retry loop. Configure an inbound retry policy appropriate to your broker. For example, save the following in components/resiliency.yaml, targeting the component name pubsub:

apiVersion: dapr.io/v1alpha1
kind: Resiliency
metadata:
  name: pubsub-retries
spec:
  policies:
    retries:
      processing:
        policy: constant
        duration: 500ms
        maxRetries: 10
  targets:
    components:
      pubsub:
        inbound:
          retry: processing

Make handlers idempotent: Dapr pub/sub is at-least-once, and a message can be delivered again after processing.

Content-based routing

Use subscribeWithOptions() to declare the routes, then attach handlers with subscribeToRoute(). Dapr evaluates the CEL expressions against the CloudEvent:

await server.pubsub.subscribeWithOptions("pubsub", "routed-orders", {
  route: {
    default: "standard",
    rules: [{ match: 'event.type == "com.example.order.priority"', path: "priority" }],
  },
});

server.pubsub.subscribeToRoute("pubsub", "routed-orders", "standard", async (data) => {
  console.log("Standard order:", data);
});

server.pubsub.subscribeToRoute("pubsub", "routed-orders", "priority", async (data) => {
  console.log("Priority order:", data);
});

Route names are logical identifiers: the SDK generates unique callback paths from the component, topic, and route. A callback supplied alongside a route object is attached to all its declared routes, not just its default route.

See message routing.

Wildcard topics

Wildcard support depends on the broker and component. For MQTT-style subscriptions, the SDK recognizes # for multiple levels and + for one level when dispatching gRPC callbacks:

await server.pubsub.subscribe("pubsub", "sensors/#", async (data) => {
  console.log("Sensor event:", data);
});

await server.pubsub.subscribe("pubsub", "devices/+/temperature", async (data) => {
  console.log("Temperature:", data);
});

Use a component that supports these topic patterns; they are not a general wildcard facility for every pub/sub broker.

Bulk subscriptions

Bulk delivery batches messages between Dapr and the application. The SDK still invokes your callback for one entry at a time, not for an array. It constructs the per-entry acknowledgement response:

await server.pubsub.subscribeBulk(
  "pubsub",
  "bulk-orders",
  async (data) => {
    console.log("Order from batch:", data);
    return DaprPubSubStatusEnum.SUCCESS;
  },
  { maxMessagesCount: 100, maxAwaitDurationMs: 40 },
);

The options also accept route and metadata. Omitted batching limits use runtime/component defaults. For routing or dead-letter configuration combined with bulk delivery, use subscribeWithOptions() with bulkSubscribe: { enabled: true, maxMessagesCount: 100, maxAwaitDurationMs: 40 }.

In this version, the HTTP bulk handler without a custom serializer extracts CloudEvent data and decodes application/octet-stream entries, but does not populate callback data for direct application/json or text/plain entries. Prefer the normal Dapr CloudEvent envelope for HTTP bulk delivery, or use a custom serializer or gRPC when consuming those direct entry formats.

See bulk publish and subscribe.

Dead-letter topics

Set deadLetterTopic (case-sensitive) on the source subscription and register a separate subscription to consume that topic:

await server.pubsub.subscribeWithOptions("pubsub", "validated-orders", {
  deadLetterTopic: "orders-deadletter",
  callback: async (data) => {
    if (!data.orderId) {
      return DaprPubSubStatusEnum.RETRY;
    }
    console.log("Validated order:", data);
    return DaprPubSubStatusEnum.SUCCESS;
  },
});

await server.pubsub.subscribe("pubsub", "orders-deadletter", async (data) => {
  console.error("Order requires investigation:", data);
  return DaprPubSubStatusEnum.SUCCESS;
});

Dapr controls forwarding after processing fails and the configured retry policy is exhausted.

Although the SDK exposes deadLetterCallback, it only registers an additional local route on the source subscription. It does not create a subscription to the dead-letter topic, and specifying the callback alone does not configure runtime dead-letter forwarding. Do not use it as a substitute for the separate subscription above.

See dead-letter topics.

6.3 - Receive events with JavaScript streaming subscriptions

Subscribe without an application server using gRPC streaming, with cancellation, acknowledgements, and failure supervision

Use DaprClient.pubsub.subscribeStream() to open a bidirectional gRPC stream from your application to the sidecar with @dapr/dapr 3.19.0 or later. It does not require DaprServer, an application callback port, or subscription discovery.

Complete the pub/sub prerequisites first. Streaming uses SubscribeTopicEventsAlpha1, an alpha gRPC API; your Dapr runtime must implement it. HTTP clients reject this operation with HTTPNotSupportedError. Streaming subscriptions do not support routing rules or bulk subscription options.

Run a streaming worker

Save the following as worker.js in an ES module application. The worker runs until SIGINT or SIGTERM, or until the stream fails:

import { CommunicationProtocolEnum, DaprClient, DaprPubSubStatusEnum } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "50001",
  communicationProtocol: CommunicationProtocolEnum.GRPC,
});

const controller = new AbortController();
const stop = () => controller.abort();
process.once("SIGINT", stop);
process.once("SIGTERM", stop);

try {
  const subscription = await client.pubsub.subscribeStream(
    "pubsub",
    "orders",
    async (data, headers) => {
      console.log("Order received:", data, "CloudEvent:", headers);
      return DaprPubSubStatusEnum.SUCCESS;
    },
    { signal: controller.signal, deadLetterTopic: "orders-deadletter" },
  );

  try {
    await subscription.closed;
  } finally {
    await subscription.close();
  }
} finally {
  process.removeListener("SIGINT", stop);
  process.removeListener("SIGTERM", stop);
  await client.stop();
}

Run it without --app-port:

dapr run --app-id orders-worker --dapr-grpc-port 50001 --resources-path ./components -- node worker.js

The subscription promise resolves only after Dapr confirms setup. Setup failures reject. See configuration and usage for connection settings, authentication, and custom serialization.

Callback data and acknowledgements

Callbacks receive deserialized payload data and CloudEvent attributes in headers. Attributes include id, source, type, specversion, datacontenttype, topic, pubsubname, and path, together with flattened extensions. Unlike server callback headers, these explicitly expose CloudEvent attributes.

Callbacks run sequentially within a stream and use the client’s serializer. Return DaprPubSubStatusEnum.SUCCESS, RETRY, DROP, or no value; completing without a value means success. Callback exceptions, deserialization errors, and invalid return values are logged and acknowledged as RETRY.

Retry and dead-letter behavior depends on the runtime resiliency policy and component. Make handlers idempotent because a message can be delivered more than once.

Streaming options and lifecycle

MemberBehavior
options.metadataSubscription metadata sent as a string map.
options.deadLetterTopicTopic for runtime dead-letter forwarding; consume it separately.
options.signalCancels setup or an active stream.
subscription.close()Idempotently cancels the stream; safe to call from its callback. Does not wait for an in-flight callback.
subscription.closedResolves after cancellation and completion of any in-flight callback. Rejects on transport/protocol failure or unexpected stream termination.

For raw payloads, pass metadata: { rawPayload: "true" } in the subscription options. See publishing raw payloads. To consume failed messages, create a separate subscription to deadLetterTopic; setting the option does not register a consumer for that topic.

TypeScript applications can import PubSubStreamingSubscription and PubSubStreamingSubscriptionOptions as types from @dapr/dapr.

Supervise failures and shut down

Observe closed throughout the subscription lifetime. The SDK logs stream failures but does not reconnect automatically. If reconnecting is appropriate, implement a supervised retry policy that creates a new subscription and respects application shutdown.

client.stop() does not close these subscriptions. Explicitly close every subscription and await its closed promise before stopping the client. Cancellation during a callback leaves that event unacknowledged, so it may be redelivered. The API does not expose .NET-style queue capacity, per-message timeout, or automatic reconnect options.

6.4 - JavaScript pub/sub configuration and usage

Configure pub/sub client lifetimes, endpoints, authentication, serialization, and reliable message processing

This guide covers shared configuration and operational guidance for pub/sub with @dapr/dapr 3.19.0 or later. See the overview to choose between server callbacks and streaming subscriptions.

Client lifetime

Reuse a long-lived DaprClient rather than creating one per message. Await outstanding operations before calling await client.stop() at shutdown.

For server subscriptions, stop the application listener with await server.stop(). For streaming subscriptions, close each subscription and await its closed promise before stopping the client; client.stop() does not close streams. See streaming lifecycle guidance.

Endpoints and authentication

The default protocol is HTTP. To explicitly configure a gRPC connection:

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "50001",
  communicationProtocol: CommunicationProtocolEnum.GRPC,
  daprApiToken: process.env.DAPR_API_TOKEN,
});

For HTTP, select CommunicationProtocolEnum.HTTP and port "3500". Port options are strings. The application callback protocol and port are configured separately on DaprServer; see server subscriptions.

When explicit connection settings are omitted, the SDK uses the environment settings for the selected protocol:

SettingBehavior
DAPR_HTTP_ENDPOINT / DAPR_GRPC_ENDPOINTSupply the sidecar endpoint for the selected protocol when explicit host and port settings are omitted.
DAPR_HTTP_PORT / DAPR_GRPC_PORTSupply the sidecar port when not otherwise configured; defaults are 3500 and 50001.
DAPR_API_TOKENSupplies the authentication token when daprApiToken is omitted.

Custom serialization

Configure an IDaprSerializer using DaprClient’s serializer option. It converts application payloads to and from strings and can supply a contentType. It applies to single and bulk publishing and streaming callbacks. For server callbacks, configure it through DaprServer’s clientOptions.serializer.

import { DaprClient, DaprServer } from "@dapr/dapr";

const serializer = {
  contentType: "application/json",
  serialize(value) {
    return JSON.stringify({ payload: value });
  },
  deserialize(data) {
    return JSON.parse(data).payload;
  },
};

const customClient = new DaprClient({ serializer });
const customServer = new DaprServer({
  serverHost: "127.0.0.1",
  serverPort: "50003",
  clientOptions: { serializer },
});

Use compatible serializers on both ends. A custom serializer replaces the default payload encoding; it does not automatically preserve a supplied CloudEvent envelope. TypeScript type parameters do not provide runtime validation or recreate class instances. TypeScript applications can import IDaprSerializer as a type from @dapr/dapr.

Transport-specific serialization limitations are documented beside bulk publishing and bulk subscriptions.

Reliable message processing

Await SDK operations inside your application’s error boundary. Publication failure behavior differs by transport, and bulk publishing can partially fail even when its promise resolves; see publish error handling.

Make subscribers idempotent and await all processing before returning an acknowledgement. Dapr pub/sub is at-least-once, so successfully processed messages can still be redelivered. Configure retries through Dapr resiliency policies and component settings, not by assuming that the SDK retries every failure.

See server acknowledgements and retry configuration and streaming failure supervision for delivery-mode-specific behavior.

7 - Logging in JavaScript SDK

Configuring logging in JavaScript SDK

Introduction

The JavaScript SDK comes with a out-of-box Console based logger. The SDK emits various internal logs to help users understand the chain of events and troubleshoot problems. A consumer of this SDK can customize the verbosity of the log, as well as provide their own implementation for the logger.

Configure log level

There are five levels of logging in descending order of importance - error, warn, info, verbose, and debug. Setting the log to a level means that the logger will emit all the logs that are at least as important as the mentioned level. For example, setting to verbose log means that the SDK will not emit debug level logs. The default log level is info.

Dapr Client

import { CommunicationProtocolEnum, DaprClient, LogLevel } from "@dapr/dapr";

// create a client instance with log level set to verbose.
const client = new DaprClient({
  daprHost,
  daprPort,
  communicationProtocol: CommunicationProtocolEnum.HTTP,
  logger: { level: LogLevel.Verbose },
});

For more details on how to use the Client, see JavaScript Client.

DaprServer

import { CommunicationProtocolEnum, DaprServer, LogLevel } from "@dapr/dapr";

// create a server instance with log level set to error.
const server = new DaprServer({
  serverHost,
  serverPort,
  clientOptions: {
    daprHost,
    daprPort,
    logger: { level: LogLevel.Error },
  },
});

For more details on how to use the Server, see JavaScript Server.

Custom LoggerService

The JavaScript SDK uses the in-built Console for logging. To use a custom logger like Winston or Pino, you can implement the LoggerService interface.

Winston based logging:

Create a new implementation of LoggerService.

import { LoggerService } from "@dapr/dapr";
import * as winston from "winston";

export class WinstonLoggerService implements LoggerService {
  private logger;

  constructor() {
    this.logger = winston.createLogger({
      transports: [new winston.transports.Console(), new winston.transports.File({ filename: "combined.log" })],
    });
  }

  error(message: any, ...optionalParams: any[]): void {
    this.logger.error(message, ...optionalParams);
  }
  warn(message: any, ...optionalParams: any[]): void {
    this.logger.warn(message, ...optionalParams);
  }
  info(message: any, ...optionalParams: any[]): void {
    this.logger.info(message, ...optionalParams);
  }
  verbose(message: any, ...optionalParams: any[]): void {
    this.logger.verbose(message, ...optionalParams);
  }
  debug(message: any, ...optionalParams: any[]): void {
    this.logger.debug(message, ...optionalParams);
  }
}

Pass the new implementation to the SDK.

import { CommunicationProtocolEnum, DaprClient, LogLevel } from "@dapr/dapr";
import { WinstonLoggerService } from "./WinstonLoggerService";

const winstonLoggerService = new WinstonLoggerService();

// create a client instance with log level set to verbose and logger service as winston.
const client = new DaprClient({
  daprHost,
  daprPort,
  communicationProtocol: CommunicationProtocolEnum.HTTP,
  logger: { level: LogLevel.Verbose, service: winstonLoggerService },
});

9 - Dapr Workflow JavaScript SDK

Author, run, and manage durable workflows with the Dapr JavaScript SDK

Use the Dapr JavaScript SDK to coordinate activities, durable timers, external events, and other workflows. WorkflowRuntime registers and executes your application code; DaprWorkflowClient schedules instances and manages their lifecycle. Both use the Dapr sidecar’s gRPC endpoint, independently of the protocol you select for a DaprClient.

These guides target @dapr/dapr 3.19.0 or later. The examples use Dapr runtime 1.19 or later so that all documented capabilities are available, including detached workflows, management operations, and the alpha binary-store building block. Use Node.js 18 or later.

npm install @dapr/dapr@3.19.0

Choose a guide

GuideWhat you learn
Author and run workflowsRegister a worker, chain and parallelize activities, use timers and events, retry failures, and continue as new.
Manage workflow instancesSchedule, inspect, wait, suspend, resume, terminate, purge, list, retrieve history, and rerun from an event.
Multi-app workflows and historyCall remote activities and child workflows, schedule detached workflows, and inspect propagated ancestor history.
Workflow versioningUse replay-safe patch decisions to evolve running workflows.
Workflow serializationShare a serializer between clients and workers and preserve compatibility with persisted history.
Workflow data references (alpha)Offload large payloads to a binary store, forward explicit references, and stream data only where needed.

Runtime configuration

WorkflowRuntime and DaprWorkflowClient accept an options object:

OptionBehavior
daprHost, daprPortSidecar host and gRPC port. Defaults come from SDK settings, normally 127.0.0.1:50001. Without explicit host or port options, DAPR_GRPC_ENDPOINT is honored.
daprApiTokenDapr API token; otherwise uses DAPR_API_TOKEN.
loggerSDK logging options. Use ctx.logger inside workflow code to suppress replayed messages.
serializerPayload serializer. Defaults to JSON; if omitted with payloads configured, uses the resolver’s serializer.
payloadsOptional BinaryStoreWorkflowPayloadResolver. Register on every client and worker that reads or writes references.
grpcOptionsSupports grpc.max_receive_message_length and grpc.max_send_message_length, each defaulting to 128 MiB, and Connect-compatible interceptors. Other legacy grpc-js channel settings, such as keepalive, credentials, or compression, are not applied.
maxConcurrentActivitiesWorker-only positive integer limiting simultaneous activity executions. Additional activities queue locally; workflow work items are not limited. Omit to leave concurrency to the Dapr runtime’s workflow.maxConcurrentActivityInvocations.

Await runtime.start() before scheduling work. It resolves when the worker connects and is ready, and rejects if the initial connection fails. runtime.isReady reports current connection readiness; the worker reconnects after a lost connection. Call runtime.stop() and client.stop() during application shutdown.

The SDK propagates W3C trace context when scheduling workflows and creates WorkflowActivity <name> spans under the Dapr.Workflow tracer when an activity has a parent trace context. Configure an OpenTelemetry provider and exporter in your application to collect these spans.

Upgrade considerations

This SDK uses its own workflow engine rather than the shared DurableTask client library. Re-test existing workflows when upgrading, including replay, status comparisons, serializers, and transport options. Compare statuses with WorkflowRuntimeStatus members instead of hard-coded strings or numeric values. The engine does not support the stateful workflow client; propagated ancestor history and getWorkflowHistory are separate capabilities.

Next steps

9.1 - How to: Author and run workflows with the JavaScript SDK

Register workflows and activities, coordinate durable tasks, and handle retries and events

Workflows are generator functions that yield SDK tasks. The runtime records task results in history and replays the generator to reconstruct its state. Activities are ordinary functions, optionally asynchronous, that perform I/O or other work outside the deterministic workflow.

Prerequisites

  • Node.js 18 or later and @dapr/dapr 3.19.0 or later.
  • An initialized self-hosted Dapr environment with runtime 1.19 or later.
  • Workflow infrastructure: an actor state store, placement, and scheduler. The default dapr init environment provides these for local development.

Register and run a workflow

Install the package in your application directory:

npm init -y
npm install @dapr/dapr@3.19.0

Save this complete example as app.mjs. It uses JavaScript ES modules; the same APIs are available in TypeScript.

import {
  DaprWorkflowClient,
  WorkflowRuntime,
  WorkflowRuntimeStatus,
} from "@dapr/dapr";

async function hello(_ctx, name) {
  return `Hello ${name}!`;
}

function* sequence(ctx) {
  const cities = [];
  for (const city of ["Tokyo", "Seattle", "London"]) {
    cities.push(yield ctx.callActivity(hello, city));
  }
  ctx.logger.info("Greeting sequence completed");
  return cities;
}

const options = { daprHost: "127.0.0.1", daprPort: "50001" };
const runtime = new WorkflowRuntime(options);
const client = new DaprWorkflowClient(options);
runtime.registerWorkflow(sequence).registerActivity(hello);

try {
  await runtime.start();
  const id = await client.scheduleNewWorkflow(sequence);
  const state = await client.waitForWorkflowCompletion(id, true, 30);
  if (!state || state.runtimeStatus !== WorkflowRuntimeStatus.COMPLETED) {
    throw new Error(
      state?.workflowFailureDetails?.getErrorMessage() ?? "Workflow did not complete successfully",
    );
  }
  console.log(state.getOutput());
} finally {
  await runtime.stop();
  await client.stop();
}

Run it with a sidecar:

dapr run --app-id sequence-workflow --dapr-grpc-port 50001 -- node app.mjs

The output includes ["Hello Tokyo!", "Hello Seattle!", "Hello London!"].

Registration names

registerWorkflow(fn) and registerActivity(fn) use the function name as the durable registration name. For explicit, stable names, use:

runtime.registerWorkflowByName("greeting-v1", sequence);
runtime.registerActivityWithName("hello-v1", hello);

Call these registrations by string, for example ctx.callActivity("hello-v1", city) or client.scheduleNewWorkflow("greeting-v1"). Keep names stable across deployments and minification. registerWorkflowWithName is a deprecated alias of registerWorkflowByName.

Keep workflow execution deterministic

Put network calls, database operations, filesystem access, and nondeterministic computation in activities. Use yield with ctx.callActivity, timers, event waits, and task-composition helpers; do not substitute await or Promise.all for durable tasks. Both synchronous and async generators are supported, but an async generator does not make arbitrary I/O replay-safe.

NeedWorkflow API
Current timectx.getCurrentUtcDateTime(), not new Date() or Date.now()
Generated identifierctx.newGuid(), not a random UUID
Instance or registration namectx.getWorkflowInstanceId(), ctx.getWorkflowName()
Logging without duplicate replay messagesctx.logger.info(...), .warn(...), .error(...), or .debug(...)
Conditional replay handlingctx.isReplaying()
Externally visible progressctx.setCustomStatus({ step: "processing" })

Activities can be delivered more than once. Make external side effects idempotent. An activity receives WorkflowActivityContext with getWorkflowInstanceId(), getWorkflowActivityId(), and getTaskExecutionId(). The execution ID is a persisted activity key, also exposed as task.executionId on the workflow’s activity task, and is reused across SDK retry attempts. It can be undefined for legacy history or runtimes that do not supply it; do not assume it is always present or that it guarantees exactly-once delivery.

Run activities in parallel

Create all tasks before yielding whenAll:

function* parallelGreetings(ctx, cities) {
  const tasks = cities.map((city) => ctx.callActivity("hello", city));
  return yield ctx.whenAll(tasks);
}

whenAll returns results in input order and fails if a task fails. whenAny returns the first completed task, not its result, and does not cancel the other tasks. Yield the winning task or call its getResult() to observe its result or error.

Retry activities and child workflows

Pass retryPolicy in activity or child-workflow options:

function* resilientGreeting(ctx, name) {
  return yield ctx.callActivity("hello", name, {
    retryPolicy: {
      firstRetryInterval: 1,
      maxNumberOfAttempts: 4,
      backoffCoefficient: 2,
      maxRetryInterval: 30,
      retryTimeout: 120,
    },
  });
}

All intervals are in seconds. maxNumberOfAttempts includes the initial call; backoffCoefficient defaults to 1. The first interval and optional limits must be positive and finite; attempts must be a positive integer and the coefficient must be at least 1. A non-retriable failure stops retries. retryTimeout limits scheduling further attempts; it is not a timeout that interrupts a running activity. Failures are thrown into the generator at yield, where you can catch them and schedule compensating activities.

Wait for external events

An external event lets another process signal an instance. The SDK matches event names case-insensitively and buffers events that arrive before their wait is created.

import { TaskTimeoutError } from "@dapr/dapr";

function* approval(ctx) {
  ctx.setCustomStatus({ step: "awaiting-approval" });
  try {
    const decision = yield ctx.waitForExternalEvent("approval", {
      timeoutInSeconds: 3600,
    });
    return decision;
  } catch (error) {
    if (!(error instanceof TaskTimeoutError)) throw error;
    return { approved: false, reason: "Approval deadline expired" };
  }
}

From a client, call await client.raiseEvent(instanceId, "approval", { approved: true }). From another workflow in the same app, use ctx.sendEvent(instanceId, "approval", { approved: true }); sendEvent does not return a task and is not yielded. Do not expose the client or make client RPCs inside workflow code.

Use durable timers and cancellation

ctx.createTimer(60) waits 60 seconds of workflow time. You can also supply an absolute Date computed from ctx.getCurrentUtcDateTime(). Do not use setTimeout for a durable delay.

For a race between an event and a timer:

function* approvalRace(ctx) {
  const event = ctx.waitForExternalEvent("approval");
  const timer = ctx.createTimer(3600);
  const winner = yield ctx.whenAny([event, timer]);
  if (winner === event) {
    timer.cancel();
    return event.getResult();
  }
  event.cancel();
  timer.getResult();
  return { approved: false, reason: "Approval deadline expired" };
}

Timers and event waits implement CancellableTask, exposing cancel() and isCanceled. Canceling stops the SDK’s local wait and rejects it with TaskCanceledError; it does not cancel runtime-side work, an activity, or an entire workflow. Cancel unused waits explicitly after a race.

Continue as new

For long-running loops, start a fresh history generation under the same instance ID:

function* batchLoop(ctx, input) {
  const next = yield ctx.callActivity("processBatch", input);
  ctx.continueAsNew(next, true);
  return;
}

Return immediately after continueAsNew. The second argument, saveEvents, defaults to true and retains unconsumed external events; set it to false to discard them. Do not schedule more work after requesting continuation. Register the activity used by this example before starting the worker.

Next steps

9.2 - Manage workflow instances with the JavaScript SDK

Schedule, inspect, control, list, and rerun workflow instances with DaprWorkflowClient

Use DaprWorkflowClient from application or administrative code, not inside a workflow generator. This guide targets @dapr/dapr 3.19.0 or later and runtime 1.19 or later.

import { DaprWorkflowClient, WorkflowRuntimeStatus } from "@dapr/dapr";

const client = new DaprWorkflowClient({
  daprHost: "127.0.0.1",
  daprPort: "50001",
});

The following snippets reuse client. The workflow name must be registered on the destination worker. Call await client.stop() when the application shuts down.

Choose the management client

DaprWorkflowClient is separate from DaprClient.workflow, the basic workflow API. The basic API supports HTTP or gRPC with scheduleNewWorkflow, getWorkflowState, raiseEvent, pause, resume, terminate, and purge. Its old start, get, and raise names are deprecated aliases. It returns a plain WorkflowGetResponseType with properties, rather than the WorkflowState described here.

Use DaprWorkflowClient for durable start/completion waits, listing, history retrieval, reruns, and integrated payload-reference handling. Its methods and options are not interchangeable with the basic API: for example, it uses suspendWorkflow rather than pause, and getWorkflowState requires the getInputsAndOutputs argument.

Schedule and wait

const instanceId = await client.scheduleNewWorkflow(
  "orderWorkflow",
  { orderId: "order-123" },
  "order-123",
  new Date(Date.now() + 60_000),
);

await client.waitForWorkflowStart(instanceId, false, 120);
const state = await client.waitForWorkflowCompletion(instanceId, true, 300);
if (!state) throw new Error(`Workflow ${instanceId} was not found`);

if (state.runtimeStatus === WorkflowRuntimeStatus.COMPLETED) {
  console.log(state.getOutput());
} else {
  console.error(state.runtimeStatus, state.workflowFailureDetails);
}

The positional scheduling arguments are (workflow, input?, instanceId?, startAt?, signal?). workflow can be a registered name or a function; omitted instance IDs are generated. startAt is an optional absolute Date. Using wall-clock time here is safe because this is client code, not replayed workflow code.

Both wait methods accept (instanceId, fetchPayloads = true, timeoutInSeconds = 60, signal?). A wait deadline or RPC failure rejects the promise. A workflow that reaches a failed or terminated state can return a state rather than throw: check runtimeStatus, not just whether the wait resolved.

Inspect state and failure details

await client.getWorkflowState(instanceId, true) returns a WorkflowState snapshot, or undefined for a missing instance. Pass false to omit inputs and outputs when only metadata is needed.

MemberMeaning
name, instanceId, existsRegistration name, instance ID, and existence (true for a returned state).
runtimeStatusCompare with WorkflowRuntimeStatus.RUNNING, COMPLETED, CONTINUED_AS_NEW, FAILED, CANCELED, TERMINATED, PENDING, SUSPENDED, or STALLED.
createdAt, lastUpdatedAtJavaScript Date values.
getInput(), getOutput(), getCustomStatus()Deserialize with the client’s configured serializer; TypeScript callers can provide a generic result type.
serializedInput, serializedOutput, customStatusSerialized strings, not already-parsed objects. With payload support, resolved strings are prepared where possible.
workflowFailureDetailsgetErrorType(), getErrorMessage(), getStackTrace(), isNonRetriable, and getInnerFailure() for nested details. innerFailure exposes the raw nested protobuf value.
getInputReference(), getOutputReference(), getCustomStatusReference()Original top-level binary-store reference, if present. See data references.

Typed getters do not validate application schemas or reconstruct class instances automatically. Missing payload fields return undefined. Missing binary-store objects or integrity failures are errors, not “workflow not found.”

Send events and control execution

MethodBehavior
raiseEvent(id, eventName, payload?, signal?)Deliver an event to a workflow’s durable event wait.
suspendWorkflow(id, reason?, signal?)Suspend instance execution.
resumeWorkflow(id, reason?, signal?)Resume a suspended instance.
terminateWorkflow(id, output?, signal?)Request termination with optional output; does not execute application compensation logic.
purgeWorkflow(id, signal?)Purge retained instance state/history; returns whether an instance was deleted. Subject to runtime eligibility checks.

For example:

await client.suspendWorkflow(instanceId, "Investigating an order");
await client.resumeWorkflow(instanceId, "Investigation complete");
await client.raiseEvent(instanceId, "approval", { approved: true });

Do not use suspension or termination as a way to undo activity side effects. A canceled client request also does not terminate the instance. Purging history does not delete binary-store payloads.

Cancel client requests

Scheduling, state reads, waits, event delivery, suspend/resume, termination, and purge accept an optional AbortSignal in their final argument:

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10_000);
try {
  await client.waitForWorkflowCompletion(instanceId, true, 60, controller.signal);
} finally {
  clearTimeout(timeout);
}

Aborting stops the request and associated payload I/O where supported, not the durable workflow. A request may already have been accepted by the runtime before cancellation. Listing, history retrieval, and rerun do not expose an AbortSignal parameter in this release.

List instances

listWorkflowInstanceIds(continuationToken?, pageSize?) returns { instanceIds, continuationToken }. Continue until no token remains:

let token;
do {
  const page = await client.listWorkflowInstanceIds(token, 100);
  for (const id of page.instanceIds) {
    console.log(id);
  }
  token = page.continuationToken;
} while (token);

The SDK validates pageSize as an integer from 1 to 4294967295; the runtime may impose additional limits. The response contains IDs, not state snapshots or filtered query results.

Retrieve execution history

const history = await client.getWorkflowHistory(instanceId);
for (const event of history) {
  console.log(event.eventId, event.eventType.case);
}

This returns the complete HistoryEvent[] using the SDK’s protobuf types. Payload fields remain serialized wire data, including any reference envelopes; retrieving history is not automatic payload hydration. Missing-instance and other runtime errors are propagated.

Rerun from an event

Inspect the source history to choose the event ID accepted by the runtime, then create a new instance:

const rerunId = await client.rerunWorkflowFromEvent(instanceId, eventId, {
  newInstanceId: "order-123-rerun",
  input: { correctedAmount: 125 },
});

eventId is a history event ID, not the array index, and must be an unsigned 32-bit integer. This operation creates a new instance; it does not modify the source instance. Replacement input applies to the activity targeted by the event, not the original workflow input.

newInstanceId is optional but must not be empty. overwriteInput defaults to true when input is supplied; set it to false to preserve the original activity input. The runtime decides which histories and events are eligible and propagates errors for missing instances or invalid events. Reruns can repeat side effects: review idempotency and retain any binary objects still referenced by the copied history.

Next steps

9.3 - Multi-app workflows and history propagation in the JavaScript SDK

Coordinate remote activities, child workflows, and detached workflows and inspect ancestor history

Use targetAppId to route activity and workflow calls to another Dapr application. This guide uses @dapr/dapr 3.19.0 or later and Dapr runtime 1.19 or later. See Multi Application Workflows for runtime architecture and constraints.

Requirements

Participating apps must run in the same namespace and use the same workflow actor state store. Each destination must register the name being called and run a workflow worker. Omit targetAppId for a local call. For cross-language calls, agree on names, input/output schemas, and serialization.

Call an activity or child workflow

import { HistoryPropagationScope } from "@dapr/dapr";

function* orderWorkflow(ctx, order) {
  const payment = yield ctx.callActivity("chargePayment", order, {
    targetAppId: "payments",
    retryPolicy: { firstRetryInterval: 2, maxNumberOfAttempts: 3 },
    propagationScope: HistoryPropagationScope.OWN_HISTORY,
  });

  return yield ctx.callChildWorkflow(
    "fulfillmentWorkflow",
    { order, payment },
    undefined,
    {
      targetAppId: "fulfillment",
      propagationScope: HistoryPropagationScope.LINEAGE,
    },
  );
}

An activity call returns the activity result; a child call waits for the child and returns its output. Failures are thrown at the corresponding yield. callSubWorkflow is an alias of callChildWorkflow. The positional child arguments are (workflow, input?, instanceId?, options?); omit the child ID to let the SDK derive it. A retry policy can retry a child workflow as well as an activity. A custom child ID applies to the first attempt; later retry attempts receive generated child IDs.

Schedule an independent, detached workflow

Detached workflows require Dapr runtime 1.19 or later:

function* dispatchWorkflow(ctx, input) {
  const startAt = new Date(ctx.getCurrentUtcDateTime().getTime() + 60_000);
  const detachedId = yield ctx.scheduleNewDetachedWorkflow(
    "notificationWorkflow",
    input,
    undefined,
    startAt,
    { targetAppId: "notifications" },
  );
  return { detachedId };
}

The task returns the detached workflow’s instance ID, not its output. The workflow runs independently: its completion or failure does not flow back to the caller. Use a client to inspect the returned ID through the destination app’s sidecar. The arguments are (workflow, input?, instanceId?, startAt?, options?). Detached options support targetAppId and allowPayloadReferences, but not retries or history propagation. Always yield the scheduling task so the workflow can record the scheduling action.

Inspect propagated history

Activity and child-workflow options accept:

HistoryPropagationScopeHistory shared with the callee
NONENo history propagation; the default.
OWN_HISTORYThe calling workflow’s own history.
LINEAGEThe caller’s history together with its inherited ancestor history.

From a workflow or activity, call ctx.getPropagatedHistory(). It returns undefined when no history was supplied, otherwise a read-only PropagatedHistoryView:

async function auditActivity(ctx, input) {
  const history = ctx.getPropagatedHistory();
  if (history) {
    console.log(history.appIDs);
    for (const ancestor of history.workflows) {
      console.log(ancestor.workflowName, ancestor.instanceId, ancestor.eventCount);
    }
  }
  return input;
}
MemberMeaning
scope, chunksRequested scope and ancestor chunks, oldest ancestor first and immediate parent last.
events, appIDsFlattened events and distinct app IDs across chunks.
workflowsChunks with startEventIndex and eventCount in the flattened view.
getWorkflowByName(name)Oldest matching ancestor, or null.
getEventsByAppID(id), getEventsByInstanceID(id)Events matching an app or instance.
getEventsByWorkflowName(name)Events from every matching workflow instance, not just the first match.

Each chunk contains appId, instanceId, workflowName, decoded events, and original rawEvents, rawSignatures, and signingCertChains. The SDK preserves these bytes but does not verify signatures. Do not treat this view as SDK-verified evidence. Payloads in history remain serialized; reference envelopes are not automatically resolved in this view.

History propagation increases data transferred to callees and can expose ancestor inputs, outputs, and events. Select the narrowest scope needed. This is distinct from the unsupported stateful workflow client.

Cross-app data references

With binary-store payload support, cross-app activity, child, and detached workflow inputs are inline by default. Set allowPayloadReferences: true only when the destination understands the reference contract and can access the same stored objects.

The JavaScript SDK shares the version-1 reference envelope with the .NET SDK. Interoperation requires compatible serializers, contracts, and storage access; JavaScript does not implement .NET payload-stage reversal. Inlining only applies to the top-level call input: results, events, nested references, and propagated history are not made compatible by that default. See cross-app reference handling before enabling offloading in a multi-app topology.

9.4 - Workflow versioning in the JavaScript SDK

Evolve workflow code with replay-safe named patches

Workflow code must reproduce the task sequence recorded in history when replaying an existing instance. Changing activity order, adding durable tasks, or changing branches without a migration strategy can cause nondeterministic replay failures.

With @dapr/dapr 3.19.0 or later, use ctx.isPatched(patchName) for named code changes. Patch evaluation is validated by the Dapr runtime; the examples in this documentation use runtime 1.19 or later. See Workflow versioning for the runtime-level concepts.

Introduce a patch

Suppose an existing workflow calls chargePayment directly, and a new deployment must validate the order first:

function* orderWorkflow(ctx, order) {
  if (ctx.isPatched("validate-order-before-charge")) {
    yield ctx.callActivity("validateOrder", order);
  }
  return yield ctx.callActivity("chargePayment", order);
}

When an unrecorded patch is first encountered during replay, isPatched returns false so the old execution path remains intact. Outside replay, or when the patch is already recorded, it returns true. The SDK records evaluated patches for runtime version validation.

Names are case-sensitive and must not be empty or whitespace. Use a distinct name for each logical change; never reuse a name for an unrelated change or compute it from nondeterministic values.

Keep old execution paths available

Keep the original branch and patch check until every instance that needs that history has finished and is no longer eligible for replay or rerun. Do not reorder patch checks or remove them merely because a new deployment has started. Account for retained histories, suspended instances, child workflows, and administrative reruns when planning retirement.

For a larger contract change, register a new workflow name and send new instances to it while keeping the old implementation available for existing instances. Patching does not convert persisted serialized data or turn an incompatible payload schema into a compatible one.

Next steps

9.5 - Workflow serialization in the JavaScript SDK

Configure payload serialization consistently across JavaScript workflow clients and workers

The JavaScript SDK uses JSON by default. With @dapr/dapr 3.19.0 or later, clients and workers can share a custom IDaprSerializer through their serializer option. Serializer selection is client-side; it does not by itself require binary-store support.

Default JSON behavior

Workflow payloads use JSON.stringify and JSON.parse. Use JSON-compatible application contracts: dates deserialize as strings, prototypes are not reconstructed, BigInt requires explicit handling, and cyclic values cannot be serialized. TypeScript generics describe the expected result but do not validate or instantiate that type at runtime.

WorkflowState.getInput<T>(), getOutput<T>(), and getCustomStatus<T>() use the client’s serializer. Their serialized counterparts are strings, not parsed application values.

Configure a serializer

The serializer contract converts values to and from strings:

import {
  DaprWorkflowClient,
  JsonSerializer,
  WorkflowRuntime,
  type IDaprSerializer,
} from "@dapr/dapr";

const serializer: IDaprSerializer = new JsonSerializer();
const options = { daprHost: "127.0.0.1", daprPort: "50001", serializer };
const client = new DaprWorkflowClient(options);
const runtime = new WorkflowRuntime(options);

For a custom format, implement serialize<T>(value: T): string and deserialize<T = unknown>(data: string): T. An optional contentType identifies the media type for SDK APIs that use it; it is not a workflow wire-format negotiation mechanism. Binary formats can use Base64 to satisfy the string contract, at the cost of added size. Serialize deterministically and throw on invalid input rather than silently returning defaults.

For cross-language workflows, agree on the wire format and property names. The default JavaScript JSON behavior is not a replacement for the .NET SDK’s case-insensitive deserialization or other language-specific conversion rules.

Combine serialization with data references

Workflow data references offload the serialized UTF-8 string to a binary store. Offloading is separate from choosing a serializer and is evaluated after serialization. The reference envelope itself is versioned JSON, independent of the application serializer.

Supply the same serializer to the BinaryStoreWorkflowPayloadResolver and all clients and workers. When a workflow client or runtime has payloads but no explicit serializer, it uses payloads.serializer. An explicit serializer overrides that choice; do not configure incompatible serializers in those two places. Only top-level PayloadReference objects receive special envelope handling, not references nested within an application object.

9.6 - Workflow data references in the JavaScript SDK

Offload large workflow payloads to a binary store and defer materialization with explicit references

Workflow data references keep large payloads out of inline workflow history. The JavaScript SDK stores the serialized value in a Dapr binary store and records a small, versioned reference envelope instead. You can keep ordinary workflow inputs and let the SDK retrieve data automatically, or forward explicit PayloadReference values without downloading the document in the workflow.

Choose how to handle payloads

ApproachBehaviorBest for
Inline (default)Serialized values travel in workflow history. No binary-store access is required.Small payloads.
Automatic offloadingPreferReference stores eligible large values. Ordinary input/result contracts trigger automatic download and deserialization.Reducing history size while preserving existing application contracts.
Explicit referencesRegister reference inputs and request reference results; forward envelopes without downloading the data. Resolve or stream only where needed.Workflows that coordinate large documents but do not inspect their contents.

Performance and memory tradeoffs

Offloading replaces repeated large history values with small references, reducing history storage and workflow protocol traffic. It does not make storage I/O free:

  • Automatic offloading still materializes values. An ordinary workflow input, activity result, or event must be downloaded and deserialized when consumed. Replays repeat those reads; the cache is scoped to a single orchestrator execution, not shared across replays.
  • Explicit references avoid unnecessary reads. A workflow can pass the same reference through several activities without loading the document itself. Only consumers that need the content resolve or stream it.
  • Streaming avoids full buffering. It trades the convenience of a deserialized object for incremental access to the raw serialized bytes.

For a document of size S forwarded through multiple steps, inline history repeatedly carries data on the order of S; reference-based history repeatedly carries the much smaller envelope. This is a traffic model, not a JavaScript latency or allocation benchmark. Actual performance depends on document size, replay frequency, store latency, and how many consumers materialize the data. Small values may be faster inline. Do not apply the .NET benchmark results to JavaScript; measure your own workload.

Set up payload support

Configure a binary-store component

For a disposable local experiment, save this as components\workflow-payloads.yaml:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: workflow-payloads
spec:
  type: binarystore.in-memory
  version: v1
  metadata:
    - name: maxSize
      value: "4Mi"
    - name: maxTotalSize
      value: "512Mi"

See the binary-store guide for component and transport details. Adding a component alone does not enable workflow offloading.

Register a resolver on clients and workers

import {
  BinaryStoreWorkflowPayloadResolver,
  CommunicationProtocolEnum,
  DaprClient,
  DaprWorkflowClient,
  PayloadWritePreference,
  WorkflowRuntime,
} from "@dapr/dapr";

const connection = { daprHost: "127.0.0.1", daprPort: "50001" };
const dapr = new DaprClient({
  ...connection,
  communicationProtocol: CommunicationProtocolEnum.GRPC,
});
const payloads = new BinaryStoreWorkflowPayloadResolver(dapr.binaryStore, {
  storeName: "workflow-payloads",
  writePreference: PayloadWritePreference.PreferReference,
  offloadThreshold: 64 * 1024,
});
const client = new DaprWorkflowClient({ ...connection, payloads });
const runtime = new WorkflowRuntime({ ...connection, payloads });

The resolver uses DaprClient.binaryStore, which supports HTTP or gRPC; the workflow transport itself still uses gRPC. Configure endpoints and API tokens consistently if the clients connect to different endpoints. storeName must match the component name. All readers need access to the referenced store/key and compatible serialization. Merely constructing a resolver uses Preserve, not automatic offloading; the example explicitly selects PreferReference.

Use automatic offloading

With the registration above, keep ordinary inputs:

function* documentWorkflow(ctx, document) {
  return yield ctx.callActivity("measureDocument", document);
}

async function measureDocument(_ctx, document) {
  return document.content.length;
}

runtime.registerWorkflow(documentWorkflow).registerActivity(measureDocument);

try {
  await runtime.start();
  const id = await client.scheduleNewWorkflow(documentWorkflow, {
    content: "a".repeat(128 * 1024),
  });
  const state = await client.waitForWorkflowCompletion(id, true, 60);
  console.log(state?.getOutput());
} finally {
  await runtime.stop();
  await client.stop();
  await dapr.stop();
}

Combine this snippet with the imports and resolver setup above in app.mjs, then run:

dapr run --app-id document-workflow --dapr-grpc-port 50001 --resources-path .\components -- node app.mjs

The document exceeds the threshold and is offloaded at eligible boundaries. The workflow still receives an ordinary object and downloads it on replay; the small numeric result stays inline.

Pass explicit references

JavaScript types are erased at runtime. A TypeScript annotation such as PayloadReference<DocumentPayload> alone does not change how inputs are read. Register each workflow and activity that accepts an envelope with { inputAsReference: true }.

Use this alternative to the automatic example, with the same resolver setup:

function* referenceWorkflow(ctx, documentReference) {
  return yield ctx.callActivity("measureReference", documentReference);
}

async function measureReference(ctx, documentReference) {
  const document = await ctx.payloads.resolve(documentReference);
  return document.content.length;
}

runtime
  .registerWorkflow(referenceWorkflow, { inputAsReference: true })
  .registerActivity(measureReference, { inputAsReference: true });

try {
  await runtime.start();
  const instanceId = "document-reference-1";
  const reference = await payloads.store(
    { content: "a".repeat(128 * 1024) },
    instanceId,
  );
  await client.scheduleNewWorkflow(referenceWorkflow, reference, instanceId);
  const state = await client.waitForWorkflowCompletion(instanceId, true, 60);
  console.log(state?.getOutput());
} finally {
  await runtime.stop();
  await client.stop();
  await dapr.stop();
}

store(value, instanceId, signal?) explicitly stores the serialized value, independently of the automatic threshold or write preference. The workflow only forwards its reference; the activity downloads the document. A reference-input contract rejects an inline value instead of silently storing or converting it.

Keep activity and child results as references

Activities can return a reference created by ctx.payloads.store(value, ctx.getWorkflowInstanceId()). Request resultAsReference: true on ctx.callActivity or ctx.callChildWorkflow to receive that reference without automatically resolving it:

function* documentPipeline(ctx, input) {
  const reference = yield ctx.callActivity("createDocument", input, {
    resultAsReference: true,
  });
  return yield ctx.callActivity("measureReference", reference);
}

The producing activity must actually return a reference, or its result must be offloaded by policy. An inline result fails a reference-result contract, including a small result below the offload threshold. resultAsReference does not force a write. Forward PayloadReference objects directly, not a plain object containing one: nested references are not recursively converted or resolved.

Resolve inside a workflow only when necessary

ctx.hasPayloadResolver() reports whether payload support is configured. yield ctx.resolvePayload(reference) downloads and deserializes the data through an SDK task. yield ctx.openPayload(reference) obtains a raw Node.js readable stream; consuming it asynchronously requires an async generator. These read-only APIs are supported, but repeat during replay and sacrifice the principal benefit of keeping the document out of the orchestrator. Prefer resolving or streaming in activities. Do not perform arbitrary storage writes from workflow code.

Stream payloads in activities

ctx.payloads.open(reference, signal?) returns a Node.js Readable over the raw stored UTF-8 serialized bytes. It does not deserialize the document:

async function countStoredBytes(ctx, reference) {
  const stream = ctx.payloads.open(reference);
  let bytes = 0;
  try {
    for await (const chunk of stream) {
      bytes += chunk.length;
    }
  } finally {
    stream.destroy();
  }
  return bytes;
}

runtime.registerActivity(countStoredBytes, { inputAsReference: true });

Streaming bypasses maxInMemoryRehydrationSize, but also bypasses buffered SHA-256 verification and payload-stage reversal. If needed, compute and verify a hash while consuming the stream. Destroy streams on early exit and handle stream errors. Creating the stream alone does not download all data. The byte count includes JSON syntax and encoding, not just the length of an application string.

Configuration and policy reference

Offloading uses the serialized value’s UTF-8 byte count, not JavaScript string length. Keys have the form <keyPrefix>/<instanceId>/<sha256>. References contain the store, key, size, SHA-256 hash, format version, and optional stage metadata. The version-1 wire envelope uses the reserved $daprRef property.

Resolver optionDefault and behavior
storeNameRequired binary-store component name. Reads use the store recorded in each reference.
keyPrefix"workflow-payloads"; prefix for new content-addressed keys.
writePreferencePayloadWritePreference.Preserve. See the policy table below.
offloadSitesPayloadKind.All; bitwise combination of eligible boundary kinds.
offloadThreshold64 * 1024 bytes, inclusive. Positive integer; null offloads every nonempty serialized snapshot eligible under PreferReference.
skipWriteIfExiststrue; tolerates an already-existing content-addressed object rather than overwriting it. Does not mean the SDK skips every upload attempt.
maxInMemoryRehydrationSize16 * 1024 * 1024 bytes. Enforced against both the envelope size and actual buffered bytes.
verifyIntegrityOnReadtrue; buffered reads verify the SHA-256 hash.
resolutionCacheSize128 entries per orchestrator execution. 0 disables caching.
resolutionCacheBytes64 * 1024 * 1024 summed envelope bytes per execution. 0 disables caching; values exceeding the budget are read without being cached.
manifestOptional durable, idempotent IWorkflowPayloadManifest implementation for explicit cleanup.

The cache retains serialized reads, not a durable cross-replay object cache. Both the entry and byte bounds apply.

Write preferenceOrdinary valuesExisting top-level references
PreserveStay inline.Pass through, except where a cross-app input requires inlining.
PreferReferenceOffload if the site and threshold allow it.Pass through, except where a cross-app input requires inlining.
PreferInlineStay inline.Resolve before writing the outgoing value.

offloadSites combines PayloadKind.WorkflowInput, WorkflowOutput, ActivityInput, ActivityOutput, ExternalEvent, ContinueAsNewInput, CustomStatus, ChildWorkflowInput, and ChildWorkflowOutput. For example, PayloadKind.ActivityInput | PayloadKind.ActivityOutput limits automatic offloading to activity boundaries. PayloadKind.None disables new automatic offloading without disabling reads or forwarding. Detached inputs use WorkflowInput, termination output uses WorkflowOutput, and rerun replacement input uses WorkflowInput.

Read limits and errors

Buffered resolution rejects staged references because JavaScript does not implement stage reversal. It also throws PayloadRehydrationLimitError above the configured limit, PayloadNotFoundError for a missing object, and PayloadIntegrityError for a hash mismatch when verification is enabled. PayloadNotResolvableError indicates missing resolver support. Invalid or unsupported reserved reference envelopes throw rather than being treated as ordinary application data.

Changing the write threshold affects new writes only. Lowering the rehydration limit can break existing ordinary-value consumers and PreferInline conversions. Plan that change against all retained payloads; use explicit references and streaming for values that must not be fully buffered.

Read client results

getWorkflowState and the wait methods prepare referenced input, output, and custom status before returning. With a compatible resolver, the ordinary getters use the resolved serialized data. This means fetching payloads can trigger downloads even when you intend to inspect a reference.

state.getInputReference(), getOutputReference(), and getCustomStatusReference() expose the original reference. If buffered resolution was refused because the resolver is absent or the size exceeds its limit, the reference remains accessible, while the corresponding typed getter throws the preparation error:

const state = await client.getWorkflowState(instanceId, true);
const reference = state?.getOutputReference();
if (reference) {
  const stream = payloads.open(reference);
  try {
    for await (const chunk of stream) {
      process.stdout.write(chunk);
    }
  } finally {
    stream.destroy();
  }
}

Use an appropriate rehydration limit on clients intended to stream large results. Missing objects and integrity failures reject the state request instead of being deferred. Serialized getters can still contain the raw envelope when preparation could not resolve it; do not treat that envelope as an application object. Metadata-only state reads omit input/output fields. History retrieval and propagated-history views preserve their wire payloads and do not hydrate them.

Cross-app and cross-language calls

Cross-app activity, child, and detached workflow inputs are inline by default, even under PreferReference. The sender resolves an existing top-level reference before submission; a failed read prevents submission. The destination should accept an ordinary value for that input.

To forward references, explicitly allow them on the call:

const result = yield ctx.callActivity("remoteDocumentConsumer", reference, {
  targetAppId: "document-service",
  allowPayloadReferences: true,
});

This permits the resolver’s write policy; it does not force offloading or override PreferInline. Every reader must resolve the recorded store and key to the same binary object through its own sidecar. Separate in-memory components do not meet that requirement.

The JavaScript and .NET SDKs share a version-1 envelope, but cross-language use also requires compatible serialization and contracts. JavaScript buffered reads do not reverse .NET payload stages. Use unstaged, compatible payloads for such calls; other SDKs must not be assumed to support this scheme.

The inline default protects only top-level call inputs. It does not inline nested references, return values, external events, or references in propagated ancestor history. Coordinate payload support across the entire topology before enabling it; do not mix payload-aware and payload-unaware workers for one app ID.

Events and continuation

client.raiseEvent and workflow ctx.sendEvent apply the ExternalEvent write policy. ctx.waitForExternalEvent(name) resolves an offloaded event as an ordinary value; use ctx.waitForExternalEvent(name, { asReference: true }) to receive an envelope without downloading. The sender must actually supply a reference or produce an offloaded event for that contract.

continueAsNew applies the ContinueAsNewInput policy. Unconsumed reference events preserved with saveEvents: true are carried forward without being materialized. Ensure the next generation’s input contract is compatible with the new input.

Adopt references in existing applications

First deploy resolver support with Preserve everywhere that may read the histories. This keeps ordinary new values inline while allowing existing references to be read. Next, enable PreferReference at selected sites after assessing storage durability, read limits, and every downstream consumer.

Moving from ordinary input to inputAsReference: true is a contract change: old inline histories cannot satisfy it. Introduce a new registration name/version and route new instances to that contract, keeping the old implementation for existing instances. Likewise, resultAsReference and asReference must not be introduced into histories that still contain inline results or events.

To stop creating new automatic references, use Preserve; to inline outgoing references as well, use PreferInline. Keep the resolver registered while any history, result, event, or consumer can still contain a reference. Switching write policy does not rewrite retained history.

Retention and cleanup

Neither workflow completion nor purgeWorkflow deletes binary-store objects. A failed scheduling request can also leave an uploaded object behind. Retain objects for suspended workflows, replay, reruns, descendants, and forwarded references; deleting a source instance’s objects can break another instance that still uses them.

For explicit cleanup, supply a durable IWorkflowPayloadManifest with asynchronous record(instanceId, reference, signal?), list(instanceId, signal?), and clear(instanceId, signal?) methods, then call await payloads.deletePayloads(instanceId) only when no consumer needs those objects. The resolver records writes in the manifest; it does not discover every forwarded-reference consumer. Cleanup without a manifest throws. Missing objects are tolerated during deletion; other errors propagate, and the manifest is cleared only after deletions succeed.

Use application-level retention and access control appropriate to the data. A reference is a locator, not authorization or a substitute for protecting the binary-store component.

10 - Dapr Configuration JavaScript SDK

Read application configuration and subscribe to changes with the Dapr JavaScript SDK

Use DaprClient.configuration to retrieve application configuration from a configured store and subscribe to changes. You can read selected keys or all keys, watch selected keys or the whole store, and pass store-specific request metadata.

These guides target @dapr/dapr 3.19.0 or later. Configuration operations in the JavaScript SDK require gRPC; the HTTP client throws HTTPNotSupportedError for both reads and subscriptions, even though the Dapr runtime exposes an HTTP Configuration API.

The client reads and watches configuration; it does not create, update, or delete configuration items. Make those changes directly in the backing store using its own tools or API. This building block manages application settings, not the Dapr runtime’s Configuration resource.

Start with the configuration how-to guide for a runnable example. Refer to the client usage guidance for the complete method surface, metadata, response shapes, subscription lifetime, and error handling.

10.1 - How to: Manage configuration with the Dapr JavaScript SDK

Retrieve application settings and subscribe to configuration changes using the Dapr JavaScript SDK

Use DaprClient.configuration to read application settings and react to changes without calling the configuration store directly. This guide uses Redis for local development.

Prerequisites

In your application directory, initialize an npm project if needed and install the SDK:

npm init -y
npm install @dapr/dapr@3.19.0

The examples use JavaScript ES modules saved as .mjs files and do not require a TypeScript compiler. The same APIs are available in TypeScript.

Configure a store

Start a dedicated local Redis instance on port 6380 to avoid conflicting with Redis started by dapr init:

docker run --name js-config-redis -p 127.0.0.1:6380:6379 -d redis:7

The Redis configuration component enables the keyspace notifications it needs when a subscription starts, so no extra Redis flags are required. This example has no authentication and binds Redis to loopback for local development only. For production, configure authentication and TLS as described in the Redis configuration store reference.

Create a components directory in your application directory and save the following as components\configstore.yaml:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: configstore
spec:
  type: configuration.redis
  version: v1
  metadata:
    - name: redisHost
      value: "localhost:6380"
    - name: redisPassword
      value: ""

The component’s metadata.name, configstore, is the store name supplied to each SDK method. A Redis state store component is not a configuration store; the component type must be configuration.redis.

Populate the store:

docker exec js-config-redis redis-cli MSET orderId1 "101||1" orderId2 "102||1"

This example uses Redis configuration values in value||version format. The SDK returns the value and version as separate strings.

Retrieve configuration items

Save the following as get-configuration.mjs:

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: process.env.DAPR_GRPC_PORT ?? "50001",
  communicationProtocol: CommunicationProtocolEnum.GRPC,
});

try {
  const config = await client.configuration.get(
    "configstore",
    ["orderId1", "orderId2"],
  );

  for (const [key, item] of Object.entries(config.items)) {
    console.log(`${key}: ${item.value} (version ${item.version})`);
  }

  const order = config.items["orderId1"];
  if (order === undefined) {
    throw new Error("Required configuration key orderId1 was not returned");
  }
} finally {
  await client.stop();
}

Run it with a Dapr sidecar:

dapr run --app-id js-config-reader --dapr-grpc-port 50001 --resources-path .\components -- node get-configuration.mjs

For the seeded values, the application prints:

orderId1: 101 (version 1)
orderId2: 102 (version 1)

To request all items instead, omit the keys argument or pass an empty array:

const config = await client.configuration.get("configstore");

An empty key list is sent to the component. Whether it can return all keys, and which keys are included, depends on the store. Check required keys explicitly rather than assuming every requested key is present.

Subscribe to configuration changes

Save the following as watch-configuration.mjs:

import { setTimeout } from "node:timers/promises";
import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: process.env.DAPR_GRPC_PORT ?? "50001",
  communicationProtocol: CommunicationProtocolEnum.GRPC,
});

let subscription;

try {
  subscription = await client.configuration.subscribeWithKeys(
    "configstore",
    ["orderId1", "orderId2"],
    async (changes) => {
      for (const [key, item] of Object.entries(changes.items)) {
        console.log(`${key}: ${item.value} (version ${item.version})`);
      }
    },
  );

  console.log("Subscription requested; watching for 60 seconds");
  await setTimeout(60_000);
} finally {
  try {
    if (subscription) {
      await subscription.stop();
    }
  } finally {
    await client.stop();
  }
}

Run the subscriber after the reader has exited:

dapr run --app-id js-config-watcher --dapr-grpc-port 50001 --resources-path .\components -- node watch-configuration.mjs

In a separate terminal, change a watched key while the subscriber is running:

docker exec js-config-redis redis-cli SET orderId1 "103||2"

Once the subscription is established, the callback prints orderId1: 103 (version 2). The subscribe promise returns a local handle, not a server-readiness acknowledgment. If you update the store immediately after starting the application, the subscription might not yet be active.

To watch the whole store, replace the subscribeWithKeys call with:

subscription = await client.configuration.subscribe(
  "configstore",
  async (changes) => {
    console.log("Configuration changes:", changes.items);
  },
);

All-key subscription behavior, including whether keys added later are watched, depends on the component. Each subscription has its own handle; stop every subscription during shutdown. No DaprServer, application callback route, or --app-port is required: updates arrive on the client’s gRPC stream.

Next steps

Read the client usage guidance for request metadata, response handling, and important subscription failure and cleanup limitations.

10.2 - Configuration client usage

Guidance for configuration reads, metadata, subscriptions, client lifetime, and error handling in JavaScript

Configuration is available through DaprClient.configuration. This guide targets @dapr/dapr 3.19.0 or later and describes the implementation details and limitations to account for when using this release.

Client lifetime and configuration

Reuse a long-lived DaprClient for configuration operations rather than creating one for each read or subscription. Configuration requires gRPC; the SDK’s default protocol is HTTP and is not supported for these methods.

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: process.env.DAPR_GRPC_PORT ?? "50001",
  communicationProtocol: CommunicationProtocolEnum.GRPC,
});

Port values are strings. When you omit explicit host and port options, the SDK can resolve the sidecar address from DAPR_GRPC_ENDPOINT. Otherwise, it falls back to the configured host and port, with defaults of 127.0.0.1 and DAPR_GRPC_PORT (or 50001). Explicit host and port options take precedence over endpoint settings.

The API token defaults to DAPR_API_TOKEN; you can also supply daprApiToken in the client options. Request metadata described below is passed to the configuration component and is not a substitute for sidecar authentication.

Stop all subscriptions before calling await client.stop() at application shutdown. Do not rely on client shutdown to explicitly unsubscribe each handle.

Choose an operation

All methods take the configuration component’s name, not its type or YAML file name.

MethodPurposeResult
get(storeName, keys?, metadata?)Read selected keys, or request all keys when keys are omitted or emptyPromise resolving to { items }
subscribe(storeName, callback)Watch the store with an empty key listPromise resolving to a subscription handle
subscribeWithKeys(storeName, keys, callback)Watch selected keysPromise resolving to a subscription handle
subscribeWithMetadata(storeName, keys, metadata, callback)Watch keys with store-specific request metadataPromise resolving to a subscription handle

The gRPC implementation removes empty strings ("") from key arrays. If the resulting array is empty, it sends an all-key request; it does not mean “read or watch nothing.” Use explicit nonempty keys when you need a bounded selection. Support for all-key requests is component-specific.

There are no configuration write or delete methods and no public client.configuration.unsubscribe(...) method. Update the backing store directly and use the returned handle’s stop() to stop a subscription.

Understand response shapes

Both get and subscription callbacks expose an items object indexed by configuration key:

{
  "items": {
    "orderId1": {
      "key": "orderId1",
      "value": "101",
      "version": "1",
      "metadata": {}
    }
  }
}

Each item’s key repeats its map key. value and version are strings; versions are store-specific and may be empty. metadata contains the response metadata supplied by the component.

The SDK does not deserialize values into numbers, booleans, or application objects. Parse and validate them in application code. For example, a JSON setting requires JSON.parse(item.value), with appropriate error handling.

Handle missing keys explicitly. A successful response can have an empty items object; do not assume a missing key causes the read to reject.

Subscription callbacks receive the items in each update, not a guaranteed full-store snapshot. Empty-item stream messages are skipped. The SDK keeps the runtime subscription ID internally and does not expose it in the handle or callback response.

Pass request metadata

get accepts metadata as its third argument. subscribeWithMetadata accepts metadata before the callback. Use the component’s documented metadata keys and string values, without an HTTP metadata. prefix.

For example, after configuring an Azure App Configuration component named appconfig, select a label:

const config = await client.configuration.get(
  "appconfig",
  ["application:theme"],
  { label: "production" },
);
console.log(config.items);

const subscription = await client.configuration.subscribeWithMetadata(
  "appconfig",
  ["application:theme"],
  { label: "production" },
  async (changes) => {
    console.log("Updated settings:", changes.items);
  },
);

// Later, during shutdown:
await subscription.stop();

This snippet assumes an existing gRPC client. Pass [] as the keys argument to request an all-key subscription with metadata, subject to component support.

Metadata is passed through to the store. It is not automatically copied into each returned item’s metadata. Supported fields, filtering, polling intervals, and change-notification behavior depend on the component; Redis, for example, does not implement the Azure label selector.

Subscription lifetime and callbacks

Each subscribe call starts an independent background stream and returns a handle with stop(). Multiple subscriptions can share one client, including subscriptions for the same keys. Retain and stop each handle separately.

Provide an async callback, or a function returning Promise<void>. The SDK awaits each callback before consuming the next message on that subscription. Keep callbacks short; a slow callback delays subsequent updates. Handle processing failures explicitly inside the callback and report them through your application’s logging or monitoring.

Do not assume that subscribing immediately provides current values. Initial-value delivery depends on the runtime and component, and the SDK does not perform an initial get for you. Read required values with get and reconcile subsequent updates according to your application’s consistency needs. Separate reads and subscriptions are not an atomic snapshot-and-watch operation.

Awaiting a subscribe method only returns the local handle; it does not wait for a server acknowledgment or first update. There is no readiness promise, completion promise, or error callback on the handle.

Stop a subscription

Use the handle returned by the subscribe method:

await subscription.stop();

In the reviewed versions, stop() aborts the local stream and, if a runtime subscription ID has arrived, attempts an explicit unsubscribe request. The implementation is asynchronous even though the published SubscribeConfigurationStream type declares stop: () => void; using await waits for that implementation’s cleanup.

Cleanup is best-effort: unsubscribe failures are suppressed, and stopping before an ID arrives cannot issue an ID-based unsubscribe. A resolved stop() is not confirmation of server-side removal. Stopping also does not wait for an already-running callback to finish or cancel application work started by that callback.

The configuration methods do not accept an AbortSignal or a per-call timeout option. Use stop() to cancel a subscription; do not pass binary-store-style cancellation options to configuration methods.

Error handling and recovery

Await reads and subscription creation at your application’s error boundary. A failed get rejects, for example when the component is missing, the store cannot be reached, or the sidecar returns an error. gRPC runtime errors use the Connect transport’s error codes rather than HTTP status codes. Calling configuration methods on an HTTP client rejects with HTTPNotSupportedError.

Report callback errors within the callback itself. For applications that require fresh settings, add independent freshness checks or periodic reads and an application-level recovery policy; do not treat possession of a subscription handle, or a lack of updates, as proof that the watch is healthy. If you recreate subscriptions, stop the previous handles and reconcile current values.

See the configuration how-to guide for a complete read-and-watch example and the Configuration API reference for runtime semantics.

11 - Dapr Conversation JavaScript SDK

Converse with large language models using the Dapr JavaScript SDK

Use DaprClient.conversation to send messages to a large language model through a Dapr conversation component. The JavaScript SDK supports the Conversation Alpha2 API over HTTP and gRPC, including message history, function tool calls, structured output, PII scrubbing, prompt cache retention, and token usage statistics.

These guides target @dapr/dapr 3.19.0 or later. The underlying Conversation API is alpha. Basic Alpha2 requests require Dapr runtime 1.16 or later; some options require newer runtimes and support from the selected component and model.

GuideDescription
How to: Converse with an LLMInstall the package, configure a component, and send your first request.
Conversation client usageConfigure the client, work with messages and responses, and use advanced request options.
How to: Call toolsDefine functions, validate model-generated arguments, and return tool results to the model.

11.1 - How to: Converse with an LLM using the Dapr JavaScript SDK

Configure a conversation component and send messages using the Dapr JavaScript SDK

Use client.conversation.converse(request) to send role-based messages to a configured conversation component. You do not need a DaprServer or a provider-specific JavaScript SDK.

Prerequisites

Installation

In your application directory, initialize an npm project if you do not already have one, then install the SDK:

npm init -y
npm install @dapr/dapr@3.19.0

The examples use JavaScript ES modules and run as .mjs files without a TypeScript compiler. The same APIs are available in TypeScript, with request and response types exported from @dapr/dapr.

Configure a conversation component

Create a components directory and save the following as components\conversation.yaml:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: echo
spec:
  type: conversation.echo
  version: v1

The echo component returns the input text without calling an LLM. Use it to check local connectivity, not to test model reasoning, structured output, or tool calling.

The component’s metadata.name, echo, is the name passed in a conversation request. It is not the component type or the YAML file name.

Send a message

Save the following as app.mjs:

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "3500",
  communicationProtocol: CommunicationProtocolEnum.HTTP,
});

try {
  const response = await client.conversation.converse({
    name: "echo",
    inputs: [
      {
        messages: [
          { ofUser: { content: [{ text: "What is Dapr?" }] } },
        ],
      },
    ],
  });

  for (const output of response.outputs) {
    for (const choice of output.choices) {
      console.log("Finish reason:", choice.finishReason);
      if (choice.message) {
        console.log("Conversation output:", choice.message.content);
      }
    }
  }
} finally {
  await client.stop();
}

Start the application with a Dapr sidecar:

dapr run --app-id conversation-example --dapr-http-port 3500 --dapr-grpc-port 50001 --resources-path ./components -- node app.mjs

The echoed message is available at response.outputs[0].choices[0].message.content. Unlike Alpha1 examples in other SDKs, the JavaScript Alpha2 response does not have an output.result property.

To use gRPC, change communicationProtocol to CommunicationProtocolEnum.GRPC and daprPort to "50001". The request and response objects are the same for both protocols.

Use an LLM provider

Replace the echo component with a supported conversation component. For example, use this OpenAI component with a local environment variable secret store:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: conversation-secrets
spec:
  type: secretstores.local.env
  version: v1
---
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: llm
spec:
  type: conversation.openai
  version: v1
  metadata:
    - name: key
      secretKeyRef:
        name: OPENAI_API_KEY
        key: OPENAI_API_KEY
    - name: model
      value: gpt-4o-mini
auth:
  secretStore: conversation-secrets

Set OPENAI_API_KEY in the environment inherited by dapr run, restart the sidecar, and change the request’s name to "llm". Do not put provider credentials in request metadata or commit them to a component file. See the OpenAI component reference for provider settings and model support.

Next steps

Read the client usage guidance for message history, structured output, caching, token limits, and errors. Follow the tool-calling guide to let a model request application-defined functions.

11.2 - Dapr Conversation JavaScript client usage

Configure conversation requests, message history, structured output, caching, and response handling in JavaScript

DaprClient.conversation exposes one asynchronous method:

converse(request: ConversationRequestAlpha2): Promise<ConversationResponseAlpha2>;

Import these types, and the other Conversation* types used below, from @dapr/dapr. This guidance targets 3.19.0 or later.

Runtime and provider support

CapabilityMinimum Dapr runtime
Alpha2 messages, tool calling, temperature, and PII scrubbing1.16
responseFormat, promptCacheRetention, and token usage statistics1.17
Model name in the response1.18
maxTokens1.19

Availability also depends on the conversation component and model. An SDK option does not make an unsupported provider feature available. See the Conversation API reference and your component reference.

Client configuration and lifetime

Create a long-lived DaprClient and reuse it across requests. Call await client.stop() when the application shuts down, not after each operation on a shared client.

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "50001",
  communicationProtocol: CommunicationProtocolEnum.GRPC,
  daprApiToken: process.env.DAPR_API_TOKEN,
});

Use CommunicationProtocolEnum.HTTP with the sidecar’s HTTP port for HTTP requests. The SDK calls /v1.0-alpha2/conversation/{name}/converse over HTTP and ConverseAlpha2 over gRPC. You pass plain JavaScript objects, not protobuf messages or provider-native request objects.

The client also supports DAPR_HTTP_ENDPOINT and DAPR_GRPC_ENDPOINT for endpoint configuration. DAPR_API_TOKEN authenticates to the sidecar; it is separate from the provider API key configured on the component. See JavaScript client configuration for shared client options.

Messages and conversation history

A request contains one or more inputs, each with a nonempty messages array. Each message must contain exactly one role property:

Role propertyContent
ofDeveloperDeveloper instructions, with optional participant name.
ofSystemSystem instructions, with optional participant name.
ofUserUser input, with optional participant name.
ofAssistantPrevious assistant text and/or toolCalls, with optional participant name. Content can be omitted for a tool-only message.
ofToolFunction result with required nonempty name, text content, and toolId to correlate with the assistant’s tool call.

Message content is an array of { text: string } objects, not a single string. The SDK’s conversation message types support text, not image or audio content parts.

The following example reuses a configured client and an LLM component named llm:

const messages = [
  { ofSystem: { content: [{ text: "Give concise, factual answers." }] } },
  { ofUser: { name: "reader", content: [{ text: "What is Dapr?" }] } },
];

const response = await client.conversation.converse({
  name: "llm",
  contextId: "chat-123",
  inputs: [{ messages }],
});

const choice = response.outputs[0]?.choices[0];
if (!choice?.message || choice.finishReason !== "stop") {
  throw new Error("Expected a completed text response before continuing the chat");
}

messages.push(
  { ofAssistant: { content: [{ text: choice.message.content }] } },
  { ofUser: { content: [{ text: "How does it help with pub/sub?" }] } },
);

const followup = await client.conversation.converse({
  name: "llm",
  contextId: response.contextId || "chat-123",
  inputs: [{ messages }],
});

contextId is a correlation value, not server-side chat memory. Retain the history in your application and resend the messages needed for every turn. Choose one assistant choice when continuing a conversation; do not merge alternative choices into the same history.

Request options

PropertyDescription
nameRequired, nonempty component name.
inputsRequired, nonempty array of conversation inputs.
contextIdOptional correlation value.
temperatureOptional finite number controlling randomness. The component and model determine the accepted range. Explicit 0 is preserved.
scrubPiiOptional boolean enabling PII obfuscation in response text. Set inputs[].scrubPii independently for input text.
metadataOptional string-to-string conversation tags, not provider configuration or credentials.
tools, toolChoiceOptional function definitions and tool selection policy. See tool calling.
responseFormatOptional JSON Schema object for structured output.
promptCacheRetentionOptional nonnegative Temporal.Duration for prompt cache retention.
maxTokensOptional integer from 1 through 2147483647 limiting generated completion tokens.
parametersOptional JSON object reserved by Dapr. It is not a general-purpose provider options bag. The SDK encodes its values internally.

PII scrubbing and metadata

Input and output scrubbing are separate settings:

const response = await client.conversation.converse({
  name: "llm",
  inputs: [
    {
      scrubPii: true,
      messages: [
        { ofUser: { content: [{ text: "Contact me at reader@example.com." }] } },
      ],
    },
  ],
  scrubPii: true,
  temperature: 0,
  metadata: { user_id: "user-123", session_id: "chat-123" },
});

Metadata supports up to 16 pairs, with keys up to 64 characters and values up to 512 characters. Keep API keys and component connection settings in component configuration or referenced secrets.

Structured output

Pass the JSON Schema directly as responseFormat, not an OpenAI response_format wrapper or a JSON-encoded string:

const response = await client.conversation.converse({
  name: "llm",
  inputs: [
    {
      messages: [
        { ofUser: { content: [{ text: "Answer with the name of Dapr's pub/sub building block." }] } },
      ],
    },
  ],
  responseFormat: {
    type: "object",
    properties: { answer: { type: "string" } },
    required: ["answer"],
    additionalProperties: false,
  },
});

const choice = response.outputs[0]?.choices[0];
if (!choice?.message || choice.finishReason !== "stop") {
  throw new Error("Expected a completed structured response");
}
const value = JSON.parse(choice.message.content);
if (
  value === null ||
  typeof value !== "object" ||
  Array.isArray(value) ||
  typeof value.answer !== "string" ||
  Object.keys(value).length !== 1
) {
  throw new Error("Response does not match the expected answer schema");
}
console.log(value.answer);

The SDK returns content as a string; it does not parse or validate the generated JSON against your schema. Handle JSON parsing failures and validate the result before using it. A truncated or filtered response might not contain complete JSON.

Prompt cache retention and token limits

Import Temporal from the SDK to construct the retention duration:

import { Temporal } from "@dapr/dapr";

const response = await client.conversation.converse({
  name: "llm",
  inputs: [
    {
      messages: [
        { ofUser: { content: [{ text: "Explain Dapr in one paragraph." }] } },
      ],
    },
  ],
  promptCacheRetention: Temporal.Duration.from({ minutes: 30 }),
  maxTokens: 200,
});

promptCacheRetention must be a fixed, nonnegative duration. Days, hours, minutes, seconds, and subsecond fields are supported; calendar years, months, and weeks are not. Fields must be safe integers and the total must fit the protobuf duration range. Do not pass milliseconds as a number or a duration string instead of a Temporal.Duration.

Retention is a request to the component/provider, not a guarantee of a cache hit or stored conversation history. Provider-specific caching limits still apply.

Some components also expose responseCacheTTL in their YAML metadata to cache complete responses. This is component configuration, separate from promptCacheRetention; do not try to configure it through request metadata.

maxTokens requires Dapr 1.19 or later and overrides the component’s token cap where supported. Check finishReason for "length" rather than treating a capped answer as complete.

Responses and token usage

The SDK returns ConversationResponseAlpha2 with an optional contextId and an outputs array. Each output contains choices, an optional model, and optional usage. Each choice contains index, finishReason, and an optional message. A message has string content and a toolCalls array.

Do not assume there is only one output or choice, or that every choice has text. Common finish reasons are "stop", "length", "tool_calls", and "content_filter"; handle other provider values too.

for (const output of response.outputs) {
  console.log("Model:", output.model);
  if (output.usage) {
    console.log("Total tokens:", output.usage.totalTokens);
    console.log("Prompt tokens:", output.usage.promptTokens);
    console.log("Completion tokens:", output.usage.completionTokens);
    console.log("Cached tokens:", output.usage.promptTokensDetails?.cachedTokens);
    console.log("Reasoning tokens:", output.usage.completionTokensDetails?.reasoningTokens);
  }
}

Prompt token details also include audioTokens. Completion token details include acceptedPredictionTokens, rejectedPredictionTokens, audioTokens, and reasoningTokens. These are provider-reported statistics, not evidence that the SDK accepts audio input. Usage and detail objects can be absent; some components return zero counts when the provider omits usage.

Token counts and choice indexes are JavaScript numbers. The SDK rejects values outside the safe-integer range instead of silently losing precision.

Errors and API limits

converse rejects its promise for invalid local input, sidecar errors, provider failures, or response conversion failures. Invalid message roles or content and invalid JSON objects produce TypeError; invalid numeric limits and durations can produce RangeError. Schema and parameter objects must contain only finite JSON values, without cycles, undefined, BigInt, or class instances.

Catch errors at your application’s error boundary and report or propagate them. Do not turn a failed request into a successful empty answer. Provider errors can include rate limits, invalid tool histories, unsupported options, and authentication failures; their representation can differ between HTTP and gRPC.

The exposed method returns a complete response. It does not expose streaming completions, automatic tool execution, or a per-request AbortSignal, timeout, or cancellation parameter. Configure Dapr resiliency policies for sidecar-to-provider timeouts and retries. A JavaScript Promise.race timeout alone does not cancel the underlying request.

11.3 - How to: Call tools with the Dapr Conversation JavaScript SDK

Define conversation tools, validate function arguments, and send tool results back to an LLM in JavaScript

Tool calling lets a model request an application-defined function. Dapr and the JavaScript SDK transport tool definitions and calls; your application validates arguments, authorizes and executes the function, and sends the result back.

Prerequisites

Complete the conversation setup guide using @dapr/dapr 3.19.0 or later, Dapr runtime 1.16 or later, and a component/model that supports tool calling. The echo component is not sufficient.

Define a tool and handle its result

The following example uses the llm component configured in the setup guide. Save it as tools.mjs and run it with the same dapr run command, replacing app.mjs with tools.mjs.

import { DaprClient } from "@dapr/dapr";

const client = new DaprClient({ daprHost: "127.0.0.1", daprPort: "3500" });
const messages = [
  { ofUser: { content: [{ text: "Look up the description of Dapr." }] } },
];
const tools = [
  {
    function: {
      name: "lookupProject",
      description: "Look up a project description by its exact name.",
      parameters: {
        type: "object",
        properties: { name: { type: "string", enum: ["Dapr"] } },
        required: ["name"],
        additionalProperties: false,
      },
    },
  },
];

try {
  const first = await client.conversation.converse({
    name: "llm",
    inputs: [{ messages }],
    tools,
    toolChoice: "lookupProject",
  });

  const choice = first.outputs[0]?.choices[0];
  const calls = choice?.message?.toolCalls;
  if (choice?.finishReason !== "tool_calls" || !calls?.length) {
    throw new Error("Expected the model to request a tool");
  }

  messages.push({
    ofAssistant: {
      content: [{ text: choice.message.content }],
      toolCalls: calls,
    },
  });

  const seenIds = new Set();
  for (const call of calls) {
    if (!call.id || seenIds.has(call.id)) {
      throw new Error("Missing or duplicate tool call ID");
    }
    seenIds.add(call.id);
    if (call.function.name !== "lookupProject") {
      throw new Error("Unexpected tool function");
    }

    const args = JSON.parse(call.function.arguments);
    if (
      args === null ||
      typeof args !== "object" ||
      Array.isArray(args) ||
      args.name !== "Dapr" ||
      Object.keys(args).length !== 1
    ) {
      throw new Error("Invalid lookupProject arguments");
    }

    const result = {
      name: args.name,
      description: "Dapr provides APIs for building distributed applications.",
    };
    messages.push({
      ofTool: {
        toolId: call.id,
        name: call.function.name,
        content: [{ text: JSON.stringify(result) }],
      },
    });
  }

  const final = await client.conversation.converse({
    name: "llm",
    contextId: first.contextId,
    inputs: [{ messages }],
    tools,
    toolChoice: "none",
  });

  const answer = final.outputs[0]?.choices[0];
  if (!answer?.message || answer.finishReason !== "stop") {
    throw new Error("Expected a completed answer after the tool results");
  }
  console.log(answer.message.content);
} finally {
  await client.stop();
}

Tool definitions use { function: { name, description, parameters } }. Do not add a provider-native type: "function" wrapper. parameters is a JSON Schema object; the function’s name must be nonempty.

Returned call.function.arguments is model-generated string data, not a parsed object or trusted instruction. The example allowlists one read-only function and validates its exact arguments. For real tools, also enforce authorization, input limits, and any required confirmation before side effects. Never use eval or dispatch arbitrary function names supplied by the model.

Preserve the tool-call history

For each selected assistant choice, append the assistant message containing the original tool calls, followed by a tool result for every call. Preserve each call’s id, function name, and arguments. Set the tool result’s toolId to that call’s ID, not its function name.

Tool-only assistant messages can omit content. Tool result messages require a nonempty name and a content array. An assistant can request multiple calls in one turn, so do not process only toolCalls[0]. Alternative response choices are separate conversation branches.

Resend the original messages, assistant tool calls, and tool results in order on the next request. Passing only contextId does not preserve that history.

Choose a tool policy

toolChoiceBehavior
"auto"The model can answer directly or request tools.
"none"The model must answer without calling tools.
"required"The model must request one or more tools.
An exact function name, such as "lookupProject"Request that specific function.

The chosen component and model must support the policy. A named choice must match a declared tool; requiring tools without providing definitions is invalid.

The example forces a named tool on the first request and disables tools on the second, bounding the interaction to two requests. If you use "auto" for a repeated tool loop, handle direct text responses, limit the number of turns and calls, and surface failures rather than retrying indefinitely.

12 - Dapr Jobs JavaScript SDK

Schedule, retrieve, delete, and handle Dapr Jobs using the JavaScript SDK

Use the Dapr JavaScript SDK to schedule future operations in Node.js applications. Jobs can run once, at a fixed interval, or on a calendar-based schedule, with an optional payload and failure policy.

This documentation targets @dapr/dapr 3.19.0 or later and Dapr runtime 1.16 or later, with a running Scheduler service.

The SDK exposes Jobs through the existing client and server:

APIPurpose
DaprClient.jobs.schedule(name, options)Create a job or explicitly overwrite an existing job.
DaprClient.jobs.get(name)Retrieve a job’s schedule, payload, and other settings.
DaprClient.jobs.delete(name)Delete a job by name.
DaprServer.jobs.listen(name, callback)Register an asynchronous handler for a named job.
CronExpressionBuilderBuild a six-field cron expression with optional time zone.

Both HTTP and gRPC support job management and JSON callbacks. Opaque binary payloads require gRPC for scheduling, retrieval, and callbacks. The SDK automatically selects the stable or Alpha1 Jobs API for the connected runtime.

These are the JavaScript SDK’s Jobs APIs in this package version. It does not expose job listing, bulk deletion, deletion by prefix, or per-operation cancellation options. Do not assume that all management methods in another language SDK are available in JavaScript.

To get started, follow the Jobs how-to guide. Read the client and callback usage guidance for options, serialization, compatibility, and errors, or the cron builder guide for calendar-based scheduling.

See the Jobs overview and Jobs API reference for the underlying building block.

12.1 - How to: Author and manage Dapr Jobs using the JavaScript SDK

Learn how to schedule one-time and recurring jobs and handle their callbacks using JavaScript

Let’s register an endpoint to receive a job notification, then schedule the job from the same application. This guide uses DaprServer.jobs for callbacks and its embedded DaprClient.jobs for job management. There is no separate Jobs client package to install.

Prerequisites

In your application directory, initialize an npm project if needed and install the SDK:

npm init -y
npm install @dapr/dapr@3.19.0

The examples use JavaScript ES modules in a .mjs file, so no TypeScript compiler is required. The same APIs are available in TypeScript.

Register a handler and schedule a recurring job

Save the following as app.mjs:

import { CommunicationProtocolEnum, DaprServer } from "@dapr/dapr";

const server = new DaprServer({
  serverHost: "127.0.0.1",
  serverPort: "50002",
  communicationProtocol: CommunicationProtocolEnum.GRPC,
  clientOptions: {
    daprHost: "127.0.0.1",
    daprPort: "50001",
    communicationProtocol: CommunicationProtocolEnum.GRPC,
  },
});

const jobName = "daily-report";

try {
  await server.jobs.listen(jobName, async (data) => {
    console.log(`Received ${jobName}:`, data);
  });

  await server.start();

  await server.client.jobs.schedule(jobName, {
    schedule: "@every 10s",
    repeats: 3,
    data: { report: "daily", format: "csv" },
    failurePolicy: { type: "constant", interval: "1s", maxRetries: 3 },
    overwrite: true,
  });

  const job = await server.client.jobs.get(jobName);
  console.log("Scheduled:", job.name, job.schedule, job.data);
  console.log("Waiting for callbacks. Press Ctrl+C to stop.");

  await new Promise((resolve) => {
    process.once("SIGINT", resolve);
    process.once("SIGTERM", resolve);
  });
} finally {
  try {
    await server.stop();
  } finally {
    await server.client.stop();
  }
}

Start the application and sidecar:

dapr run --app-id jobs-example --app-port 50002 --app-protocol grpc --dapr-grpc-port 50001 --dapr-http-port 3500 -- node app.mjs

The application listens on port 50002; its client sends management requests to the sidecar on port 50001. The job triggers three times at ten-second intervals. The callback receives the payload itself, not a job envelope or the job name. Capture the name in the handler’s closure if needed.

Register handlers before calling server.start(). Await all work in the handler; throw an error or reject its promise when processing fails so Dapr can apply the job’s failure policy. Delivery is at least once, so make handlers idempotent.

The example deliberately uses overwrite: true so it can be rerun while a job with the same name still exists. Without this option, scheduling an existing name fails. Overwriting replaces the existing job, including its timing and repeat settings; do not blindly reset a production schedule whenever a replica starts.

Jobs belong to the scheduling sidecar’s application ID and namespace. Callbacks go to that application, not to an arbitrary target application. Register the handler on every replica that can receive a callback. A handler can use other Dapr APIs to invoke another service or publish an event.

Stopping the Node.js server does not delete jobs stored in the Scheduler. The example also stops the embedded client explicitly because server.stop() only stops the callback server.

Use HTTP instead

For JSON payloads, you can use HTTP for both management requests and callbacks. Change the server’s communicationProtocol to CommunicationProtocolEnum.HTTP, and change its clientOptions to:

clientOptions: {
  daprHost: "127.0.0.1",
  daprPort: "3500",
  communicationProtocol: CommunicationProtocolEnum.HTTP,
},

Run it with the matching app protocol:

dapr run --app-id jobs-example --app-port 50002 --app-protocol http --dapr-grpc-port 50001 --dapr-http-port 3500 -- node app.mjs

The SDK registers the HTTP callback route POST /job/:name; you do not need to implement this route yourself. Client and callback protocols can also differ. See the protocol guidance.

Schedule a one-time job

For a one-time job, supply dueTime without schedule:

await server.client.jobs.schedule("send-notification", {
  dueTime: "30s",
  data: { recipientId: "customer-123" },
});

To use an absolute time, pass an RFC3339 timestamp:

const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000);

await server.client.jobs.schedule("send-notification-tomorrow", {
  dueTime: tomorrow.toISOString(),
  data: { recipientId: "customer-123" },
});

These snippets reuse the running server from the example. To receive these additional jobs, register a matching server.jobs.listen(...) handler for each name before starting the server. The remaining scheduling examples follow the same pattern.

Limit a recurring job’s lifetime

Combine schedule with dueTime to delay when a recurring job becomes active. Use repeats to limit the number of scheduled triggers, ttl to set expiration, or both:

await server.client.jobs.schedule("hourly-cleanup", {
  schedule: "@every 1h",
  dueTime: "10m",
  repeats: 5,
  ttl: "6h",
});

Omitting repeats leaves a recurring job running until it expires or is deleted. Relative dueTime and ttl values are calculated from job creation, not from the previous callback. See the option reference for accepted time formats.

For calendar-based scheduling, use a six-field cron string or a CronExpressionBuilder:

await server.client.jobs.schedule("morning-report", {
  schedule: "CRON_TZ=America/New_York 0 30 9 * * 1-5",
  data: { report: "daily" },
});

Retrieve or delete a job

Retrieve the stored configuration by name:

const job = await server.client.jobs.get("hourly-cleanup");
console.log(job.name, job.schedule, job.dueTime, job.ttl, job.repeats);
console.log(job.data, job.failurePolicy);

To stop future triggers, delete the job:

await server.client.jobs.delete("hourly-cleanup");

Deletion does not cancel callback work that is already running. It also does not unregister the application’s in-memory handler. To change a stored job instead, call schedule with its name, the replacement options, and overwrite: true.

Next steps

Read the client and callback usage guidance for payloads, retries, validation, and runtime compatibility, or the cron builder guide for fluent scheduling.

12.2 - Build cron schedules using the JavaScript SDK

Use raw cron expressions, descriptors, time zones, and CronExpressionBuilder to schedule Dapr Jobs

Use a cron schedule for calendar-based jobs, such as a report at 09:30 every weekday. The JavaScript SDK accepts a raw schedule string or a CronExpressionBuilder directly in JobOptions.schedule. These examples target @dapr/dapr 3.19.0 or later.

Provide a schedule string

Dapr cron expressions use six fields, including seconds, not the five fields used by many Unix cron tools:

second minute hour dayOfMonth month dayOfWeek

For example, "0 30 9 * * 1-5" runs at 09:30 on weekdays. Sunday is 0 and Saturday is 6.

await client.jobs.schedule("weekday-report", {
  schedule: "0 30 9 * * 1-5",
  data: { report: "daily" },
});

The snippet assumes a configured DaprClient. Register a matching handler in the receiving application as shown in the how-to guide.

You can also pass runtime-supported descriptors directly:

DescriptorSchedule
@every 5mA fixed five-minute interval.
@hourlyThe beginning of every hour.
@daily or @midnightMidnight each day.
@weeklyMidnight on Sunday.
@monthlyMidnight on the first day of each month.
@yearly or @annuallyMidnight on January 1.

Use dueTime without schedule for a one-time job. The runtime validates raw schedule strings; the SDK does not parse them locally.

Use CronExpressionBuilder

The fluent builder follows the .NET SDK’s on, through, each, and every model, but uses string period names instead of .NET enums:

import { CronExpressionBuilder } from "@dapr/dapr";

const schedule = new CronExpressionBuilder()
  .on("second", 0)
  .on("minute", 30)
  .on("hour", 9)
  .through("dayOfWeek", 1, 5)
  .withTimeZone("America/New_York");

console.log(schedule.toString());
// CRON_TZ=America/New_York 0 30 9 * * 1-5

await client.jobs.schedule("weekday-report", {
  schedule,
  data: { report: "daily" },
});

All fields initially contain *. An untouched builder produces "* * * * * *", which matches every second, not once per minute. Set the seconds field explicitly when you want minute-level or less frequent execution.

Periods and bounds

The exported TypeScript CronPeriod type accepts these names:

PeriodInteger values
second0-59
minute0-59
hour0-23
dayOfMonth1-31
month1-12
dayOfWeek0-6, starting with Sunday

Builder operations

MethodResult
on(period, ...values)Select values, such as on("hour", 9, 17). Values are sorted and deduplicated. At least one is required.
through(period, from, to)Select an inclusive range, such as through("dayOfWeek", 1, 5). from must be less than to.
each(period)Reset a field to *.
every(period, interval)Set a step expression, such as every("minute", 15) producing */15.
withTimeZone(timeZone)Prefix the expression with CRON_TZ= using a validated IANA time zone.
toString()Return the six-field expression as a string, including its time zone if set.

Each fluent method mutates and returns the same builder. A later call for a period replaces that field; it does not append another constraint. For example, on("hour", 9).on("hour", 17) selects only 17, while on("hour", 9, 17) selects both.

Values and range endpoints must be integers within the period’s bounds. A step must be a positive integer no larger than the number of values in that field: up to 60 for seconds or minutes, 24 for hours, 31 for days of the month, 12 for months, and 7 for days of the week. Invalid fields or values throw RangeError.

Steps are cron field steps, not elapsed-time intervals. For example, every("hour", 2) selects every second hour of the day; it does not mean two hours after registration. Use "@every 2h" for an elapsed interval.

toString() can be used independently of the Jobs client. Passing the builder to schedule takes its current string value; mutating the builder afterward does not update an already stored job. Reschedule with overwrite: true to change the stored schedule.

Specify a time zone

By default, Dapr evaluates a cron schedule in the Scheduler service’s local time zone, not necessarily the Node.js process’s time zone. Use an explicit IANA zone for a predictable wall-clock schedule:

const schedule = new CronExpressionBuilder()
  .on("second", 0)
  .on("minute", 0)
  .on("hour", 9)
  .withTimeZone("Europe/Rome");

The builder validates the time zone using Node.js Intl.DateTimeFormat. Empty, whitespace-containing, or unrecognized names are rejected. A subsequent withTimeZone call replaces the previous zone.

You can also pass the runtime’s time zone prefix in a raw string, for example "CRON_TZ=Europe/Rome 0 0 9 * * *" or "CRON_TZ=Europe/Rome @daily". Use "UTC" when daylight saving changes are not desired. With a regional zone, Dapr evaluates the local wall-clock schedule across daylight saving transitions.

Do not combine a time zone prefix with @every: fixed-duration intervals do not have a wall-clock time zone and the runtime rejects that combination. See the Jobs API reference for runtime schedule syntax.

12.3 - JavaScript Jobs client and callback usage

Configure the JavaScript Jobs client and understand scheduling options, payloads, failure policies, and errors

Jobs is part of DaprClient and DaprServer in @dapr/dapr 3.19.0 or later. Use client.jobs to manage scheduled jobs and server.jobs to register callbacks. The how-to guide contains a complete runnable application.

Client configuration and lifetime

Create a long-lived DaprClient rather than a new client for every operation. Jobs uses the client’s existing sidecar connection and API token:

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "50001",
  communicationProtocol: CommunicationProtocolEnum.GRPC,
  daprApiToken: process.env.DAPR_API_TOKEN,
});

try {
  await client.start();
  await client.jobs.schedule("cleanup", { dueTime: "1h" });
  const job = await client.jobs.get("cleanup");
  console.log(job);
  await client.jobs.delete("cleanup");
} finally {
  await client.stop();
}

For HTTP, use CommunicationProtocolEnum.HTTP and the sidecar’s HTTP port, normally "3500". Client-only management does not require an app listener, but the target application must expose its callback server when the job fires.

When using DaprServer, configure its embedded client through clientOptions and reuse server.client.jobs. server.start() starts both the callback server and the embedded client. server.stop() stops only the server; also call server.client.stop() during application shutdown. Neither operation deletes stored jobs.

Client protocol and callback protocol

The two connections are configured independently:

ConnectionSDK settingDapr CLI setting
Application to sidecarDaprClient.communicationProtocol, or DaprServer.clientOptions.communicationProtocol, with the corresponding daprPort--dapr-http-port or --dapr-grpc-port
Sidecar to applicationDaprServer.communicationProtocol and serverPortMatching --app-protocol and --app-port

JSON payloads support any combination of HTTP and gRPC. For arbitrary bytes, use gRPC on both connections. Selecting gRPC for the client alone does not change the sidecar’s callback protocol.

Runtime compatibility and API selection

Jobs requires Dapr runtime 1.16 or later and a running Scheduler service. Before making a Jobs request, the SDK reads the sidecar’s metadata to select the API:

Runtime versionHTTP API prefixgRPC management methods
1.16 and 1.17v1.0-alpha1ScheduleJobAlpha1, GetJobAlpha1, DeleteJobAlpha1
1.18 prereleasesv1.0-alpha1Alpha1 methods
1.18 stable and later releasesv1.0ScheduleJob, GetJob, DeleteJob

Prereleases after 1.18 and the edge runtime identifier select the stable API. Both stable and Alpha1 gRPC callback services are registered by DaprServer.

The SDK caches successful API selection per client. Metadata lookup failures, missing or unrecognized versions, and unsupported runtime versions reject the operation rather than guessing an API. A subsequent call can retry a failed metadata lookup. A Jobs request that fails is not automatically replayed against a different API version.

The SDK package version and runtime version are separate requirements: using SDK 3.19.0 does not require runtime 1.19 merely to use Jobs.

Scheduling options

schedule(name, options) returns Promise<void>. Supply at least one of schedule or dueTime; both may be supplied together.

OptionTypeBehavior
schedulestring or CronExpressionBuilderA six-field cron expression, a descriptor such as @daily, or an interval such as @every 5m. The builder is converted to a string.
dueTimestringWhen the job becomes active, or the trigger time for a one-time job without schedule.
ttlstringJob expiration. Relative values are calculated from job creation.
repeatsnumberLimits scheduled triggers. Omit for unlimited recurrence; zero is passed to the runtime, not converted to an omitted value.
dataJSON value or Uint8ArrayOptional callback payload. Node.js Buffer is also accepted by gRPC.
overwritebooleanDefaults to false. Set to true to replace an existing job with the same name.
failurePolicyJobFailurePolicyControls retries when callback processing fails. Omit to use the runtime default.

dueTime and ttl accept RFC3339 timestamps, Go duration strings such as "30s" or "1h30m", and non-repeating ISO8601 durations such as "PT30M". Pass strings, not JavaScript Date objects. For an absolute time, use date.toISOString() or an RFC3339 timestamp with an explicit offset.

The runtime validates raw cron expressions, time strings, and scheduling combinations. Jobs is a durable scheduler, not a precision timer: a trigger will not arrive before its due time, but may arrive later.

Job names and input validation

Names are case-sensitive. Pass a plain name, not a URL-encoded value; the HTTP client encodes the path segment.

The SDK rejects empty names, surrounding whitespace, . and .., control characters, and the characters /, \, ?, #, %, and |. Internal spaces are supported. These SDK restrictions also apply with a newer runtime that allows additional characters, such as |.

The SDK also validates:

  • schedule, dueTime, and ttl, when present, must be nonempty strings after converting a builder.
  • repeats and failurePolicy.maxRetries must be integers between 0 and 4294967295.
  • overwrite, when present, must be a boolean.
  • Payloads and failure policies must have supported values.

Invalid options reject the asynchronous operation before a Jobs request is sent. The cron builder performs additional local validation of its fields.

Retrieving and deleting jobs

get(name) returns Promise<Job> containing name and the optional schedule, dueTime, ttl, repeats, data, and failurePolicy properties. The SDK exports the related types for TypeScript callers:

import type { Job, JobFailurePolicy, JobOptions } from "@dapr/dapr";

const failurePolicy: JobFailurePolicy = {
  type: "constant",
  interval: "1s",
  maxRetries: 3,
};

const options: JobOptions = { dueTime: "1h", failurePolicy };
await client.jobs.schedule("cleanup", options);

const job: Job = await client.jobs.get("cleanup");
console.log(job.name, job.failurePolicy);

JobOptions describes scheduling input, and Job describes retrieved configuration. overwrite is a scheduling option, not a returned job property. Returned timing strings use the runtime’s representation, which may differ from the input, and a builder’s schedule is returned as a string.

Retrieval returns configuration, not execution history, a next-run calculation, or a count of completed callbacks. A missing job causes get to reject, rather than returning undefined. delete(name) returns Promise<void>; deleting an already absent job is accepted by supported runtimes, while transport and service errors still propagate.

Payload serialization

JSON payloads

Both client transports and both callback protocols support JSON values: plain objects, arrays, strings, finite numbers, booleans, and null. Pass the value directly rather than manually JSON-stringifying it:

await client.jobs.schedule("create-report", {
  dueTime: "30s",
  data: { reportId: "report-123", includeArchived: false },
});

get(name).data and the callback argument expose the decoded value. Values such as false, 0, "", and null are preserved rather than treated as missing payloads.

The SDK rejects non-JSON data such as Date instances, BigInt, non-finite numbers, nested undefined values, and circular references. Convert such values to a JSON-compatible representation first.

Jobs uses its own payload encoding, not the client’s custom serializer. JSON payloads are packed as google.protobuf.Value for gRPC; the decoder also understands google.protobuf.Struct payloads from other clients. You do not need to construct protobuf envelopes yourself.

Omitting data is not identical across transports. HTTP scheduling sends JSON null for compatibility with older runtimes; gRPC leaves the payload absent. Do not rely on distinguishing an omitted payload from null across protocols.

Opaque binary payloads

Use gRPC to schedule and retrieve raw Uint8Array or Node.js Buffer payloads, including empty buffers:

const data = Buffer.from([0x00, 0x80, 0xff]);

await client.jobs.schedule("process-bytes", {
  dueTime: "1h",
  data,
});

const job = await client.jobs.get("process-bytes");
console.log(Buffer.isBuffer(job.data)); // true

For this example, configure client with gRPC. Use a gRPC DaprServer and --app-protocol grpc to receive the same bytes in a callback. Dapr’s HTTP app channel treats job callback bodies as JSON and is not suitable for arbitrary bytes.

HTTP scheduling rejects byte payloads rather than converting them to JSON. Opaque gRPC payloads are returned as Node.js buffers. The SDK uses the dapr.io/schedule/jobpayload type URL also used by the .NET Jobs SDK; it does not JSON-serialize or text-encode these bytes. For cross-language interoperability, agree on whether the payload is protobuf-wrapped JSON or raw bytes and decode accordingly.

Failure policies

The SDK accepts the following policies:

PolicyBehavior
OmittedLeave Dapr’s default policy in control. The runtime default retries up to three times with a one-second delay.
{ type: "drop" }Do not retry a failed delivery.
{ type: "constant", interval: "1s", maxRetries: 3 }Retry with a constant delay, up to the specified number of retries after the initial attempt.

For a constant policy, omitting maxRetries allows unlimited retries. Setting maxRetries: 0 disables retries. Omitting interval, or setting it to "0s", requests immediate retries.

Unlike dueTime and ttl, the retry interval uses protobuf duration JSON syntax: seconds with an s suffix and up to nine fractional digits. Use "0.2s" for 200 milliseconds, "60s" for a minute, or "0.000000001s" for a nanosecond. Go-style strings such as "200ms" and "1m" are rejected here, as are negative durations.

await client.jobs.schedule("retryable-task", {
  dueTime: "30s",
  failurePolicy: {
    type: "constant",
    interval: "0.5s",
    maxRetries: 2,
  },
});

This policy permits the initial attempt plus two retries. repeats controls scheduled triggers; it is not a retry limit.

Callback behavior

server.jobs.listen(name, callback) returns Promise<void> and registers an asynchronous (data) => Promise<void> handler. Registration does not schedule a job. Register before starting the server, using exactly the same name as the scheduled job.

The SDK waits for the handler’s promise before acknowledging delivery. A resolved handler means success; its return value is not a response payload or retry instruction. Throw or reject to signal failure. Do not start unawaited background work and return early, or catch processing errors without propagating them if you want Dapr to retry.

Duplicate registration for a name rejects instead of replacing the existing handler. The SDK does not provide a handler removal API.

SituationHTTP callback responsegRPC callback response
Handler completes200Successful JobEventResponse
No matching handler404NotFound
Handler throws or rejects500Internal

Missing handlers and failed handlers are logged and reported as errors, not acknowledged as successful deliveries. Design handlers for at-least-once delivery and use application-level deduplication when processing has non-idempotent side effects.

Error handling

All Jobs methods return promises. Await them so validation errors, metadata failures, sidecar errors, and Scheduler errors reach your application’s error handling.

Input validation can produce TypeError or RangeError; malformed runtime responses also reject. Remote failures propagate through the selected transport. HTTP failures include status and response details in the error message; gRPC failures use the gRPC transport’s error information. There is no Jobs-specific exception class shared across transports.

Do not assume an unsuccessful scheduling request proves that nothing was stored: a response can be lost after the server accepts a request. Reconcile by name before retrying, and choose overwrite deliberately. Failure policies govern callback delivery, not retries of client management requests.

These Jobs methods do not accept an AbortSignal, timeout argument, or cancellation token. Rejecting a local wait does not cancel a job; use delete(name) to remove its future schedule.

13 - Dapr Cryptography JavaScript SDK

Encrypt and decrypt data using the Dapr JavaScript SDK

The Dapr JavaScript SDK lets Node.js applications encrypt and decrypt messages and files through a configured cryptography component. Use DaprClient.crypto with either HTTP or gRPC, passing data directly or working with Node.js streams.

These guides target @dapr/dapr version 3.19.0 or later. The underlying Cryptography API is alpha. Prefer gRPC for large payloads and production streaming: Dapr buffers HTTP cryptography output until the request is fully consumed.

To get started, follow the Cryptography how-to guide. See the client usage guidance for configuration, supported inputs, encryption options, stream handling, and limitations.

The client implements encryption and decryption using the Dapr Crypto Scheme v1. It does not expose general-purpose signing, signature verification, hashing, or key-management methods. Provision keys through your component’s backing store; the application passes key references, not key material, to client.crypto.

13.1 - How to: Encrypt and decrypt data with the Dapr JavaScript SDK

Configure a cryptography component and encrypt and decrypt messages and files using the Dapr JavaScript SDK

Use DaprClient.crypto to encrypt and decrypt data without passing cryptographic keys to your application. This guide uses a local key for development, then shows how to work with buffers and files.

Prerequisites

In your application directory, initialize an npm project if you do not already have one, then install the SDK:

npm init -y
npm install @dapr/dapr@3.19.0

The examples use JavaScript ES modules and run as .mjs files without a TypeScript compiler. The same APIs are available in TypeScript.

Configure a local cryptography component

Create a keys directory and generate a random 256-bit symmetric key:

node -e "const fs = require('node:fs'); fs.mkdirSync('keys', { recursive: true }); fs.writeFileSync(require('node:path').join('keys', 'symmetric256'), require('node:crypto').randomBytes(32));"

Run this command only for initial setup. Rerunning it replaces the key, preventing you from decrypting data encrypted with the previous key unless you retained a copy.

Create a separate components directory and save the following as components\crypto.yaml:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: crypto-local
spec:
  type: crypto.dapr.localstorage
  version: v1
  metadata:
    - name: path
      value: '.\keys'

The path is resolved from the sidecar’s working directory. Run the commands below from your application directory; for other deployments, set the path to a directory accessible to the sidecar.

The component name, crypto-local, is the componentName used by the client. The key name, symmetric256, is the file name in the key directory. This example pairs a 256-bit symmetric key with A256KW key wrapping.

Encrypt and decrypt a message

Save the following as app.mjs:

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "3500",
  communicationProtocol: CommunicationProtocolEnum.HTTP,
});

try {
  const plaintext = Buffer.from("A message to encrypt", "utf8");
  const ciphertext = await client.crypto.encrypt(plaintext, {
    componentName: "crypto-local",
    keyName: "symmetric256",
    keyWrapAlgorithm: "A256KW",
  });

  const decrypted = await client.crypto.decrypt(ciphertext, {
    componentName: "crypto-local",
  });

  if (!decrypted.equals(plaintext)) {
    throw new Error("Decrypted bytes do not match the original message");
  }
  console.log("Message round-trip succeeded");
} finally {
  await client.stop();
}

Start the application with a sidecar that loads the component:

dapr run --app-id crypto-example --dapr-http-port 3500 --dapr-grpc-port 50001 --resources-path .\components -- node app.mjs

Passing data and options returns a Promise<Buffer> for both encryption and decryption. Encryption also accepts a string, encoded as UTF-8. Decryption uses the key reference embedded in the encrypted document by default, so this example does not need to supply keyName again.

To run the same example over gRPC, change communicationProtocol to CommunicationProtocolEnum.GRPC and daprPort to "50001".

Encrypt and decrypt files with streams

For files, pass only the options object to obtain a Node.js Duplex. Write plaintext into an encryption stream and read ciphertext from it; a decryption stream accepts ciphertext and emits plaintext.

Create a nonempty plaintext.txt in your application directory and save this example as files.mjs. It uses gRPC, the recommended protocol for large payloads and production streaming:

import { createReadStream, createWriteStream } from "node:fs";
import { pipeline } from "node:stream/promises";
import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "50001",
  communicationProtocol: CommunicationProtocolEnum.GRPC,
});

try {
  await pipeline(
    createReadStream("plaintext.txt"),
    await client.crypto.encrypt({
      componentName: "crypto-local",
      keyName: "symmetric256",
      keyWrapAlgorithm: "A256KW",
    }),
    createWriteStream("ciphertext.out"),
  );

  await pipeline(
    createReadStream("ciphertext.out"),
    await client.crypto.decrypt({ componentName: "crypto-local" }),
    createWriteStream("plaintext.out"),
  );

  console.log("Encrypted ciphertext.out and decrypted plaintext.out");
} finally {
  await client.stop();
}

Run it with the same component and ports:

dapr run --app-id crypto-files-example --dapr-http-port 3500 --dapr-grpc-port 50001 --resources-path .\components -- node files.mjs

Await each pipeline so failures propagate and encryption finishes before decryption starts. Use new streams for each operation. Do not combine encryption and decryption in one pipeline on runtimes affected by the concurrent cryptography limitation.

The stream overloads also work over HTTP, but the runtime buffers HTTP output until the entire input has arrived. A stream-shaped API does not remove that runtime memory cost.

Next steps

Read the client usage guidance for choosing ciphers, controlling embedded key references, preserving binary ciphertext, and handling errors. See the cryptography quickstart for additional examples.

13.2 - Cryptography client usage

Configure and use JavaScript cryptography operations, encryption options, and streams

Cryptography is available through DaprClient.crypto. This page describes @dapr/dapr version 3.19.0 or later, which supports both HTTP and gRPC. Both transports use the alpha Cryptography API.

Client lifetime and configuration

Reuse a long-lived DaprClient rather than creating one for each operation. Await outstanding operations and stream pipelines before calling await client.stop() during application shutdown.

HTTP is the default protocol and uses sidecar port "3500" by default. For gRPC, configure the protocol and its port explicitly:

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "50001",
  communicationProtocol: CommunicationProtocolEnum.GRPC,
  daprApiToken: process.env.DAPR_API_TOKEN,
});

Use CommunicationProtocolEnum.HTTP and the sidecar’s HTTP port for HTTP. Port options are strings. If you omit explicit host and port options, the SDK can resolve the selected protocol’s endpoint from DAPR_HTTP_ENDPOINT or DAPR_GRPC_ENDPOINT. The API token defaults to DAPR_API_TOKEN when it is not supplied explicitly. See the JavaScript client configuration for more information.

Choose an overload

The argument count selects whether an operation returns a stream or buffers its output:

MethodInputResult
encrypt(data, options)Buffer, ArrayBuffer, ArrayBufferView, or stringPromise<Buffer> containing ciphertext
decrypt(data, options)Buffer, ArrayBuffer, or ArrayBufferViewPromise<Buffer> containing plaintext
encrypt(options)Write plaintext to the returned streamPromise<Duplex> emitting ciphertext
decrypt(options)Write ciphertext to the returned streamPromise<Duplex> emitting plaintext

An ArrayBufferView includes typed arrays such as Uint8Array and DataView. The SDK uses the view’s byte offset and byte length, not the entire backing buffer.

Encryption converts strings to UTF-8. Decryption expects binary input, not a string. To decrypt ciphertext stored as base64, decode it first:

const decrypted = await client.crypto.decrypt(Buffer.from(savedBase64, "base64"), {
  componentName: "crypto-local",
});

This snippet assumes an existing client and a savedBase64 string previously produced with ciphertext.toString("base64"). Keep ciphertext as bytes unless you deliberately encode it; converting arbitrary ciphertext with .toString("utf8") corrupts it. Convert decrypted bytes to UTF-8 only if the original plaintext was UTF-8 text.

Encryption options

Pass the same encryption options with either overload:

OptionRequiredBehavior
componentNameYesName of the configured Dapr cryptography component, not its component type.
keyNameYesKey reference in the component’s backing store, optionally including a version where the provider supports it.
keyWrapAlgorithmYesAlgorithm used to wrap the data-encryption key. Must be compatible with the referenced key and component.
dataEncryptionCipherNo"aes-gcm" (default) or "chacha20-poly1305" for encrypting the payload.
omitDecryptionKeyNameNoDefaults to false. Set to true to omit the key reference from the encrypted document; decryption must then supply keyName.
decryptionKeyNameNoAlternate key reference to embed in the document. Defaults to the encryption key reference and is ignored when omitDecryptionKeyName is true.

The SDK’s keyWrapAlgorithm type accepts "A256KW", "A128CBC", "A192CBC", "A256CBC", "RSA-OAEP-256", "AES", and "RSA". This list describes the SDK type, not a guarantee that every component, key, or runtime accepts every value. Choose a value supported by your provider; for example, the setup guide uses A256KW with a 256-bit symmetric key. The wrapping algorithm and payload cipher are separate settings.

TypeScript applications can import the request types from the package’s type module:

import type { DecryptRequest, EncryptRequest } from "@dapr/dapr/types/crypto/Requests";

const encryptOptions: EncryptRequest = {
  componentName: "crypto-local",
  keyName: "symmetric256",
  keyWrapAlgorithm: "A256KW",
  dataEncryptionCipher: "chacha20-poly1305",
};

const decryptOptions: DecryptRequest = {
  componentName: "crypto-local",
};

These request types are not exported from the package root in 3.19.0.

Decryption and key references

Decryption takes a required componentName and an optional keyName. The runtime reads the cipher and wrapped key information from the encrypted document; do not pass encryption options such as keyWrapAlgorithm to decrypt.

By default, the document contains the encryption key reference. An explicit decryption keyName overrides that embedded reference. The component must have access to the corresponding decryption key; overriding the name does not make an unrelated key valid.

To keep the key reference out of the document:

const ciphertext = await client.crypto.encrypt("A message to encrypt", {
  componentName: "crypto-local",
  keyName: "symmetric256",
  keyWrapAlgorithm: "A256KW",
  omitDecryptionKeyName: true,
});

const plaintext = await client.crypto.decrypt(ciphertext, {
  componentName: "crypto-local",
  keyName: "symmetric256",
});

Run this snippet with an existing client inside an async function or an ES module. Without an explicit decryption keyName, this document cannot be decrypted.

Use decryptionKeyName instead when the recipient knows the same key under a different reference, such as an alias or a key in another configured component:

const ciphertext = await client.crypto.encrypt("A message for a recipient", {
  componentName: "sender-crypto",
  keyName: "sender-key",
  keyWrapAlgorithm: "RSA",
  decryptionKeyName: "recipient-key",
});

const plaintext = await client.crypto.decrypt(ciphertext, {
  componentName: "recipient-crypto",
});

This example requires separately configured components and a matching RSA key pair: the sender must be able to wrap with the public key, and the recipient must be able to unwrap with the corresponding private key. decryptionKeyName only changes the embedded reference; it does not create, copy, or rotate keys.

Streams and memory use

Calling encrypt(options) or decrypt(options) resolves to a Duplex, not the completed operation. Its writable side receives input; its readable side emits output. Consume the output while supplying input and end the writable side when input is complete. Prefer await pipeline(source, cryptoStream, destination) from node:stream/promises to propagate errors and close participating streams.

The file example demonstrates this pattern. Stream in binary mode when handling ciphertext. Do not set a text encoding on a ciphertext source or treat encrypted output as text.

The data overloads accumulate the complete result in memory, even when gRPC streams internally. Use them only when input and output fit comfortably in memory.

  • HTTP: The SDK supports both overloads and sends raw bytes, but Dapr buffers cryptography output until it has consumed the request. Do not wait for output before finishing input. Prefer gRPC for large payloads and production streaming.
  • gRPC: Uses bidirectional streaming and splits writes into 2 KiB request chunks. The crypto API has no configurable chunkSize. The current stream implementation can queue input and output internally; do not assume that using streams guarantees bounded memory at arbitrary producer or consumer speeds.
  • Empty input: HTTP supports empty plaintext with either overload. In the 3.19.0 gRPC implementation, options are sent with the first nonempty input chunk, so an empty input does not send an initial options frame. Use HTTP for empty plaintext rather than relying on gRPC empty-input behavior.

Ciphertext produced over HTTP can be decrypted over gRPC and vice versa, provided the component can access the correct key. The document uses Dapr’s encryption format, not a bare AES or RSA ciphertext.

Cancellation and errors

EncryptRequest and DecryptRequest have no AbortSignal, timeout, or cancellation-token option. Do not assume cancellation support from other building blocks applies to cryptography. Destroying an HTTP crypto Duplex aborts its underlying request; the gRPC crypto overloads do not expose an equivalent request-cancellation option. A timeout implemented by racing promises alone does not cancel the operation.

Await buffer operations and handle their rejected promises. For streams, errors can occur after the promise has returned the Duplex, so awaiting client.crypto.encrypt(options) alone is not sufficient. Await the pipeline and handle its rejection.

Missing components, missing or incompatible keys, unsupported algorithms, malformed ciphertext, and authentication failures during decryption cause operations to fail rather than returning an empty successful result. HTTP runtime errors are Error instances whose message contains JSON with status, error, and error_msg; network and stream errors can have different shapes. gRPC errors come from the Connect transport and should not be interpreted as HTTP status codes.

A failed or cancelled file pipeline may leave a partial output file. Remove or otherwise handle that file, and do not treat decrypted output as complete until the whole operation succeeds.

Concurrent operations

Runtimes affected by dapr/kit#170 can fail when decryption overlaps other cryptography operations. On affected sidecars, serialize decryption with all other cryptography work, including encryption, across callers sharing that sidecar. Serializing only within one function or one client does not protect against other callers.

In particular, finish encryption before starting decryption rather than chaining them in one pipeline or using Promise.all for round-trips. Check the issue and your runtime version before enabling overlapping operations.

14 - Dapr Binary Store JavaScript SDK

Store and retrieve binary files using the Dapr JavaScript SDK

The Dapr JavaScript SDK lets Node.js applications store, retrieve, stream, and delete binary files in a configured Dapr binary store component. Use DaprClient.binaryStore with either HTTP or gRPC to stream large files or work with bytes already held in memory.

Binary Store requires Dapr runtime 1.19 or later and JavaScript SDK 3.19.0 or later. The underlying Binary Store API is alpha.

To get started, follow the Binary Store how-to guide. See the client usage guidance for configuration, memory use, cancellation, and errors.

14.1 - How to: Manage binary files with the Dapr JavaScript SDK

Learn how to store, retrieve, stream, and delete binary files using the Dapr JavaScript SDK

Use DaprClient.binaryStore to store and retrieve files through a Dapr binary store component. The client supports binary streams so you can transfer large files without loading the entire file into your application’s memory. This guide covers component configuration, uploads, downloads, overwrites, and deletes.

Prerequisites

In your application directory, initialize an npm project if you do not already have one, then install the SDK:

npm init -y
npm install @dapr/dapr@3.19.0

The examples use JavaScript ES modules and run as a .mjs file without a TypeScript compiler. The same APIs are available in TypeScript.

Configure an in-memory binary store

Create a components directory in your application directory and save the following as components\filestore.yaml:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: filestore
spec:
  type: binarystore.in-memory
  version: v1
  metadata:
    - name: maxSize
      value: "4Mi"
    - name: maxTotalSize
      value: "512Mi"

This component stores files in the sidecar’s memory, with a per-file limit of 4 MiB and a combined capacity of 512 MiB. Files are lost when the sidecar stops and are not shared across sidecars. Use it for local development and testing, not durable storage. See the in-memory component reference for capacity settings.

The component’s metadata.name, filestore, is the store name passed to every client operation. It is not the component’s type or the YAML file name.

Upload and download a file

Place a report.pdf file no larger than 4 MiB in your application’s working directory. Save the following as app.mjs:

import { createReadStream, createWriteStream } from "node:fs";
import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "3500",
  communicationProtocol: CommunicationProtocolEnum.HTTP,
});

const storeName = "filestore";
const fileName = "uploads/report.pdf";

try {
  await client.binaryStore.setBinaryFile(
    storeName,
    fileName,
    createReadStream("report.pdf"),
  );

  await client.binaryStore.getBinaryFile(
    storeName,
    fileName,
    createWriteStream("downloaded-report.pdf"),
  );

  console.log("Downloaded report.pdf to downloaded-report.pdf");
} finally {
  await client.stop();
}

Start the application with a Dapr sidecar, loading the component from components:

dapr run --app-id binary-store-example --dapr-http-port 3500 --dapr-grpc-port 50001 --resources-path .\components -- node app.mjs

The example uploads report.pdf under the store-relative name uploads/report.pdf and downloads it to downloaded-report.pdf. getBinaryFile waits for the destination stream to finish and ends it on success.

To use gRPC instead, change communicationProtocol to CommunicationProtocolEnum.GRPC and daprPort to "50001". All binary store methods work with either protocol.

The examples below reuse client, storeName, and fileName. Add them inside the try block, after the download and before the client is stopped. Run the delete example last. If you rerun the application against a sidecar that still holds the file, delete it first or explicitly enable overwriting.

Replace an existing file

Uploads do not overwrite existing files by default. To replace a file, pass overwrite: true in the options object:

await client.binaryStore.setBinaryFile(
  storeName,
  fileName,
  createReadStream("report.pdf"),
  { overwrite: true, chunkSize: 64 * 1024 },
);

Uploads use a maximum chunk size of 32 KiB by default. Set chunkSize to another positive safe integer, in bytes, to tune upload chunking. This does not change the component’s per-file or total-capacity limits.

File names can include nested paths such as uploads/report.pdf. Do not use a leading or trailing slash, empty path segments, . or .. segments, or the characters \, *, ?, [, ], and #. Pass the name as plain text, not a URL or a pre-encoded value; the HTTP client URL-encodes it.

Work with in-memory content

For small files already held in memory, upload a Uint8Array or Node.js Buffer. Use getBinaryFileBytes to retrieve the complete file as a Buffer:

const content = Buffer.from([0x00, 0x80, 0xff]);
await client.binaryStore.setBinaryFile(storeName, "data.bin", content);

const downloaded = await client.binaryStore.getBinaryFileBytes(
  storeName,
  "data.bin",
);
console.log("Bytes match:", downloaded.equals(content));

The SDK transfers raw bytes without JSON serialization or text encoding. Empty files are supported; for example, upload Buffer.alloc(0) to create an empty file.

getBinaryFileBytes buffers the entire download in memory. Prefer getBinaryFile or getBinaryFileChunks for large files.

Read a file in chunks

getBinaryFileChunks returns an AsyncIterable<Buffer>. Consume it with for await...of to process the bytes in order without buffering the entire file:

let totalBytes = 0;
for await (const chunk of client.binaryStore.getBinaryFileChunks(
  storeName,
  fileName,
)) {
  totalBytes += chunk.length;
}
console.log("Downloaded bytes:", totalBytes);

Chunk boundaries can differ between protocols and do not necessarily match the upload chunks. Breaking out of the loop closes the download stream.

Delete a file

await client.binaryStore.deleteBinaryFile(storeName, fileName);
await client.binaryStore.deleteBinaryFile(storeName, "data.bin");

A missing file causes downloads and deletes to fail rather than returning empty content or silently succeeding.

Next steps

Read the client usage guidance for cancellation, stream ownership, client configuration, and error handling.

14.2 - Binary Store client usage

Essential guidance for configuring and using the JavaScript Binary Store client

Binary Store is available through DaprClient.binaryStore in JavaScript SDK 3.19.0 or later and requires Dapr runtime 1.19 or later. Both HTTP and gRPC use the alpha Binary Store API.

Client lifetime and configuration

Reuse a long-lived DaprClient rather than creating a client for each operation. Await outstanding operations before calling await client.stop() during application shutdown.

The default protocol is HTTP. To explicitly configure a gRPC connection:

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "50001",
  communicationProtocol: CommunicationProtocolEnum.GRPC,
  daprApiToken: process.env.DAPR_API_TOKEN,
});

Use CommunicationProtocolEnum.HTTP and the sidecar’s HTTP port (by default "3500") for HTTP. Supply port values as strings.

When you omit explicit host and port options, the SDK can use DAPR_HTTP_ENDPOINT or DAPR_GRPC_ENDPOINT for the selected protocol. Otherwise, it uses the configured host and port, falling back to 127.0.0.1 and DAPR_HTTP_PORT or DAPR_GRPC_PORT (defaults: 3500 and 50001). The API token defaults to DAPR_API_TOKEN if daprApiToken is not supplied. Binary store requests use the same authentication settings as other client operations.

Choose an operation

All methods take a component name and a store-relative file name. The optional options object is the last argument.

MethodContent or destinationResult
setBinaryFile(storeName, fileName, content, options?)Uint8Array, Buffer, or binary Node.js ReadablePromise<void>
getBinaryFile(storeName, fileName, destination, options?)Node.js WritablePromise<void>
getBinaryFileBytes(storeName, fileName, options?)No destination; buffers the complete filePromise<Buffer>
getBinaryFileChunks(storeName, fileName, options?)No destination; yields ordered chunksAsyncIterable<Buffer>
deleteBinaryFile(storeName, fileName, options?)No content or destinationPromise<void>

TypeScript applications can import IClientBinaryStore, BinaryStoreOptions, and BinaryStoreSetOptions as types from @dapr/dapr. BinaryStoreOptions contains signal?: AbortSignal. Uploads accept BinaryStoreSetOptions, which also provides overwrite?: boolean and chunkSize?: number.

Streaming and memory use

Use a binary Readable, such as createReadStream, for large uploads. The stream must emit Uint8Array or Buffer chunks, not strings. Do not set a text encoding on the source stream. Once initial validation succeeds, the client consumes the source from its current position and destroys it when the upload finishes or fails. If validation fails or the signal is already aborted before the transfer starts, the caller remains responsible for closing the source. Create a new source stream for each attempt.

The upload chunk size defaults to 32 KiB. A custom chunkSize must be a positive safe integer. Smaller chunks do not bypass the component’s file-size or total-capacity limits.

getBinaryFile uses a stream pipeline that respects destination backpressure. It ends the destination on success and destroys it on failure. Do not reuse that destination for another download. A failed download may leave a partial local file; your application should remove or otherwise handle it.

getBinaryFileChunks lets you process each Buffer as it arrives. Consume it with for await...of; breaking out of the loop closes the response stream. Download chunk sizes are determined by the runtime and transport, not by the upload’s chunkSize.

Use getBinaryFileBytes only when the entire file fits comfortably in memory. It accumulates all chunks and concatenates them into a Buffer, so peak memory use can exceed the size of the file.

Cancellation

Every binary store operation accepts signal in its options object. Pass an AbortSignal to cancel work when the caller disconnects or an application deadline expires. For example, add a 30-second deadline to a download:

import { createWriteStream } from "node:fs";

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 30_000);

try {
  await client.binaryStore.getBinaryFile(
    "filestore",
    "uploads/report.pdf",
    createWriteStream("downloaded-report.pdf"),
    { signal: controller.signal },
  );
} finally {
  clearTimeout(timeout);
}

This snippet assumes an existing client. Keep the import at module scope and run the remaining code inside an async function or an ES module. Cancellation rejects the operation and can destroy the associated streams. Do not assume every protocol reports cancellation using the same error class.

For chunk enumeration, pass the signal to getBinaryFileChunks itself. This example creates a fresh 30-second timeout signal:

for await (const chunk of client.binaryStore.getBinaryFileChunks(
  "filestore",
  "uploads/report.pdf",
  { signal: AbortSignal.timeout(30_000) },
)) {
  console.log("Received bytes:", chunk.length);
}

Errors

Await each operation and handle failures at your application’s error boundary. For getBinaryFileChunks, errors surface while iterating, so error handling must surround the for await...of loop.

Runtime failures reject rather than returning success-shaped defaults. Examples include an upload to an existing file without overwrite: true, downloading or deleting a missing file, a missing component, and exceeding component capacity.

  • HTTP: Runtime errors are JavaScript Error instances whose message is a JSON string containing status (HTTP status code), error (status text), and error_msg (runtime response body). Transport and stream errors can have different shapes; do not assume every error message is JSON.
  • gRPC: Runtime errors preserve the ConnectError codes from @connectrpc/connect. Do not handle them as HTTP status codes.
  • Invalid arguments: Empty store or file names, invalid file path segments or characters, unsupported content types, or a destination that is not a Writable result in a TypeError. Invalid upload chunk sizes result in a RangeError. Non-binary source chunks are rejected during the transfer; the transport may wrap the underlying TypeError.

See the Binary Store API reference for runtime error semantics and the how-to guide for file-name restrictions and complete setup.

15 - Dapr Search JavaScript SDK

Index, retrieve, and query documents using the Dapr JavaScript SDK

The Dapr JavaScript SDK lets Node.js applications manage indexes and index, retrieve, delete, and query JSON documents in a configured Dapr search component. Use DaprClient.search with either HTTP or gRPC for full-text search, filters, sorting, highlighting, and pagination.

Search requires Dapr runtime 1.19 or later and JavaScript SDK 3.19.0 or later. The underlying Search API is alpha. Available query features depend on the search component.

To get started, follow the Search how-to guide. See the client usage guidance for method signatures, types, write acknowledgements, cancellation, and errors.

15.1 - How to: Index and search documents with the Dapr JavaScript SDK

Learn how to configure a search component and manage and query documents using the Dapr JavaScript SDK

Use DaprClient.search to manage indexes and search JSON documents through a Dapr search component. This guide uses Meilisearch and covers index creation, document writes, full-text search, filtering, sorting, pagination, retrieval, and deletion. All examples work over HTTP or gRPC.

Prerequisites

In your application directory, initialize an npm project if you do not already have one, then install the SDK:

npm init -y
npm install @dapr/dapr@3.19.0

The examples use JavaScript ES modules and run as a .mjs file without a TypeScript compiler. The same APIs are available in TypeScript.

Configure Meilisearch

Start a local Meilisearch instance:

docker run -d --name meilisearch -p 7700:7700 -e MEILI_MASTER_KEY=masterKey getmeili/meilisearch:v1.43

Create a components directory in your application directory and save the following as components\search.yaml:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: mysearch
spec:
  type: search.meilisearch
  version: v1
  metadata:
    - name: host
      value: "http://localhost:7700"
    - name: apiKey
      value: "masterKey"

The component’s metadata.name, mysearch, is the store name passed to every client operation. It is not the component type or the YAML file name. See the Meilisearch component reference for settings and provider restrictions.

Create an index and search documents

Save the following as app.mjs:

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "3500",
  communicationProtocol: CommunicationProtocolEnum.HTTP,
});

const storeName = "mysearch";
const indexName = "articles";
const waitOptions = {
  mode: "waitForCompletion",
  waitTimeout: "10s",
  onWaitTimeout: "failRequest",
};

try {
  await client.search.createIndex(storeName, indexName, {
    metadata: {
      filterableAttributes: "category,price",
      sortableAttributes: "price",
      searchableAttributes: "title",
    },
  });

  const indexed = await client.search.indexDocuments(
    storeName,
    indexName,
    [
      {
        id: "doc-1",
        content: { title: "Getting started with Dapr", category: "guide", price: 10 },
        metadata: { author: "alice" },
      },
      {
        id: "doc-2",
        content: { title: "Dapr pub/sub deep dive", category: "guide", price: 25 },
      },
      {
        id: "doc-3",
        content: { title: "Search API reference", category: "reference", price: 30 },
      },
    ],
    waitOptions,
  );
  if (indexed.failedItems.length > 0) {
    console.error("Documents not indexed:", indexed.failedItems);
    throw new Error("Document indexing had item failures");
  }
  console.log("Write acknowledgement:", indexed.ack);

  const query = {
    text: "dapr",
    filter: { category: { $eq: "guide" } },
    sort: [{ field: "price", order: "asc" }],
    highlightFields: ["title"],
    includeContent: true,
    topK: 1,
  };
  let continuationToken;
  do {
    const page = await client.search.search(storeName, indexName, {
      ...query,
      continuationToken,
    });
    for (const hit of page.hits) {
      console.log(hit.document.id, hit.document.content, hit.score, hit.highlights);
    }
    continuationToken = page.continuationToken;
  } while (continuationToken);
} finally {
  await client.stop();
}

Start the application with a Dapr sidecar, loading the component from components:

dapr run --app-id search-example --dapr-http-port 3500 --dapr-grpc-port 50001 --resources-path .\components -- node app.mjs

To use gRPC instead, change communicationProtocol to CommunicationProtocolEnum.GRPC and daprPort to "50001". No other search code changes are required.

The example waits for indexing to complete before searching, then returns doc-1 and doc-2 in ascending price order across two pages. Meilisearch highlights matches with <em> tags. Scores and total-hit estimates are provider dependent; do not depend on a particular score value.

Creating an existing index fails with ALREADY_EXISTS and does not update its settings. Before rerunning this application against the same Meilisearch instance, delete the example index using deleteIndex below, or choose a new index name. Unlike an in-memory store, Meilisearch can retain documents across sidecar restarts.

The following snippets reuse client, storeName, indexName, and waitOptions. Add them inside the try block after the search loop and before the client is stopped. Run the deletion examples last.

Inspect indexes

const indexes = await client.search.listIndexes(storeName);
console.log("Indexes:", indexes);

const index = await client.search.getIndex(storeName, indexName);
console.log(index.index, index.documentCount, index.properties);

documentCount is an approximate count, or 0 if the provider cannot supply one efficiently. For Meilisearch, properties includes primaryKey and the configured attribute settings. Meilisearch requires filter and sort attributes to be declared at index creation; the component adds id as a sortable tie-breaker for pagination.

Retrieve and update documents

const documents = await client.search.getDocuments(
  storeName,
  indexName,
  ["doc-2", "missing", "doc-1"],
  { includeContent: true },
);
console.log(documents);

const updated = await client.search.indexDocuments(
  storeName,
  indexName,
  [
    {
      id: "doc-1",
      content: { title: "Updated Dapr guide", category: "guide", price: 12 },
      metadata: { author: "alice", revision: "2" },
    },
  ],
  waitOptions,
);
if (updated.failedItems.length > 0) {
  console.error("Documents not updated:", updated.failedItems);
  throw new Error("Document update had item failures");
}

Retrieval returns found documents in request order and omits missing IDs. Without includeContent: true, it returns IDs and stored metadata, with content undefined. Indexing uses keyed upserts: submit an existing ID to update that document. Supply the complete intended content and metadata rather than assuming a partial-patch operation.

Pass ordinary JSON objects as content; the SDK serializes them and handles transport encoding, including base64 for HTTP. Do not base64-encode or stringify content yourself. Stored document metadata is opaque and is not searchable or filterable. Fields to search, filter, or sort must be in content.

For Meilisearch, document IDs must be 1 to 511 bytes of ASCII letters, digits, hyphens, and underscores. Content must not contain the reserved top-level keys id, daprMetadata, daprPayload, or keys beginning with _. Invalid content is reported in failedItems, while valid documents in the batch can still be indexed.

Shape search results

Restrict full-text search to selected fields and return only the content fields you need:

const projected = await client.search.search(storeName, indexName, {
  text: "dapr",
  searchFields: ["title"],
  filter: {
    $and: [
      { category: { $eq: "guide" } },
      { price: { $lt: 20 } },
    ],
  },
  includeContent: true,
  returnFields: ["title", "price"],
  topK: 10,
});
console.log(projected.hits.map((hit) => hit.document.content));

returnFields alone does not request content; set includeContent: true as well. Omit the query to match all documents, or specify only a filter to browse a subset:

const all = await client.search.search(storeName, indexName);
const guides = await client.search.search(storeName, indexName, {
  filter: { category: "guide" },
  includeContent: true,
});
console.log(all.hits, guides.hits);

Each call returns one page, not necessarily every match. Follow continuationToken as in the complete example. Keep all query fields, including topK, unchanged between pages; only change the token. Tokens are opaque: do not decode, edit, or URL-encode them.

Use a provider-native query

Use native instead of text for provider-specific search features:

const native = await client.search.search(storeName, indexName, {
  native: { q: "dapr", matchingStrategy: "all" },
  topK: 10,
  includeContent: true,
});
console.log(native.hits);

text and native are mutually exclusive. For Meilisearch, native queries must not set offset, limit, page, hitsPerPage, or sort; use the portable topK, continuationToken, and sort fields instead. A native filter is ANDed with the portable filter.

Delete documents and the index

const ack = await client.search.deleteDocuments(
  storeName,
  indexName,
  ["doc-2", "missing"],
  waitOptions,
);
console.log("Delete acknowledgement:", ack);

await client.search.deleteIndex(storeName, indexName);

Deleting missing document IDs is not an error. Document deletion uses the same write acknowledgement options as indexing, but returns the acknowledgement string directly. Deleting an index removes the index and all of its documents.

Next steps

Read the client usage guidance for the full query and result shapes, write acknowledgement modes, cancellation, and error handling. See the Search API overview and Search API reference for runtime semantics.

15.2 - Search client usage

Essential guidance for configuring and using the JavaScript Search client

Search is available through DaprClient.search in JavaScript SDK 3.19.0 or later and requires Dapr runtime 1.19 or later. HTTP and gRPC use the alpha Search API and expose the same methods and JavaScript result shapes.

Client lifetime and configuration

Reuse a long-lived DaprClient. Await outstanding operations before calling await client.stop() during application shutdown.

HTTP is the default protocol. To explicitly configure gRPC:

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "50001",
  communicationProtocol: CommunicationProtocolEnum.GRPC,
  daprApiToken: process.env.DAPR_API_TOKEN,
});

For HTTP, use CommunicationProtocolEnum.HTTP and the sidecar’s HTTP port (default "3500"). Port values are strings. When host and port are not explicitly supplied, the SDK can use DAPR_HTTP_ENDPOINT or DAPR_GRPC_ENDPOINT for the selected protocol, or fall back to the host and port defaults and DAPR_HTTP_PORT or DAPR_GRPC_PORT. The API token defaults to DAPR_API_TOKEN. See client configuration.

Choose an operation

storeName is the search component’s metadata.name. All methods except listIndexes also take an index name. The optional options object is the last argument.

MethodResult
createIndex(storeName, index, options?)Promise<void>
getIndex(storeName, index, options?)Promise<SearchIndex>
listIndexes(storeName, options?)Promise<string[]>
deleteIndex(storeName, index, options?)Promise<void>
indexDocuments<T>(storeName, index, documents, options?)Promise<IndexDocumentsResult>
getDocuments<T>(storeName, index, ids, options?)Promise<SearchDocument<T>[]>
deleteDocuments(storeName, index, ids, options?)Promise<IndexAck>
search<T>(storeName, index, query?, options?)Promise<SearchResult<T>>

TypeScript applications can import IClientSearch, SearchOptions, IndexingOptions, SearchDocument, SearchIndex, SearchQuery, SearchResult, IndexAck, and IndexDocumentsResult as types from @dapr/dapr.

Every operation accepts SearchOptions: metadata?: Readonly<Record<string, string>> and signal?: AbortSignal. This metadata is component-specific request metadata, separate from stored document metadata. For example, createIndex uses it for provider-specific index settings. The SDK handles HTTP body or query-string placement and gRPC request fields.

indexDocuments and deleteDocuments accept IndexingOptions, which extends those options with write acknowledgement controls. getDocuments also accepts includeContent?: boolean.

Document content and TypeScript types

SearchDocument<T> contains id: string, optional content: T, and optional string-map metadata. Content must serialize to a JSON object for indexing. The SDK uses JSON serialization independently of the application’s custom DaprClient serializer, converts content to UTF-8 bytes, and decodes returned content automatically. Do not pass pre-encoded base64 or JSON text as content.

Returned content is undefined unless requested with includeContent: true on getDocuments or on the search query. Stored metadata defaults to an empty object when absent. It is returned unchanged and is never indexed, searched, or filtered.

Generics describe the returned content at compile time; they do not validate documents at runtime. Content defaults to unknown when a type is not supplied. In TypeScript, annotate reusable option objects to retain their string-literal types:

import type { IndexingOptions, SearchDocument, SearchQuery } from "@dapr/dapr";

type Article = { title: string; category: string; price: number };

const documents: SearchDocument<Article>[] = [
  { id: "doc-1", content: { title: "Dapr guide", category: "guide", price: 10 } },
];
const waitOptions: IndexingOptions = {
  mode: "waitForCompletion",
  waitTimeout: "10s",
  onWaitTimeout: "failRequest",
};
const query: SearchQuery = {
  text: "dapr",
  includeContent: true,
  returnFields: ["title", "price"],
};

const indexed = await client.search.indexDocuments(storeName, indexName, documents, waitOptions);
if (indexed.failedItems.length > 0) {
  console.error("Documents not indexed:", indexed.failedItems);
  throw new Error("Document indexing had item failures");
}
const result = await client.search.search<Pick<Article, "title" | "price">>(
  storeName,
  indexName,
  query,
);
for (const hit of result.hits) {
  console.log(hit.document.content?.title);
}

This snippet assumes an existing client, storeName, and indexName. Use a content type that reflects the selected returnFields, not the complete stored document type, when projecting fields.

Write acknowledgements and partial failures

Indexing documents and deleting documents can be asynchronous. IndexAck is "queued" or "completed"; these are SDK strings, not the protobuf enum names used by raw HTTP requests.

OptionsBehavior
Omitted, or mode: "returnOnAcceptance"Return when the provider accepts the write. "queued" does not mean the documents are searchable or deleted yet. A provider can return "completed" if work is already finished.
mode: "waitForCompletion"Wait for completion up to waitTimeout. Requires both a positive protobuf duration, such as "10s" or "0.250s", and an explicit onWaitTimeout.
onWaitTimeout: "continueAsync"On wait timeout, return "queued" and let provider-side work continue. Requires a provider that supports queued acknowledgement.
onWaitTimeout: "failRequest"On wait timeout, reject with DEADLINE_EXCEEDED. Provider-side work may still complete.

Do not supply waitTimeout or onWaitTimeout without mode: "waitForCompletion". Keep waitTimeout shorter than the request deadline. If a component resiliency timeout is configured, leave a margin of at least 10% of that timeout and at least 100 ms, as described in the Search API overview.

indexDocuments resolves to { ack, failedItems }. Always inspect failedItems, even when the promise resolves and ack is "completed". Each failure contains id, numeric errorCode (a canonical non-OK google.rpc.Code), errorMessage, and details. For example, non-object content can produce an item failure with code 3 (INVALID_ARGUMENT) while valid documents in the same batch are indexed.

Only failures known at the acknowledgement boundary are returned. Queued work can fail later, and this API does not provide an operation handle or a method to retrieve its eventual outcome. Use completion mode when subsequent operations depend on write visibility. deleteDocuments returns the acknowledgement string directly, not { ack }.

Document IDs must be non-empty and unique within a write request. Duplicate IDs reject the request rather than producing partial item failures. Upserting the same IDs allows idempotent retries, but do not assume a timeout, cancellation, or failed wait means the provider rolled back the write.

Query options

Pass SearchQuery directly as the third argument of client.search.search, not inside a { query: ... } wrapper.

FieldBehavior
textPortable full-text search string. Mutually exclusive with native.
nativeProvider-native JSON query object. Mutually exclusive with text.
filterPortable JSON filter over content fields. Supports $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $exists, $and, $or, and $not. Dotted paths address nested fields.
topKPage size. Omitted or 0 uses the provider default. Must be an integer from 0 to 4294967295; providers can impose smaller limits.
continuationTokenOpaque token from the previous page. Pass it unchanged with the same query shape.
includeContentDefaults to false. Set to true to return document content.
returnFieldsContent fields to project. Does not implicitly enable includeContent.
searchFieldsFields to search with the portable text query.
sortOrdered { field, order?: "asc" | "desc" } clauses. Omitted order means ascending.
highlightFieldsFields for which to return highlighted fragments.

Omitting both text and native, or omitting the query entirely, matches all documents subject to filter. A field assigned directly to a value in a filter is an equality test; conditions in the same object are ANDed. See the portable filter language.

Provider capabilities determine supported query options and field restrictions. Meilisearch requires filterable and sortable attributes to be declared at index creation. Native query syntax is provider specific and is not portable between components.

Results and pagination

SearchResult<T> has the following fields:

FieldMeaning
hitsArray of { document: SearchDocument<T>, score: number, highlights: Record<string, string> }. Empty when there are no matches.
totalHitsOptional best-effort count. Undefined when unavailable, not necessarily 0.
totalHitsRelation"exact", "lowerBound", "estimate", or "unspecified". Describes the accuracy of totalHits.
continuationTokenOptional token for the next page. Stop paginating when no token is returned.

Scores are provider-specific, unnormalized values. Compare them only within the same result set. Highlight fragments are provider-generated strings, not a substitute for content; handle them appropriately before rendering in an application.

For each subsequent page, copy the original query and change only continuationToken. Changing text or native queries, page size, filters, sort, or field options invalidates the token. If a provider cursor expires, the runtime reports FAILED_PRECONDITION with reason SEARCH_CONTINUATION_EXPIRED; start a new query rather than editing the token.

Unlike raw HTTP responses, the SDK returns documentCount and totalHits as JavaScript numbers. Values beyond Number.MAX_SAFE_INTEGER reject with RangeError rather than silently losing precision. SearchIndex contains index, documentCount, and provider-specific string-map properties.

Cancellation

Every search method accepts an AbortSignal in its final options argument. For example, cancel a search after 30 seconds:

const result = await client.search.search(
  storeName,
  indexName,
  { text: "dapr", includeContent: true },
  { signal: AbortSignal.timeout(30_000) },
);
console.log(result.hits);

This snippet assumes an existing client and index. For writes, include signal alongside mode, waitTimeout, and onWaitTimeout in the same options object. An already-aborted signal rejects without sending the search operation. Cancellation is distinct from the provider’s waitTimeout and does not guarantee cancellation of provider-side writes. Error shapes can differ by protocol.

Errors

Await each operation and handle rejected promises at your application’s error boundary, in addition to checking resolved indexing results for failedItems.

  • HTTP: Runtime failures are JavaScript Error instances whose message is a JSON string containing status (HTTP status), error (status text), and error_msg (runtime response body). Transport, cancellation, and local validation failures can have different shapes; do not assume every error message is JSON.
  • gRPC: Runtime failures preserve ConnectError codes from @connectrpc/connect, not HTTP status codes.
  • Invalid arguments: Empty store, index, or document IDs, store or index names equal to . or .., non-string metadata values, conflicting text and native, or invalid option types reject locally. Invalid topK values and non-positive wait durations are rejected; invalid protobuf duration syntax also rejects. Provider-specific validation still happens at the runtime or component.

Creating an existing index fails with ALREADY_EXISTS; missing stores or indexes and unsupported queries also reject. Missing IDs in getDocuments are omitted, and deleting missing document IDs is not an error.

Per-item details are protobuf JSON objects over HTTP. Over gRPC, each detail is represented as { "@type": typeUrl, value: base64ProtobufBytes }, because provider-specific schemas might not be available to decode it. Do not assume identical detail payloads across protocols.

See the Search API reference for runtime error semantics and the how-to guide for a complete setup and runnable example.

16 - Dapr Vector JavaScript SDK

Store embeddings and query similar vectors using the Dapr JavaScript SDK

The Dapr JavaScript SDK lets Node.js applications manage collections, store precomputed dense vectors, and run similarity searches through a configured Dapr vector store component. Use DaprClient.vector with either HTTP or gRPC.

Vector requires JavaScript SDK 3.19.0 or later and a Vector-capable Dapr runtime from the 1.19 release line or later.

The SDK accepts embeddings you generate elsewhere; it does not generate embeddings from text, images, or payloads. Records can also carry opaque payload bytes and structured, filterable metadata.

To get started, follow the Vector how-to guide. See the client usage guidance for method signatures, indexing behavior, query semantics, cancellation, and errors.

16.1 - How to: Store and query vectors with the Dapr JavaScript SDK

Learn how to manage collections, store embeddings, and query similar vectors using the Dapr JavaScript SDK

Use DaprClient.vector to store precomputed embeddings and find similar records through a Dapr vector store component. This guide covers collection configuration, upserts, retrieval, similarity queries, batch queries, and deletes.

Prerequisites

  • Dapr CLI
  • An initialized Dapr environment with a Vector-capable runtime from the 1.19 release line or later
  • Node.js 18 or later
  • The @dapr/dapr package, version 3.19.0 or later
  • A configured vector store component, such as the in-memory component below

In your application directory, initialize an npm project if you do not already have one, then install the SDK:

npm init -y
npm install @dapr/dapr@3.19.0

The examples use JavaScript ES modules and run as a .mjs file without a TypeScript compiler. The same APIs are available in TypeScript.

Configure an in-memory vector store

Create a components directory in your application directory and save the following as vectorstore.yaml inside it:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: vectorstore
spec:
  type: vector.in-memory
  version: v1

This component stores collections and records in the sidecar’s memory. Data is lost when the sidecar stops and is not shared across sidecars. Use it for local development and testing, not durable storage.

The component’s metadata.name, vectorstore, is the store name passed to every client operation. It is not the component’s type or the YAML file name. A collection is a separate, named group of records within that store.

Create a collection and query vectors

Save the following as app.mjs:

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "3500",
  communicationProtocol: CommunicationProtocolEnum.HTTP,
});

const storeName = "vectorstore";
const collection = "documents";
const writeOptions = {
  mode: "waitForCompletion",
  waitTimeout: "10s",
  onWaitTimeout: "failRequest",
};

try {
  await client.vector.createCollection(storeName, collection, {
    dimensions: 3,
    metric: "cosine",
  });

  const write = await client.vector.upsertVectors(
    storeName,
    collection,
    [
      {
        id: "doc-1",
        values: [1, 0, 0],
        payload: Buffer.from("Introduction to Dapr"),
        metadata: { category: "guide", details: { language: "en" } },
      },
      {
        id: "doc-2",
        values: [0.8, 0.6, 0],
        payload: Buffer.from("Building applications with Dapr"),
        metadata: { category: "guide", details: { language: "en" } },
      },
      {
        id: "doc-3",
        values: [0, 1, 0],
        metadata: { category: "reference", details: { language: "en" } },
      },
    ],
    writeOptions,
  );

  if (write.failedItems.length > 0) {
    console.error("Vector upsert failures:", write.failedItems);
    throw new Error("Not all vector records were stored");
  }
  if (write.ack !== "completed") {
    throw new Error("Vector indexing has not completed");
  }

  const result = await client.vector.queryVectors(storeName, collection, {
    vector: [1, 0, 0],
    topK: 2,
    includePayload: true,
    filter: { category: { $eq: "guide" } },
  });

  console.log("Metric:", result.metric);
  for (const match of result.matches) {
    console.log(
      match.record.id,
      match.score,
      match.record.payload === undefined
        ? "(no payload)"
        : Buffer.from(match.record.payload).toString("utf8"),
    );
  }
} finally {
  await client.stop();
}

Start the application with a Dapr sidecar, loading the component from components:

dapr run --app-id vector-example --dapr-http-port 3500 --dapr-grpc-port 50001 --resources-path ./components -- node app.mjs
dapr run --app-id vector-example --dapr-http-port 3500 --dapr-grpc-port 50001 --resources-path .\components -- node app.mjs

The example creates a three-dimensional cosine collection, waits for indexing, and finds the two closest guide records. With the in-memory component, doc-1 has a score of approximately 1 and doc-2 approximately 0.8. These illustrative vectors are supplied directly; the SDK does not generate embeddings.

To use gRPC instead, change communicationProtocol to CommunicationProtocolEnum.GRPC and daprPort to "50001". All vector methods work with either protocol.

The examples below reuse client, storeName, collection, and writeOptions. Add them inside the try block, after the query and before the client is stopped. Run the delete examples last. Creating a collection that already exists fails; if you rerun against the same sidecar, reuse the existing collection or explicitly delete it first.

Inspect collections and retrieve records

console.log(await client.vector.listCollections(storeName));

const info = await client.vector.getCollection(storeName, collection);
console.log(info.collection, info.dimensions, info.metric, info.recordCount);

const records = await client.vector.getVectors(
  storeName,
  collection,
  ["doc-2", "missing", "doc-1"],
  { includeValues: true },
);
console.log(records);

getCollection returns the collection name, dimensions, effective metric, approximate record count, and component-specific properties. The count is 0 when unavailable.

getVectors omits missing IDs and retains request order for records it finds. It always retrieves payloads and record metadata, but returns values: [] unless includeValues is true. Payloads are returned as Uint8Array, not automatically decoded text or JSON.

Query by an existing record and filter metadata

Instead of supplying a vector, use byId to search using a stored record’s vector:

const similar = await client.vector.queryVectors(storeName, collection, {
  byId: "doc-1",
  topK: 5,
  includeValues: true,
  includePayload: true,
  scoreThreshold: 0.5,
  filter: {
    $and: [
      { category: { $eq: "guide" } },
      { "details.language": { $eq: "en" } },
    ],
  },
});
console.log(similar.matches);

Supply exactly one of vector or byId. A missing source record causes a single query to fail. Whether a byId query includes the source record among its matches depends on the component.

Filters apply to record metadata, not payload bytes. The portable filter DSL supports $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $exists, and the boolean operators $and, $or, and $not. Use dotted paths for nested metadata fields.

For cosine and dot product, higher scores are better; for Euclidean distance, lower scores are better. scoreThreshold is inclusive on the metric’s raw scale: a minimum score for cosine or dot product, or a maximum distance for Euclidean. It is not a normalized probability.

Run multiple queries

batchQueryVectors runs queries against the same store and collection and returns one result per query in request order:

const results = await client.vector.batchQueryVectors(storeName, collection, [
  { vector: [1, 0, 0], topK: 1 },
  { byId: "missing", topK: 1 },
]);

for (const [index, result] of results.entries()) {
  if (result.error !== undefined) {
    console.error("Query failed:", index, result.error.code, result.error.message);
  } else {
    console.log("Query matches:", index, result.response.matches);
  }
}

An individual runtime or provider failure stays in that query’s result slot. Request-wide failures reject the whole call. Do not treat a resolved batch promise as proof that every query succeeded.

Delete records and collections

const ack = await client.vector.deleteVectors(
  storeName,
  collection,
  ["doc-1", "missing"],
  writeOptions,
);
console.log("Delete acknowledgement:", ack);

await client.vector.deleteCollection(storeName, collection);

Deleting missing record IDs is not an error. Deleting a collection removes the collection and its records; operations against a missing collection fail.

Next steps

Read the client usage guidance for indexing acknowledgements, record replacement, query defaults, cancellation, and error handling.

16.2 - Vector client usage

Essential guidance for configuring and using the JavaScript Vector client

Vector is available through DaprClient.vector in JavaScript SDK 3.19.0 or later. Both HTTP and gRPC use the alpha Vector API and require a Vector-capable Dapr runtime from the 1.19 release line or later. Installing the JavaScript SDK alone does not enable the Vector API.

Client lifetime and configuration

Reuse a long-lived DaprClient rather than creating a client for each operation. Await outstanding operations before calling await client.stop() during application shutdown.

The default protocol is HTTP. To explicitly configure a gRPC connection:

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "50001",
  communicationProtocol: CommunicationProtocolEnum.GRPC,
  daprApiToken: process.env.DAPR_API_TOKEN,
});

Use CommunicationProtocolEnum.HTTP and the sidecar’s HTTP port (by default "3500") for HTTP. Supply port values as strings.

When you omit explicit host and port options, the SDK can use DAPR_HTTP_ENDPOINT or DAPR_GRPC_ENDPOINT for the selected protocol. Otherwise, it uses the configured host and port, falling back to 127.0.0.1 and DAPR_HTTP_PORT or DAPR_GRPC_PORT (defaults: 3500 and 50001). The API token defaults to DAPR_API_TOKEN if daprApiToken is not supplied. Vector requests use the same authentication settings as other client operations.

Choose an operation

Every method takes a component name. Collection-bound operations also take a collection name. The options object is the last argument.

MethodResult
createCollection(storeName, collection, options)Promise<void>
getCollection(storeName, collection, options?)Promise<VectorCollection>
listCollections(storeName, options?)Promise<string[]>
deleteCollection(storeName, collection, options?)Promise<void>
upsertVectors(storeName, collection, records, options?)Promise<VectorUpsertResult>
getVectors(storeName, collection, ids, options?)Promise<VectorRecord[]>
deleteVectors(storeName, collection, ids, options?)Promise<VectorIndexAck>
queryVectors(storeName, collection, query, options?)Promise<VectorQueryResult>
batchQueryVectors(storeName, collection, queries, options?)Promise<VectorBatchQueryResult[]>

TypeScript applications can import these result types, IClientVector, VectorRecord, VectorQuery, VectorDistanceMetric, VectorOptions, VectorCreateCollectionOptions, VectorIndexingOptions, VectorError, and VectorErrorDetail as types from @dapr/dapr.

VectorOptions provides metadata?: Readonly<Record<string, string>> and signal?: AbortSignal. Request metadata is component-specific and string-valued; it is separate from the structured, filterable metadata attached to each record.

Collections and records

Creating a collection requires dimensions, a positive integer no greater than 4294967295. Supported metric names are "cosine", "dotProduct", and "euclidean". Omitting metric selects the component’s documented default; a metric the component does not support is rejected by the runtime as an invalid argument. The metadata passed to createCollection carries component-specific collection settings, such as index parameters. getCollection returns the collection’s dimensions, its effective metric, an approximate recordCount (which some components report as 0), and component-specific properties. Component capabilities and limits can be narrower than the SDK’s accepted inputs.

The alpha API covers dense vectors only. Sparse or named vectors, hybrid fusion, and result pagination are not available.

A VectorRecord contains a nonempty string id, a values array of numbers, optional payload bytes, and optional structured JSON metadata. IDs are scoped to a collection. Upserting an existing ID replaces its record rather than merging individual fields; supply all values, payload, and metadata you want to retain.

Provide dense embeddings as ordinary number arrays, not Float32Array. Coordinates must be finite and within the finite float32 range; float32 serialization can round them. Vector lengths must match the collection’s dimensions, and components typically reject zero-magnitude vectors for cosine similarity. Dimension mismatches and other runtime record validation failures can appear in failedItems rather than rejecting the upsert.

Payloads accept Uint8Array or Node.js Buffer and are returned as Uint8Array. The SDK does not apply a custom serializer or generate embeddings from payloads. Encode and decode application content explicitly. Record metadata and query filters must be JSON objects.

getVectors always retrieves payloads and metadata. Set { includeValues: true } to retrieve embeddings; otherwise values is an empty array. Missing IDs are omitted, and found records retain request order. Deleting missing IDs succeeds.

Indexing acknowledgements and partial failures

upsertVectors and deleteVectors accept VectorIndexingOptions. The default mode, "returnOnAcceptance", allows a write to return before indexing completes. An acknowledgement of "queued" means work was accepted, not that it succeeded or is queryable. Queued work can still fail later; this API does not expose its eventual result.

For a read-after-write flow, explicitly wait for completion:

const write = await client.vector.upsertVectors(
  "vectorstore",
  "documents",
  [{ id: "doc-1", values: [1, 0, 0], metadata: { category: "guide" } }],
  {
    mode: "waitForCompletion",
    waitTimeout: "10s",
    onWaitTimeout: "failRequest",
  },
);

if (write.failedItems.length > 0) {
  console.error("Failed records:", write.failedItems);
  throw new Error("Vector upsert had partial failures");
}
if (write.ack !== "completed") {
  throw new Error("Vector indexing has not completed");
}

This snippet assumes an existing client and collection. Run it inside an async function or an ES module.

Waiting requires a positive protobuf duration string, such as "10s" or "0.250s", and an explicit timeout action. waitTimeout must be shorter than the remaining request deadline. When the store has a resiliency timeout policy, it must be shorter than that timeout by at least a tenth of the timeout and by no less than 100 milliseconds.

onWaitTimeoutBehavior
"continueAsync"Return a "queued" acknowledgement if the wait expires; work can continue asynchronously. Invalid when the component has no queued acknowledgement.
"failRequest"Fail the request with a deadline-exceeded error (HTTP 504, gRPC DeadlineExceeded) if the wait expires. Provider-side work is not guaranteed to be cancelled.

Do not supply waitTimeout or onWaitTimeout with "returnOnAcceptance" or without explicitly selecting "waitForCompletion"; the SDK rejects these combinations before sending the request.

upsertVectors returns { ack, failedItems }. Inspect failedItems even when the promise resolves and ack is "completed": completion does not mean every record succeeded. Each failed item contains its id and a structured error. Failures reported are those known at the acknowledgement boundary; a queued acknowledgement is not a final per-record report.

deleteVectors returns only "queued" or "completed". Empty upsert and delete arrays are supported. Duplicate IDs in an upsert request are rejected by the runtime; do not rely on last-write-wins ordering within a batch.

Queries, scores, and inclusion flags

A VectorQuery requires exactly one source: vector: number[] or byId: string. A vector array must be nonempty; the runtime rejects an empty array as an invalid argument. The remaining fields are optional:

FieldBehavior
topKNonnegative uint32. Omitted or 0 uses the component’s default match count.
filterPortable filter DSL applied to record metadata, including dotted paths. Payloads are not filterable.
includeValuesDefaults to false; omitted embeddings are returned as values: [].
includePayloadDefaults to false; omitted payloads are returned as undefined.
metricOmission uses the collection’s metric. Unsupported overrides fail explicitly.
scoreThresholdFinite, inclusive cutoff on the metric’s unnormalized scale.

Record metadata is returned with query matches independently of the values and payload flags. The result is { matches, metric }; each match contains { record, score }, and metric identifies the effective metric even when the query omitted it.

Cosine and dot product scores are higher-is-better; Euclidean distances are lower-is-better. Thresholds are minimum scores for cosine and dot product, and maximum distances for Euclidean. Do not treat scores as normalized probabilities or compare scores across different metrics as though they shared a scale. Whether a byId query returns the source record itself is component-dependent.

All queries in batchQueryVectors target the same store and collection, with their own query fields. Results preserve request order and contain exactly one of { response: VectorQueryResult } or { error: VectorError }. An empty query array returns an empty result array. Inspect every slot; individual runtime/provider failures do not reject the entire batch, but request-wide failures do.

Cancellation

Every vector operation accepts signal in its options object. Pass an AbortSignal to cancel a request when an application deadline expires or a caller disconnects:

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 30_000);

try {
  const result = await client.vector.queryVectors(
    "vectorstore",
    "documents",
    { vector: [1, 0, 0], topK: 5 },
    { signal: controller.signal },
  );
  console.log(result.matches);
} finally {
  clearTimeout(timeout);
}

This snippet assumes an existing client and collection. Run it inside an async function or an ES module. Cancellation rejects the request but does not guarantee cancellation or rollback of provider-side indexing work. Do not assume both protocols report cancellation with the same error class.

Errors

Await each operation and handle request failures at your application’s error boundary. Check both upsertVectors().failedItems and each batchQueryVectors() result for failures that do not reject the call.

  • HTTP request failures: Runtime failures are JavaScript Error instances whose message is a JSON string containing status (HTTP status code), error (status text), and error_msg (runtime response body). Transport and cancellation errors can have different shapes; do not assume every error message is JSON.
  • gRPC request failures: Runtime failures preserve ConnectError codes from @connectrpc/connect. Do not handle them as HTTP status codes.
  • Structured item/query failures: VectorError contains a canonical non-OK google.rpc.Code number (1 through 16), a message, and details. These codes are not HTTP status codes. Each detail contains typeUrl and value: HTTP preserves protobuf JSON fields without @type, while gRPC preserves the original protobuf bytes as Uint8Array.
  • Invalid arguments: Empty store/collection names or record IDs, store/collection names equal to . or .., invalid payload types, unsupported metric names, nonfinite numbers, invalid JSON, and ambiguous query sources are rejected before the request is sent. Invalid dimensions, topK, or coordinates outside the finite float32 range are also rejected. Type and range failures use TypeError or RangeError; malformed protobuf duration strings can produce parser errors.

Creating an existing collection (HTTP 409, gRPC AlreadyExists), accessing a missing collection or querying by a missing ID (HTTP 404, gRPC NotFound), unsupported component capabilities, and invalid filters (HTTP 400, gRPC InvalidArgument) cause runtime failures. In a batch, a missing byId source fails only that slot with VectorError code 5. A valid query with no matches instead returns an empty matches array. Collection counts exceeding JavaScript’s safe integer range and malformed responses are rejected rather than silently losing precision or substituting defaults.

See the Vector how-to guide for complete setup and runnable examples.

17 - Dapr Secrets JavaScript SDK

Retrieve individual and bulk secrets using the Dapr JavaScript SDK

Use DaprClient.secret to retrieve secrets from a configured Dapr secret store in JavaScript or TypeScript. The SDK provides single-secret and bulk retrieval over HTTP and gRPC. It does not provide methods to create, update, delete, or watch secrets.

These guides target @dapr/dapr 3.19.0 or later. Secrets use the stable Dapr Secrets API; this is not a new building block introduced in SDK 3.19.

Follow the how-to guide for local setup and retrieval. Read the client usage guidance for configuration, response shapes, metadata, protocol limitations, and errors.

See the Secrets building block overview and supported secret stores for runtime capabilities and component-specific requirements.

17.1 - How to: Retrieve secrets with the Dapr JavaScript SDK

Configure a secret store and retrieve individual and bulk secrets with the Dapr JavaScript SDK

This guide uses DaprClient.secret to retrieve a single secret and all accessible secrets from a local secret store. The examples use JavaScript ES modules; the same methods are available in TypeScript.

Prerequisites

In your application directory, initialize an npm project if needed and install the SDK:

npm init -y
npm install @dapr/dapr@3.19.0

The explicit version ensures that npm installs the 3.19.0 release used in this guide. You can substitute a later version.

Configure a local secret store

Create a file named secrets.json in your application’s working directory:

{
  "api-key": "development-api-key",
  "database": {
    "username": "development-user",
    "password": "development-password"
  }
}

Create a components directory and save the following as components\local-secret-store.yaml:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: local-secret-store
spec:
  type: secretstores.local.file
  version: v1
  metadata:
    - name: secretsFile
      value: secrets.json
    - name: multiValued
      value: "true"

The component’s metadata.name, local-secret-store, is the store name passed to the SDK. The secretsFile path is relative to the directory where you run dapr run, not the component file. Setting multiValued to "true" lets the database secret contain both username and password.

See the local file secret store reference for flattening and multi-valued secret behavior.

Retrieve individual and bulk secrets

Save the following as app.mjs. Using .mjs lets Node.js run the ES module without changing your project’s package configuration.

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "3500",
  communicationProtocol: CommunicationProtocolEnum.HTTP,
});

const storeName = "local-secret-store";

try {
  const apiKey = await client.secret.get(storeName, "api-key");
  if (typeof apiKey["api-key"] !== "string") {
    throw new Error("The required api-key secret was not returned");
  }

  const database = await client.secret.get(storeName, "database");
  if (
    typeof database.username !== "string" ||
    typeof database.password !== "string"
  ) {
    throw new Error("The required database credentials were not returned");
  }

  const secrets = await client.secret.getBulk(storeName);
  if (
    typeof secrets.database?.username !== "string" ||
    typeof secrets.database?.password !== "string"
  ) {
    throw new Error("Bulk retrieval did not return the database credentials");
  }

  console.log("Retrieved the required secrets without logging their values");
} finally {
  await client.stop();
}

Start the application and its sidecar from the directory containing secrets.json:

dapr run --app-id secrets-example --dapr-http-port 3500 --dapr-grpc-port 50001 --resources-path .\components -- node app.mjs

This is a client-only application; it does not need a DaprServer, an application port, or an inbound secrets handler.

Single-secret responses

get(storeName, key) returns the secret’s key-value data directly. For a name/value secret, the only key is the secret name itself; for a multi-valued secret, the keys are its fields and the secret name is not included. With the sample data, get(storeName, "api-key") returns:

{
  "api-key": "development-api-key"
}

Over HTTP, get(storeName, "database") returns both values:

{
  "username": "development-user",
  "password": "development-password"
}

Bulk responses

getBulk(storeName) returns an object keyed by secret name, with a nested object containing each secret’s data:

{
  "api-key": {
    "api-key": "development-api-key"
  },
  "database": {
    "username": "development-user",
    "password": "development-password"
  }
}

Bulk retrieval is not an array or a paginated iterator. It loads the full response into memory. Use it only when the application needs all accessible secrets and the component supports bulk retrieval.

Use gRPC

Configure CommunicationProtocolEnum.GRPC and the sidecar’s gRPC port, "50001", instead of HTTP and "3500". The same get and getBulk methods are available.

Pass store-specific metadata over HTTP

The third argument to get is a query-string string, not a metadata object. Include the metadata. prefix on each parameter and omit the leading ?. Use URLSearchParams to encode values.

For example, with an AWS Secrets Manager component named aws-secrets, retrieve a particular version:

const metadata = new URLSearchParams({
  "metadata.version_id": "your-version-id",
}).toString();

const secret = await client.secret.get(
  "aws-secrets",
  "database-password",
  metadata,
);

This snippet assumes an existing HTTP client and a separately configured AWS component; it is not needed for the local file example. Supported metadata depends on the secret store component. For example, Kubernetes supports metadata.namespace, while some cloud stores support version selection.

The gRPC implementation ignores this argument in SDK 3.19.0. getBulk has no metadata argument with either protocol. See the client usage guidance for the complete API surface and error behavior.

17.2 - Secrets client usage

Configure the JavaScript secrets client and understand metadata, response shapes, protocol differences, and errors

Secrets are available through DaprClient.secret; there is no separate secrets client package or builder. This guidance targets @dapr/dapr 3.19.0 or later.

Client lifetime and configuration

Reuse a long-lived DaprClient across operations. Its secrets client shares the parent client’s connection and authentication settings. Await outstanding operations before calling await client.stop() during application shutdown; do not stop a shared client after each secret lookup.

HTTP is the default protocol. To explicitly configure gRPC:

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "50001",
  communicationProtocol: CommunicationProtocolEnum.GRPC,
  daprApiToken: process.env.DAPR_API_TOKEN,
});

Use CommunicationProtocolEnum.HTTP with the HTTP port (default "3500") for HTTP. Port values are strings. Selecting the SDK protocol controls client-to-sidecar communication; it does not require an application server.

If neither daprHost nor daprPort is supplied, the SDK can use DAPR_HTTP_ENDPOINT or DAPR_GRPC_ENDPOINT for the selected protocol. Explicit host or port options take precedence over the endpoint environment variable. Otherwise, the SDK defaults to 127.0.0.1 and DAPR_HTTP_PORT or DAPR_GRPC_PORT, falling back to 3500 or 50001. The token defaults to DAPR_API_TOKEN when daprApiToken is omitted.

The API token authenticates to the Dapr sidecar, not directly to the secret store provider. Configure provider credentials and access in the secret store component. Use secret scopes to restrict which secrets an application can retrieve, and component scopes to restrict which applications can use a component.

Available operations

The public signatures are:

get(secretStoreName: string, key: string, metadata?: string): Promise<object>;
getBulk(secretStoreName: string): Promise<object>;

Call these as client.secret.get(...) and client.secret.getBulk(...).

CapabilityHTTPgRPC in 3.19.0
Retrieve a single secretReturns the full data mapReturns only the first key-value pair, or {} for an empty data map
Per-request metadata on getQuery string with metadata.-prefixed keysArgument is accepted but ignored
Bulk retrievalNested secret data mapsNested secret data maps, preserving all values
Per-request metadata on getBulkNot exposedNot exposed

These limitations are present in both the 3.19.0 HTTP implementation and gRPC implementation, and remain in the latest source reviewed for this guide. Check the implementation and release notes when upgrading.

The SDK does not expose secret writes, deletes, subscriptions, typed secret generation, or automatic secret refresh. Manage secret creation and rotation in the underlying provider. If your application caches retrieved values, it must decide when to fetch them again.

Metadata and names

HTTP get appends the metadata string directly to the request URL after ?. Do not include a leading ?, pass an object, or omit the metadata. prefixes. For example:

const metadata = new URLSearchParams({
  "metadata.namespace": "payments",
}).toString();

const secret = await client.secret.get(
  "kubernetes",
  "database-credentials",
  metadata,
);

This requires a Kubernetes secret store and an existing HTTP client. Metadata keys are component-specific; see the Secrets API reference and component reference.

Store names and secret names are case-sensitive. The HTTP implementation interpolates them into URL paths without encoding them. If a secret name contains URL-reserved characters, encode the secret name with encodeURIComponent before passing it to HTTP get. Do not encode gRPC secret names, which are sent as protobuf fields.

Response shapes and TypeScript

For a successful single-secret request, the data is a map of string keys to string values. Some stores return the secret name as the only key; others, such as Kubernetes, return several fields without the secret name as a wrapper. HTTP preserves all these fields. Do not rely on gRPC’s first-entry selection to choose a particular field from a multi-valued secret.

A successful bulk response is a map from secret names to their data maps. It is not a flat map, and the field names inside each secret are determined by the component. Bulk availability and the returned secrets depend on the component and access configuration.

Both methods are declared as Promise<object>, not as generic or strongly typed dictionaries. TypeScript callers should validate a response before indexing it, rather than assuming the declaration guarantees string-valued fields:

function isStringMap(value: unknown): value is Record<string, string> {
  return (
    typeof value === "object" &&
    value !== null &&
    !Array.isArray(value) &&
    Object.values(value).every((entry) => typeof entry === "string")
  );
}

const secret = await client.secret.get("local-secret-store", "api-key");
if (!isStringMap(secret) || typeof secret["api-key"] !== "string") {
  throw new Error("The required api-key secret was not returned");
}

const apiKey = secret["api-key"];

This snippet assumes an existing client and runs inside an async function or an ES module. For bulk responses, first validate the outer object, then validate the data map for each secret your application requires. Checking only that the response is an object does not guarantee that a required secret exists.

Errors and missing secrets

Await each operation and handle failures at your application’s error boundary. Missing components, denied access, unsupported bulk retrieval, provider failures, and sidecar connection failures can reject the promise.

  • HTTP runtime errors: Non-success responses become JavaScript Error instances whose message is a JSON string containing status (HTTP status), error (status text), and error_msg (runtime response body). Network and startup errors can have different shapes; do not assume every error message is JSON.
  • gRPC runtime errors: RPC failures use ConnectError from @connectrpc/connect, with a gRPC/Connect error code rather than an HTTP status.
  • Missing or empty secrets: Do not assume a missing secret always rejects. Behavior depends on the component and runtime response. HTTP 204 No Content resolves to an empty string in this SDK, despite the Promise<object> declaration. gRPC resolves an empty data map to {}. Validate required fields and report their absence explicitly.

See the Secrets API reference for runtime status codes. Avoid logging returned secrets or raw error payloads that might contain sensitive provider information.

Cancellation and bulk retrieval limits

Neither method exposes an AbortSignal, per-call timeout, or request options argument. Do not assume cancellation support from another SDK building block also applies to secrets. Racing a request against a timer only limits how long your application waits; it does not cancel the underlying request.

getBulk returns the entire response in memory, without pagination, streaming, or a key filter. Prefer individual requests when you need only a few secrets. Using bulk retrieval to work around the gRPC multi-value limitation may retrieve more secrets than your application needs; HTTP get is usually the narrower choice.

Testing your application

Keep secret access behind an application-owned interface or inject the methods your code needs. Test single-value and multi-valued responses, nested bulk responses, missing required fields, and rejected promises. Use the actual object return type rather than assuming a typed secret model.

For integration tests, run a sidecar with a development secret store such as the local file example. Test HTTP and gRPC separately if your application supports both; a single-value success case does not exercise metadata or multi-valued response differences.