Building notification workflows with the Novu Framework
The Novu framework allows you to build and manage advanced notification workflows with code, and expose no-code controls for non-technical users to modify.
Workflows are the building blocks of your customer notification experience, they will define the what, when, how and where of your notifications.
Building blocks
Each Novu workflow is composed of 3 main components:
- Trigger - The event that will start the workflow
- Channel Steps - The delivery method of the notification with the content
- Action Steps - Actions that will happen before and after a given channel step is executed.
Let’s take a look at a simple example of a workflow that sends an email after one day:
import { workflow } from "@novu/framework";
workflow("sample-workflow", async (step) => {
await step.delay("delay", async () => {
return {
unit: "days",
amount: 1,
};
});
await step.email("email-step", async () => {
return {
subject: "Welcome to Novu",
body: "Hello, welcome to Novu!",
};
});
});
Trigger
The trigger is the event that will start the workflow. In our current example the sample-workflow
identifier will be used as our trigger id.
Workflow identifiers should be unique to your application and should be descriptive of the workflow’s purpose.
Channel Steps
Channel Steps are the delivery methods of the notification. In our example, we have an email Channel Step that will send an email with the subject Welcome to Novu
and the body Hello, welcome to Novu!
.
Novu’s durable workflow execution engine will select the relevant delivery provider configured for this channel and send the notification with the specified content.
Novu supports a variety of common notification channels out-of-the-box, including email, SMS, push, inbox, and chat.
To read more about the full list of parameters, check out the full SDK reference.
Action Steps
Action Steps are purpose built functions that help you manage the flow of your workflow. In our example, we have a delay Acction Step that will pause the workflow for one day before sending the email.
You can also use Action Steps to perform other tasks such as fetching data from an external API, updating a database, or sending a notification to another channel.
Novu supported the following Action Steps: delay, custom and digest.
Create a Workflow
Here’s a bare-bones example of a Workflow to send a notification in response to a trigger:
import { workflow } from "@novu/framework";
const myWorkflow = workflow(
"new-signup",
async ({ step, payload }) => {
// Send a welcome email
await step.email("send-email", async () => {
return {
subject: `Welcome to Acme, ${payload.name}`,
body: "We look forward to helping you achieve mission.",
};
});
// JSON Schema or compatible library like Zod for validation and type-safety
},
{ payloadSchema: z.object({ name: z.string() }) }
);
We’ll build on top of this basic code block in the following examples below.
Just-in-time data fetching
You can add any custom logic needed into your steps. For example, you might want to fetch more information about your new user from a database during the Workflow execution. You can achieve this with the following changes:
const myWorkflow = workflow(
"new-signup",
async ({ step, payload }) => {
await step.email("send-email", async () => {
// Fetch the user from your database
const user = await db.getUser(payload.userId);
return {
subject: `Welcome to Acme ${user.productTier} tier, ${user.name}.`,
// 'Welcome to Acme Business tier, John Doe.'
body: "We look forward to helping you achieve mission.",
};
});
},
{ payloadSchema: z.object({ userId: z.string() }) }
);
We call this just-in-time notification data fetching. It allows you pull in data from the relevant sources during the Workflow execution, removing the need to store all of your subscriber data in Novu.
Multi-step workflow
What if you want to send another update to the same user in one week? But you don’t want to send the follow-up if the user opted out. We can add more steps to the Workflow to achieve this.
const myWorkflow = workflow(
"new-signup",
async ({ step, payload }) => {
await step.email("send-email", async () => {
const user = await db.getUser(payload.userId);
return {
subject: `Welcome to Acme ${user.productTier} tier, ${user.name}.`,
body: "We look forward to helping you achieve mission.",
};
});
// Wait for 1 week before continuing. After 1 week, Novu will continue
// executing the Workflow from here.
await step.delay("onboarding-follow-up", async () => ({
amount: 1,
unit: "weeks",
}));
// 1 week passed, let's follow up with an in-app notification.
await step.inApp(
"onboarding-follow-up",
async (controls) => {
const user = await db.getUser(payload.userId);
// The `feedbackUrl` can be updated in Novu Web without changing code.
// This helps you to create re-usable Workflow snippets.
return {
body: `Hey ${user.name}! How do you like the product?
Let us know <a href="${controls.feedbackUrl}">here</a>
if you have any questions.`,
};
},
{
// Don't follow up if the Subscriber opted out.
skip: () => !payload.shouldFollowUp,
// Add validation to ensure `feedbackUrl` provided in Web is a `uri`
controlSchema: {
type: "object",
properties: {
feedbackUrl: {
type: "string",
format: "uri",
default: "https://acme.com/feedback",
},
},
required: ["feedbackUrl"],
additionalProperties: false,
} as const,
}
);
},
{
payloadSchema: {
type: "object",
properties: {
userId: { type: "string" },
shouldFollowUp: { type: "boolean", default: true },
},
required: ["userId", "shouldFollowUp"],
additionalProperties: false,
} as const,
}
);
With this simple Workflow, we:
- Sent a new signup email
- Waited 1 week
- Sent a follow-up notification in-app
That’s the flexibility that Novu Framework offers.
Read the section on Controls next to learn how to expose Novu’s no-code editing capabilities to your peers.
Payload Schema
Payload schema defines the payload passed during the novu.trigger
method. This is useful for ensuring that the payload is correctly formatted and that the data is valid.
workflow(
"comment-on-post",
async ({ step, payload }) => {
await step.email("send-email", async () => {
return {
subject: `You have a new comment from: ${payload.author_name}.`,
body: render(<ReactEmailContent comment={payload.comment} />),
};
});
},
{
payloadSchema: {
// Always `object`
type: "object",
// Specify the properties to validate. Supports deep nesting.
properties: {
post_id: { type: "number" },
author_name: { type: "string" },
comment: { type: "string", maxLength: 200 },
},
// Specify the array of which properties are required.
required: ["post_id", "comment"],
// Used to enforce full type strictness, with no rogue properties.
additionalProperties: false,
// The `as const` is important to let Typescript know that this
// type won't change, enabling strong typing on `inputs` via type
// inference of the provided JSON Schema.
} as const,
}
);
Tags
Tags are used to categorize the workflows. They also allow you to filter and group notifications for your Inbox tabs.
workflow(
"acme-login-alert",
async ({ step, payload }) => {
await step.inApp('inbox', async () => {
return {
subject: 'New Login Detected',
body: 'We noticed a login from a new device or location. If this wasn’t you, change your password immediately.',
};
});
},
{
tags: ['security'],
}
);
workflow(
"acme-password-change",
async ({ step, payload }) => {
await step.inApp('inbox', async () => {
return {
subject: 'Password Changed',
body: 'Your password was successfully updated. If you didn’t request this, contact support right away.',
};
});
},
{
tags: ['security'],
}
);
Passing Payload
Here is an example of the validated payload during trigger:
novu.trigger("comment-on-post", {
to: "subscriber_id",
payload: {
post_id: 1234,
author_name: "John Doe",
comment: "Looks good!",
},
});
Was this page helpful?