docs/agents/custom-code-agent/frameworks/other.mdx
Already connecting an agent? For Mastra, start with Connect Mastra to Slack. This page is the generic Framework API for stacks without a dedicated Novu adapter.
Import agent from @novu/framework when you are not using @novu/framework/ai-sdk or @novu/framework/langchain. Call your LLM, orchestration library, or business logic yourself, then reply through Novu's handler API. Use this path for Mastra, the OpenAI SDK, Anthropic SDK, or any stack Novu does not ship a dedicated adapter for.
Completed a connect guide? Your agent, bridge, and project are already set up - jump to Minimal agent.
Otherwise connect first, then come back here for Framework patterns.
Install the framework package:
npm install @novu/framework
Bring your own LLM or agent library - Novu does not require a specific one.
Import agent from @novu/framework, call your logic in onMessage, and reply by returning a string or calling ctx.reply():
import { agent } from '@novu/framework';
export const supportAgent = agent('support-bot', {
onMessage: async (message, ctx) => {
const response = await yourLLM.chat({
messages: buildMessages(ctx.history),
});
return response.text;
},
});
Make sure the agent id ('support-bot') matches the Identifier in your dashboard, and that this handler is registered on your bridge route - see Connecting your app.
For handlers, replies, signals, typing, and tool approval, see Building blocks. The sections below cover what's specific to wiring your own LLM stack - return types and manual tool approval.
onMessageOn this path, onMessage accepts the standard reply types - not AI SDK results:
| Return | Behavior |
|---|---|
string / JSX Card | Delivered as a reply - see Reply |
ctx.toolApproval.request(...) | Posts the Approve / Deny card and pauses the turn - see Tool approval |
| nothing | After you call ctx.reply() yourself |
Other handlers (onAction, onReaction, onResolve) accept string, Card, or nothing.
Shorthand when you only need onMessage: pass the handler directly.
const supportAgent = agent('support-bot', async (message, ctx) => {
const text = await runAgent(message, ctx);
return text;
});
When a tool needs human approval, return ctx.toolApproval.request() from onMessage:
import { agent } from '@novu/framework';
export const supportAgent = agent('support-bot', {
onMessage: async (message, ctx) => {
const toolCall = detectToolCall(message, ctx);
if (toolCall?.needsApproval) {
return ctx.toolApproval.request(toolCall);
}
return runToolAndReply(toolCall, ctx);
},
onToolApproval: async (decision, ctx) => {
if (!decision.approved) {
await decision.approvalMessage.delete();
return 'Cancelled.';
}
const result = await issueRefund(decision.toolCall.input);
await decision.approvalMessage.delete();
return `Refund issued for order ${result.orderId}.`;
},
});
See Tool approval for the full API, card customization, and default behavior when you omit onToolApproval.