Write event handlers for your custom code agent and understand the context object that Novu passes on every turn, including messages, users, and providers.
Novu gives your handler the same building blocks whether the message came from Slack, email, or another provider, and whether you call an LLM or plain TypeScript.
Event handlers are functions that respond to events in a conversation. Your agent can respond to these event types:
Handler
When it runs
Common use case
onMessage
A user sends a message in the conversation
Process the message and reply
onAction
A user clicks a button or selects a value in an interactive card
Handle form submissions, button clicks, dropdown selections
onReaction
A user adds or removes a reaction
Capture feedback or trigger a follow-up
onResolve
The conversation is marked as resolved
Clean up state, log analytics, or send a summary
onToolApproval
A user approves or denies a gated tool call
Run or skip the tool, audit the decision. See Tool approval
onError
A turn fails (handler throw, promise rejection, delivery error)
Log, suppress, send custom copy, or rely on Novu’s generic reply
Handlers are where the communication layer connects to your application logic. For example, an onMessage handler receives the user’s message, passes conversation context to an LLM or custom function, and sends the response back through Novu.
Each event handler receives a context object with the information needed to understand the current event and respond. Depending on the event type, it can include:
The incoming message
The current conversation state and metadata
The resolved subscriber (when available)
Recent conversation history
Provider information
Platform-specific details, such as thread or channel identifiers
Methods for replying, updating metadata, triggering workflows, or resolving the conversation
The context object is how your code talks to Novu. You do not call Slack, Teams, or email APIs directly in the handler.
A user sends a message from a connected provider (for example, Slack).
2
Novu receives the event
The provider delivers the inbound message to your agent endpoint.
3
Novu maps the thread
Novu maps the provider thread to a conversation and resolves the platform user to a subscriber, when possible.
4
Novu calls onMessage
Novu calls the agent’s onMessage handler with the context object.
5
Handler passes context to agent logic
Your handler forwards the message and ctx.history to your LLM or custom logic.
6
Agent logic decides next action
Your agent logic decides what should happen next.
7
Handler sends reply or signals
Your handler returns a reply, calls ctx.reply(), or emits signals.
8
Novu delivers the reply
The agent response is posted back to the original provider thread.
9
Novu records conversation state
Novu records messages, participants, metadata, signals, and conversation status.
The same agent logic works across all connected providers because Novu handles the provider-specific communication layer. Connecting a new provider does not require changing your agent code.
Inbound messages can include file attachments when the platform supports them. Novu normalizes files into message.attachments with short-lived signed URLs. Keep in mind:
Download attachment URLs promptly inside your handler.
Signed links are valid for 15 minutes.
Inbound attachments are limited to 25 MB per file.
import { agent } from '@novu/framework';export const myAgent = agent('my-agent', { onMessage: async (message, ctx) => { const attachments = message.attachments ?? []; for (const file of attachments) { // file.type: 'image', 'document', 'audio', 'video' // file.url: short-lived download URL // file.name: original filename // file.mimeType: e.g. 'image/jpeg' // file.size: size in bytes } },});
onAction fires when a user clicks a button or selects a value in an interactive card. The action payload carries action.id (the element’s id prop), action.value (its value prop, if set), and action.sourceMessageId. See Interactive cards.
onReaction fires when a user adds or removes an emoji reaction on a message. The reaction payload carries reaction.emoji.name, reaction.added (true when added, false when removed), and reaction.messageId.
import { agent } from '@novu/framework';export const myAgent = agent('my-agent', { onReaction: async (reaction, ctx) => { if (reaction.emoji.name === 'thumbs_up' && reaction.added) { ctx.metadata.set('userSatisfied', true); } else if (reaction.emoji.name === 'thumbs_down' && reaction.added) { ctx.metadata.set('userUnsatisfied', true); } await ctx.reply('Thank you for your feedback!'); },});
onToolApproval fires when a user approves or denies a gated tool call. It receives a decision with the tool call, the verdict, and a handle to the approval card.
AI SDK
LangChain
Custom code
On the AI SDK path, tools gated with needsApproval: true in onMessage trigger this handler when the user clicks Approve or Deny. Register it when you need a hook after the click — for example to audit the decision or change how the card is handled.
When a turn fails — your handler throws, a returned promise rejects, or message delivery fails inside the handler — Novu runs an error pipeline on the bridge before the turn ends:
Log the failure locally on your bridge
Call onError if you registered one
If onError does not handle the failure, auto-report { error: true } to Novu
Novu delivers safe generic copy to the user in the thread
Clear the typing indicator for the turn (even when no reply is sent)
By default, users see:
Something went wrong while processing your message. Please try again in a moment.
Return from onError
Behavior
nothing / undefined
Auto-report — Novu sends the generic message
{ suppress: true }
No user-visible reply (failure is logged only)
string / JSX Card
Custom reply delivered through ctx.reply()
The first argument is an AgentError (use toAgentError() from @novu/framework if you normalize errors in your own code). Reply delivery failures surface as AgentDeliveryError, a subclass of AgentError, with statusCode and responseBody for debugging.
When onError returns message content, Novu posts it like a normal reply. When it returns nothing, the framework auto-reports the turn failure and Novu sends the generic message.
Use { suppress: true } when a failure should be logged on your bridge but should not post another message in the thread. A common case is side-effect handlers such as onReaction or onAction: if analytics or feedback logging fails, a generic “Something went wrong…” reply is usually more confusing than helpful.
For onMessage failures, prefer a custom reply or the default generic message so the user knows their message was not handled.Runtime adapters forward onError from the same agent config. For adapter-specific failure behavior, see AI SDK, LangChain, or Other frameworks.
Reporting turn failures is best-effort. If the bridge cannot reach Novu, the failure is logged locally and is not re-thrown. Failures inside onError itself are logged and do not recurse.