novu-framework-integration

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Framework Integration

框架集成

Use
@novu/framework
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.
Use this skill when building workflows in code. For workflows authored in the Novu Dashboard, just trigger them via
trigger-notification
— no Framework needed.
使用
@novu/framework
在代码中构建通知工作流,与你的应用源码一同管理。工作流存储在你的代码仓库中,内容使用你已熟悉的库(React Email、Vue Email、Svelte Email)渲染,单个HTTP端点(即Bridge)允许Novu Cloud通过你的服务提供的实时数据执行这些工作流。
当你需要在代码中构建工作流时使用此技能。若工作流是在Novu仪表盘中创建的,只需通过
trigger-notification
触发即可——无需使用Framework。

When to Use the Framework

何时使用框架

Use FrameworkUse Dashboard Workflows
Workflows must live in source control / GitOpsNon-technical peers own all the content
Need just-in-time data from your DB / APIsAll data fits in the trigger payload
Render emails with React/Vue/Svelte EmailBlock editor is enough
Execute custom code (LLMs, third-party APIs)Pure send-only flows
Need typed payload + step controlsQuick 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

工作原理

  1. You define workflows in code with
    workflow(...)
    from
    @novu/framework
    .
  2. You expose a single
    /api/novu
    HTTP route in your app — the Bridge Endpoint.
  3. You sync the bridge URL to Novu Cloud (via
    npx novu sync
    or GitHub Action).
  4. 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, …)
  1. 你使用
    @novu/framework
    中的
    workflow(...)
    在代码中定义工作流。
  2. 在你的应用中暴露单个
    /api/novu
    HTTP路由——即Bridge Endpoint
  3. 通过
    npx novu sync
    或GitHub Action将Bridge URL同步到Novu Cloud。
  4. 在工作流执行期间,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
/api/novu
route.
bash
npx novu init --secret-key=<YOUR_NOVU_SECRET_KEY>
此命令会创建一个包含示例工作流、环境文件和可用
/api/novu
路由的Bridge示例应用。

2. Or add to an existing app

2. 或添加到现有应用

bash
npm install @novu/framework zod @react-email/components react-email
bash
NOVU_SECRET_KEY=<YOUR_NOVU_SECRET_KEY>
bash
npm install @novu/framework zod @react-email/components react-email
bash
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
http://localhost:2022
to preview workflows, edit controls, and trigger test events. The Studio creates a public tunnel automatically so Novu Cloud can reach your local bridge.
bash
npx novu@latest dev --port <YOUR_APP_PORT>
打开
http://localhost:2022
预览工作流、编辑控件并触发测试事件。Studio会自动创建公共隧道,让Novu Cloud能够访问你的本地Bridge。

Workflow Anatomy

工作流结构

Designing the workflow itself? See
design-workflow/
for channel selection, severity,
critical
, 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;
design-workflow/
covers what to decide.
typescript
workflow(workflowId, handler, options);
ParamTypeDescription
workflowId
string
Unique identifier in your environment
handler
({ step, payload, subscriber }) => Promise<void>
Workflow body — calls steps in order
options
WorkflowOptions
Schema, name, description, tags, preferences
想要设计工作流本身?查看
design-workflow/
了解渠道选择、优先级、
critical
、默认摘要、步骤条件以及9个参考模板(订单确认、支付失败、账户暂停、评论、试用到期、密码重置、Webhook扇出、先获取再通知)。Framework SKILL涵盖如何在代码中表达这些决策;
design-workflow/
则涵盖需要做出哪些决策。
typescript
workflow(workflowId, handler, options);
参数类型描述
workflowId
string
环境中的唯一标识符
handler
({ step, payload, subscriber }) => Promise<void>
工作流主体——按顺序调用步骤
options
WorkflowOptions
schema、名称、描述、标签、偏好设置

Workflow Options

工作流选项

OptionTypePurpose
payloadSchema
ZodSchema | JsonSchema | ClassValidatorClass
Validates the trigger payload, infers TS type for
payload
name
string
Human-readable name shown in Dashboard /
<Inbox />
description
string
Description shown in Dashboard
tags
string[]
Categorize for filtering / Inbox tabs
severity
'low' | 'medium' | 'high'
Visual prioritization in the Inbox. Leave unset for most workflows.
critical
boolean
Bypasses subscriber preferences, skips digest, runs without delays. Reserve for must-deliver events.
preferences
WorkflowPreferences
Default channel preferences and
readOnly
flag
选项类型用途
payloadSchema
ZodSchema | JsonSchema | ClassValidatorClass
验证触发负载,为
payload
推断TS类型
name
string
在仪表盘/
<Inbox />
中显示的可读名称
description
string
在仪表盘中显示的描述
tags
string[]
分类以便过滤/收件箱标签页
severity
'low' | 'medium' | 'high'
收件箱中的视觉优先级。大多数工作流无需设置。
critical
boolean
绕过订阅者偏好,跳过摘要,无延迟运行。仅用于必须送达的事件。
preferences
WorkflowPreferences
默认渠道偏好和
readOnly
标志

Workflow Context

工作流上下文

The handler receives
{ step, payload, subscriber }
:
  • step
    — channel and action step builders (
    step.email
    ,
    step.delay
    ,
    step.digest
    , …)
  • payload
    — strongly-typed data passed at trigger time, validated against
    payloadSchema
  • subscriber
    { subscriberId, firstName?, lastName?, locale?, data?, ... }
    of the recipient
处理器接收
{ 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?);
StepOutput RequiredNotable OutputsReturns Result
step.email
subject
,
body
attachments
,
from
,
replyTo
No
step.sms
body
No
step.push
title
(or
subject
),
body
data
,
image
,
icon
No
step.chat
body
— (override per-provider)No
step.inApp
body
subject
,
avatar
,
redirect
,
primaryAction
,
secondaryAction
,
data
{ seen, read, lastSeenDate, lastReadDate }
所有渠道步骤都具有相同的结构:
typescript
await step.<channel>(stepId, resolver, options?);
步骤必填输出值得关注的输出返回结果
step.email
subject
,
body
attachments
,
from
,
replyTo
step.sms
body
step.push
title
(或
subject
),
body
data
,
image
,
icon
step.chat
body
—(按服务商覆盖)
step.inApp
body
subject
,
avatar
,
redirect
,
primaryAction
,
secondaryAction
,
data
{ seen, read, lastSeenDate, lastReadDate }

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
{ seen, read, lastSeenDate, lastReadDate }
— use it to drive the
skip
of subsequent steps.
typescript
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 }
——可用于控制后续步骤的
skip
逻辑。

SMS / 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.delay

Pause workflow execution before the next step.
typescript
await step.delay("wait-a-day", async () => ({
  unit: "days",
  amount: 1,
}));
Supported
unit
values:
seconds
,
minutes
,
hours
,
days
,
weeks
,
months
.
在执行下一步之前暂停工作流。
typescript
await step.delay("wait-a-day", async () => ({
  unit: "days",
  amount: 1,
}));
支持的
unit
值:
seconds
minutes
hours
days
weeks
months

step.digest

step.digest

Aggregate 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
cron: "0 0 * * *"
instead of
unit
/
amount
for cron-based digests. Each digest event has
{ id, time, payload }
. Only one digest per workflow — chain a second workflow via
step.custom
if you need a two-stage digest.
在指定时间段内将多个触发事件聚合为单个通知。
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: "0 0 * * *"
替代
unit
/
amount
实现基于 cron 的摘要。每个摘要事件包含
{ id, time, payload }
每个工作流仅支持一个摘要步骤——如果需要两级摘要,可通过
step.custom
调用第二个工作流。

step.http

step.http

Call 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
responseBodySchema
. Only properties declared in the schema are addressable as
{{ 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步骤必须声明
responseBodySchema
。只有schema中声明的属性才能通过
{{ steps.<http-step-id>.<property> }}
访问。

step.custom

step.custom

Run 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
resolver
,
providers
, and
skip
functions — not in step controls.
运行任意代码并将结果持久化供后续步骤使用。
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,
});
自定义步骤的结果可用于后续步骤的
resolver
providers
skip
函数——不可用于步骤控件。

Step 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

skip

Conditionally 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)

providers
(按步骤服务商覆盖)

Customize the request sent to the underlying provider — e.g. Slack
blocks
or SendGrid
cc
.
typescript
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
deep-merges into the final provider request — typed provider keys take precedence over
_passthrough
.
自定义发送给底层服务商的请求——例如Slack的
blocks
或SendGrid的
cc
typescript
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

disableOutputSanitization

disableOutputSanitization

Allow raw HTML / unescaped characters in the output (e.g.
&
in In-App
data.link
):
typescript
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
payload
and runtime validation.
负载是触发时传递的数据。定义schema以获得类型化的
payload
和运行时验证。

With 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,
});
The
as const
is required for TS to infer the payload type from 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,
});
需要添加
as const
才能让TS从JSON Schema推断负载类型。

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
class-validator
,
class-validator-jsonschema
,
reflect-metadata
. See
references/schema-validation.md
.
typescript
import { IsString, IsNumber } from "class-validator";

class CommentPayload {
  @IsNumber() postId!: number;
  @IsString() authorName!: string;
  @IsString() comment!: string;
}

workflow("comment", handler, { payloadSchema: CommentPayload });
需要依赖
class-validator
class-validator-jsonschema
reflect-metadata
。详见
references/schema-validation.md

Step 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:
  • {{subscriber.firstName}}
    — any subscriber attribute
  • {{payload.userId}}
    — any payload field defined in
    payloadSchema
  • {{payload.invoiceDate | date: '%a, %b %d, %y'}}
    — Liquid filters
  • {{subscriber.firstName | append: ': ' | append: payload.status | capitalize}}
    — chained filters
Type
{{
in the Dashboard UI to autocomplete available variables.
控件值支持LiquidJS模板:
  • {{subscriber.firstName}}
    — 任何订阅者属性
  • {{payload.userId}}
    payloadSchema
    中定义的任何负载字段
  • {{payload.invoiceDate | date: '%a, %b %d, %y'}}
    — Liquid过滤器
  • {{subscriber.firstName | append: ': ' | append: payload.status | capitalize}}
    — 链式过滤器
在仪表盘UI中输入
{{
即可自动补全可用变量。

Controls vs Payload

控件 vs 负载

ControlsPayload
Edited byNon-technical peers in DashboardDevelopers in code
Schema
controlSchema
per step
payloadSchema
per workflow
PersistenceStored in Novu Cloud per environmentSent at trigger time
Use caseSubject, copy, styling, behaviour togglesDynamic per-trigger data
控件负载
编辑者非技术人员在仪表盘中编辑开发者在代码中设置
Schema每个步骤的
controlSchema
每个工作流的
payloadSchema
存储位置按环境存储在Novu Cloud中触发时传递
使用场景主题、文案、样式、行为开关每个触发事件的动态数据

Workflow Preferences

工作流偏好设置

Define default channel preferences in code. See
manage-preferences
for the full preference resolution model.
typescript
workflow("system-alert", handler, {
  preferences: {
    all: { enabled: true, readOnly: false },
    channels: {
      email: { enabled: true },
      sms: { enabled: false },
      inApp: { enabled: true },
    },
  },
});
  • all.readOnly: true
    makes the workflow critical — subscribers cannot disable it.
  • all.enabled
    is the fallback for any channel not in
    channels
    .
  • Default if omitted:
    enabled: true
    ,
    readOnly: false
    for all channels.
在代码中定义默认渠道偏好。详见
manage-preferences
了解完整的偏好解析模型。
typescript
workflow("system-alert", handler, {
  preferences: {
    all: { enabled: true, readOnly: false },
    channels: {
      email: { enabled: true },
      sms: { enabled: false },
      inApp: { enabled: true },
    },
  },
});
  • all.readOnly: true
    会将工作流标记为critical——订阅者无法禁用它。
  • all.enabled
    channels
    中未列出渠道的默认值。
  • 若省略,默认值为:所有渠道
    enabled: true
    readOnly: false

Bridge Endpoint Setup

Bridge Endpoint 配置

The Bridge is a single HTTP route (
/api/novu
by default) where Novu Cloud calls your app to:
  • Discover registered workflows (
    GET
    )
  • Resolve step content for a given subscriber + payload (
    POST
    )
  • Verify HMAC signatures on requests
Each framework ships a
serve
wrapper that handles parsing, HMAC verification, and response shaping.
Bridge是单个HTTP路由(默认
/api/novu
),Novu Cloud通过它调用你的应用以:
  • 发现已注册的工作流(
    GET
    请求)
  • 解析给定订阅者+负载的步骤内容(
    POST
    请求)
  • 验证请求的HMAC签名
每个框架都提供了
serve
包装器,用于处理解析、HMAC验证和响应格式化。

Next.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
NovuModule.registerAsync
— see
references/bridge-endpoint.md
.
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 {}
如需依赖注入,使用
NovuModule.registerAsync
——详见
references/bridge-endpoint.md

Remix

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
references/bridge-endpoint.md
for the full custom handler signature.
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();
完整的自定义处理器签名详见
references/bridge-endpoint.md

Local 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 dev

Defaults: --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 参数

FlagDefaultPurpose
-p
,
--port
4000
Your app's port
-r
,
--route
/api/novu
Bridge route path
-o
,
--origin
http://localhost
Bridge origin
-d
,
--dashboard-url
https://dashboard.novu.co
Dashboard URL — use
https://eu.dashboard.novu.co
for EU
-sp
,
--studio-port
2022
Studio UI port
-t
,
--tunnel
autoSelf-hosted tunnel URL (e.g. ngrok)
-H
,
--headless
false
Skip the Studio UI
bash
npx novu@latest dev --port 3002 --dashboard-url https://eu.dashboard.novu.co
The 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
    process.env.NODE_ENV=development
    — HMAC verification is off to allow Studio access
  • Has a "Sync" button to push state to Cloud (use CI/CD for real deployments)
参数默认值用途
-p
,
--port
4000
你的应用端口
-r
,
--route
/api/novu
Bridge路由路径
-o
,
--origin
http://localhost
Bridge源地址
-d
,
--dashboard-url
https://dashboard.novu.co
仪表盘URL——欧盟地区使用
https://eu.dashboard.novu.co
-sp
,
--studio-port
2022
Studio UI端口
-t
,
--tunnel
自动自托管隧道URL(例如ngrok)
-H
,
--headless
false
跳过Studio UI
bash
npx novu@latest dev --port 3002 --dashboard-url https://eu.dashboard.novu.co
Studio功能:
  • 自动创建稳定的隧道URL,例如
    https://<id>.novu.sh/api/novu
  • 允许你编辑步骤控件和负载以预览不同状态
  • 运行在
    process.env.NODE_ENV=development
    环境下——HMAC验证关闭以允许Studio访问
  • 提供“同步”按钮将状态推送到Cloud(正式部署请使用CI/CD)

Triggering Workflows

触发工作流

Code-defined workflows are triggered the same way as Dashboard workflows — using
@novu/api
from your trigger surface (server, queue worker, webhook handler):
typescript
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
step.custom
of another workflow:
typescript
await step.custom("trigger-summary", async () => {
  return await summaryWorkflow.trigger({
    to: subscriber.subscriberId,
    payload: { events: events.map(e => e.payload) },
  });
});
See
trigger-notification
for full trigger options (bulk, broadcast, topics, overrides, transactionId, cancel).
代码定义的工作流与仪表盘工作流的触发方式相同——在你的触发入口(服务器、队列 worker、Webhook处理器)中使用
@novu/api
typescript
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.custom
中触发工作流:
typescript
await step.custom("trigger-summary", async () => {
  return await summaryWorkflow.trigger({
    to: subscriber.subscriberId,
    payload: { events: events.map(e => e.payload) },
  });
});
完整的触发选项(批量、广播、主题、覆盖、transactionId、取消)详见
trigger-notification

React Email (and friends)

React Email(及同类工具)

Render emails using your existing component library.
使用你已有的组件库渲染邮件。

React Email

React Email

bash
npm install @react-email/components react-email
tsx
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.md
.
bash
npm install @react-email/components react-email
tsx
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.md

Translations (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
subscriber.locale
inside the resolver.
typescript
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
references/translations.md
for a complete i18next + React Email example.
对于基于Framework的工作流,翻译逻辑在你的代码中实现(而非Novu翻译系统,后者针对仪表盘工作流)。使用任意i18n库(例如i18next),在resolver中根据
subscriber.locale
解析内容。
typescript
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.md

Tags

标签

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
tabs={[{ label: "Security", filter: { tags: ["security"] } }]}
(see
inbox-integration
).
为工作流添加标签,将其与相关通知分组(用于收件箱标签页和仪表盘过滤):
typescript
workflow("login-alert", handler, { tags: ["security"] });
workflow("password-change", handler, { tags: ["security"] });
在收件箱中,通过
tabs={[{ label: "安全", filter: { tags: ["security"] } }]}
渲染“安全”标签页(详见
inbox-integration
)。

Deployment

部署

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.co

GitHub 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.co
yaml
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.co

GitOps Workflow

GitOps工作流

  1. Develop locally with the Studio against your own machine.
  2. Open a PR — CI runs
    npx novu sync
    against the Development environment to test e2e.
  3. Merge to
    main
    — CI runs
    npx novu sync
    against Production.
GitLab CI, Jenkins, CircleCI, Bitbucket, Azure DevOps, and Travis CI all work via the CLI.
  1. 本地使用Studio开发,连接到你的本地机器。
  2. 提交PR——CI运行
    npx novu sync
    同步到开发环境进行端到端测试。
  3. 合并到
    main
    分支——CI运行
    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
    NODE_ENV !== "development"
    . The
    serve
    wrapper handles this — you don't need to write any code. Each request includes a
    Novu-Signature
    header (
    t=timestamp,v1=signature
    ) that's verified against
    NOVU_SECRET_KEY
    .
  • Disable HMAC for local dev automatically via
    NODE_ENV=development
    . Don't disable it in production.
  • Vercel Preview URLs are protected by default — enable Protection Bypass for Automation and pass the bypass token as
    ?x-vercel-protection-bypass=<token>
    in your bridge URL.
  • 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:
  • NOVU_SECRET_KEY
    — your secret key
  • NOVU_API_URL
    — defaults to
    https://api.novu.co
    (use
    https://eu.api.novu.co
    for EU)
全局覆盖默认设置:
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

常见陷阱

  1. Bridge URL must be publicly reachable
    localhost
    won't work for Novu Cloud. Use the Studio tunnel locally; deploy publicly for production.
  2. workflowId
    is the trigger identifier
    — same id you'll pass to
    novu.trigger({ workflowId })
    . Use kebab-case and keep it stable.
  3. Step
    id
    s must be unique within a workflow
    — duplicates throw at registration.
  4. as const
    on JSON Schema
    — without it, TS infers
    string
    instead of literal types and
    payload
    becomes
    unknown
    .
  5. Only one
    step.digest
    per workflow
    — chain a second workflow via
    step.custom
    for two-stage digest patterns.
  6. Digest / delay results from one trigger don't influence other triggers — they're per workflow run.
  7. Custom step results aren't usable in step controls — only in subsequent step
    resolver
    ,
    providers
    , or
    skip
    callbacks.
  8. Sync after every workflow change — Novu Cloud needs to know about new/renamed workflows and updated control schemas. Add
    npx novu sync
    to your CI/CD.
  9. HMAC fails locally if
    NODE_ENV !== "development"
    — set it to
    development
    for the Studio to reach your bridge, or disable strict auth in your
    Client
    .
  10. Don't store the
    secretKey
    in the client bundle
    — it's server-only. Keep workflows + bridge route inside server code, not in any
    "use client"
    module.
  11. Provider override
    _passthrough
    is unvalidated
    — typos won't error at compile time. Use known typed provider keys whenever possible.
  12. Changing a delay/digest step's content does not affect already-scheduled events — content is captured at the time of the original trigger.
  13. Workflow handlers must be deterministic across retries — Novu re-invokes the bridge to resolve step content. Avoid side-effects outside
    step.custom
    (custom is the only step whose result is durably persisted).
  14. @novu/framework
    requires Node.js ≥ 20
    .
  1. Bridge URL必须可公开访问——
    localhost
    无法被Novu Cloud访问。本地使用Studio隧道;生产环境部署到公开地址。
  2. workflowId
    是触发标识符
    ——与你传递给
    novu.trigger({ workflowId })
    的ID一致。使用短横线命名法,并保持稳定。
  3. 步骤
    id
    s在工作流中必须唯一
    ——重复ID会在注册时抛出错误。
  4. JSON Schema需添加
    as const
    ——否则TS会推断为
    string
    而非字面量类型,
    payload
    会变为
    unknown
  5. 每个工作流仅支持一个
    step.digest
    ——如需两级摘要,通过
    step.custom
    调用第二个工作流。
  6. 一个触发事件的摘要/延迟结果不会影响其他触发事件——它们是每个工作流实例独立的。
  7. 自定义步骤结果不可用于步骤控件——仅可用于后续步骤的
    resolver
    providers
    skip
    回调。
  8. 每次工作流变更后需同步——Novu Cloud需要了解新增/重命名的工作流以及更新的控件schema。将
    npx novu sync
    添加到你的CI/CD流程中。
  9. 如果
    NODE_ENV !== "development"
    ,本地HMAC验证会失败
    ——设置为
    development
    让Studio能够访问你的Bridge,或在
    Client
    中禁用严格认证。
  10. 请勿在客户端包中存储
    secretKey
    ——它仅用于服务端。将工作流+Bridge路由放在服务端代码中,不要放在任何
    "use client"
    模块中。
  11. 服务商覆盖的
    _passthrough
    未经过验证
    ——拼写错误不会在编译时报错。尽可能使用已知的带类型服务商键。
  12. 修改延迟/摘要步骤的内容不会影响已调度的事件——内容在原始触发时已被捕获。
  13. 工作流处理器在重试时必须是确定性的——Novu会重新调用Bridge解析步骤内容。避免在
    step.custom
    之外产生副作用(custom是唯一持久化结果的步骤)。
  14. @novu/framework
    要求Node.js ≥ 20

Code Style Tips

代码风格建议

  • One file per workflow under
    src/novu/workflows/<workflow-id>.ts
    , re-exported from a barrel
    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
    step.custom
    logic into helpers (
    fetchUser(payload.userId)
    ) for reuse.
  • For NestJS, use
    NovuModule.registerAsync
    with a
    NotificationService
    so workflow definitions can inject services.
  • 每个工作流对应一个文件,放在
    src/novu/workflows/<workflow-id>.ts
    ,并在
    src/novu/workflows/index.ts
    中统一导出。
  • 优先使用Zod schema——自动补全和类型推断效果最佳。仅当需要Zod不支持的特性(
    oneOf
    if/then/else
    $ref
    )时使用JSON Schema。
  • 将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
    serve
    , NestJS DI
  • 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
    npx novu sync
    , GitHub Action, GitOps recipe, EU region
  • Production & Security — HMAC, public bridge requirements, Vercel preview bypass
  • Examples Cookbook — multi-step onboarding, digest, delay-then-skip, LLM-powered digest
  • Bridge Endpoint 配置——所有框架包装器、自定义
    serve
    、NestJS依赖注入
  • 工作流与步骤API——完整选项、所有步骤类型、条件逻辑模式
  • Schema验证——Zod、JSON Schema、Class Validator深度解析
  • 邮件模板——React、Vue、Svelte Email集成
  • 翻译——基于i18next的本地化工作流
  • 本地Studio与CLI——所有参数、隧道模式、无头模式
  • 部署——
    npx novu sync
    、GitHub Action、GitOps方案、欧盟地区
  • 生产环境与安全——HMAC、Bridge公开要求、Vercel预览绕过
  • 示例手册——多步骤入站、摘要、延迟后跳过、LLM驱动的摘要