Skip to main content
Sonzai Docs
Tutorial·~15 min

Custom States & Workflow Events

Custom states let you store arbitrary structured data alongside an agent's memory — think performance metrics, task completion, milestone flags, or any application-specific context. Workflow events let the agent react to things that happen outside the conversation (milestone reached, task completed, goal achieved). By the end you'll know how to read, write, and listen for both.

What you'll build

  • → A custom state that tracks a user's progress score and tier, updated after every session
  • → A workflow event trigger that fires when the user hits a milestone, causing the agent to react
  • → A bulk-read of all custom states for a user's dashboard
  • → An upsert pattern for idempotent state updates from your backend

What Are Custom States?

A custom state is a key-value record scoped to an agent + user (or just an agent). Values can be any JSON-serializable type: strings, numbers, booleans, arrays, or nested objects.

Unlike memory (which is unstructured text extracted from conversations), custom states are structured data you write explicitly from your backend. The agent can read them via the get_custom_state tool during conversation, so it always knows the user's current tier, streak, balance, etc.

Custom States (you write)

  • · Structured JSON data
  • · Your backend controls it
  • · Task progress, scores, milestones
  • · Updated via SDK or REST

Memory (auto-extracted)

  • · Free-form text facts
  • · Platform extracts it from chat
  • · Preferences, events, goals
  • · Auto-updated after each message

1. Create a Custom State

Call create the first time you set a state for a user. Subsequent writes should use upsert (see Step 3) for idempotent updates.

import { Sonzai } from "@sonzai-labs/agents";

const client = new Sonzai({ apiKey: process.env.SONZAI_API_KEY! });
const AGENT_ID = "agent_abc";
const USER_ID  = "user_123";

const state = await client.agents.customStates.create(AGENT_ID, {
  userId: USER_ID,
  key:    "user_progress",
  value: {
    tier:            "silver",
    score:           2340,
    score_to_next:   3000,
    streak_days:     12,
    milestones:      ["first_chat", "50_tasks", "7_day_streak"],
  },
});

console.log("Created:", state.state_id, state.key);

2. Read State Back During Chat

When the agent has access to the get_custom_state tool (enabled automatically when custom states exist), it fetches current state at the start of a conversation. You can also read it from your backend at any time.

// Read by key from your backend
const state = await client.agents.customStates.getByKey(AGENT_ID, {
  userId: USER_ID,
  key: "user_progress",
});

const progress = state.value as {
  tier: string; score: number; score_to_next: number; streak_days: number;
};

console.log(`${progress.tier} tier · ${progress.score}/${progress.score_to_next} pts · ${progress.streak_days}-day streak`);

During conversation, the agent calls get_custom_state("user_progress") and incorporates the progress data into its responses naturally — no prompt injection required.

3. Upsert State Idempotently

Use upsert from your backend whenever the user's state changes — after a session ends, after a purchase, or on a schedule. upsert creates the state if it doesn't exist, or replaces it if it does.

// Called after each work session ends
async function onSessionEnd(userId: string, sessionScore: number) {
  const current = await client.agents.customStates.getByKey(AGENT_ID, {
    userId,
    key: "user_progress",
  }).catch(() => null);

  const tiers = ["bronze", "silver", "gold", "platinum"];
  const prev = (current?.value ?? { tier: "bronze", score: 0, score_to_next: 1000, streak_days: 0 }) as {
    tier: string; score: number; score_to_next: number; streak_days: number; milestones: string[];
  };

  const newScore    = prev.score + sessionScore;
  const promoted    = newScore >= prev.score_to_next;
  const tierIndex   = tiers.indexOf(prev.tier);
  const newTier     = promoted ? (tiers[tierIndex + 1] ?? prev.tier) : prev.tier;

  await client.agents.customStates.upsert(AGENT_ID, {
    userId,
    key: "user_progress",
    value: {
      tier:          newTier,
      score:         promoted ? newScore - prev.score_to_next : newScore,
      score_to_next: promoted ? prev.score_to_next * 1.5 : prev.score_to_next,
      streak_days:   prev.streak_days + 1,
      milestones:    prev.milestones,
    },
  });

  if (promoted) {
    // Notify the agent so it can congratulate the user next session
    await client.agents.triggerGameEvent(AGENT_ID, {
      userId,
      eventType: "tier_promotion",
      payload: { new_tier: newTier, previous_tier: prev.tier },
    });
  }
}

4. Trigger a Workflow Event

Workflow events let your backend tell the agent about something that happened outside the conversation. The next time the user chats, the agent sees the pending event and reacts naturally.

// Trigger from your backend when something notable happens
await client.agents.triggerGameEvent(AGENT_ID, {
  userId: USER_ID,
  eventType: "task_complete",
  payload: {
    task_name:     "Q1 Revenue Analysis",
    deliverable:   "Revenue Report",
    category:      "Analytics",
    time_taken:    "3h 42m",
  },
});

// Next time the user opens a conversation:
// Agent: "I see you finished the Q1 Revenue Analysis! That report is a key
//         deliverable. Want to discuss the findings or start the next task?"

Event delivery

Workflow events are queued and delivered on the next conversation turn. They don't interrupt an active session. The agent consumes pending events at the start of the next chat or chatStream call and incorporates them into its opening message or first response.

5. List All States for a User

Useful for building admin dashboards, user profile pages, or debugging. Returns all custom states for an agent + user pair.

const { states } = await client.agents.customStates.list(AGENT_ID, {
  userId: USER_ID,
});

for (const state of states) {
  console.log(`[${state.key}]`, JSON.stringify(state.value, null, 2));
}
// [user_progress]  { tier: "silver", score: 340, ... }
// [preferences]    { theme: "dark", notifications: true }
// [daily_summary]  { last_active: "2025-03-20", sessions_today: 2 }

6. Update a Specific Field

Use update when you want to change a state by its state_id. Unlike upsert, update does a partial merge — you only need to pass the fields you want to change.

// Add a milestone without overwriting the whole state
const state = await client.agents.customStates.getByKey(AGENT_ID, {
  userId: USER_ID,
  key: "user_progress",
});

const progress = state.value as { milestones: string[]; [k: string]: unknown };

await client.agents.customStates.update(AGENT_ID, state.state_id, {
  value: {
    ...progress,
    milestones: [...progress.milestones, "100_tasks"],
  },
});

7. Delete State

Delete a state by its ID or by key. On next conversation, the agent won't have access to it.

// Delete by key (finds and removes the state)
await client.agents.customStates.deleteByKey(AGENT_ID, {
  userId: USER_ID,
  key: "user_progress",
});

// Or delete by state_id if you already have it
await client.agents.customStates.delete(AGENT_ID, stateId);

Common Patterns

Onboarding state

Create a onboarding state on sign-up with { step: 0, completed: false }. The agent checks it at the start of early conversations and guides the user through setup naturally.

Subscription context

Store { plan: 'pro', expires_at: '...' } so the agent knows which features to offer or upsell without you having to pass it in every chat request.

Daily summary cache

Write a daily_summary state at the end of each day with key metrics. The agent opens the next-day conversation referencing the user's activity — "Yesterday you completed 3 tasks and hit a 12-day streak. Ready to keep going?"

Next Steps

  • → Read the Custom States & Tools reference for the full API
  • → Add Inventory tracking to enrich states with asset portfolios
  • → Set up webhooks to get notified when the agent triggers specific events back to your backend
  • → Explore Personality to see how events influence the agent's emotional evolution