From 4a1d4c791118d26819b12001d4d63e945cb7db34 Mon Sep 17 00:00:00 2001 From: Adam Chmara Date: Wed, 15 Apr 2026 18:12:41 +0300 Subject: [PATCH 01/10] feat(root): add agent template to `novu init` CLI scaffolding fixes NV-7371 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend `npx novu init` with an interactive template prompt and `--template` flag so users can scaffold either the existing notifications app or a new conversational agent app. The agent template ships a rich demo support-triage bot that showcases Cards, Buttons, onAction, ctx.metadata, ctx.resolve(), markdown replies, and conversation history — all working without an LLM API key. Made-with: Cursor --- packages/novu/src/commands/init/create-app.ts | 4 +- packages/novu/src/commands/init/index.ts | 29 ++++- .../templates/app-agent/ts/README-template.md | 73 +++++++++++ .../app-agent/ts/app/api/novu/route.ts | 6 + .../templates/app-agent/ts/app/globals.css | 19 +++ .../templates/app-agent/ts/app/layout.tsx | 19 +++ .../app-agent/ts/app/novu/agents/index.ts | 1 + .../ts/app/novu/agents/support-agent.ts | 60 +++++++++ .../app-agent/ts/app/page.module.css | 118 ++++++++++++++++++ .../init/templates/app-agent/ts/app/page.tsx | 59 +++++++++ .../init/templates/app-agent/ts/eslintrc.json | 3 + .../init/templates/app-agent/ts/gitignore | 36 ++++++ .../init/templates/app-agent/ts/next-env.d.ts | 5 + .../templates/app-agent/ts/next.config.mjs | 4 + .../init/templates/app-agent/ts/tsconfig.json | 26 ++++ .../novu/src/commands/init/templates/index.ts | 64 ++++++---- .../novu/src/commands/init/templates/types.ts | 1 + packages/novu/src/index.ts | 1 + 18 files changed, 498 insertions(+), 30 deletions(-) create mode 100644 packages/novu/src/commands/init/templates/app-agent/ts/README-template.md create mode 100644 packages/novu/src/commands/init/templates/app-agent/ts/app/api/novu/route.ts create mode 100644 packages/novu/src/commands/init/templates/app-agent/ts/app/globals.css create mode 100644 packages/novu/src/commands/init/templates/app-agent/ts/app/layout.tsx create mode 100644 packages/novu/src/commands/init/templates/app-agent/ts/app/novu/agents/index.ts create mode 100644 packages/novu/src/commands/init/templates/app-agent/ts/app/novu/agents/support-agent.ts create mode 100644 packages/novu/src/commands/init/templates/app-agent/ts/app/page.module.css create mode 100644 packages/novu/src/commands/init/templates/app-agent/ts/app/page.tsx create mode 100644 packages/novu/src/commands/init/templates/app-agent/ts/eslintrc.json create mode 100644 packages/novu/src/commands/init/templates/app-agent/ts/gitignore create mode 100644 packages/novu/src/commands/init/templates/app-agent/ts/next-env.d.ts create mode 100644 packages/novu/src/commands/init/templates/app-agent/ts/next.config.mjs create mode 100644 packages/novu/src/commands/init/templates/app-agent/ts/tsconfig.json diff --git a/packages/novu/src/commands/init/create-app.ts b/packages/novu/src/commands/init/create-app.ts index 5c41a4e8f3c..36ca04d00c0 100644 --- a/packages/novu/src/commands/init/create-app.ts +++ b/packages/novu/src/commands/init/create-app.ts @@ -16,6 +16,7 @@ export class DownloadError extends Error {} export async function createApp({ appPath, packageManager, + templateChoice, typescript, eslint, srcDir, @@ -26,6 +27,7 @@ export async function createApp({ }: { appPath: string; packageManager: PackageManager; + templateChoice: string; typescript: boolean; eslint: boolean; srcDir: boolean; @@ -36,7 +38,7 @@ export async function createApp({ }): Promise { let repoInfo: RepoInfo | undefined; const mode: TemplateMode = typescript ? 'ts' : 'js'; - const template: TemplateType = 'app-react-email'; + const template: TemplateType = templateChoice === 'agent' ? 'app-agent' : 'app-react-email'; const root = path.resolve(appPath); diff --git a/packages/novu/src/commands/init/index.ts b/packages/novu/src/commands/init/index.ts index 741c8b76b1a..1d251c272fd 100644 --- a/packages/novu/src/commands/init/index.ts +++ b/packages/novu/src/commands/init/index.ts @@ -28,6 +28,7 @@ export interface IInitCommandOptions { secretKey?: string; projectPath?: string; apiUrl: string; + template?: string; } export async function init(program: IInitCommandOptions, anonymousId?: string): Promise { @@ -148,11 +149,30 @@ export async function init(program: IInitCommandOptions, anonymousId?: string): process.exit(1); } + let templateChoice = program.template; + + if (!templateChoice) { + const res = await prompts({ + onState: onPromptState, + type: 'select', + name: 'template', + message: 'What type of Novu app do you want to create?', + choices: [ + { title: 'Notifications', value: 'notifications', description: 'Workflows, email templates, and in-app inbox' }, + { title: 'Agent', value: 'agent', description: 'Conversational AI agent with chat platform support' }, + ], + initial: 0, + }); + + templateChoice = res.template; + } + + if (!templateChoice) { + console.error('No template selected.'); + process.exit(1); + } + const preferences = {} as Record; - /** - * If the user does not provide the necessary flags, prompt them for whether - * to use TS or JS. - */ const defaults: typeof preferences = { typescript: true, eslint: true, @@ -175,6 +195,7 @@ export async function init(program: IInitCommandOptions, anonymousId?: string): await createApp({ appPath: resolvedProjectPath, packageManager: 'npm', + templateChoice, typescript: defaults.typescript as boolean, eslint: defaults.eslint as boolean, srcDir: defaults.srcDir as boolean, diff --git a/packages/novu/src/commands/init/templates/app-agent/ts/README-template.md b/packages/novu/src/commands/init/templates/app-agent/ts/README-template.md new file mode 100644 index 00000000000..086ec953889 --- /dev/null +++ b/packages/novu/src/commands/init/templates/app-agent/ts/README-template.md @@ -0,0 +1,73 @@ +# Novu Agent + +A conversational AI agent powered by [Novu](https://novu.co) and [Next.js](https://nextjs.org). + +## Getting Started + +1. Start the development server: + +```bash +npm run dev +``` + +2. Open Novu Studio: + +```bash +npx novu dev +``` + +3. Connect a chat platform (Slack, Teams, WhatsApp) in the [Novu Dashboard](https://dashboard.novu.co). + +Your agent is served at `/api/novu` and handles incoming messages via the Novu Bridge protocol. + +## Project Structure + +``` +app/ + api/novu/route.ts → Bridge endpoint serving your agent + novu/agents/ + index.ts → Agent exports + support-agent.ts → Your agent handler (edit this!) + page.tsx → Landing page +``` + +## Agent API + +Your agent handler receives a context object with: + +| Method / Property | Description | +|---|---| +| `ctx.message` | The inbound message (text, author, timestamp) | +| `ctx.conversation` | Current conversation state and metadata | +| `ctx.history` | Recent conversation history | +| `ctx.subscriber` | Resolved subscriber info | +| `ctx.platform` | Source platform (slack, teams, whatsapp) | +| `ctx.reply(content)` | Send a reply (text, markdown, or Card) | +| `ctx.metadata.set(k, v)` | Set conversation metadata | +| `ctx.resolve(summary?)` | Mark conversation as resolved | +| `ctx.trigger(workflowId)` | Trigger a Novu workflow | + +## Wiring Up Your LLM + +Replace the demo handler in `app/novu/agents/support-agent.ts` with your LLM call: + +```typescript +onMessage: async (ctx) => { + const response = await openai.chat.completions.create({ + model: 'gpt-4', + messages: [ + { role: 'system', content: 'You are a helpful support agent.' }, + ...ctx.history.map((h) => ({ role: h.role, content: h.content })), + { role: 'user', content: ctx.message?.text ?? '' }, + ], + }); + + await ctx.reply(response.choices[0].message.content ?? ''); +}, +``` + +## Learn More + +- [Novu Agent Docs](https://docs.novu.co/agents) +- [Novu Framework SDK](https://docs.novu.co/framework) +- [Next.js Documentation](https://nextjs.org/docs) diff --git a/packages/novu/src/commands/init/templates/app-agent/ts/app/api/novu/route.ts b/packages/novu/src/commands/init/templates/app-agent/ts/app/api/novu/route.ts new file mode 100644 index 00000000000..74faa4fa17e --- /dev/null +++ b/packages/novu/src/commands/init/templates/app-agent/ts/app/api/novu/route.ts @@ -0,0 +1,6 @@ +import { serve } from '@novu/framework/next'; +import { supportAgent } from '../../novu/agents'; + +export const { GET, POST, OPTIONS } = serve({ + agents: [supportAgent], +}); diff --git a/packages/novu/src/commands/init/templates/app-agent/ts/app/globals.css b/packages/novu/src/commands/init/templates/app-agent/ts/app/globals.css new file mode 100644 index 00000000000..d8e9f7044f7 --- /dev/null +++ b/packages/novu/src/commands/init/templates/app-agent/ts/app/globals.css @@ -0,0 +1,19 @@ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background: #fafafa; + color: #171717; +} + +@media (prefers-color-scheme: dark) { + body { + background: #0a0a0a; + color: #fafafa; + } +} diff --git a/packages/novu/src/commands/init/templates/app-agent/ts/app/layout.tsx b/packages/novu/src/commands/init/templates/app-agent/ts/app/layout.tsx new file mode 100644 index 00000000000..67fe1c366c3 --- /dev/null +++ b/packages/novu/src/commands/init/templates/app-agent/ts/app/layout.tsx @@ -0,0 +1,19 @@ +import type { Metadata } from 'next'; +import './globals.css'; + +export const metadata: Metadata = { + title: 'Novu Agent', + description: 'Conversational AI agent powered by Novu', +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + {children} + + ); +} diff --git a/packages/novu/src/commands/init/templates/app-agent/ts/app/novu/agents/index.ts b/packages/novu/src/commands/init/templates/app-agent/ts/app/novu/agents/index.ts new file mode 100644 index 00000000000..2ed8d1e7c4f --- /dev/null +++ b/packages/novu/src/commands/init/templates/app-agent/ts/app/novu/agents/index.ts @@ -0,0 +1 @@ +export { supportAgent } from './support-agent'; diff --git a/packages/novu/src/commands/init/templates/app-agent/ts/app/novu/agents/support-agent.ts b/packages/novu/src/commands/init/templates/app-agent/ts/app/novu/agents/support-agent.ts new file mode 100644 index 00000000000..1ef06064cd1 --- /dev/null +++ b/packages/novu/src/commands/init/templates/app-agent/ts/app/novu/agents/support-agent.ts @@ -0,0 +1,60 @@ +import { agent, Card, CardText, Actions, Button } from '@novu/framework'; + +export const supportAgent = agent('support-agent', { + onMessage: async (ctx) => { + const text = (ctx.message?.text ?? '').toLowerCase(); + const isFirstMessage = ctx.conversation.messageCount <= 1; + + if (isFirstMessage) { + ctx.metadata.set('topic', 'unknown'); + await ctx.reply( + Card({ + title: "Hi, I'm Support Agent", + children: [ + CardText('How can I help you today?'), + Actions([ + Button({ label: 'Billing question', actionId: 'topic', value: 'billing' }), + Button({ label: 'Technical issue', actionId: 'topic', value: 'technical' }), + Button({ label: 'Something else', actionId: 'topic', value: 'other' }), + ]), + ], + }) + ); + + return; + } + + if (text.includes('resolve') || text.includes('thanks')) { + ctx.resolve(`Resolved by user: ${text}`); + await ctx.reply('Glad I could help! Marking this resolved.'); + + return; + } + + // Replace this block with your LLM call (OpenAI, Anthropic, etc.) + ctx.metadata.set('lastMessage', text); + await ctx.reply({ + markdown: + `**Got it.** You said: "${ctx.message?.text}"\n\n` + + `_This is a demo agent. Replace this handler with your LLM call._\n\n` + + `**Conversation so far:** ${ctx.history.length} messages | ` + + `**Topic:** ${ctx.conversation.metadata?.topic ?? 'unknown'}`, + }); + }, + + onAction: async (ctx) => { + const { actionId, value } = ctx.action!; + if (actionId === 'topic') { + ctx.metadata.set('topic', value!); + await ctx.reply({ + markdown: `Topic set to **${value}**. Describe your issue and I'll help.`, + }); + } + }, + + onResolve: async (ctx) => { + ctx.metadata.set('resolvedAt', new Date().toISOString()); + // Trigger a follow-up workflow when a conversation is resolved: + // ctx.trigger('follow-up-survey', { to: ctx.subscriber?.subscriberId }); + }, +}); diff --git a/packages/novu/src/commands/init/templates/app-agent/ts/app/page.module.css b/packages/novu/src/commands/init/templates/app-agent/ts/app/page.module.css new file mode 100644 index 00000000000..2bc9033913b --- /dev/null +++ b/packages/novu/src/commands/init/templates/app-agent/ts/app/page.module.css @@ -0,0 +1,118 @@ +.main { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + padding: 2rem; + font-family: + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + sans-serif; +} + +.container { + max-width: 640px; + width: 100%; +} + +.title { + font-size: 2.5rem; + font-weight: 700; + margin-bottom: 0.5rem; + color: #171717; +} + +.description { + font-size: 1.1rem; + color: #525252; + margin-bottom: 2rem; +} + +.code { + background: #f5f5f5; + padding: 0.2em 0.4em; + border-radius: 4px; + font-size: 0.9em; + font-family: monospace; +} + +.steps, +.features { + margin-bottom: 2rem; +} + +.steps h2, +.features h2 { + font-size: 1.2rem; + font-weight: 600; + margin-bottom: 0.75rem; + color: #171717; +} + +.steps ol { + padding-left: 1.5rem; + line-height: 1.8; + color: #525252; +} + +.features ul { + padding-left: 1.5rem; + line-height: 1.8; + color: #525252; +} + +.steps a { + color: #2563eb; + text-decoration: none; +} + +.steps a:hover { + text-decoration: underline; +} + +.links { + display: flex; + gap: 1.5rem; + padding-top: 1rem; + border-top: 1px solid #e5e5e5; +} + +.links a { + color: #2563eb; + text-decoration: none; + font-weight: 500; +} + +.links a:hover { + text-decoration: underline; +} + +@media (prefers-color-scheme: dark) { + .title { + color: #fafafa; + } + + .description { + color: #a3a3a3; + } + + .code { + background: #262626; + } + + .steps h2, + .features h2 { + color: #fafafa; + } + + .steps ol, + .features ul { + color: #a3a3a3; + } + + .links { + border-top-color: #404040; + } +} diff --git a/packages/novu/src/commands/init/templates/app-agent/ts/app/page.tsx b/packages/novu/src/commands/init/templates/app-agent/ts/app/page.tsx new file mode 100644 index 00000000000..c0d55d961eb --- /dev/null +++ b/packages/novu/src/commands/init/templates/app-agent/ts/app/page.tsx @@ -0,0 +1,59 @@ +import styles from './page.module.css'; + +export default function Home() { + return ( +
+
+

Novu Agent

+

+ Your conversational agent is running at /api/novu +

+ +
+

Get started

+
    +
  1. + Open app/novu/agents/support-agent.ts to edit your agent +
  2. +
  3. + Connect a chat platform (Slack, Teams, WhatsApp) in the{' '} + + Novu Dashboard + +
  4. +
  5. + Replace the demo handler with your LLM call (OpenAI, Anthropic, etc.) +
  6. +
  7. + Run npx novu dev to open Novu Studio +
  8. +
+
+ +
+

What the demo agent shows

+
    +
  • Interactive Cards — buttons and actions rendered natively per platform
  • +
  • onAction handler — respond to button clicks and selections
  • +
  • Conversation metadata — track state across messages
  • +
  • Resolve lifecycle — close conversations and trigger follow-ups
  • +
  • Markdown replies — rich formatted responses
  • +
  • Conversation history — multi-turn awareness
  • +
+
+ + +
+
+ ); +} diff --git a/packages/novu/src/commands/init/templates/app-agent/ts/eslintrc.json b/packages/novu/src/commands/init/templates/app-agent/ts/eslintrc.json new file mode 100644 index 00000000000..bffb357a712 --- /dev/null +++ b/packages/novu/src/commands/init/templates/app-agent/ts/eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/packages/novu/src/commands/init/templates/app-agent/ts/gitignore b/packages/novu/src/commands/init/templates/app-agent/ts/gitignore new file mode 100644 index 00000000000..fd3dbb571a1 --- /dev/null +++ b/packages/novu/src/commands/init/templates/app-agent/ts/gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/packages/novu/src/commands/init/templates/app-agent/ts/next-env.d.ts b/packages/novu/src/commands/init/templates/app-agent/ts/next-env.d.ts new file mode 100644 index 00000000000..4f11a03dc6c --- /dev/null +++ b/packages/novu/src/commands/init/templates/app-agent/ts/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/packages/novu/src/commands/init/templates/app-agent/ts/next.config.mjs b/packages/novu/src/commands/init/templates/app-agent/ts/next.config.mjs new file mode 100644 index 00000000000..4678774e6d6 --- /dev/null +++ b/packages/novu/src/commands/init/templates/app-agent/ts/next.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +export default nextConfig; diff --git a/packages/novu/src/commands/init/templates/app-agent/ts/tsconfig.json b/packages/novu/src/commands/init/templates/app-agent/ts/tsconfig.json new file mode 100644 index 00000000000..e7ff90fd276 --- /dev/null +++ b/packages/novu/src/commands/init/templates/app-agent/ts/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/packages/novu/src/commands/init/templates/index.ts b/packages/novu/src/commands/init/templates/index.ts index 5adfbf18757..40b0a0f7445 100644 --- a/packages/novu/src/commands/init/templates/index.ts +++ b/packages/novu/src/commands/init/templates/index.ts @@ -148,26 +148,46 @@ export const installTemplate = async ({ } /* write .env file */ - const val = Object.entries({ - NOVU_SECRET_KEY: secretKey, - NEXT_PUBLIC_NOVU_APPLICATION_IDENTIFIER: applicationId, - NEXT_PUBLIC_NOVU_SUBSCRIBER_ID: userId, - }).reduce((acc, [key, value]) => { + const envVars = + template === TemplateTypeEnum.APP_AGENT + ? { NOVU_SECRET_KEY: secretKey } + : { + NOVU_SECRET_KEY: secretKey, + NEXT_PUBLIC_NOVU_APPLICATION_IDENTIFIER: applicationId, + NEXT_PUBLIC_NOVU_SUBSCRIBER_ID: userId, + }; + + const val = Object.entries(envVars).reduce((acc, [key, value]) => { return `${acc}${key}=${value}${os.EOL}`; }, ''); await fs.writeFile(path.join(root, '.env.local'), val); - /* write github action */ - await copy(copySource, `${root}/.github`, { - parents: true, - cwd: path.join(__dirname, `./github`), - }); + /* write github action (skip for agent template) */ + if (template !== TemplateTypeEnum.APP_AGENT) { + await copy(copySource, `${root}/.github`, { + parents: true, + cwd: path.join(__dirname, `./github`), + }); + } /** Copy the version from package.json or override for tests. */ const version = '16.2.1'; /** Create a package.json for the new project and write it to disk. */ + const isAgentTemplate = template === TemplateTypeEnum.APP_AGENT; + + const baseDependencies: Record = { + react: '^19', + 'react-dom': '^19', + next: version, + '@novu/framework': 'latest', + }; + + if (!isAgentTemplate) { + baseDependencies['@novu/nextjs'] = '^2.5.0'; + } + const packageJson: any = { name: appName, version: '0.1.0', @@ -178,22 +198,10 @@ export const installTemplate = async ({ start: 'next start', lint: 'next lint', }, - /** - * Default dependencies. - */ - dependencies: { - react: '^19', - 'react-dom': '^19', - next: version, - '@novu/framework': 'latest', - '@novu/nextjs': '^2.5.0', - }, + dependencies: baseDependencies, devDependencies: {}, }; - /** - * TypeScript projects will have type definitions and other devDependencies. - */ if (mode === 'ts') { packageJson.devDependencies = { ...packageJson.devDependencies, @@ -204,7 +212,6 @@ export const installTemplate = async ({ }; } - /* Add Tailwind CSS dependencies. */ if (template === TemplateTypeEnum.APP_REACT_EMAIL) { packageJson.devDependencies = { ...packageJson.devDependencies, @@ -218,7 +225,14 @@ export const installTemplate = async ({ '@react-email/tailwind': '0.0.18', }; - /* Zod dependencies used in react email example */ + packageJson.dependencies = { + ...packageJson.dependencies, + zod: '^3.23.8', + 'zod-to-json-schema': '^3.23.1', + }; + } + + if (isAgentTemplate) { packageJson.dependencies = { ...packageJson.dependencies, zod: '^3.23.8', diff --git a/packages/novu/src/commands/init/templates/types.ts b/packages/novu/src/commands/init/templates/types.ts index 48295690a5b..1038731dd6a 100644 --- a/packages/novu/src/commands/init/templates/types.ts +++ b/packages/novu/src/commands/init/templates/types.ts @@ -5,6 +5,7 @@ export enum TemplateTypeEnum { APP = 'app', DEFAULT_REACT_EMAIL = 'default-react-email', APP_REACT_EMAIL = 'app-react-email', + APP_AGENT = 'app-agent', } export type TemplateType = `${TemplateTypeEnum}`; diff --git a/packages/novu/src/index.ts b/packages/novu/src/index.ts index eb508e1c3e8..2e8346ff6d9 100644 --- a/packages/novu/src/index.ts +++ b/packages/novu/src/index.ts @@ -97,6 +97,7 @@ program `The Novu development environment Secret Key. Note that your Novu app won't work outside of local mode without it.` ) .option('-a, --api-url ', 'The Novu Cloud API URL', 'https://api.novu.co') + .option('-t, --template ', 'The template to use (notifications or agent)') .action(async (options: IInitCommandOptions) => { return await init(options, anonymousId); }); From 351bc55246075bda23429270f0dbb6e99c97af51 Mon Sep 17 00:00:00 2001 From: Adam Chmara Date: Wed, 15 Apr 2026 18:32:46 +0300 Subject: [PATCH 02/10] fix(root): bump eslint to ^9 to match eslint-config-next@16 peer dep Made-with: Cursor --- packages/novu/src/commands/init/templates/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/novu/src/commands/init/templates/index.ts b/packages/novu/src/commands/init/templates/index.ts index 40b0a0f7445..0d189fc2ea2 100644 --- a/packages/novu/src/commands/init/templates/index.ts +++ b/packages/novu/src/commands/init/templates/index.ts @@ -244,7 +244,7 @@ export const installTemplate = async ({ if (eslint) { packageJson.devDependencies = { ...packageJson.devDependencies, - eslint: '^8', + eslint: '^9', 'eslint-config-next': version, }; } From 1dea3f6404a5492842b8788163e2250991eb95df Mon Sep 17 00:00:00 2001 From: Adam Chmara Date: Wed, 15 Apr 2026 18:33:07 +0300 Subject: [PATCH 03/10] Revert "fix(root): bump eslint to ^9 to match eslint-config-next@16 peer dep" This reverts commit 7c950be65fd4ef5a924808bc543ee968f60d20a2. --- packages/novu/src/commands/init/templates/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/novu/src/commands/init/templates/index.ts b/packages/novu/src/commands/init/templates/index.ts index 0d189fc2ea2..40b0a0f7445 100644 --- a/packages/novu/src/commands/init/templates/index.ts +++ b/packages/novu/src/commands/init/templates/index.ts @@ -244,7 +244,7 @@ export const installTemplate = async ({ if (eslint) { packageJson.devDependencies = { ...packageJson.devDependencies, - eslint: '^9', + eslint: '^8', 'eslint-config-next': version, }; } From c876e1c8b21635eca5af21f785dc77dbc3920c1a Mon Sep 17 00:00:00 2001 From: Adam Chmara Date: Wed, 15 Apr 2026 18:52:21 +0300 Subject: [PATCH 04/10] fix(root): bump eslint to ^9 to match eslint-config-next@16 peer dep Made-with: Cursor --- packages/novu/src/commands/init/templates/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/novu/src/commands/init/templates/index.ts b/packages/novu/src/commands/init/templates/index.ts index 40b0a0f7445..0d189fc2ea2 100644 --- a/packages/novu/src/commands/init/templates/index.ts +++ b/packages/novu/src/commands/init/templates/index.ts @@ -244,7 +244,7 @@ export const installTemplate = async ({ if (eslint) { packageJson.devDependencies = { ...packageJson.devDependencies, - eslint: '^8', + eslint: '^9', 'eslint-config-next': version, }; } From 50d0ebcdb100dd3dfac5b98132928e9881ac26fb Mon Sep 17 00:00:00 2001 From: Adam Chmara Date: Wed, 15 Apr 2026 18:54:32 +0300 Subject: [PATCH 05/10] fix(root): use correct Button id prop from chat SDK Made-with: Cursor --- .../app-agent/ts/app/novu/agents/support-agent.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/novu/src/commands/init/templates/app-agent/ts/app/novu/agents/support-agent.ts b/packages/novu/src/commands/init/templates/app-agent/ts/app/novu/agents/support-agent.ts index 1ef06064cd1..1478657b836 100644 --- a/packages/novu/src/commands/init/templates/app-agent/ts/app/novu/agents/support-agent.ts +++ b/packages/novu/src/commands/init/templates/app-agent/ts/app/novu/agents/support-agent.ts @@ -13,9 +13,9 @@ export const supportAgent = agent('support-agent', { children: [ CardText('How can I help you today?'), Actions([ - Button({ label: 'Billing question', actionId: 'topic', value: 'billing' }), - Button({ label: 'Technical issue', actionId: 'topic', value: 'technical' }), - Button({ label: 'Something else', actionId: 'topic', value: 'other' }), + Button({ id: 'topic-billing', label: 'Billing question', value: 'billing' }), + Button({ id: 'topic-technical', label: 'Technical issue', value: 'technical' }), + Button({ id: 'topic-other', label: 'Something else', value: 'other' }), ]), ], }) @@ -44,8 +44,8 @@ export const supportAgent = agent('support-agent', { onAction: async (ctx) => { const { actionId, value } = ctx.action!; - if (actionId === 'topic') { - ctx.metadata.set('topic', value!); + if (actionId.startsWith('topic-') && value) { + ctx.metadata.set('topic', value); await ctx.reply({ markdown: `Topic set to **${value}**. Describe your issue and I'll help.`, }); From 6159b3c348c754c07b6ddbb67d9d9e9bb5e41613 Mon Sep 17 00:00:00 2001 From: Adam Chmara Date: Wed, 15 Apr 2026 19:01:20 +0300 Subject: [PATCH 06/10] fix(root): replace Studio reference with sync in agent template docs Made-with: Cursor --- .../init/templates/app-agent/ts/README-template.md | 8 ++++---- .../src/commands/init/templates/app-agent/ts/app/page.tsx | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/novu/src/commands/init/templates/app-agent/ts/README-template.md b/packages/novu/src/commands/init/templates/app-agent/ts/README-template.md index 086ec953889..99668c40366 100644 --- a/packages/novu/src/commands/init/templates/app-agent/ts/README-template.md +++ b/packages/novu/src/commands/init/templates/app-agent/ts/README-template.md @@ -10,14 +10,14 @@ A conversational AI agent powered by [Novu](https://novu.co) and [Next.js](https npm run dev ``` -2. Open Novu Studio: +2. Create an agent and connect a chat platform (Slack, Teams, WhatsApp) in the [Novu Dashboard](https://dashboard.novu.co). + +3. Deploy your bridge endpoint and sync: ```bash -npx novu dev +npx novu sync -b /api/novu -s ``` -3. Connect a chat platform (Slack, Teams, WhatsApp) in the [Novu Dashboard](https://dashboard.novu.co). - Your agent is served at `/api/novu` and handles incoming messages via the Novu Bridge protocol. ## Project Structure diff --git a/packages/novu/src/commands/init/templates/app-agent/ts/app/page.tsx b/packages/novu/src/commands/init/templates/app-agent/ts/app/page.tsx index c0d55d961eb..176c9462734 100644 --- a/packages/novu/src/commands/init/templates/app-agent/ts/app/page.tsx +++ b/packages/novu/src/commands/init/templates/app-agent/ts/app/page.tsx @@ -16,7 +16,7 @@ export default function Home() { Open app/novu/agents/support-agent.ts to edit your agent
  • - Connect a chat platform (Slack, Teams, WhatsApp) in the{' '} + Create an agent and connect a chat platform (Slack, Teams, WhatsApp) in the{' '} Novu Dashboard @@ -25,7 +25,8 @@ export default function Home() { Replace the demo handler with your LLM call (OpenAI, Anthropic, etc.)
  • - Run npx novu dev to open Novu Studio + Deploy your bridge endpoint and sync with{' '} + npx novu sync
  • From 87d1a35b799406016279b9667457dbe83849a54d Mon Sep 17 00:00:00 2001 From: Adam Chmara Date: Wed, 15 Apr 2026 19:01:50 +0300 Subject: [PATCH 07/10] chore(root): simplify agent template getting started steps Made-with: Cursor --- .../init/templates/app-agent/ts/README-template.md | 8 ++------ .../commands/init/templates/app-agent/ts/app/page.tsx | 10 +++------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/packages/novu/src/commands/init/templates/app-agent/ts/README-template.md b/packages/novu/src/commands/init/templates/app-agent/ts/README-template.md index 99668c40366..26454b0dd9e 100644 --- a/packages/novu/src/commands/init/templates/app-agent/ts/README-template.md +++ b/packages/novu/src/commands/init/templates/app-agent/ts/README-template.md @@ -10,13 +10,9 @@ A conversational AI agent powered by [Novu](https://novu.co) and [Next.js](https npm run dev ``` -2. Create an agent and connect a chat platform (Slack, Teams, WhatsApp) in the [Novu Dashboard](https://dashboard.novu.co). +2. Connect a chat platform in the [Novu Dashboard](https://dashboard.novu.co). -3. Deploy your bridge endpoint and sync: - -```bash -npx novu sync -b /api/novu -s -``` +3. Replace the demo handler in `app/novu/agents/support-agent.ts` with your LLM call. Your agent is served at `/api/novu` and handles incoming messages via the Novu Bridge protocol. diff --git a/packages/novu/src/commands/init/templates/app-agent/ts/app/page.tsx b/packages/novu/src/commands/init/templates/app-agent/ts/app/page.tsx index 176c9462734..75b15f24352 100644 --- a/packages/novu/src/commands/init/templates/app-agent/ts/app/page.tsx +++ b/packages/novu/src/commands/init/templates/app-agent/ts/app/page.tsx @@ -13,20 +13,16 @@ export default function Home() {

    Get started

    1. - Open app/novu/agents/support-agent.ts to edit your agent + Edit your agent in app/novu/agents/support-agent.ts
    2. - Create an agent and connect a chat platform (Slack, Teams, WhatsApp) in the{' '} + Connect a chat platform in the{' '} Novu Dashboard
    3. - Replace the demo handler with your LLM call (OpenAI, Anthropic, etc.) -
    4. -
    5. - Deploy your bridge endpoint and sync with{' '} - npx novu sync + Replace the demo handler with your LLM call
    From 1ba8e281f1ad290a4f7f2245c8b21a67fe3feca5 Mon Sep 17 00:00:00 2001 From: Adam Chmara Date: Wed, 15 Apr 2026 19:07:57 +0300 Subject: [PATCH 08/10] feat(root): use JSX syntax for agent Card components Switch support-agent from function-call API to JSX with per-file @jsxImportSource pragma. Set jsx to react-jsx in tsconfig. Made-with: Cursor --- .../templates/app-agent/ts/README-template.md | 4 ++-- .../{support-agent.ts => support-agent.tsx} | 20 +++++++++---------- .../init/templates/app-agent/ts/app/page.tsx | 2 +- .../init/templates/app-agent/ts/tsconfig.json | 2 +- 4 files changed, 13 insertions(+), 15 deletions(-) rename packages/novu/src/commands/init/templates/app-agent/ts/app/novu/agents/{support-agent.ts => support-agent.tsx} (77%) diff --git a/packages/novu/src/commands/init/templates/app-agent/ts/README-template.md b/packages/novu/src/commands/init/templates/app-agent/ts/README-template.md index 26454b0dd9e..9188f7754c3 100644 --- a/packages/novu/src/commands/init/templates/app-agent/ts/README-template.md +++ b/packages/novu/src/commands/init/templates/app-agent/ts/README-template.md @@ -12,7 +12,7 @@ npm run dev 2. Connect a chat platform in the [Novu Dashboard](https://dashboard.novu.co). -3. Replace the demo handler in `app/novu/agents/support-agent.ts` with your LLM call. +3. Replace the demo handler in `app/novu/agents/support-agent.tsx` with your LLM call. Your agent is served at `/api/novu` and handles incoming messages via the Novu Bridge protocol. @@ -23,7 +23,7 @@ app/ api/novu/route.ts → Bridge endpoint serving your agent novu/agents/ index.ts → Agent exports - support-agent.ts → Your agent handler (edit this!) + support-agent.tsx → Your agent handler (edit this!) page.tsx → Landing page ``` diff --git a/packages/novu/src/commands/init/templates/app-agent/ts/app/novu/agents/support-agent.ts b/packages/novu/src/commands/init/templates/app-agent/ts/app/novu/agents/support-agent.tsx similarity index 77% rename from packages/novu/src/commands/init/templates/app-agent/ts/app/novu/agents/support-agent.ts rename to packages/novu/src/commands/init/templates/app-agent/ts/app/novu/agents/support-agent.tsx index 1478657b836..11ccb0834c5 100644 --- a/packages/novu/src/commands/init/templates/app-agent/ts/app/novu/agents/support-agent.ts +++ b/packages/novu/src/commands/init/templates/app-agent/ts/app/novu/agents/support-agent.tsx @@ -1,3 +1,4 @@ +/** @jsxImportSource @novu/framework */ import { agent, Card, CardText, Actions, Button } from '@novu/framework'; export const supportAgent = agent('support-agent', { @@ -8,17 +9,14 @@ export const supportAgent = agent('support-agent', { if (isFirstMessage) { ctx.metadata.set('topic', 'unknown'); await ctx.reply( - Card({ - title: "Hi, I'm Support Agent", - children: [ - CardText('How can I help you today?'), - Actions([ - Button({ id: 'topic-billing', label: 'Billing question', value: 'billing' }), - Button({ id: 'topic-technical', label: 'Technical issue', value: 'technical' }), - Button({ id: 'topic-other', label: 'Something else', value: 'other' }), - ]), - ], - }) + + How can I help you today? + +