Skip to main content
Standalone Memory Layer

Pattern 2: Post-Session Batch Processing

You own the entire conversation. Sonzai never sees it in real time. When the conversation ends, send the full transcript to /process or sessions.end({ messages }).

You own the entire conversation. Sonzai never sees it in real time. When the conversation ends, you send the full transcript to either /process or sessions.end({ messages }). Sonzai extracts facts, updates the user's behavioral profile, and makes the insights available via the API — ready for personalization, analytics, push notifications, or next-session context.

This pattern is ideal when Sonzai being in the hot path is undesirable (or impossible) — latency-sensitive real-time interactions, apps with their own LLM loop already in production, or cases where you want to process transcripts in bulk after the fact.

┌─────────────┐     ┌──────────────────┐     ┌──────────────┐
│  Your App   │     │   Sonzai API     │     │   Your LLM   │
└──────┬──────┘     └────────┬─────────┘     └──────┬───────┘
     │                     │                       │
     │  GET /context       │                       │
     │────────────────────>│ (optional pre-session │
     │  <── user profile ──│  personalization)     │
     │                     │                       │
     │  ══ Your conversation (Sonzai not involved) ═════════│
     │                     │                       │              │
     │  Chat ──────────────┼──────────────────────>│             │
     │  <── reply ─────────┼───────────────────────│             │
     │  [N turns, your loop, your tools]            │             │
     │                     │                       │             │
     │  ════════════════════════════════════════════════════════│
     │                     │                       │
     │  /process or sessions.end({ messages })     │
     │────────────────────>│── extract facts,      │
     │  (full transcript)  │   personality, mood,  │
     │                     │   habits, interests   │
     │  <── extractions ───│   (Sonzai LLM)        │
     │                     │                       │
     │  Use insights       │                       │
     │  (push notif,       │                       │
     │   dashboard update, │                       │
     │   exercises, etc.)  │                       │
     └─────────────────────┴───────────────────────┘

Pick one trigger, not both

/process and sessions.end({ messages }) are functionally equivalent for batch ingest — both extract facts and side-effects from the full transcript inline. Don't do both for the same transcript or extraction runs twice. Use /process if you want a single call (it auto-creates the session and surfaces the generated session_id in the response). Use sessions.start + sessions.end({ messages }) if you want explicit lifecycle, async polling, or session-scoped tools.

Core steps

Option A — /process only. One call. Auto-creates a session if you don't pass one. Returns the auto-generated session_id so you can correlate later.

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

const sonzai = new Sonzai({ apiKey: process.env.SONZAI_API_KEY! });

async function processTranscript(
agentId: string,
userId: string,
transcript: { role: "user" | "assistant" | "tool"; content: string; tool_calls?: any[] }[]
) {
const result = await sonzai.agents.process(agentId, {
  userId,
  messages: transcript,                          // tool messages allowed
  provider: "gemini",                            // optional override
  model: "gemini-3.1-flash-lite-preview",        // optional override
});

// result.session_id is the auto-created session id when none was passed.
// Read the extracted facts/mood/etc. via the dedicated endpoints below.
return result;
}

Option B — Explicit sessions.start + sessions.end({ messages }). Use this when you want async processing, session-scoped tools, or explicit lifecycle ownership.

async function processTranscript(
agentId: string,
userId: string,
transcript: { role: "user" | "assistant" | "tool"; content: string }[]
) {
const sessionId = `session-${Date.now()}`;

const session = await sonzai.agents.sessions.start(agentId, { userId, sessionId });

// Pass the full transcript on end — extraction happens here, not via /process.
// sessions.end({ messages }) is functionally equivalent to /process({ messages }).
const result = await session.end({
  messages: transcript,
  totalMessages: transcript.length,
});

return result;
}

Pick one. The two options are equivalent for fact extraction — chaining them just runs extraction twice on the same messages.

Use case: AI tutoring app

Before the session, pull the student's profile to personalize the curriculum. After the session, extract what was learned and generate targeted practice exercises. One call to /process is enough.

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

const sonzai = new Sonzai({ apiKey: process.env.SONZAI_API_KEY! });

async function beforeTutoringSession(agentId: string, studentId: string, topic: string) {
const ctx = await sonzai.agents.getContext(agentId, {
  userId: studentId,
  query: `${topic} concepts learned struggles difficulty level`,
});

const knownConcepts = (ctx.loaded_facts ?? [])
  .filter((f: any) => f.fact_type === "semantic")
  .map((f: any) => f.atomic_text);

const weakAreas = (ctx.habits ?? [])
  .filter((h: any) => h.label?.toLowerCase().includes("struggle"))
  .map((h: any) => h.label);

return { knownConcepts, weakAreas };
}

async function afterTutoringSession(
agentId: string,
studentId: string,
topic: string,
transcript: { role: "user" | "assistant"; content: string }[]
) {
await sonzai.agents.process(agentId, { userId: studentId, messages: transcript });

const [memory, interests, mood] = await Promise.all([
  sonzai.agents.memory.list(agentId, { userId: studentId }),
  sonzai.agents.getInterests(agentId, { userId: studentId }),
  sonzai.agents.getMood(agentId, { userId: studentId }),
]);

const allFacts = Object.values(memory.contents ?? {}).flat();
const conceptsLearned = allFacts
  .filter((f: any) => f.fact_type === "semantic")
  .map((f: any) => f.atomic_text);
const engagedTopics = (interests ?? []).map((i: any) => i.topic);
const confidenceLevel = mood?.valence ?? 0;

const exercises = await generateExercises({ topic, conceptsLearned, engagedTopics });

await sendStudentReport(studentId, {
  summary: `Covered: ${conceptsLearned.slice(0, 3).join(", ")}`,
  exercises,
  encouragement: confidenceLevel > 0.2 ? "Great session today!" : "Keep going — this takes practice.",
});

return { conceptsLearned, exercises };
}

Use case: gym / fitness app

Pull the user's fitness context before the workout for a personalized greeting. After the workout, send the session log to Sonzai to track habits, mood, and progress — without Sonzai ever being in the real-time exercise loop.

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

const sonzai = new Sonzai({ apiKey: process.env.SONZAI_API_KEY! });

async function beforeWorkout(agentId: string, userId: string): Promise<string> {
const ctx = await sonzai.agents.getContext(agentId, {
  userId,
  query: "fitness goals workout habits recent exercise progress",
});

const goals = (ctx.active_goals ?? []).map((g: any) => g.description);
const recentHabits = (ctx.habits ?? []).map((h: any) => h.label);

const greeting = await yourLLM.chat({
  system: "Generate a short, energetic workout motivation message (2 sentences max).",
  message: `User goals: ${goals.join(", ")}. Recent habits: ${recentHabits.join(", ")}.`,
});

await playVoiceMessage(userId, greeting);
return greeting;
}

async function afterWorkout(
agentId: string,
userId: string,
workoutTranscript: { role: "user" | "assistant"; content: string }[]
) {
await sonzai.agents.process(agentId, { userId, messages: workoutTranscript });

const [memory, habits, mood] = await Promise.all([
  sonzai.agents.memory.list(agentId, { userId }),
  sonzai.agents.listHabits(agentId, { userId }),
  sonzai.agents.getMood(agentId, { userId }),
]);

const habitsReinforced = habits ?? [];
const allFacts = Object.values(memory.contents ?? {}).flat();
const personalRecords = allFacts.filter((f: any) =>
  /record|pb|best|personal/i.test(f.atomic_text ?? "")
);

await sendPushNotification(userId, {
  title: "Workout complete",
  body: personalRecords.length > 0
    ? `New record: ${personalRecords[0].atomic_text}`
    : `Great session — ${habitsReinforced[0]?.label ?? "keep it up"}!`,
});
}

Use case: sales CRM intelligence

Your sales team runs calls through their existing tooling (Gong, Zoom, your own recorder). After each call, send the transcript to Sonzai to build a persistent customer profile.

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

const sonzai = new Sonzai({ apiKey: process.env.SONZAI_API_KEY! });

async function processSalesCall(
agentId: string,
customerId: string,
callId: string,
callTranscript: { role: "user" | "assistant"; content: string }[],
durationSeconds: number
) {
// Use the explicit lifecycle so we can pass durationSeconds.
const session = await sonzai.agents.sessions.start(agentId, {
  userId: customerId,
  sessionId: `call-${callId}`,
});

const result = await session.end({
  messages: callTranscript,
  totalMessages: callTranscript.length,
  durationSeconds,
});

// Read extractions back from the analytics endpoints.
const personality = await sonzai.agents.personality.get(agentId);

// ...build CRM update from result + dedicated read endpoints
return result;
}

Use case: language learning app

Track vocabulary mastered, grammar struggles, pronunciation patterns, and learning pace across lessons.

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

const sonzai = new Sonzai({ apiKey: process.env.SONZAI_API_KEY! });

async function afterLanguageLesson(
agentId: string,
studentId: string,
targetLanguage: string,
lessonTranscript: { role: "user" | "assistant"; content: string }[]
) {
await sonzai.agents.process(agentId, { userId: studentId, messages: lessonTranscript });

const [memory, interests, mood] = await Promise.all([
  sonzai.agents.memory.list(agentId, { userId: studentId }),
  sonzai.agents.getInterests(agentId, { userId: studentId }),
  sonzai.agents.getMood(agentId, { userId: studentId }),
]);

const allFacts = Object.values(memory.contents ?? {}).flat();
const newVocab = allFacts
  .filter((f: any) => f.fact_type === "semantic")
  .map((f: any) => f.atomic_text);
const engagementAreas = interests ?? [];
const confidenceDelta = mood?.valence ?? 0;

await updateLearningDashboard(studentId, {
  language: targetLanguage,
  vocabularyAdded: newVocab.length,
  newWords: newVocab,
  strongestArea: engagementAreas[0]?.topic ?? "conversation",
  confidenceTrend: confidenceDelta > 0.1 ? "↑" : confidenceDelta < -0.1 ? "↓" : "→",
});

return { newVocab, engagementAreas };
}

Use case: mental health & wellness journaling

Your app handles the journaling conversation. After each session, send to Sonzai to track mood trends, detect emotional breakthroughs, and surface proactive insights.

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

const sonzai = new Sonzai({ apiKey: process.env.SONZAI_API_KEY! });

async function afterJournalingSession(
agentId: string,
userId: string,
journalTranscript: { role: "user" | "assistant"; content: string }[]
) {
await sonzai.agents.process(agentId, { userId, messages: journalTranscript });

// After /process, extracted state is available on the read endpoints.
// Proactive outreach (check-ins, reminders) is exposed via the
// notifications resource — not on the /process response.
const [mood, notifications] = await Promise.all([
  sonzai.agents.getMood(agentId, { userId }),
  sonzai.agents.notifications.list(agentId),
]);

if ((mood?.valence ?? 0) < -0.4) {
  await sendWellnessAlert(userId, {
    message: "It sounds like you're going through a tough time. We're here for you.",
  });
}

for (const notif of notifications) {
  if (notif.user_id === userId) {
    await scheduleReminder(userId, notif.generated_message, notif.scheduled_for);
  }
}

await updateMoodDashboard(userId, { valence: mood?.valence, energy: mood?.arousal });
}

Next

On this page