Custom State
Per-user counters, flags, and strings the agent reads and writes — energy, currency, progress flags, game state — with a stable schema you control.
Custom State is simple structured per-user data the agent can read and modify during conversations. Use it for counters, flags, or any state your product tracks per user. Unlike memory (which the platform extracts from conversation text), Custom State is data you write explicitly from your backend — and the agent sees it immediately.
What you can build with it
- Game loops — energy, currency, turn counters, progression flags
- Feature flags — per-user toggles for experimental features
- Session-scoped state — timers, streaks, active-quest identifiers
- Progress markers — "completed onboarding", "has premium", "saw-tutorial-X"
- Rate limits / quotas — message counts, daily-action remaining
Quickstart
Create an energy state for a user, starting at 100.
import { Sonzai } from "@sonzai-labs/agents";
const client = new Sonzai({ apiKey: process.env.SONZAI_API_KEY! });
await client.agents.customStates.create("agent-id", {
key: "energy",
value: 100,
scope: "user",
userId: "user-123",
});Core concepts
Typed values
Every state has a content_type that tells the platform how to interpret value:
content_type | Value | Example |
|---|---|---|
"text" (default) | string | "active", "silver" |
"json" | any JSON-serializable type | { "score": 340, "tier": "silver" } |
"binary" | base64-encoded bytes | raw binary payloads |
Scoping model
Global State
Per Instance — Shared across all users in an instance. Use for environment configuration, agent status, or global event flags.
Per-User State
Per Instance + User — Scoped to one user. Use for energy, currency, progress, preferences, and any per-player data.
Instances
All states are scoped to an instanceId — one deployment context of your agent (e.g. a workspace or game world). Omit instanceId to use the default instance. See Instances for details.
Agent reads and writes
When the agent has access to custom states, it reads current state at the start of each conversation via the get_custom_state tool — no prompt injection required. The agent can also update state during a conversation if you define a Custom Tool that calls your backend.
Distinct from Inventory
| Custom State | Inventory | |
|---|---|---|
| Shape | Simple typed field | Structured item with a KB-linked schema |
| Use case | Counters, flags, strings | Items with multiple properties (medications, holdings, pets) |
| Schema | You define the key + content_type | Defined in your Knowledge Base |
| Best for | energy: 80, tier: "gold" | { name: "Metformin", dose_mg: 500, frequency: "twice daily" } |
Use Custom State for primitives and simple objects. Reach for Inventory when items have their own identity, multiple typed fields, and a shared schema across users.
Full API
Create
// Global state (shared across all users in an instance)
await client.agents.customStates.create("agent-id", {
key: "current_status",
value: "Processing requests",
scope: "global",
contentType: "text",
instanceId: "workspace-1",
});
// Per-user state
await client.agents.customStates.create("agent-id", {
key: "energy",
value: 100,
scope: "user",
contentType: "json",
userId: "user-123",
});Upsert (create or update by key)
Upsert creates the state if the key doesn't exist, or replaces the value if it does. Idempotent — safe to call on every update cycle from your backend.
await client.agents.customStates.upsert("agent-id", {
key: "energy",
value: 80,
scope: "user",
userId: "user-123",
});Get by key
Retrieve a specific state by its composite key (key + scope + user_id + instance_id).
const state = await client.agents.customStates.getByKey("agent-id", {
key: "energy",
scope: "user",
userId: "user-123",
});
console.log(state.value); // 80
console.log(state.updatedAt); // ISO timestampList
Return all states for an agent, optionally filtered by scope or user.
// All global states for an instance
const globals = await client.agents.customStates.list("agent-id", {
scope: "global",
instanceId: "workspace-1",
});
// All per-user states for a specific user
const userStates = await client.agents.customStates.list("agent-id", {
scope: "user",
userId: "user-123",
});Update by state ID
Update a state you already have the state_id for. Only value and content_type can be changed.
await client.agents.customStates.update("agent-id", stateId, {
value: 60,
});Delete
Delete by state ID or by composite key.
// Delete by key
await client.agents.customStates.deleteByKey("agent-id", {
key: "energy",
scope: "user",
userId: "user-123",
});
// Delete by state_id
await client.agents.customStates.delete("agent-id", stateId);Method summary
| Method | Returns | Description |
|---|---|---|
Create(ctx, agentID, opts) | *CustomState | Create a new state entry |
Upsert(ctx, agentID, opts) | *CustomState | Create or replace by composite key |
GetByKey(ctx, agentID, opts) | *CustomState | Fetch one state by key + scope |
List(ctx, agentID, opts) | *CustomStateListResponse | List states, filtered by scope / user |
Update(ctx, agentID, stateID, opts) | *CustomState | Update value by state ID |
Delete(ctx, agentID, stateID) | — | Delete by state ID |
DeleteByKey(ctx, agentID, opts) | — | Delete by composite key |
Combines with other features
With Custom Tools — tools that read and write state
Define a tool that lets the agent trigger a state change from inside a conversation. Your backend executes the tool call and calls upsert to apply the new value.
await client.agents.sessions.setTools("agent-id", "session-id", [
{
name: "spend_energy",
description: "Deduct energy from the user. Call when the user takes an action that costs energy.",
parameters: {
type: "object",
properties: {
amount: { type: "number", description: "Energy to deduct (1–50)" },
},
required: ["amount"],
},
},
]);
// In your tool handler:
// 1. Receive externalToolCall { name: "spend_energy", arguments: { amount: 10 } }
// 2. Read current energy with getByKey
// 3. Upsert the new value
// 4. Return the result in the next chat messageWith Inventory — when state is structured, use inventory
Custom State is the right tool for primitive values and simple flat objects: energy: 80, tier: "gold", onboarding_complete: true. When a piece of data has its own identity, multiple typed properties, and a shared schema across users — a medication, a stock holding, a pet — use Inventory instead.
| Situation | Use |
|---|---|
| Single number or string per key | Custom State |
| A flag that is true/false | Custom State |
| A flat object with a few fields | Custom State |
| An item with a schema defined in the Knowledge Base | Inventory |
| A collection of items of the same type per user | Inventory |
With Sessions — session-scoped vs persistent state
Custom State is persistent by default — it survives across sessions and is visible in every future conversation. If you need state that only exists for the duration of one conversation (a temporary form-fill context, a one-time confirmation token), scope it at the session level instead by passing it in the chat request's context fields rather than writing it as a Custom State.
Tutorials
- Custom States walkthrough — end-to-end example: create, upsert, read during chat, trigger events on state changes
Next steps
Priming
Bootstrap agents with user context — display names, demographic facts, chat transcripts from legacy systems, or bulk imports from CSV/CRM.
Sessions
A session is one continuous conversation between an agent and a user. Sonzai uses session boundaries to scope fact extraction, consolidation, and recall.