State Provider

The State provider allows you to store and retrieve data on the Vonage Cloud Runtime platform. It supports key-value storage, hash maps, ordered lists, and full-text search — all backed by Redis.

Initialization

Warning: Do not call vcr.createSession() at global/module scope to initialize State. This creates a random, ephemeral session ID — data stored under it will be invisible to other replicas and other requests. See Session Scoping below.

Node.js:

import { vcr, State } from '@vonage/vcr-sdk';

// WRONG — random session, data siloed to this replica
// const session = vcr.createSession();
// const state = new State(session);

// CORRECT — shared state across all replicas
const state = vcr.getInstanceState();

// CORRECT — per-conversation state with a deterministic ID
app.post('/onCall', async (req, res) => {
  const session = vcr.createSessionWithId(req.body.conversation_uuid);
  const state = new State(session);
  // ...
});

Python:

from vonage_cloud_runtime.vcr import VCR
from vonage_cloud_runtime.providers.state.state import State

vcr = VCR()

# WRONG — random session, data siloed to this replica
# session = vcr.createSession()
# state = State(session)

# CORRECT — shared state across all replicas
state = vcr.getInstanceState()

# CORRECT — per-conversation state with a deterministic ID
session = vcr.createSessionWithId(conversation_uuid)
state = State(session)

An optional namespace prefix can be passed as a second argument to avoid key collisions:

const state = new State(session, 'user:123:');

State Scoping

Important: State data is scoped to the session used to create the State instance. If you use a random session (vcr.createSession()), the data is only accessible with that exact session ID. Unless you persist and share that ID, no other request or replica can reach the data. Always choose your session deliberately.

Scope How to access Visibility
Session state new State(session) Isolated to the specific session. Deleted when the session TTL expires.
Instance state vcr.getInstanceState() Shared across all replicas of the deployed instance.
Account state vcr.getAccountState() Shared across all applications in the Vonage account.

When to use each scope:

  • Instance state — Use for shared counters, caches, configuration, or any data that all replicas need to read/write. This is the correct default for most shared state.
  • Session state with deterministic ID — Use for data scoped to a specific conversation, user, or workflow. Create the session with vcr.createSessionWithId(id) using an ID from the request context (e.g., conversation_uuid, sender phone number, user ID).
  • Account state — Use for cross-application data like shared blocklists or configuration.
// Session state — per-conversation, using a deterministic ID from a callback
const session = vcr.createSessionWithId(req.body.conversation_uuid);
const conversationState = new State(session);

// Instance state — shared across all replicas
const instanceState = vcr.getInstanceState();

// Account state — shared across all applications
const accountState = vcr.getAccountState();

See Stateless Architecture for detailed guidance on session scoping and common pitfalls.

Key-Value Operations

Method Signature Returns Description
set set<T>(key, value) Promise<string> ("OK") Store a value under a key
get get<T>(key) Promise<T> Retrieve the value for a key
delete delete(key) Promise<string> ("1"/"0") Delete a key. Returns "1" if deleted, "0" if not found
increment increment(key, value) Promise<string> Atomically increment a numeric key by value
decrement decrement(key, value) Promise<string> Atomically decrement a numeric key by value
expire expire(key, seconds, option?) Promise<string> ("1"/"0") Set a TTL on a key in seconds

Expire Options

The optional option parameter on expire accepts an EXPIRE_OPTION enum value:

Option Description
NX Set expiry only if the key has no existing expiry
XX Set expiry only if the key already has an expiry
GT Set expiry only if the new expiry is greater than the current one
LT Set expiry only if the new expiry is less than the current one
import { vcr, State, EXPIRE_OPTION } from '@vonage/vcr-sdk';

const state = vcr.getInstanceState();

await state.set('counter', 0);
const value = await state.get('counter');       // 0

await state.increment('counter', 5);            // "5"
await state.decrement('counter', 2);            // "3"

await state.expire('counter', 3600);            // expires in 1 hour
await state.expire('counter', 600, EXPIRE_OPTION.LT); // only if < current TTL

await state.delete('counter');                  // "1"

HashMap Operations

Operate on fields within a named hash table.

Method Signature Returns Description
mapSet mapSet(table, keyValuePairs) Promise<string> Set one or more key-value pairs in a hash table
mapGetValue mapGetValue(table, key) Promise<string> Get a single field value
mapGetMultiple mapGetMultiple(table, keys) Promise<string[]> Get multiple field values by key
mapGetAll mapGetAll(table) Promise<Record<string, string>> Get all fields and values
mapGetValues mapGetValues(table) Promise<string[]> Get all values (without keys)
mapDelete mapDelete(table, keys) Promise<string> Delete one or more fields
mapExists mapExists(table, key) Promise<string> ("1"/"0") Check if a field exists
mapIncrement mapIncrement(table, key, value) Promise<string> Atomically increment a field value
mapLength mapLength(table) Promise<string> Get the number of fields in the table
mapScan mapScan(table, cursor, pattern?, count?) Promise<[string, string[]]> Iterate fields with a cursor
// Store a user profile as a hash map
await state.mapSet('user:123', {
  name: 'Alice',
  email: 'alice@example.com',
  loginCount: '0',
});

const name = await state.mapGetValue('user:123', 'name');       // "Alice"
const profile = await state.mapGetAll('user:123');               // { name, email, loginCount }
const values = await state.mapGetValues('user:123');             // ["Alice", "alice@example.com", "0"]
const [email, login] = await state.mapGetMultiple('user:123', ['email', 'loginCount']);

await state.mapIncrement('user:123', 'loginCount', 1);          // "1"
const exists = await state.mapExists('user:123', 'name');       // "1"
const length = await state.mapLength('user:123');               // "3"

await state.mapDelete('user:123', ['email']);

Scanning a Hash Table

mapScan iterates over fields using a cursor. Start at "0" and continue until the returned cursor is "0" again:

let cursor = '0';

do {
  const [nextCursor, fields] = await state.mapScan('user:123', cursor, 'name*', 10);
  cursor = nextCursor;
  console.log(fields); // alternating [field, value, field, value, ...]
} while (cursor !== '0');

List Operations

Ordered list storage backed by Redis lists.

Method Signature Returns Description
listAppend listAppend<T>(list, value) Promise<string> Add a value to the end of the list
listPrepend listPrepend<T>(list, value) Promise<string> Add a value to the beginning of the list
listEndPop listEndPop<T>(list, count?) Promise<T[]> Remove and return values from the end (default: 1)
listStartPop listStartPop<T>(list, count?) Promise<T[]> Remove and return values from the beginning (default: 1)
listRemove listRemove<T>(list, value, count?) Promise<string> Remove occurrences of a value. Positive count: from head; negative: from tail; 0: all
listTrim listTrim(list, startPos, endPos) Promise<string> ("OK") Trim the list to the specified range
listInsert listInsert<T>(list, before, pivot, value) Promise<string> Insert a value before (true) or after (false) a pivot value
listIndex listIndex<T>(list, position) Promise<T> Get the value at a position
listSet listSet<T>(list, position, value) Promise<string> ("OK") Set the value at a position
listLength listLength(list) Promise<string> Get the number of elements in the list
listRange listRange<T>(list, startPos?, endPos?) Promise<T[]> Get a range of values (default: entire list)
// Build an activity log
await state.listAppend('activity', { action: 'login', ts: Date.now() });
await state.listAppend('activity', { action: 'purchase', ts: Date.now() });
await state.listPrepend('activity', { action: 'signup', ts: Date.now() });

const length = await state.listLength('activity');          // "3"
const all = await state.listRange('activity');              // all items
const last10 = await state.listRange('activity', -10, -1); // last 10 items

// Keep only the most recent 100 entries
await state.listTrim('activity', -100, -1);

// Remove and process items
const [item] = await state.listStartPop('activity');
const [last] = await state.listEndPop('activity');

// Insert before a known value
await state.listInsert('activity', true, { action: 'login' }, { action: 'pre-login' });

// Get and update by position
const first = await state.listIndex('activity', 0);
await state.listSet('activity', 0, { action: 'updated', ts: Date.now() });

// Remove all occurrences of a specific value
await state.listRemove('activity', { action: 'login' }, 0);

Create searchable indexes over hash map data stored in State.

Method Signature Returns Description
createIndex createIndex(name, options) Promise<string> Create a search index
search search(index, query, options?) Promise<string> Search an index
dropIndex dropIndex(index, deleteDocs?) Promise<boolean> Delete an index. Pass true to also delete the indexed documents

Creating an Index

await state.createIndex('users-index', {
  on: 'HASH',
  prefix: {
    count: 1,
    prefixes: ['user:'],
  },
  schema: [
    { fieldName: 'name', type: 'TEXT', sortable: true },
    { fieldName: 'email', type: 'TEXT' },
    { fieldName: 'loginCount', type: 'NUMERIC', sortable: true },
  ],
});

Searching

// Full-text search
const results = await state.search('users-index', '@name:Alice');

// With options
const results = await state.search('users-index', '@name:Alice', {
  limit: { offset: 0, num: 10 },
  sortBy: { field: 'loginCount', order: 'DESC' },
  withScores: true,
});

Dropping an Index

// Drop index only (keep the underlying data)
await state.dropIndex('users-index');

// Drop index and delete all indexed documents
await state.dropIndex('users-index', true);