Skip to main content

novu-framework-integration

Build code-first notification workflows with @novu/framework. Use when defining workflows in TypeScript (Zod / JSON Schema / Class Validator), composing channel steps (email, SMS, push, chat, in-app) with action steps (delay, digest, custom), exposing Step Controls for non-technical teammates, rendering React/Vue/Svelte Email templates, hosting the Bridge Endpoint inside Next.js, Express, NestJS, Remix, Nuxt, SvelteKit, H3, or AWS Lambda, syncing to Novu Cloud via CLI / GitHub Actions, securing production with HMAC, or implementing translations, hydration, multi-channel orchestration, and LLM-powered notification logic in code.

来源信息

仓库
novuhq/novu
最近来源活动
2026年6月26日 07:02
检测到的 SKILL.md 语言
英语
星标
40,151
分支
4,513

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

文件资源管理器
10 个文件

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
name
novu-framework-integration
description
Build code-first notification workflows with @novu/framework. Use when defining workflows in TypeScript (Zod / JSON Schema / Class Validator), composing channel steps (email, SMS, push, chat, in-app) with action steps (delay, digest, custom), exposing Step Controls for non-technical teammates, rendering React/Vue/Svelte Email templates, hosting the Bridge Endpoint inside Next.js, Express, NestJS, Remix, Nuxt, SvelteKit, H3, or AWS Lambda, syncing to Novu Cloud via CLI / GitHub Actions, securing production with HMAC, or implementing translations, hydration, multi-channel orchestration, and LLM-powered notification logic in code.
inputs
[{"name":"NOVU_SECRET_KEY","description":"Server-side API key from https://dashboard.novu.co/api-keys. Used by @novu/framework and the Bridge Endpoint.","required":true,"type":"secret"}]
# 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`](../trigger-notification) — no Framework needed. ## 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. ## 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, …) ``` ## Quick Start ### 1. Bootstrap a project ```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. ### 2. Or add to an existing app ```bash npm install @novu/framework zod @react-email/components react-email ``` ```bash NOVU_SECRET_KEY=<YOUR_NOVU_SECRET_KEY> ``` ### 3. Define a workflow ```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"], } ); ``` ### 4. Mount the Bridge Endpoint Pick the wrapper that matches your framework — see [Bridge Endpoint Setup](#bridge-endpoint-setup) below. ### 5. Run the Local 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. ## Workflow Anatomy > Designing the workflow itself? See [`design-workflow/`](../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); ``` | Param | Type | Description | | --- | --- | --- | | `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 | ### Workflow Options | Option | Type | Purpose | | --- | --- | --- | | `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 | ### 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 ## Channel Steps All channel steps share the same shape: ```typescript await step.<channel>(stepId, resolver, options?); ``` | Step | Output Required | Notable Outputs | Returns 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 }` | ### 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", })); ``` ### 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. ### SMS / Push / Chat ```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`, })); ``` ## Action Steps ### `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`. ### `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. ### `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> }}`. ### `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. ## 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 }); ``` ### `skip` Conditionally skip a step. Receives the resolved controls. ```typescript await step.email("follow-up", resolver, { skip: () => inAppNotification.read === true, }); ``` ### `providers` (Per-Step Provider Overrides) 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",
在 GitHub 查看
这个 SKILL.md 很大,SkillsMP 这里只预览前一段内容。 在 GitHub 查看