This guide assumes you know what a notification workflow is and why you need it. It also assumes you are aware of Nuxt and VueEmail. Fantastic. Now that we are all on the same page let’s proceed.

Novu recently released a new way of baking notification workflows into your apps via code, and it’s huge! Let me walk you through it.

It’s called Novu Framework and you can learn more about it by visiting the related documentation. You might also benefit from reading this excellent Code-First Approach to Managing Notification Workflows article.

If you prefer to clone the repository from GitHub and get your hands dirty, we aren’t going to stop you. You can find it here.

Still here? Good, I wrote this guide for you!

Getting Started

1. Initialize New Nuxt Project

We will follow the official installation guide from Nuxt, open your terminal / IDE, and run the following command:

npx nuxi@latest init <project-name>

2. Integrate Novu Framework Into Our App (Notification Workflow)

  1. We will install the @novu/framework package in the root of our Nuxt app directory by running the following command:
npm install @novu/framework
  1. We must provide an API endpoint for the Novu Dev Studio to fetch our notification workflow. (Don’t worry; we guide you through all the steps.) Navigate to your app’s app/server directory and create a new directory named api.
cd app/server

mkdir api

Create a file within the app/server/api directory and name it novu.ts. Copy and paste the code snippet below:

// app/server/api/novu.ts
import { serve } from "@novu/framework/nuxt";
import { client, myWorkflow } from "../novu/workflows";

export default defineEventHandler(serve({ client: client, workflows: [myWorkflow] }));
  1. Now, let’s create the instance where we will build and maintain our notification workflow:

Navigate to your app/server directory and create another directory named novu.

cd app/server

mkdir novu

Create a file within the app/server/novu directory and name it workflows.ts. Copy and paste the code snippet below:

// app/server/novu/workflows.ts
// This workflow already renders vue email template.

import { config } from '@vue-email/compiler'
import { Client, workflow } from '@novu/framework';

const vueEmail = config('templates', {
  verbose: false,
  options: {
  },
})

export const client = new Client({
  /**
   * Disable this flag only during local development
   * For production this should be true
   */
  strictAuthentication: process.env.NODE_ENV !== "development"
});

export const myWorkflow = workflow('hello-world', async ({ step, payload }) => {
    await step.email(
        'send-email',
        async () => {
            const template = await vueEmail.render('sample-email.vue', {
                props: payload,
            });
            return {
                subject: `You have a new invitation from: ${payload.username}.`,
                body: template.html,
            }
        });
},
    {
        payloadSchema: {
            // Always `object`
            type: 'object',
            // Specify the properties to validate. Supports deep nesting.
            properties: {
                        username: { type: "string" },
                        invitedByEmail: { type: "string" },
                        inviteLink: { type: "string" },
                        inviteFromIp: { type: "string" },
                        inviteFromLocation: { type: "string" }
            },
            // 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,
    },
);

We haven’t finished the guide yet but have everything to build a notification workflow.

Below, We can see a “plain” Client instance and shaping your desired workflow.

// app/server/novu/workflows.ts
// This is Client instance concept - not a working instance!

import { Client, workflow } from '@novu/framework';

export const client = new Client({
  /**
   * Disable this flag only during local development
   * For production this should be true
   */
  strictAuthentication: process.env.NODE_ENV !== "development"
});

export const myWorkflow = workflow(
	'<WORKFLOW_NAME>', // The Workflow name. Must be unique across your Bridge application.
	// Workflow resolver, the entry-point for your Workflow steps
	async ({
		// Helper function to declare your Workflow Steps.
		step,
		// Workflow Trigger payload
		payload,
	}) => {
	// ...your Workflow Steps
}, {
	// JSON Schema for validation and type-safety. Zod, and others coming soon.
	// https://json-schema.org/draft-07/json-schema-release-notes

	// The schema for the Workflow payload passed dynamically via Novu Trigger API
	// Defaults to an empty schema.
	payloadSchema: { properties: { name: { type: 'string' }}},
	// The schema for the Workflow inputs passed statically via Novu Web
	// Defaults to an empty schema.
	inputSchema: { properties: { brandColor: { type: 'string' }}},
});

We can:

  • Select a name for our workflow.
  • Declare Workflow Steps as we see fit for our use case
  • Configure what could be passed in the payload of the Workflow Trigger (payloadSchema)
  • Configure what inputs could be passed statically via Novu Web (inputSchema)

Note: You can create and manage as many workflows as you wish within app/server/novu/workflows.ts; make sure to provide each workflow with a unique name.

3. Adding Vuemail in our notification workflow

  1. We will install the vue-email package in the root of our Nuxt app directory by running the following command:
npm install vue-email
  1. We will create one more directory in the root of our directory and name it templates. That is where our vue-email templates will be stored.
mkdir templates
  1. In the app/templates directory, we will create a file for our first vue-email template and name it sample-email.vue.

You can find examples of Vue email templates here.

// app/templates/sample-email.vue

<script setup lang="ts">
import { defineProps, withDefaults } from 'vue'

interface Props {
  invitedByUsername?: string
  teamName?: string
  username?: string
  invitedByEmail?: string
  inviteLink?: string
  inviteFromIp?: string
  inviteFromLocation?: string
  showFootnote?: boolean;
  components: string[] | null
}

const baseUrl = "https://react-email-demo-bdj5iju9r-resend.vercel.app";

const props = withDefaults(defineProps<Props>(), {
  teamName: 'Cleaning',
  username: 'John Doe',
  invitedByEmail: 'anpch@example.com',
  inviteLink: 'https://acme.com/projects/accept/foo',
  inviteFromIp: '172.0.0.1',
  inviteFromLocation: 'San Francisco, CA',
  showFootnote: true,
  components: null
})

const previewText = `Join ${props.invitedByUsername} on Vercel`
</script>

<template>
  <ETailwind>
    <EHtml>
      <EHead />
      <EPreview>{{ previewText }}</EPreview>
      <EBody class="bg-white my-auto mx-auto font-sans">
        <EContainer class="border border-solid border-[#eaeaea] p-[20px] md:p-7 rounded my-[40px] mx-auto max-w-[465px]">
          <div v-for="item in components || []">
            <EButton v-if="item === 'button'" px="20" py="12" class="bg-[#000000] rounded text-white text-[12px] font-semibold no-underline text-center" :href="inviteLink"> Accept </EButton>
            <EText v-if="item === 'text'" class="text-black text-[14px] leading-[24px]"> Hello {{ username }}, </EText>
            <EHr v-if="item === 'divider'" class="border border-solid border-[#eaeaea] my-[26px] mx-0 w-full" />
          </div>
          <ESection class="mt-[32px]">
            <EImg :src="`${baseUrl}/static/vercel-logo.png`" width="40" height="37" alt="Vercel" class="my-0 mx-auto" />
          </ESection>
          <Hello />
          <EHeading class="text-black text-[24px] font-normal text-center p-0 my-[30px] mx-0">
            New Project <strong>{{ teamName }}</strong> available
          </EHeading>
          <EText class="text-black text-[14px] leading-[24px]"> Hello {{ username }}, </EText>
          <EText class="text-black text-[14px] leading-[24px]">
            <strong>{{invitedByUsername}}</strong> (
            <ELink :href="`mailto:${invitedByEmail}`" class="text-blue-600 no-underline">
              {{ invitedByEmail }}
            </ELink>
            ) has invited you to the <strong>{{ teamName }}</strong> project on <strong>Acme</strong>.
          </EText>
          <ESection>
            <ERow>
              <EColumn align="right">
                <EImg class="rounded-full" :src="`${baseUrl}/static/vercel-user.png`" width="64" height="64" />
              </EColumn>
              <EColumn align="center">
                <EImg :src="`${baseUrl}/static/vercel-arrow.png`" width="12" height="9" alt="invited you to" />
              </EColumn>
              <EColumn align="left">
                <EImg class="rounded-full" :src="`${baseUrl}/static/vercel-team.png`" width="64" height="64" />
              </EColumn>
            </ERow>
          </ESection>
          <ESection class="text-center mt-[32px] mb-[32px]">
            <EButton px="20" py="12" class="bg-[#000000] rounded text-white text-[12px] font-semibold no-underline text-center" :href="inviteLink"> Accept </EButton>
            <EButton px="20" py="12" class="rounded text-[12px] font-semibold no-underline text-center" :href="inviteLink"> Decline </EButton>

          </ESection>
          <EText class="text-black text-[14px] leading-[24px]">
            or copy and paste this URL into your browser:
            <ELink :href="inviteLink" class="text-blue-600 no-underline">
              {{ inviteLink }}
            </ELink>
          </EText>

          <EHr class="border border-solid border-[#eaeaea] my-[26px] mx-0 w-full" />
          <EText v-if="showFootnote" class="text-[#666666] text-[12px] leading-[24px]">
            This invitation was intended for
            <span class="text-black">{{ username }} </span>.This invite was sent from <span class="text-black">{{ inviteFromIp }}</span> located in
            <span class="text-black">{{ inviteFromLocation }}</span
            >. If you were not expecting this invitation, you can ignore this email. If you are concerned about your account's safety, please reply to this email to get in touch
            with us.
          </EText>

        </EContainer>
      </EBody>
    </EHtml>
  </ETailwind>
</template>

4. Launching The Dev Studio

Now that we have everything in our notification workflow configured along with the vue-email template we will use, it’s time to see a visual representation of what we have built.

  1. If you haven’t run the development environment for your Nuxt app, now is the time.

    npm run dev
    
  2. Do you remember that we exposed an Bridge API endpoint in our app for Dev Studio to catch? This is where it happens.

    Run the following command in a separate terminal:

    npx novu-labs@latest echo
    

    If you’ve followed this guide, you should see this:

    Launching the dev studio

    Here is where our exact API endpoint goes. If our application runs on a different port, we should change the URL manually to point Dev Studio in the right direction.

    Provide Echo endpoint

    Also, if we have configured everything correctly, we should see that Dev Studio sees our workflow identifier (Workflow unique name).

    Dev studio locates our workflow

  3. Click the “View Workflow” button in the Dev Studio to see the workflow.

    We should see the following:

    Viewing the workflow

Click on the workflow step node called send-email. We should see a preview of our email template and the Step Input and Payload variables we have configured in the workflow schemas.

Preview of our email template

  1. This is the time to adjust the email template, step input schema, or define the properties we anticipate in the payload. We have a live representation of everything. You can add more steps like In-App, SMS, and chat.

5. Syncing our workflow to Novu Cloud

Having completed crafting, designing, and modifying our notification workflow to suit our requirements, we’re ready to push it to Novu Cloud. There, it will handle the heavy lifting and seamlessly execute all the steps we’ve configured whenever we trigger the workflow.

  1. Click on the Sync to Cloud button on the top right.

    Sync to Cloud

  2. We will need to create a local tunnel that the Novu Cloud environment can reach for local experimentation purposes.

    Create a local tunnel

    On a separate terminal, run the following command:

    // Change to the port where the app is currently running
    
    npx localtunnel --port 3000
    

    We should get something like:

    your url is: https://famous-pumas-clap.loca.lt
    

    Local tunnel url should be the same in the image here

  3. Click the Create Diff button. To push (merge) the workflow code to the cloud, an API Key from our Novu account should be added to our Framework Client instance.

    Create Diff

    Let’s navigate to ../app/server/novu/workflows.ts file and add our Novu API Key.

// app/server/novu/workflows.ts
// This workflow already renders vue email template.

import { config } from '@vue-email/compiler'
import { Client, workflow } from '@novu/framework';

const vueEmail = config('templates', {
  verbose: false,
  options: {
  },
})

export const client = new Client({
  apiKey: process.env.NOVU_API_KEY, // <<-- Your Novu API KEY
  /**
   * Disable this flag only during local development
   * For production this should be true
   */
  strictAuthentication: process.env.NODE_ENV !== "development"
});

export const myWorkflow = workflow('hello-world', async ({ step, payload }) => {
    await step.email(
        'send-email',
        async () => {
            const template = await vueEmail.render('sample-email.vue', {
                props: payload,
            });
            return {
                subject: `You have a new invitation from: ${payload.username}.`,
                body: template.html,
            }
        });
},
    {
        payloadSchema: {
            // Always `object`
            type: 'object',
            // Specify the properties to validate. Supports deep nesting.
            properties: {
                        username: { type: "string" },
                        invitedByEmail: { type: "string" },
                        inviteLink: { type: "string" },
                        inviteFromIp: { type: "string" },
                        inviteFromLocation: { type: "string" }
            },
            // 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,
    },
);

We will now click the Create Diff button again.

Launching the dev studio

This is where you can review all the changes made to the workflow. Once you have verified that everything is in order, go ahead and click the Deploy Changes button.

Deploy changes

6. Testing Our Notification Workflow

There are many ways to test and trigger our workflow, but we’ll make a cURL API call to Novu Cloud from our terminal in this guide.

If we don’t have any subscribers or users in our database or within our Novu Cloud organization, we’ll send the test to ourselves for simplicity.

  • In the subscriberId key, input a random number or even our email address (as long as we do not try to assign the same ID key to another subscriber, we should be good).

  • For the email key, we will need to insert a valid email address so we can actually receive the email.

Note: Learn more about the structure of subscriber properties.

We can leave the payload empty or add properties aligned with the payloadSchema we established in the workflow definition.


curl -X POST https://api.novu.co/v1/events/trigger \
  -H "Authorization: ApiKey <NOVU_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "hello-world",
    "to": {
      "subscriberId": "<UNIQUE_SUBSCRIBER_IDENTIFIER>",
      "email": "john@doemail.com",
    },
    "payload": {
        "username": "Jane Doe",
		    "invitedByEmail": "Jane@doemail.com",
		    "inviteLink": "https://acme.com/projects/accept/foo",
		    "inviteFromIp": "172.0.0.1",
		    "inviteFromLocation": "New York, DC"
    }
  }'

Now, let’s check our email inbox.

  • No longer limited by UI notification steps and rigidity.
  • No longer limited by notification content editors and systems. The more, the merrier!
  • Now an IFTTT (If-This-Then-That) pro engineer!

Once you’ve built the workflow, you might want read one of our other guides on how to empower product teams to manage notification workflows. Have fun, and share your use case on Discord with us!