docs/.mintlify/skills/framework-integration/references/email-templates.md
@novu/framework lets you render emails with libraries you already know — React Email, Vue Email, Svelte Email — by returning the rendered HTML string from your step.email resolver.
<table>-based layoutsnpm install @react-email/components react-email
// emails/welcome.tsx
import {
Body,
Container,
Head,
Html,
render,
} from "@react-email/components";
import * as React from "react";
interface WelcomeEmailProps {
name: string;
}
export const WelcomeEmail = ({ name }: WelcomeEmailProps) => (
<Html>
<Head />
<Body>
<Container>
Hello {name}, welcome to your first React Email template!
</Container>
</Body>
</Html>
);
export default WelcomeEmail;
export function renderWelcome(name: string) {
return render(<WelcomeEmail name={name} />);
}
import { workflow } from "@novu/framework";
import { z } from "zod";
import { renderWelcome } from "./emails/welcome";
export const welcomeWorkflow = workflow(
"welcome",
async ({ step, payload }) => {
await step.email("send-email", async (controls) => ({
subject: controls.subject,
body: renderWelcome(payload.userName),
}), {
controlSchema: z.object({
subject: z.string().default("Welcome to {{payload.appName}}"),
}),
});
},
{
payloadSchema: z.object({
userName: z.string(),
appName: z.string().default("Acme"),
}),
}
);
React Email ships its own preview dev server:
npx react-email dev
This is independent of the Novu Studio — useful for designing your templates before wiring them into a workflow.
You can derive template props from controls:
export const NewSignUp = ({
hideBanner,
components,
}: {
hideBanner: boolean;
components: { type: string; content: string }[];
}) => (
<Html>
<Body>
{!hideBanner && <Banner />}
{components.map((c, i) => <Section key={i}>{c.content}</Section>)}
</Body>
</Html>
);
await step.email("send-email", async (controls) => ({
subject: controls.subject,
body: render(
<NewSignUp hideBanner={controls.hideBanner} components={controls.components} />
),
}), {
controlSchema: z.object({
hideBanner: z.boolean().default(false),
subject: z.string().default("Welcome"),
components: z.array(
z.object({
type: z.enum(["header", "cta-row", "footer"]),
content: z.string(),
})
),
}),
});
npm install @react-email/components react-email
Place templates in app/emails/:
// app/emails/sample-email.tsx
import { Button, Html, render } from "@react-email/components";
function Email(props: { name: string }) {
return (
<Html>
<Button
href="https://example.com"
style={{ background: "#000", color: "#fff", padding: "12px 20px" }}
>
Click me
</Button>
</Html>
);
}
export function renderEmail(props: { name: string }) {
return render(<Email {...props} />);
}
import { renderEmail } from "../emails/sample-email";
import { workflow } from "@novu/framework";
workflow("welcome", async ({ step }) => {
await step.email("send-email", async (inputs) => ({
subject: "Welcome to Remix and React Email",
body: renderEmail(inputs as { name: string }),
}));
});
A complete example app: novuhq/novu-framework-remix-example.
npm install @vue-email/components
// nuxt.config.ts
export default defineNuxtConfig({
build: { transpile: ["@vue/email"] },
nitro: {
esbuild: { options: { target: "esnext" } },
},
});
<!-- emails/welcome.vue -->
<script setup lang="ts">
import { VueEmail, Button, Container, Head, Html, Preview } from "@vue-email/components";
defineProps<{ name: string }>();
</script>
<template>
<VueEmail>
<Html>
<Head />
<Preview>Welcome to Vue Email</Preview>
<Container>
<h1>Welcome, {{ name }}!</h1>
<p>Thanks for trying Vue Email.</p>
</Container>
</Html>
</VueEmail>
</template>
import { workflow } from "@novu/framework";
import { renderEmail } from "./emails/welcome";
import { z } from "zod";
export const welcomeWorkflow = workflow("welcome", async ({ step, payload }) => {
await step.email("send-email", async (controls) => ({
subject: controls.subject,
body: renderEmail(payload.userName),
}), {
controlSchema: z.object({
subject: z.string().default("Welcome from {{payload.userName}}"),
}),
});
}, {
payloadSchema: z.object({ userName: z.string().default("John Doe") }),
});
A complete example app: novuhq/novu-framework-nuxt-example.
npm install svelte-email
<!-- src/emails/welcome.svelte -->
<script lang="ts">
import { Body, Container, Head, Html, Preview } from "svelte-email";
export let name: string;
</script>
<Html>
<Head />
<Preview>Welcome to Svelte Email</Preview>
<Body>
<Container>
<h1>Welcome, {name}!</h1>
<p>Thanks for trying Svelte Email.</p>
</Container>
</Body>
</Html>
// src/emails/welcome.ts
import { render } from "svelte-email";
import Welcome from "./welcome.svelte";
export function renderEmail(name: string) {
return render({ template: Welcome, props: { name } });
}
import { workflow } from "@novu/framework";
import { renderEmail } from "./emails/welcome";
import { z } from "zod";
export const welcomeWorkflow = workflow("welcome", async ({ step, payload }) => {
await step.email("send-email", async (controls) => ({
subject: controls.subject,
body: renderEmail(payload.userName),
}), {
controlSchema: z.object({
subject: z.string().default("Welcome to Acme"),
}),
});
}, {
payloadSchema: z.object({ userName: z.string().default("John Doe") }),
});
A complete example app: novuhq/novu-svelte-email.
src/novu/workflows/welcome/template.tsx next to src/novu/workflows/welcome/index.ts.render(<Component />) is sync for React Email (returns a string). Avoid awaiting render unless your library requires it.step.custom or inside the step.email resolver, then pass plain props.npx react-email dev, etc.) for fast iteration on the design without round-tripping through the Studio.