Skip to main content

Email a meeting summary

This guide builds a complete serverless function that emails a formatted summary whenever a session produces one. It’s a great showcase of the three things that make functions powerful:

  • a named event handler that subscribes automatically,
  • the pre-authenticated ctx.cortex client,
  • an npm dependency (node-mailjet) and environment variables for credentials.

What it does​

When a session summary is generated, Cortex emits a session.annotation.created event. Our function handles it, looks up the session to get its title, and sends the summary by email through Mailjet.

You’ll need:

  • Functions activated for your project, on runtime v6 (the current version — see Runtime versions).
  • The session summary plugin enabled for the project, so a summary is produced when a session ends.
  • A Mailjet account with a verified sender address.

1. Add the dependency​

In the function’s settings, declare the npm package:

await project.functions.updateDependencies({
dependencies: [{ name: "node-mailjet", version: "^6.0.0" }],
});

Once declared, its types are available in the editor and you can import it.

2. Add environment variables​

Add your Mailjet API credentials as environment variables. Mark the credentials with isSecret: true so they are encrypted at rest and masked in the dashboard. Pick any names you like — except the reserved CORTEX_ prefix:

await project.functions.updateEnvVars({
envVars: [
{ key: "MJ_APIKEY_PUBLIC", value: "your-mailjet-public-key", isSecret: true },
{ key: "MJ_APIKEY_PRIVATE", value: "your-mailjet-private-key", isSecret: true },
{ key: "SUMMARY_FROM", value: "meetings@your-company.example" }, // a verified Mailjet sender
{ key: "SUMMARY_TO", value: "team@your-company.example" },
],
});

Read them in your code with process.env.

3. Write the function​

import { Client, SendEmailV3_1, LibraryResponse } from 'node-mailjet';

/**
* Cortex Serverless Function (TypeScript)
*
* Functions are written in TypeScript with full IntelliSense for events and context.
* A function can handle HTTP requests and/or Cortex events.
*/

// =============================================================================
// HTTP Handler (Required) — called for HTTP requests to your function URL
// =============================================================================
exports.handler = async (event: HttpEvent, ctx: HttpContext) => {
return {
statusCode: 200,
body: {
message: 'Hello from your function!',
timestamp: new Date().toISOString(),
},
};
};

// =============================================================================
// Event Handler — exporting on<Event> subscribes the function automatically.
// session.annotation.created → onSessionAnnotationCreated
// =============================================================================
exports.onSessionAnnotationCreated = async (
event: SessionAnnotationCreatedEvent,
ctx: EventContext,
) => {
const { sessionId, type, content } = event.payload.data;

// Session annotations can come from several plugins — only handle summaries.
if (type !== 'summary') {
return;
}

const mailjet = new Client({
apiKey: process.env.MJ_APIKEY_PUBLIC,
apiSecret: process.env.MJ_APIKEY_PRIVATE,
});

// ctx.cortex is a pre-authenticated, project-scoped Cortex client.
const session = await ctx.cortex.sessions.get(sessionId);

const data: SendEmailV3_1.Body = {
Messages: [
{
From: { Email: process.env.SUMMARY_FROM as string },
To: [{ Email: process.env.SUMMARY_TO as string }],
Subject: 'Meeting Summary: ' + session.title,
HTMLPart: content.shareableHtml as string,
TextPart: content.shareablePlainText as string,
},
],
};

const result: LibraryResponse<SendEmailV3_1.Response> = await mailjet
.post('send', { version: 'v3.1' })
.request(data);

const { Status } = result.body.Messages[0];
console.log('Mailjet status:', Status);
};

A few things worth noting:

  • The handler name is the subscription. Exporting onSessionAnnotationCreated is all it takes to receive session.annotation.created events — there’s no separate event list.
  • event.payload.data is the event’s data. For a session annotation that is the annotationId, the sessionId, the annotation type (summary here) and the plugin-defined content — for a summary, ready-made shareableHtml and shareablePlainText versions.
  • ctx.cortex needs no setup — ctx.cortex.sessions.get(...) just works, and the returned object exposes its fields directly (e.g. session.title).

4. Publish and deploy​

const fn = await project.functions.get(functionId);
await fn.publish({ changelog: "Email summary on session annotation" });
await fn.deploy();

On publish, Cortex transpiles your TypeScript, snapshots it as an immutable version, and detects that it subscribes to session.annotation.created. On deploy, it goes live on ODIN Fleet and starts receiving events. (The function also keeps its HTTP handler, reachable at the invokeUrl returned by deploy().)

Test it​

Generate a summary on a session and the function fires:

const session = await project.sessions.get(sessionId);
await session.regenerateSummary(); // emits session.annotation.created

Check the function logs for the Mailjet status, and your inbox for the email. If nothing arrives, check that the project is on runtime v6 and that the function is deployed — and remember that a summary needs a session with some transcript to summarize.

Make handlers idempotent

Event delivery is durable and may retry, so a handler can run more than once for the same event. If duplicate emails would be a problem, dedupe on event.id.