Skip to main content

Serverless Functions

Serverless Functions let you run your own TypeScript inside Cortex — triggered by domain events or invoked over HTTP — without standing up any infrastructure. They’re the right tool when you want custom logic close to your voice data: email a meeting summary, push a transcript to your CRM, escalate a toxic message, or expose a small HTTP endpoint.

You write the code in the dashboard editor (full IntelliSense), publish it, and Cortex transpiles, snapshots and deploys it to ODIN Fleet. Events are delivered to it automatically.

A function at a glance

A function exports an HTTP handler and any number of event handlers:

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

// Event handler — the name maps 1:1 to the event, so exporting it subscribes
// the function automatically. No event list to maintain.
exports.onMessageCreated = async (event: MessageCreatedEvent, ctx: EventContext) => {
console.log("New message:", event.payload.data.content);
};

Named event handlers

Subscribe to a domain event simply by exporting a handler named after it. The rule: split the event type on ., PascalCase each segment, and prefix on.

EventHandler export
message.createdonMessageCreated
participant.joinedonParticipantJoined
gathering.member.joinedonGatheringMemberJoined
session.annotation.createdonSessionAnnotationCreated

Each handler’s event argument is fully typed, so your editor knows the shape of event.payload.data. Exporting the handler is all it takes — there’s no separate subscription list to keep in sync.

Escape hatch: wildcards

For wildcard subscriptions (message.*, *) or platform/Fleet events that have no named handler, export an eventScopes array plus a generic eventHandler:

exports.eventScopes = ["message.*"];
exports.eventHandler = async (event, ctx) => {
console.log(`Received: ${event.type}`, event.payload);
};

ctx.cortex — a pre-authenticated client

Every handler receives a ctx whose ctx.cortex is a ready-to-use, project-scoped Cortex SDK client. No keys, no setup — call the API straight away:

exports.onSessionAnnotationCreated = async (event, ctx) => {
// Look up the session this annotation belongs to
const session = await ctx.cortex.sessions.get(event.payload.data.sessionId);
console.log("Session title:", session.title);
};

The runtime authenticates ctx.cortex for you and keeps the underlying credential out of reach of your code — you never handle a secret. (Available on runtime v2 and newer.)

Dependencies & environment variables

Functions can declare npm dependencies and environment variables — configured per project. Declared packages are available to import/require, and their types are loaded into the editor so IntelliSense works.

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

await project.functions.updateEnvVars({
envVars: [
{ key: "MJ_APIKEY_PUBLIC", value: "..." },
{ key: "MJ_APIKEY_PRIVATE", value: "..." },
],
});

Environment variables are encrypted at rest and injected into the runtime; read them with process.env. This is exactly what the “email a meeting summary” guide uses (node-mailjet + Mailjet credentials).

Preinstalled SDKs

The runtime image preinstalls @4players/odin-cortex and @4players/fleet, so you can import them without declaring them as dependencies. ctx.cortex is already an authenticated instance of the Cortex SDK.

The lifecycle: edit → publish → deploy

  1. Activate Functions for the project once — Cortex provisions the Fleet resources it needs.
  2. Create & edit a function in the dashboard. The editor type-checks your TypeScript and blocks publishing while there are errors.
  3. Publish — Cortex transpiles your TypeScript to JavaScript, snapshots it as an immutable version, and analyzes which events it subscribes to.
  4. Deploy the version to Fleet. Cortex records the running endpoint and wires up event delivery.
const fn = await project.functions.create({ name: "on-summary", code: "/* ... */" });
await fn.publish({ changelog: "Initial version" });
await fn.deploy();

Invoking over HTTP

A deployed function is reachable at a per-project URL and can be configured to be public, require an API key, or require a signed request:

POST https://<functions-host>/{projectId}/{functionSlug}

The handler export receives the request (method, path, query, headers, body) and returns a { statusCode, body }.

Runtime versions

The function runtime is published as immutable versions (v1, v2, …) and each project is pinned to a specific one, so a platform update can never silently change your function’s behavior. When a newer runtime is available, the dashboard shows an upgrade option; you upgrade when you’re ready.

Errors & debugging

When a handler throws, the response (HTTP or event) includes the error message, a trimmed stack, and a parsed location (file, line, column) pointing at the failing line in your original TypeScript — so you can jump straight to the problem. console.log output is captured per invocation.