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에서 보기