novu-framework-integration
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFramework Integration
框架集成
Use to build notification workflows in code, alongside your application source. Workflows live in your repo, content is rendered using libraries you already use (React Email, Vue Email, Svelte Email), and a single HTTP endpoint (the Bridge) lets Novu Cloud execute them with just-in-time data from your services.
@novu/frameworkUse this skill when building workflows in code. For workflows authored in the Novu Dashboard, just trigger them via— no Framework needed.trigger-notification
使用在代码中构建通知工作流,与你的应用源码一同管理。工作流存储在你的代码仓库中,内容使用你已熟悉的库(React Email、Vue Email、Svelte Email)渲染,单个HTTP端点(即Bridge)允许Novu Cloud通过你的服务提供的实时数据执行这些工作流。
@novu/framework当你需要在代码中构建工作流时使用此技能。若工作流是在Novu仪表盘中创建的,只需通过触发即可——无需使用Framework。trigger-notification
When to Use the Framework
何时使用框架
| Use Framework | Use Dashboard Workflows |
|---|---|
| Workflows must live in source control / GitOps | Non-technical peers own all the content |
| Need just-in-time data from your DB / APIs | All data fits in the trigger payload |
| Render emails with React/Vue/Svelte Email | Block editor is enough |
| Execute custom code (LLMs, third-party APIs) | Pure send-only flows |
| Need typed payload + step controls | Quick prototype |
The two approaches coexist — a single environment can have both code-defined and dashboard-defined workflows.
| 使用框架 | 使用仪表盘工作流 |
|---|---|
| 工作流必须纳入版本控制/GitOps管理 | 非技术人员全权负责所有内容 |
| 需要从数据库/API获取实时数据 | 所有数据均可放入触发负载中 |
| 使用React/Vue/Svelte Email渲染邮件 | 区块编辑器已满足需求 |
| 需要执行自定义代码(LLM、第三方API) | 仅需纯发送流 |
| 需要类型化负载+步骤控制 | 快速原型开发 |
这两种方式可以共存——单个环境中同时支持代码定义和仪表盘定义的工作流。
How It Works
工作原理
- You define workflows in code with from
workflow(...).@novu/framework - You expose a single HTTP route in your app — the Bridge Endpoint.
/api/novu - You sync the bridge URL to Novu Cloud (via or GitHub Action).
npx novu sync - Novu Cloud calls your bridge over an authenticated tunnel during workflow execution to fetch step content with the latest data.
Trigger ──► Novu Cloud Worker ──► Your Bridge (/api/novu) ──► Provider (SendGrid, FCM, …)- 你使用中的
@novu/framework在代码中定义工作流。workflow(...) - 在你的应用中暴露单个HTTP路由——即Bridge Endpoint。
/api/novu - 通过或GitHub Action将Bridge URL同步到Novu Cloud。
npx novu sync - 在工作流执行期间,Novu Cloud通过认证隧道调用你的Bridge,获取包含最新数据的步骤内容。
触发 ──► Novu Cloud Worker ──► 你的Bridge (/api/novu) ──► 服务商(SendGrid、FCM等)Quick Start
快速开始
1. Bootstrap a project
1. 初始化项目
bash
npx novu init --secret-key=<YOUR_NOVU_SECRET_KEY>This creates a sample bridge app with a workflow, env file, and a working route.
/api/novubash
npx novu init --secret-key=<YOUR_NOVU_SECRET_KEY>此命令会创建一个包含示例工作流、环境文件和可用路由的Bridge示例应用。
/api/novu2. Or add to an existing app
2. 或添加到现有应用
bash
npm install @novu/framework zod @react-email/components react-emailbash
NOVU_SECRET_KEY=<YOUR_NOVU_SECRET_KEY>bash
npm install @novu/framework zod @react-email/components react-emailbash
NOVU_SECRET_KEY=<YOUR_NOVU_SECRET_KEY>3. Define a workflow
3. 定义工作流
typescript
import { workflow } from "@novu/framework";
import { z } from "zod";
export const welcomeWorkflow = workflow(
"welcome-email",
async ({ step, payload, subscriber }) => {
await step.email("send-email", async (controls) => {
return {
subject: controls.subject,
body: `Welcome ${subscriber.firstName ?? payload.userName}!`,
};
}, {
controlSchema: z.object({
subject: z.string().default("Welcome to {{payload.appName}}"),
}),
});
},
{
payloadSchema: z.object({
userName: z.string(),
appName: z.string().default("Acme"),
}),
name: "Welcome Email",
description: "Sent when a new user signs up",
tags: ["onboarding"],
}
);typescript
import { workflow } from "@novu/framework";
import { z } from "zod";
export const welcomeWorkflow = workflow(
"welcome-email",
async ({ step, payload, subscriber }) => {
await step.email("send-email", async (controls) => {
return {
subject: controls.subject,
body: `欢迎 ${subscriber.firstName ?? payload.userName}!`,
};
}, {
controlSchema: z.object({
subject: z.string().default("欢迎来到 {{payload.appName}}"),
}),
});
},
{
payloadSchema: z.object({
userName: z.string(),
appName: z.string().default("Acme"),
}),
name: "欢迎邮件",
description: "新用户注册时发送",
tags: ["onboarding"],
}
);4. Mount the Bridge Endpoint
4. 挂载Bridge Endpoint
Pick the wrapper that matches your framework — see Bridge Endpoint Setup below.
选择与你的框架匹配的包装器——详见下方Bridge Endpoint 配置。
5. Run the Local Studio
5. 运行本地Studio
bash
npx novu@latest dev --port <YOUR_APP_PORT>Open to preview workflows, edit controls, and trigger test events. The Studio creates a public tunnel automatically so Novu Cloud can reach your local bridge.
http://localhost:2022bash
npx novu@latest dev --port <YOUR_APP_PORT>打开预览工作流、编辑控件并触发测试事件。Studio会自动创建公共隧道,让Novu Cloud能够访问你的本地Bridge。
http://localhost:2022Workflow Anatomy
工作流结构
Designing the workflow itself? Seefor channel selection, severity,design-workflow/, digest defaults, step conditions, and the 9 reference templates (order confirmation, payment failed, account suspended, comment, trial expiring, password reset, webhook fan-out, fetch-then-notify). The Framework SKILL covers how to express those decisions in code;criticalcovers what to decide.design-workflow/
typescript
workflow(workflowId, handler, options);| Param | Type | Description |
|---|---|---|
| | Unique identifier in your environment |
| | Workflow body — calls steps in order |
| | Schema, name, description, tags, preferences |
想要设计工作流本身?查看了解渠道选择、优先级、design-workflow/、默认摘要、步骤条件以及9个参考模板(订单确认、支付失败、账户暂停、评论、试用到期、密码重置、Webhook扇出、先获取再通知)。Framework SKILL涵盖如何在代码中表达这些决策;critical则涵盖需要做出哪些决策。design-workflow/
typescript
workflow(workflowId, handler, options);| 参数 | 类型 | 描述 |
|---|---|---|
| | 环境中的唯一标识符 |
| | 工作流主体——按顺序调用步骤 |
| | schema、名称、描述、标签、偏好设置 |
Workflow Options
工作流选项
| Option | Type | Purpose |
|---|---|---|
| | Validates the trigger payload, infers TS type for |
| | Human-readable name shown in Dashboard / |
| | Description shown in Dashboard |
| | Categorize for filtering / Inbox tabs |
| | Visual prioritization in the Inbox. Leave unset for most workflows. |
| | Bypasses subscriber preferences, skips digest, runs without delays. Reserve for must-deliver events. |
| | Default channel preferences and |
| 选项 | 类型 | 用途 |
|---|---|---|
| | 验证触发负载,为 |
| | 在仪表盘/ |
| | 在仪表盘中显示的描述 |
| | 分类以便过滤/收件箱标签页 |
| | 收件箱中的视觉优先级。大多数工作流无需设置。 |
| | 绕过订阅者偏好,跳过摘要,无延迟运行。仅用于必须送达的事件。 |
| | 默认渠道偏好和 |
Workflow Context
工作流上下文
The handler receives :
{ step, payload, subscriber }- — channel and action step builders (
step,step.email,step.delay, …)step.digest - — strongly-typed data passed at trigger time, validated against
payloadpayloadSchema - —
subscriberof the recipient{ subscriberId, firstName?, lastName?, locale?, data?, ... }
处理器接收:
{ step, payload, subscriber }- — 渠道和操作步骤构建器(
step、step.email、step.delay等)step.digest - — 触发时传递的强类型数据,已通过
payload验证payloadSchema - — 收件人的
subscriber信息{ subscriberId, firstName?, lastName?, locale?, data?, ... }
Channel Steps
渠道步骤
All channel steps share the same shape:
typescript
await step.<channel>(stepId, resolver, options?);| Step | Output Required | Notable Outputs | Returns Result |
|---|---|---|---|
| | | No |
| | — | No |
| | | No |
| | — (override per-provider) | No |
| | | |
所有渠道步骤都具有相同的结构:
typescript
await step.<channel>(stepId, resolver, options?);| 步骤 | 必填输出 | 值得关注的输出 | 返回结果 |
|---|---|---|---|
| | | 否 |
| | — | 否 |
| | | 否 |
| | —(按服务商覆盖) | 否 |
| | | |
Email Step
邮件步骤
typescript
await step.email("welcome", async (controls) => ({
subject: controls.subject,
body: render(<WelcomeEmail name={subscriber.firstName} />),
from: "hello@acme.com",
replyTo: "support@acme.com",
}));typescript
await step.email("welcome", async (controls) => ({
subject: controls.subject,
body: render(<WelcomeEmail name={subscriber.firstName} />),
from: "hello@acme.com",
replyTo: "support@acme.com",
}));In-App Step (rich payload)
应用内步骤(富负载)
typescript
await step.inApp("inbox", async () => ({
subject: "Welcome to Acme!",
body: "We are excited to have you on board.",
avatar: "https://acme.com/avatar.png",
redirect: { url: "/welcome", target: "_self" },
primaryAction: {
label: "Get Started",
redirect: { url: "/get-started", target: "_self" },
},
data: { entityType: "user", entityId: payload.userId },
}));The In-App step returns — use it to drive the of subsequent steps.
{ seen, read, lastSeenDate, lastReadDate }skiptypescript
await step.inApp("inbox", async () => ({
subject: "欢迎来到Acme!",
body: "我们很高兴你能加入。",
avatar: "https://acme.com/avatar.png",
redirect: { url: "/welcome", target: "_self" },
primaryAction: {
label: "开始使用",
redirect: { url: "/get-started", target: "_self" },
},
data: { entityType: "user", entityId: payload.userId },
}));应用内步骤会返回——可用于控制后续步骤的逻辑。
{ seen, read, lastSeenDate, lastReadDate }skipSMS / Push / Chat
SMS / 推送 / 聊天
typescript
await step.sms("verification", async () => ({
body: `Your code is ${payload.code}`,
}));
await step.push("new-message", async () => ({
title: "New message",
body: payload.preview,
data: { messageId: payload.id },
}));
await step.chat("notify", async () => ({
body: `:rocket: Deploy ${payload.id} succeeded`,
}));typescript
await step.sms("verification", async () => ({
body: `你的验证码是 ${payload.code}`,
}));
await step.push("new-message", async () => ({
title: "新消息",
body: payload.preview,
data: { messageId: payload.id },
}));
await step.chat("notify", async () => ({
body: `:rocket: 部署 ${payload.id} 成功`,
}));Action Steps
操作步骤
step.delay
step.delaystep.delay
step.delayPause workflow execution before the next step.
typescript
await step.delay("wait-a-day", async () => ({
unit: "days",
amount: 1,
}));Supported values: , , , , , .
unitsecondsminuteshoursdaysweeksmonths在执行下一步之前暂停工作流。
typescript
await step.delay("wait-a-day", async () => ({
unit: "days",
amount: 1,
}));支持的值:、、、、、。
unitsecondsminuteshoursdaysweeksmonthsstep.digest
step.digeststep.digest
step.digestAggregate multiple triggers into a single notification over a window.
typescript
const { events } = await step.digest("daily", async () => ({
unit: "days",
amount: 1,
digestKey: payload.projectId, // optional — group by custom key
}));
await step.email("summary", async () => ({
subject: `${events.length} updates today`,
body: render(<DigestEmail events={events} />),
}));Use instead of / for cron-based digests. Each digest event has . Only one digest per workflow — chain a second workflow via if you need a two-stage digest.
cron: "0 0 * * *"unitamount{ id, time, payload }step.custom在指定时间段内将多个触发事件聚合为单个通知。
typescript
const { events } = await step.digest("daily", async () => ({
unit: "days",
amount: 1,
digestKey: payload.projectId, // 可选——按自定义键分组
}));
await step.email("summary", async () => ({
subject: `${events.length} 条今日更新`,
body: render(<DigestEmail events={events} />),
}));使用替代/实现基于 cron 的摘要。每个摘要事件包含。每个工作流仅支持一个摘要步骤——如果需要两级摘要,可通过调用第二个工作流。
cron: "0 0 * * *"unitamount{ id, time, payload }step.customstep.http
step.httpstep.http
step.httpCall an external HTTP endpoint as part of the workflow — webhook fan-out or just-in-time data fetch.
typescript
const plan = await step.http("fetch-plan", async () => ({
method: "GET",
url: `https://api.example.com/users/${payload.userId}/plan`,
responseBodySchema: {
type: "object",
properties: { planName: { type: "string" }, renewalDate: { type: "string" } },
required: ["planName", "renewalDate"],
} as const,
}));
await step.email("notify", async () => ({
subject: `Your ${plan.planName} plan`,
body: `Renews on ${plan.renewalDate}.`,
}));Webhook-style:
typescript
await step.http("webhook", async () => ({
method: "POST",
url: payload.webhookUrl,
headers: [{ key: "Content-Type", value: "application/json" }],
body: [
{ key: "event", value: "payment_failed" },
{ key: "subscriberId", value: subscriber.subscriberId },
],
continueOnFailure: true,
}));When a subsequent step references HTTP response data, the HTTP step must declare a . Only properties declared in the schema are addressable as .
responseBodySchema{{ steps.<http-step-id>.<property> }}在工作流中调用外部HTTP端点——用于Webhook扇出或实时数据获取。
typescript
const plan = await step.http("fetch-plan", async () => ({
method: "GET",
url: `https://api.example.com/users/${payload.userId}/plan`,
responseBodySchema: {
type: "object",
properties: { planName: { type: "string" }, renewalDate: { type: "string" } },
required: ["planName", "renewalDate"],
} as const,
}));
await step.email("notify", async () => ({
subject: `你的 ${plan.planName} 套餐`,
body: `续费日期为 ${plan.renewalDate}。`,
}));Webhook风格:
typescript
await step.http("webhook", async () => ({
method: "POST",
url: payload.webhookUrl,
headers: [{ key: "Content-Type", value: "application/json" }],
body: [
{ key: "event", value: "payment_failed" },
{ key: "subscriberId", value: subscriber.subscriberId },
],
continueOnFailure: true,
}));当后续步骤引用HTTP响应数据时,HTTP步骤必须声明。只有schema中声明的属性才能通过访问。
responseBodySchema{{ steps.<http-step-id>.<property> }}step.custom
step.customstep.custom
step.customRun arbitrary code and persist its result for later steps.
typescript
const task = await step.custom("fetch-task", async () => {
const t = await db.fetchTask(payload.taskId);
return { id: t.id, title: t.title, complete: t.complete };
}, {
outputSchema: {
type: "object",
properties: {
id: { type: "string" },
title: { type: "string" },
complete: { type: "boolean" },
},
required: ["id", "complete"],
} as const,
});
await step.email("reminder", async () => ({
subject: `Reminder: ${task.title}`,
body: "Please complete your task.",
}), {
skip: () => task.complete,
});The custom step result is only usable inside subsequent step , , and functions — not in step controls.
resolverprovidersskip运行任意代码并将结果持久化供后续步骤使用。
typescript
const task = await step.custom("fetch-task", async () => {
const t = await db.fetchTask(payload.taskId);
return { id: t.id, title: t.title, complete: t.complete };
}, {
outputSchema: {
type: "object",
properties: {
id: { type: "string" },
title: { type: "string" },
complete: { type: "boolean" },
},
required: ["id", "complete"],
} as const,
});
await step.email("reminder", async () => ({
subject: `提醒:${task.title}`,
body: "请完成你的任务。",
}), {
skip: () => task.complete,
});自定义步骤的结果仅可用于后续步骤的、和函数——不可用于步骤控件。
resolverprovidersskipStep Options
步骤选项
typescript
await step.email(stepId, resolver, {
controlSchema, // Zod | JSON Schema | Class-Validator class
skip, // (controls) => boolean | Promise<boolean>
providers, // per-provider override callbacks
disableOutputSanitization, // boolean — for raw HTML in Inbox
});typescript
await step.email(stepId, resolver, {
controlSchema, // Zod | JSON Schema | Class-Validator类
skip, // (controls) => boolean | Promise<boolean>
providers, // 按服务商覆盖回调
disableOutputSanitization, // boolean——允许收件箱中的原始HTML
});skip
skipskip
skipConditionally skip a step. Receives the resolved controls.
typescript
await step.email("follow-up", resolver, {
skip: () => inAppNotification.read === true,
});有条件地跳过步骤。接收解析后的控件。
typescript
await step.email("follow-up", resolver, {
skip: () => inAppNotification.read === true,
});providers
(Per-Step Provider Overrides)
providersproviders
(按步骤服务商覆盖)
providersCustomize the request sent to the underlying provider — e.g. Slack or SendGrid .
blockscctypescript
await step.email("alert", resolver, {
providers: {
sendgrid: ({ controls, outputs }) => ({
from: "alerts@acme.com",
cc: ["ops@acme.com"],
_passthrough: {
body: { ip_pool_name: "transactional" },
headers: { "X-Custom": "value" },
},
}),
},
});_passthrough_passthrough自定义发送给底层服务商的请求——例如Slack的或SendGrid的。
blockscctypescript
await step.email("alert", resolver, {
providers: {
sendgrid: ({ controls, outputs }) => ({
from: "alerts@acme.com",
cc: ["ops@acme.com"],
_passthrough: {
body: { ip_pool_name: "transactional" },
headers: { "X-Custom": "value" },
},
}),
},
});_passthrough_passthroughdisableOutputSanitization
disableOutputSanitizationdisableOutputSanitization
disableOutputSanitizationAllow raw HTML / unescaped characters in the output (e.g. in In-App ):
&data.linktypescript
await step.inApp("link", async () => ({
body: "Check it out",
data: { link: "/p/123?active=true&env=prod" },
}), { disableOutputSanitization: true });允许输出中包含原始HTML/未转义字符(例如应用内中的):
data.link&typescript
await step.inApp("link", async () => ({
body: "查看详情",
data: { link: "/p/123?active=true&env=prod" },
}), { disableOutputSanitization: true });Payload Schema
负载Schema
The payload is the data passed at trigger time. Define a schema to get typed and runtime validation.
payload负载是触发时传递的数据。定义schema以获得类型化的和运行时验证。
payloadWith Zod
使用Zod
typescript
import { z } from "zod";
workflow("comment", handler, {
payloadSchema: z.object({
postId: z.number(),
authorName: z.string(),
comment: z.string().max(200),
}),
});typescript
import { z } from "zod";
workflow("comment", handler, {
payloadSchema: z.object({
postId: z.number(),
authorName: z.string(),
comment: z.string().max(200),
}),
});With JSON Schema
使用JSON Schema
typescript
workflow("comment", handler, {
payloadSchema: {
type: "object",
properties: {
postId: { type: "number" },
authorName: { type: "string" },
comment: { type: "string", maxLength: 200 },
},
required: ["postId", "comment"],
additionalProperties: false,
} as const,
});Theis required for TS to infer the payload type from JSON Schema.as const
typescript
workflow("comment", handler, {
payloadSchema: {
type: "object",
properties: {
postId: { type: "number" },
authorName: { type: "string" },
comment: { type: "string", maxLength: 200 },
},
required: ["postId", "comment"],
additionalProperties: false,
} as const,
});需要添加才能让TS从JSON Schema推断负载类型。as const
With Class Validator
使用Class Validator
typescript
import { IsString, IsNumber } from "class-validator";
class CommentPayload {
@IsNumber() postId!: number;
@IsString() authorName!: string;
@IsString() comment!: string;
}
workflow("comment", handler, { payloadSchema: CommentPayload });Requires , , . See .
class-validatorclass-validator-jsonschemareflect-metadatareferences/schema-validation.mdtypescript
import { IsString, IsNumber } from "class-validator";
class CommentPayload {
@IsNumber() postId!: number;
@IsString() authorName!: string;
@IsString() comment!: string;
}
workflow("comment", handler, { payloadSchema: CommentPayload });需要依赖、、。详见。
class-validatorclass-validator-jsonschemareflect-metadatareferences/schema-validation.mdStep Controls — No-Code for Your Team
步骤控件——为团队提供无代码能力
Controls are step-level inputs your non-technical peers can edit in the Novu Dashboard UI without touching code. They're validated by a schema you define (Zod / JSON Schema / Class-Validator).
typescript
await step.email("welcome", async (controls) => ({
subject: controls.subject,
body: render(<EmailTemplate hideBanner={controls.hideBanner} />),
}), {
controlSchema: z.object({
hideBanner: z.boolean().default(false),
subject: z.string().default("Hi {{subscriber.firstName | capitalize}}"),
}),
});控件是步骤级别的输入,你的非技术同事可以在Novu仪表盘UI中编辑,无需修改代码。它们由你定义的schema(Zod / JSON Schema / Class-Validator)验证。
typescript
await step.email("welcome", async (controls) => ({
subject: controls.subject,
body: render(<EmailTemplate hideBanner={controls.hideBanner} />),
}), {
controlSchema: z.object({
hideBanner: z.boolean().default(false),
subject: z.string().default("你好 {{subscriber.firstName | capitalize}}"),
}),
});Variables in Controls
控件中的变量
Control values support LiquidJS templating:
- — any subscriber attribute
{{subscriber.firstName}} - — any payload field defined in
{{payload.userId}}payloadSchema - — Liquid filters
{{payload.invoiceDate | date: '%a, %b %d, %y'}} - — chained filters
{{subscriber.firstName | append: ': ' | append: payload.status | capitalize}}
Type in the Dashboard UI to autocomplete available variables.
{{控件值支持LiquidJS模板:
- — 任何订阅者属性
{{subscriber.firstName}} - —
{{payload.userId}}中定义的任何负载字段payloadSchema - — Liquid过滤器
{{payload.invoiceDate | date: '%a, %b %d, %y'}} - — 链式过滤器
{{subscriber.firstName | append: ': ' | append: payload.status | capitalize}}
在仪表盘UI中输入即可自动补全可用变量。
{{Controls vs Payload
控件 vs 负载
| Controls | Payload | |
|---|---|---|
| Edited by | Non-technical peers in Dashboard | Developers in code |
| Schema | | |
| Persistence | Stored in Novu Cloud per environment | Sent at trigger time |
| Use case | Subject, copy, styling, behaviour toggles | Dynamic per-trigger data |
| 控件 | 负载 | |
|---|---|---|
| 编辑者 | 非技术人员在仪表盘中编辑 | 开发者在代码中设置 |
| Schema | 每个步骤的 | 每个工作流的 |
| 存储位置 | 按环境存储在Novu Cloud中 | 触发时传递 |
| 使用场景 | 主题、文案、样式、行为开关 | 每个触发事件的动态数据 |
Workflow Preferences
工作流偏好设置
Define default channel preferences in code. See for the full preference resolution model.
manage-preferencestypescript
workflow("system-alert", handler, {
preferences: {
all: { enabled: true, readOnly: false },
channels: {
email: { enabled: true },
sms: { enabled: false },
inApp: { enabled: true },
},
},
});- makes the workflow critical — subscribers cannot disable it.
all.readOnly: true - is the fallback for any channel not in
all.enabled.channels - Default if omitted: ,
enabled: truefor all channels.readOnly: false
在代码中定义默认渠道偏好。详见了解完整的偏好解析模型。
manage-preferencestypescript
workflow("system-alert", handler, {
preferences: {
all: { enabled: true, readOnly: false },
channels: {
email: { enabled: true },
sms: { enabled: false },
inApp: { enabled: true },
},
},
});- 会将工作流标记为critical——订阅者无法禁用它。
all.readOnly: true - 是
all.enabled中未列出渠道的默认值。channels - 若省略,默认值为:所有渠道,
enabled: true。readOnly: false
Bridge Endpoint Setup
Bridge Endpoint 配置
The Bridge is a single HTTP route ( by default) where Novu Cloud calls your app to:
/api/novu- Discover registered workflows ()
GET - Resolve step content for a given subscriber + payload ()
POST - Verify HMAC signatures on requests
Each framework ships a wrapper that handles parsing, HMAC verification, and response shaping.
serveBridge是单个HTTP路由(默认),Novu Cloud通过它调用你的应用以:
/api/novu- 发现已注册的工作流(请求)
GET - 解析给定订阅者+负载的步骤内容(请求)
POST - 验证请求的HMAC签名
每个框架都提供了包装器,用于处理解析、HMAC验证和响应格式化。
serveNext.js (App Router)
Next.js(App Router)
typescript
import { serve } from "@novu/framework/next";
import { welcomeWorkflow } from "@/novu/workflows";
export const { GET, POST, OPTIONS } = serve({
workflows: [welcomeWorkflow],
});typescript
import { serve } from "@novu/framework/next";
import { welcomeWorkflow } from "@/novu/workflows";
export const { GET, POST, OPTIONS } = serve({
workflows: [welcomeWorkflow],
});Next.js (Pages Router)
Next.js(Pages Router)
typescript
import { serve } from "@novu/framework/next";
import { welcomeWorkflow } from "../../novu/workflows";
export default serve({ workflows: [welcomeWorkflow] });typescript
import { serve } from "@novu/framework/next";
import { welcomeWorkflow } from "../../novu/workflows";
export default serve({ workflows: [welcomeWorkflow] });Express
Express
typescript
import express from "express";
import { serve } from "@novu/framework/express";
import { welcomeWorkflow } from "./novu/workflows";
const app = express();
app.use(express.json()); // required
app.use("/api/novu", serve({ workflows: [welcomeWorkflow] }));
app.listen(4000);typescript
import express from "express";
import { serve } from "@novu/framework/express";
import { welcomeWorkflow } from "./novu/workflows";
const app = express();
app.use(express.json()); // 必填
app.use("/api/novu", serve({ workflows: [welcomeWorkflow] }));
app.listen(4000);NestJS
NestJS
typescript
import { Module } from "@nestjs/common";
import { NovuModule } from "@novu/framework/nest";
import { welcomeWorkflow } from "./novu/workflows";
@Module({
imports: [
NovuModule.register({
apiPath: "/api/novu",
workflows: [welcomeWorkflow],
}),
],
})
export class AppModule {}For dependency injection, use — see .
NovuModule.registerAsyncreferences/bridge-endpoint.mdtypescript
import { Module } from "@nestjs/common";
import { NovuModule } from "@novu/framework/nest";
import { welcomeWorkflow } from "./novu/workflows";
@Module({
imports: [
NovuModule.register({
apiPath: "/api/novu",
workflows: [welcomeWorkflow],
}),
],
})
export class AppModule {}如需依赖注入,使用——详见。
NovuModule.registerAsyncreferences/bridge-endpoint.mdRemix
Remix
typescript
import { serve } from "@novu/framework/remix";
import { welcomeWorkflow } from "../novu/workflows";
const handler = serve({ workflows: [welcomeWorkflow] });
export { handler as action, handler as loader };typescript
import { serve } from "@novu/framework/remix";
import { welcomeWorkflow } from "../novu/workflows";
const handler = serve({ workflows: [welcomeWorkflow] });
export { handler as action, handler as loader };SvelteKit
SvelteKit
typescript
import { serve } from "@novu/framework/sveltekit";
import { welcomeWorkflow } from "$lib/novu/workflows";
export const { GET, POST, OPTIONS } = serve({ workflows: [welcomeWorkflow] });typescript
import { serve } from "@novu/framework/sveltekit";
import { welcomeWorkflow } from "$lib/novu/workflows";
export const { GET, POST, OPTIONS } = serve({ workflows: [welcomeWorkflow] });Nuxt
Nuxt
typescript
import { serve } from "@novu/framework/nuxt";
import { welcomeWorkflow } from "~/novu/workflows";
export default defineEventHandler(serve({ workflows: [welcomeWorkflow] }));typescript
import { serve } from "@novu/framework/nuxt";
import { welcomeWorkflow } from "~/novu/workflows";
export default defineEventHandler(serve({ workflows: [welcomeWorkflow] }));H3
H3
typescript
import { createApp, eventHandler, toNodeListener } from "h3";
import { createServer } from "node:http";
import { serve } from "@novu/framework/h3";
import { welcomeWorkflow } from "./novu/workflows";
const app = createApp();
app.use("/api/novu", eventHandler(serve({ workflows: [welcomeWorkflow] })));
createServer(toNodeListener(app)).listen(4000);typescript
import { createApp, eventHandler, toNodeListener } from "h3";
import { createServer } from "node:http";
import { serve } from "@novu/framework/h3";
import { welcomeWorkflow } from "./novu/workflows";
const app = createApp();
app.use("/api/novu", eventHandler(serve({ workflows: [welcomeWorkflow] })));
createServer(toNodeListener(app)).listen(4000);AWS Lambda
AWS Lambda
typescript
import { serve } from "@novu/framework/lambda";
import { welcomeWorkflow } from "./novu/workflows";
export const novu = serve({ workflows: [welcomeWorkflow] });typescript
import { serve } from "@novu/framework/lambda";
import { welcomeWorkflow } from "./novu/workflows";
export const novu = serve({ workflows: [welcomeWorkflow] });Custom (any framework)
自定义(任意框架)
typescript
import { NovuRequestHandler, ServeHandlerOptions } from "@novu/framework";
export const serve = (options: ServeHandlerOptions) =>
new NovuRequestHandler({
frameworkName: "my-framework",
...options,
handler: (req, res) => ({ /* method, headers, body, url, transformResponse */ }),
}).createHandler();See for the full custom handler signature.
references/bridge-endpoint.mdtypescript
import { NovuRequestHandler, ServeHandlerOptions } from "@novu/framework";
export const serve = (options: ServeHandlerOptions) =>
new NovuRequestHandler({
frameworkName: "my-framework",
...options,
handler: (req, res) => ({ /* method, headers, body, url, transformResponse */ }),
}).createHandler();完整的自定义处理器签名详见。
references/bridge-endpoint.mdLocal Studio
本地Studio
Live preview of your workflows with a public tunnel for Novu Cloud to reach your machine.
bash
npx novu@latest dev工作流的实时预览工具,提供公共隧道让Novu Cloud能够访问你的本地机器。
bash
npx novu@latest devDefaults: --port 4000 --route /api/novu --studio-port 2022
默认值:--port 4000 --route /api/novu --studio-port 2022
Then open `http://localhost:2022` (Chrome only).
然后打开`http://localhost:2022`(仅支持Chrome浏览器)。CLI Flags
CLI 参数
| Flag | Default | Purpose |
|---|---|---|
| | Your app's port |
| | Bridge route path |
| | Bridge origin |
| | Dashboard URL — use |
| | Studio UI port |
| auto | Self-hosted tunnel URL (e.g. ngrok) |
| | Skip the Studio UI |
bash
npx novu@latest dev --port 3002 --dashboard-url https://eu.dashboard.novu.coThe Studio:
- Auto-creates a stable tunnel URL like
https://<id>.novu.sh/api/novu - Lets you edit Step Controls and Payload to preview different states
- Runs against — HMAC verification is off to allow Studio access
process.env.NODE_ENV=development - Has a "Sync" button to push state to Cloud (use CI/CD for real deployments)
| 参数 | 默认值 | 用途 |
|---|---|---|
| | 你的应用端口 |
| | Bridge路由路径 |
| | Bridge源地址 |
| | 仪表盘URL——欧盟地区使用 |
| | Studio UI端口 |
| 自动 | 自托管隧道URL(例如ngrok) |
| | 跳过Studio UI |
bash
npx novu@latest dev --port 3002 --dashboard-url https://eu.dashboard.novu.coStudio功能:
- 自动创建稳定的隧道URL,例如
https://<id>.novu.sh/api/novu - 允许你编辑步骤控件和负载以预览不同状态
- 运行在环境下——HMAC验证关闭以允许Studio访问
process.env.NODE_ENV=development - 提供“同步”按钮将状态推送到Cloud(正式部署请使用CI/CD)
Triggering Workflows
触发工作流
Code-defined workflows are triggered the same way as Dashboard workflows — using from your trigger surface (server, queue worker, webhook handler):
@novu/apitypescript
import { Novu } from "@novu/api";
const novu = new Novu({ secretKey: process.env.NOVU_SECRET_KEY });
await novu.trigger({
workflowId: "welcome-email",
to: { subscriberId: "user-123", email: "jane@acme.com" },
payload: { userName: "Jane", appName: "Acme" },
});You can also trigger a workflow from inside a of another workflow:
step.customtypescript
await step.custom("trigger-summary", async () => {
return await summaryWorkflow.trigger({
to: subscriber.subscriberId,
payload: { events: events.map(e => e.payload) },
});
});See for full trigger options (bulk, broadcast, topics, overrides, transactionId, cancel).
trigger-notification代码定义的工作流与仪表盘工作流的触发方式相同——在你的触发入口(服务器、队列 worker、Webhook处理器)中使用:
@novu/apitypescript
import { Novu } from "@novu/api";
const novu = new Novu({ secretKey: process.env.NOVU_SECRET_KEY });
await novu.trigger({
workflowId: "welcome-email",
to: { subscriberId: "user-123", email: "jane@acme.com" },
payload: { userName: "Jane", appName: "Acme" },
});你也可以在另一个工作流的中触发工作流:
step.customtypescript
await step.custom("trigger-summary", async () => {
return await summaryWorkflow.trigger({
to: subscriber.subscriberId,
payload: { events: events.map(e => e.payload) },
});
});完整的触发选项(批量、广播、主题、覆盖、transactionId、取消)详见。
trigger-notificationReact Email (and friends)
React Email(及同类工具)
Render emails using your existing component library.
使用你已有的组件库渲染邮件。
React Email
React Email
bash
npm install @react-email/components react-emailtsx
import { Body, Container, Head, Html, render } from "@react-email/components";
export const WelcomeEmail = ({ name }: { name: string }) => (
<Html>
<Head />
<Body>
<Container>Hello {name}, welcome!</Container>
</Body>
</Html>
);
export const renderWelcome = (name: string) => render(<WelcomeEmail name={name} />);typescript
await step.email("welcome", async () => ({
subject: "Welcome",
body: renderWelcome(payload.userName),
}));Vue Email, Svelte Email, and Remix + React Email are also supported. See .
references/email-templates.mdbash
npm install @react-email/components react-emailtsx
import { Body, Container, Head, Html, render } from "@react-email/components";
export const WelcomeEmail = ({ name }: { name: string }) => (
<Html>
<Head />
<Body>
<Container>你好 {name},欢迎加入!</Container>
</Body>
</Html>
);
export const renderWelcome = (name: string) => render(<WelcomeEmail name={name} />);typescript
await step.email("welcome", async () => ({
subject: "欢迎",
body: renderWelcome(payload.userName),
}));同时支持Vue Email、Svelte Email以及Remix + React Email。详见。
references/email-templates.mdTranslations (i18n)
翻译(i18n)
For Framework-based workflows, translation lives in your code (not in the Novu Translation system, which targets Dashboard workflows). Use any i18n library (e.g. i18next) and resolve content from inside the resolver.
subscriber.localetypescript
import { workflow } from "@novu/framework";
import i18n from "./i18n";
export const localizedWorkflow = workflow(
"welcome-localized",
async ({ step, subscriber }) => {
await step.email("email", async (controls) => {
const t = i18n.getFixedT([subscriber.locale ?? controls.defaultLocale]);
return {
subject: t("welcomeEmailSubject", { username: subscriber.firstName }),
body: render(<Welcome subject={t("subject")} body={t("body")} />),
};
}, {
controlSchema: z.object({
defaultLocale: z.string().default("en_US"),
}),
});
},
);See for a complete i18next + React Email example.
references/translations.md对于基于Framework的工作流,翻译逻辑在你的代码中实现(而非Novu翻译系统,后者针对仪表盘工作流)。使用任意i18n库(例如i18next),在resolver中根据解析内容。
subscriber.localetypescript
import { workflow } from "@novu/framework";
import i18n from "./i18n";
export const localizedWorkflow = workflow(
"welcome-localized",
async ({ step, subscriber }) => {
await step.email("email", async (controls) => {
const t = i18n.getFixedT([subscriber.locale ?? controls.defaultLocale]);
return {
subject: t("welcomeEmailSubject", { username: subscriber.firstName }),
body: render(<Welcome subject={t("subject")} body={t("body")} />),
};
}, {
controlSchema: z.object({
defaultLocale: z.string().default("en_US"),
}),
});
},
);完整的i18next + React Email示例详见。
references/translations.mdTags
标签
Tag a workflow to group it with related notifications (used by Inbox tabs and Dashboard filtering):
typescript
workflow("login-alert", handler, { tags: ["security"] });
workflow("password-change", handler, { tags: ["security"] });In the Inbox, render a "Security" tab with (see ).
tabs={[{ label: "Security", filter: { tags: ["security"] } }]}inbox-integration为工作流添加标签,将其与相关通知分组(用于收件箱标签页和仪表盘过滤):
typescript
workflow("login-alert", handler, { tags: ["security"] });
workflow("password-change", handler, { tags: ["security"] });在收件箱中,通过渲染“安全”标签页(详见)。
tabs={[{ label: "安全", filter: { tags: ["security"] } }]}inbox-integrationDeployment
部署
Sync via CLI
通过CLI同步
Push your workflows to Novu Cloud:
bash
npx novu@latest sync \
--bridge-url https://api.acme.com/api/novu \
--secret-key $NOVU_SECRET_KEY \
--api-url https://api.novu.co # use https://eu.api.novu.co for EU将你的工作流推送到Novu Cloud:
bash
npx novu@latest sync \
--bridge-url https://api.acme.com/api/novu \
--secret-key $NOVU_SECRET_KEY \
--api-url https://api.novu.co # 欧盟地区使用https://eu.api.novu.coGitHub Actions
GitHub Actions
yaml
name: Sync Novu Workflows
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: novuhq/actions-novu-sync@v2
with:
secret-key: ${{ secrets.NOVU_SECRET_KEY }}
bridge-url: ${{ secrets.NOVU_BRIDGE_URL }}
api-url: https://api.novu.coyaml
name: 同步Novu工作流
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: novuhq/actions-novu-sync@v2
with:
secret-key: ${{ secrets.NOVU_SECRET_KEY }}
bridge-url: ${{ secrets.NOVU_BRIDGE_URL }}
api-url: https://api.novu.coGitOps Workflow
GitOps工作流
- Develop locally with the Studio against your own machine.
- Open a PR — CI runs against the Development environment to test e2e.
npx novu sync - Merge to — CI runs
mainagainst Production.npx novu sync
GitLab CI, Jenkins, CircleCI, Bitbucket, Azure DevOps, and Travis CI all work via the CLI.
- 本地使用Studio开发,连接到你的本地机器。
- 提交PR——CI运行同步到开发环境进行端到端测试。
npx novu sync - 合并到分支——CI运行
main同步到生产环境。npx novu sync
GitLab CI、Jenkins、CircleCI、Bitbucket、Azure DevOps和Travis CI均可通过CLI实现同步。
Production & Security
生产环境与安全
- Bridge URL must be publicly reachable over HTTPS. Novu Cloud auto-scales — no IP allowlist is published.
- HMAC verification is on by default when . The
NODE_ENV !== "development"wrapper handles this — you don't need to write any code. Each request includes aserveheader (Novu-Signature) that's verified againstt=timestamp,v1=signature.NOVU_SECRET_KEY - Disable HMAC for local dev automatically via . Don't disable it in production.
NODE_ENV=development - Vercel Preview URLs are protected by default — enable Protection Bypass for Automation and pass the bypass token as in your bridge URL.
?x-vercel-protection-bypass=<token>
- Bridge URL必须可公开访问且使用HTTPS。Novu Cloud会自动扩容——未公布IP白名单。
- 默认启用HMAC验证当时。
NODE_ENV !== "development"包装器会处理验证——你无需编写额外代码。每个请求包含serve头(Novu-Signature),会与t=timestamp,v1=signature进行验证。NOVU_SECRET_KEY - 本地开发自动禁用HMAC通过。生产环境请勿禁用。
NODE_ENV=development - Vercel预览URL默认受保护——启用自动化绕过保护,并在你的Bridge URL中传递绕过令牌。
?x-vercel-protection-bypass=<token>
Custom Client
自定义客户端
Override defaults globally:
typescript
import { Client as NovuFrameworkClient } from "@novu/framework";
import { serve } from "@novu/framework/next";
export const { GET, POST, OPTIONS } = serve({
client: new NovuFrameworkClient({
secretKey: process.env.NOVU_SECRET_KEY,
strictAuthentication: false, // disables HMAC — only for local dev
}),
workflows: [/* … */],
});Environment variables read by the Client:
- — your secret key
NOVU_SECRET_KEY - — defaults to
NOVU_API_URL(usehttps://api.novu.cofor EU)https://eu.api.novu.co
全局覆盖默认设置:
typescript
import { Client as NovuFrameworkClient } from "@novu/framework";
import { serve } from "@novu/framework/next";
export const { GET, POST, OPTIONS } = serve({
client: new NovuFrameworkClient({
secretKey: process.env.NOVU_SECRET_KEY,
strictAuthentication: false, // 禁用HMAC——仅用于本地开发
}),
workflows: [/* … */],
});客户端读取的环境变量:
- ——你的密钥
NOVU_SECRET_KEY - ——默认
NOVU_API_URL(欧盟地区使用https://api.novu.co)https://eu.api.novu.co
Common Pitfalls
常见陷阱
- Bridge URL must be publicly reachable — won't work for Novu Cloud. Use the Studio tunnel locally; deploy publicly for production.
localhost - is the trigger identifier — same id you'll pass to
workflowId. Use kebab-case and keep it stable.novu.trigger({ workflowId }) - Step s must be unique within a workflow — duplicates throw at registration.
id - on JSON Schema — without it, TS infers
as constinstead of literal types andstringbecomespayload.unknown - Only one per workflow — chain a second workflow via
step.digestfor two-stage digest patterns.step.custom - Digest / delay results from one trigger don't influence other triggers — they're per workflow run.
- Custom step results aren't usable in step controls — only in subsequent step ,
resolver, orproviderscallbacks.skip - Sync after every workflow change — Novu Cloud needs to know about new/renamed workflows and updated control schemas. Add to your CI/CD.
npx novu sync - HMAC fails locally if — set it to
NODE_ENV !== "development"for the Studio to reach your bridge, or disable strict auth in yourdevelopment.Client - Don't store the in the client bundle — it's server-only. Keep workflows + bridge route inside server code, not in any
secretKeymodule."use client" - Provider override is unvalidated — typos won't error at compile time. Use known typed provider keys whenever possible.
_passthrough - Changing a delay/digest step's content does not affect already-scheduled events — content is captured at the time of the original trigger.
- Workflow handlers must be deterministic across retries — Novu re-invokes the bridge to resolve step content. Avoid side-effects outside (custom is the only step whose result is durably persisted).
step.custom - requires Node.js ≥ 20.
@novu/framework
- Bridge URL必须可公开访问——无法被Novu Cloud访问。本地使用Studio隧道;生产环境部署到公开地址。
localhost - 是触发标识符——与你传递给
workflowId的ID一致。使用短横线命名法,并保持稳定。novu.trigger({ workflowId }) - 步骤s在工作流中必须唯一——重复ID会在注册时抛出错误。
id - JSON Schema需添加——否则TS会推断为
as const而非字面量类型,string会变为payload。unknown - 每个工作流仅支持一个——如需两级摘要,通过
step.digest调用第二个工作流。step.custom - 一个触发事件的摘要/延迟结果不会影响其他触发事件——它们是每个工作流实例独立的。
- 自定义步骤结果不可用于步骤控件——仅可用于后续步骤的、
resolver或providers回调。skip - 每次工作流变更后需同步——Novu Cloud需要了解新增/重命名的工作流以及更新的控件schema。将添加到你的CI/CD流程中。
npx novu sync - 如果,本地HMAC验证会失败——设置为
NODE_ENV !== "development"让Studio能够访问你的Bridge,或在development中禁用严格认证。Client - 请勿在客户端包中存储——它仅用于服务端。将工作流+Bridge路由放在服务端代码中,不要放在任何
secretKey模块中。"use client" - 服务商覆盖的未经过验证——拼写错误不会在编译时报错。尽可能使用已知的带类型服务商键。
_passthrough - 修改延迟/摘要步骤的内容不会影响已调度的事件——内容在原始触发时已被捕获。
- 工作流处理器在重试时必须是确定性的——Novu会重新调用Bridge解析步骤内容。避免在之外产生副作用(custom是唯一持久化结果的步骤)。
step.custom - 要求Node.js ≥ 20。
@novu/framework
Code Style Tips
代码风格建议
- One file per workflow under , re-exported from a barrel
src/novu/workflows/<workflow-id>.ts.src/novu/workflows/index.ts - Prefer Zod schemas — best autocomplete and inference. Use JSON Schema only when you need features Zod doesn't expose (,
oneOf,if/then/else).$ref - Co-locate React Email templates next to the workflow that uses them ().
src/novu/workflows/welcome/template.tsx - Wrap shared logic into helpers (
step.custom) for reuse.fetchUser(payload.userId) - For NestJS, use with a
NovuModule.registerAsyncso workflow definitions can inject services.NotificationService
- 每个工作流对应一个文件,放在,并在
src/novu/workflows/<workflow-id>.ts中统一导出。src/novu/workflows/index.ts - 优先使用Zod schema——自动补全和类型推断效果最佳。仅当需要Zod不支持的特性(、
oneOf、if/then/else)时使用JSON Schema。$ref - 将React Email模板与使用它的工作流放在一起()。
src/novu/workflows/welcome/template.tsx - 将共享的逻辑封装为工具函数(例如
step.custom)以便复用。fetchUser(payload.userId) - 对于NestJS,使用配合
NovuModule.registerAsync,让工作流定义能够注入服务。NotificationService
References
参考文档
- Bridge Endpoint Setup — every framework wrapper, custom , NestJS DI
serve - Workflow & Step API — full options, all step types, conditional logic patterns
- Schema Validation — Zod, JSON Schema, Class Validator deep dive
- Email Templates — React, Vue, Svelte Email integrations
- Translations — i18next-based localized workflows
- Local Studio & CLI — every flag, tunnel modes, headless mode
- Deployment — , GitHub Action, GitOps recipe, EU region
npx novu sync - Production & Security — HMAC, public bridge requirements, Vercel preview bypass
- Examples Cookbook — multi-step onboarding, digest, delay-then-skip, LLM-powered digest
- Bridge Endpoint 配置——所有框架包装器、自定义、NestJS依赖注入
serve - 工作流与步骤API——完整选项、所有步骤类型、条件逻辑模式
- Schema验证——Zod、JSON Schema、Class Validator深度解析
- 邮件模板——React、Vue、Svelte Email集成
- 翻译——基于i18next的本地化工作流
- 本地Studio与CLI——所有参数、隧道模式、无头模式
- 部署——、GitHub Action、GitOps方案、欧盟地区
npx novu sync - 生产环境与安全——HMAC、Bridge公开要求、Vercel预览绕过
- 示例手册——多步骤入站、摘要、延迟后跳过、LLM驱动的摘要