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);
Full-Text Search
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);