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
- Pattern 1: Memory Middleware (real-time) — when you want Sonzai-enriched context per-turn (and tool calling / multimodal handling)
- Endpoint walkthrough — full reference for
sessions.start,context,turn,process,end, and read endpoints - KB & limitations — knowledge base behavior in standalone mode
Pattern 1: Memory Middleware (Real-Time)
Per-turn integration. You control the LLM and tool-calling loop; Sonzai handles what that LLM knows about the user. Includes tool calling and multimodal/image handling.
Endpoint Walkthrough
Reference for sessions.start, session.context, session.turn, /process, sessions.end — plus the read endpoints that surface the extracted behavioral data.