Summarize every conversation and email the results, automatically
How to turn "someone has to read every transcript and decide what matters" into "the backend tells you, automatically, the moment a conversation ends." Two lifecycle rules do the whole job, with zero polling and zero app-side glue. One rule extracts a summary and topic the instant a session ends. A second rule emails a human the instant that extraction finishes. This recipe is the hands-on walkthrough; Lifecycle Rules is the terse field-by-field reference this recipe links back to instead of repeating.
The mental model
Three pieces, always in this order:
| Piece | What it is | Example |
|---|---|---|
| Event | Something the backend already noticed happened to a thread |
session_ended (a conversation just ended), analysis_updated (an insight just got written) |
| Rule | {eventType, objectType, eventConditions?, action} you create once via mgmt.lifecycle.create() |
"when a session_ended fires, run this action" |
| Action | What runs automatically, server-side, when the rule matches | triggerInsightSettingsKai (extract structured data with an LLM, per a reusable InsightSettings definition) or sendInsightEmail (email a human). See The action types for every action |
The two actions chain naturally. triggerInsightSettingsKai writes its results into the thread's thread_metadata.analysis. That write is itself an analysis_updated event, which a second rule can react to. That's the whole recipe below: rule 1 reacts to session_ended and produces analysis, rule 2 reacts to analysis_updated and emails it.
Recipe A: Extract a summary the moment a session ends
triggerInsightSettingsKai doesn't take the insight's definition inline. It references one or more InsightSettings entities by id. Define those first:
const topic = await mgmt.insightSettings.create({
key: 'TOPIC', title: 'Topic', prompt: 'What was the main topic of this conversation, in 1-3 words?', valueType: 'string',
}, adminKs);
const nextStep = await mgmt.insightSettings.create({
key: 'CUSTOM', title: 'Next step', prompt: 'One actionable next step for the support team, or "none".', valueType: 'string',
}, adminKs);
Then reference both from the rule:
await mgmt.lifecycle.create({
name: 'Summarize on session end',
systemName: 'auto_summary_v1',
eventType: 'session_ended',
objectType: 'thread',
eventConditions: [{ field: 'object.agent_id', operator: 'eq', value: '<agent-uuid>' }],
action: { actionType: 'triggerInsightSettingsKai', insightSettingsIds: [topic.id, nextStep.id] },
}, adminKs);
Notice there's no SUMMARY insight setting here. Every partner already gets one for free, see Every session already gets a SUMMARY, for free for why. TOPIC and CUSTOM are both included here because Recipe B's email preset needs all three of SUMMARY/TOPIC/CUSTOM present, see the gotcha below.
Every InsightSettings entity needs a prompt and a valueType. Omit either and insightSettings.create throws bad_request before any request. Every conversation gets a structured recap with zero app-side code: no cron job polling for "threads that just ended," no app server involved at all.
Recipe B: Email a human the moment that analysis lands
await mgmt.lifecycle.create({
name: 'Email support lead on analysis update',
systemName: 'analysis_alert_v1',
eventType: 'analysis_updated',
objectType: 'thread',
eventConditions: [
{ field: 'object.agent_id', operator: 'eq', value: '<agent-uuid>' },
{ field: 'changed_keys', operator: 'has_all', value: ['SUMMARY', 'TOPIC', 'CUSTOM'] },
],
action: {
actionType: 'sendInsightEmail',
recipients: ['support-lead-kaltura-user-id'],
presetType: 'conversationInsightExample',
},
}, adminKs);
Three things about this action that aren't obvious from the field names:
- It only fires on
analysis_updated. Attach it to asession_endedrule and it does nothing. Nothing errors, nothing sends. recipientsare Kaltura user IDs, not raw email addresses. The email address comes from that user's Kaltura profile ({USER.email}). If your account's convention is to use the email address itself as the Kaltura user ID (common on many accounts), a recipient string that looks like an email still works. That's only because it's also a valid user ID there, not because this field accepts arbitrary email strings.presetType: 'conversationInsightExample'is the zero-setup path. It needs no template setup. An explicittemplateId(instead ofpresetType) points at a template you author yourself viamgmt.emailTemplates, with your own subject, body, and branding, instead of the preset's fixed layout.
The gotcha that will bite you first: token mismatch
conversationInsightExample's template needs three insight values by key: SUMMARY, TOPIC, and CUSTOM (exactly those keys, case-sensitive). AGENTNAME, CTAURL, and USER are filled in automatically, so you never provide those. If the thread's analysis doesn't have all three of SUMMARY/TOPIC/CUSTOM, the email send is skipped at run time with no error. mgmt.lifecycle.audit(ks) flags the mismatch as template_tokens_unproduced. See Audit your rules. SUMMARY comes free from the always-on system preset (see Lifecycle Rules). Recipe A's own InsightSettings above supply the other two, with key:'TOPIC' and key:'CUSTOM' matching exactly what the template looks for.
Pick whatever prompt fits your use case for CUSTOM. The template only cares about the key, never the prompt text.
Scoping the alert to one agent
Both recipes use eventConditions with object.agent_id. The SDK requires it: see Scope every rule to an agent. The rule only matches conversations started with a KS that carries the agent id. A token minted without one leaves every thread's agent_id as "default", so it can never match. Mint with createAgentToken({ agentId }) (see Conversation token or agent token?), and see Lifecycle Rules's scoping section for the full explanation.
Reading the results back
Once triggerInsightSettingsKai finishes (it runs asynchronously, so the values are not there the moment the session ends), the values land in the thread's thread_metadata.analysis, keyed by each InsightSettings.key:
const thread = await mgmt.threads.get(threadId, adminKs);
console.log(thread.thread_metadata.analysis); // { SUMMARY: '...', TOPIC: '...', CUSTOM: '...' }
You only need this for a dashboard or a "show me the recap" UI. If all you want is the email, Recipe B already handles delivery. You don't need to read this back yourself.
Test both rules in seconds, without waiting for a real event
Waiting for a real conversation to end and a real analysis to land is not how you iterate on rule design. mgmt.lifecycle.match() answers "if this event happened right now, which rules would fire?" against data you make up, instantly, with no thread and no waiting:
const { matchedRules } = await mgmt.lifecycle.match(
'thread', 'session_ended',
{ object: { agent_id: 'agent-1', thread_id: 'thread-1', user_id: 'user-1', origin: 0 } },
adminKs,
);
object needs agent_id, thread_id, user_id and origin, see Discovery and dry-run testing. Omit one and it 400s naming the missing path. Expect to see your own rule nested inside a grouped matchedRules[] entry's rules[] array, together with the built-in summary rule.
Every partner has that built-in rule by default. It's not something you configured (see Lifecycle Rules's note on grouped matches). Run this after creating each rule to confirm it matches before you ever touch a real conversation.
Minimal runnable example
examples/lifecycle-insights-and-email.mjs lists the filterable fields, creates the InsightSettings and both rules above, dry-run tests each with match(), audits them with lifecycle.audit(), then cleans up. All against the real API, no waiting for a real session to end:
export AGENTIC_PARTNER_ID=1234567
export AGENTIC_ADMIN_SECRET=your_admin_secret_here
export DEMO_AGENT_ID=<your-agent-id>
node examples/lifecycle-insights-and-email.mjs
DEMO_AGENT_ID defaults to a placeholder agent, which the audit flags as agent_not_found. Set it to a real agent id.
Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
insightSettings.create throws bad_request: valueType must be one of... |
valueType omitted or not one of the supported types |
Add a valid valueType to every InsightSettings entity |
lifecycle.create 400s: action.insightSettingsIds.0 not found |
An id in insightSettingsIds doesn't exist, or belongs to a different partner |
Create the InsightSettings entity first and use the id it returns |
A rule references a real insightSettingsIds id but that insight never gets extracted |
The referenced InsightSettings entity has status:'disabled' |
mgmt.insightSettings.update(id, {status:'active'}, ks) |
sendInsightEmail rule never sends anything, no error at run time |
The paired triggerInsightSettingsKai rule doesn't produce every key the preset needs |
Match Recipe A's InsightSettings.key values to the preset's requirements exactly (see the gotcha above). mgmt.lifecycle.audit(ks) flags it as template_tokens_unproduced. See Audit your rules |
A sendInsightEmail rule attached to session_ended does nothing |
That action only fires on analysis_updated |
Change eventType to analysis_updated |
lifecycle.create or update throws lifecycle_unscoped |
A thread rule has no object.agent_id condition (eq with an id, or in with a non-empty array of ids), so it would run for every agent on the partner |
Add {field:'object.agent_id', operator:'eq', value:'<agent-uuid>'}, or pass { partnerWide: true } to run on every agent on purpose |
lifecycle.create or update throws lifecycle_email_unfiltered |
A sendInsightEmail rule on analysis_updated has no changed_keys condition, so it would send on every analysis update |
Add {field:'changed_keys', operator:'has_all', value:['SUMMARY', ...]}, or pass { emailOnEveryUpdate: true } to send on every update on purpose |
lifecycle.create 400s on a badge and err.body.message talks about the score range |
A type:'boolean' badge has no score |
Set score on every boolean badge. See The action types |
eventConditions on object.agent_id never matches |
The thread was created with a token minted without an agent id | Mint with mgmt.sessions.createAgentToken({agentId}) |
lifecycle.match 400s: eventData.object.user_id: Invalid input... |
A required field missing from the dry-run object |
Pass agent_id, thread_id, user_id and origin: 0 together |
A rule matches in match but never runs on a real thread |
The thread's origin is not 0 |
Actions run only for standard conversations. See Discovery and dry-run testing |
insightSettings.create throws bad_request for a missing prompt |
Every InsightSettings entity needs a prompt |
See InsightSettings |
You want to change the built-in SUMMARY insight's prompt |
No field changes it. See Every session already gets a SUMMARY | Give your own insight settings distinct keys and use those instead |
| A rule saved fine but nothing happens, or you inherited a partner's rules | A setting, template or agent the rule points at was deleted or disabled after it was saved | Run mgmt.lifecycle.audit(ks). Every finding names the rule and the fix. See Audit your rules |
You create a triggerDtcKai rule and expect to pass fields on the action |
It takes no caller-supplied fields. See The action types | Configure intellectConfig.user_properties_forms on the intellect instead; leave the action {actionType:'triggerDtcKai'} |
Related docs
| Doc | What it adds |
|---|---|
| Lifecycle Rules | The full field-by-field reference: every rule shape, the action types, InsightSettings, the full CRUD + discovery method table |
examples/lifecycle-insights-and-email.mjs |
The runnable example this recipe walks through |
| Getting Started | Where configId/agentId and the admin token in the examples above come from |