Skip to main content
SONZAI

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_typeValueExample
"text" (default)string"active", "silver"
"json"any JSON-serializable type{ "score": 340, "tier": "silver" }
"binary"base64-encoded bytesraw 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 StateInventory
ShapeSimple typed fieldStructured item with a KB-linked schema
Use caseCounters, flags, stringsItems with multiple properties (medications, holdings, pets)
SchemaYou define the key + content_typeDefined in your Knowledge Base
Best forenergy: 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 timestamp

List

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

MethodReturnsDescription
Create(ctx, agentID, opts)*CustomStateCreate a new state entry
Upsert(ctx, agentID, opts)*CustomStateCreate or replace by composite key
GetByKey(ctx, agentID, opts)*CustomStateFetch one state by key + scope
List(ctx, agentID, opts)*CustomStateListResponseList states, filtered by scope / user
Update(ctx, agentID, stateID, opts)*CustomStateUpdate 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 message

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

SituationUse
Single number or string per keyCustom State
A flag that is true/falseCustom State
A flat object with a few fieldsCustom State
An item with a schema defined in the Knowledge BaseInventory
A collection of items of the same type per userInventory

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

Next steps

On this page