# Novu and Trigger.dev integration guide (/guides/triggerdotdev)

Integrate Novu with Trigger.dev to send notifications from background jobs. Covers setup, subscribers, and practical examples.

import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
import { Card, Cards } from 'fumadocs-ui/components/card';
import { NextjsIcon } from '@/components/icons/nextjs';
import { ExpressjsIcon } from '@/components/icons/expressjs';
import { Callout } from 'fumadocs-ui/components/callout';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
import { File, Folder, Files } from 'fumadocs-ui/components/files';

This guide walks you through integrating [Novu](https://novu.co/), the open-source notification infrastructure, with [Trigger.dev](https://trigger.dev/), a powerful open-source background jobs framework for TypeScript.

By combining Novu's robust notification workflows with Trigger.Dev's event-driven background job system allows you to send notifications across multiple channels (in-app, email, SMS, push, etc.) in response to events or background tasks — all within a seamless developer experience.

Whether you're processing payments, handling user onboarding, or running scheduled tasks, Trigger.dev lets you define workflows with fine-grained control and real-time observability. Novu plugs into those workflows to manage notification content and delivery, ensuring your service / product-to-user communication has a standard.

## What You'll Learn

In this guide, you'll learn how to:

* Set up both Novu and Trigger.dev in your project
* Create and update subscribers via Novu's API
* Trigger Novu notification workflows from a Trigger.dev job
* Pass dynamic payload data to power your notification templates

<Callout type="info">
  If you haven't used or are unfamiliar with Trigger.dev, we recommend following [the quickstart guide in their docs](https://trigger.dev/docs/quick-start). This guide assumes you are familiar with the fundamentals.
</Callout>

## Getting Started

<Steps>
  <Step>
    Install Novu's SDK in your project:

    ```bash
    npm i @novu/api
    ```
  </Step>

  <Step>
    Import Novu's SDK and provide the credentials to initialize the Novu instance:

    ```typescript
    import { Novu } from "@novu/api";

    const novu = new Novu({ 
      secretKey: 'ApiKey ' + process.env['NOVU_SECRET_KEY']
    });
    ```
  </Step>
</Steps>

For the sake of this guide, we will create a new dedicated task for 2 common Novu actions:

* Trigger Notification Workflow
* Add New Subscriber

## Triggering Notifications

### Core Requirements

When calling Novu's `trigger` method, you must provide the following information:

<Steps>
  <Step>
    **Workflow ID**

    `workflowId` **(string):** This identifies the specific notification workflow you want to execute.

    **Note:** Ensure this workflow ID corresponds to an existing, active workflow in your Novu dashboard.
  </Step>

  <Step>
    **Recipient Information**

    `to` **(object):** This object specifies the recipient of the notification. At a minimum, it requires:

    * `subscriberId` **(string):** A unique identifier for the notification recipient within your system (e.g., a user ID).

    **Note:** If a subscriber with this `subscriberId` doesn't already exist in Novu, Novu will automatically create one when the workflow is triggered. You can also pass other identifiers like `email`, `phone`, etc., within the `to` object, depending on your workflow steps.
  </Step>
</Steps>

### Basic Trigger Example

Here's a simple Trigger.dev task that triggers a Novu workflow when the task runs.

```typescript
import { task, retry } from "@trigger.dev/sdk/v3";
import { Novu } from "@novu/api";

// Initialize the Novu client with your API key
// It's recommended to store your secret key securely, e.g., in environment variables.
const novu = new Novu({ 
  secretKey: 'ApiKey ' + process.env['NOVU_SECRET_KEY']
}); // Add '!' if you're sure it's defined, or handle potential undefined case

export const notifyUserTask = task({
  id: "notify-user-task",
  run: async (
    payload: { // Payload received by the Trigger.dev task
      userId: string;
      email: string;
    },
    { ctx } // Access context if needed, e.g., for logging
  ) => {
    const { userId, email } = payload;

    console.log(`Attempting to trigger Novu workflow for subscriber: ${userId}`);

    // Use retry logic for resilience against transient network issues or brief API unavailability
    await retry.onThrow(
      async () => {
        try {
          const triggerResult = await novu.trigger({
            // 1. Specify the workflow to trigger
            workflowId: "your-workflow-id", // Replace with your actual workflow ID

            // 2. Specify the recipient
            to: {
              subscriberId: userId,
              email: email, // Include other identifiers if needed by your workflow
            },

            // 3. Payload (optional) - see next section
            payload: {}, // Empty payload for this basic example
          });

          console.log("Novu workflow triggered successfully:", triggerResult.data);
          return triggerResult.data; // Return data if needed elsewhere

        } catch (error: unknown) {
          // Log the specific error for better debugging
          const errorMessage = error instanceof Error ? error.message : 'Unknown error triggering Novu';
          console.error("Failed to trigger Novu workflow:", errorMessage, error);
          // Throw a new error to ensure retry logic catches it
          throw new Error(`Novu trigger failed: ${errorMessage}`);
        }
      },
      {
        maxAttempts: 3, // Configure retry attempts
        factor: 2,      // Configure backoff strategy
        minTimeoutInMs: 1000, // Wait at least 1 second before first retry
        // Add more retry options as needed
      }
    );
  },
});
```

## Working with Dynamic Content

### Using Payloads

Often, you'll want your notifications to include dynamic data related to the event that triggered them. This is done using the `payload` property in the `novu.trigger` call.

<Accordions>
  <Accordion title="Understanding Payloads">
    The `payload` is an object containing key-value pairs that you can reference within your Novu notification templates using Handlebars syntax (e.g., `{{ name }}`, `{{ order.id }}`).
  </Accordion>
</Accordions>

### Example Use Case

Imagine you want to send an email confirming a background job's completion:

<Cards>
  <Card title="Email Template Variables">
    * **Subject:** `Job {{ jobName }} Completed Successfully!`
    * **Body:** `Hi {{ userName }}, your job '{{ jobName }}' finished processing.`
  </Card>
</Cards>

To achieve this, you would pass the `userName` and `jobName` in the `payload` object when triggering the workflow.

### Advanced Trigger Example

```typescript
import { task, retry } from "@trigger.dev/sdk/v3";
import { Novu } from "@novu/api";

const novu = new Novu({ 
  secretKey: 'ApiKey ' + process.env['NOVU_SECRET_KEY']
});

export const notifyOnJobCompletion = task({
  id: "notify-on-job-completion",
  run: async (
    payload: {
      userId: string;
      email: string;
      name: string;
      jobId: string;
    }
  ) => {
    const { userId, email, name, jobId } = payload;

    await retry.onThrow(
      async () => {
        try {
          // Construct the payload specifically for Novu
          // This often uses data from the task's payload, but can include other computed values
          const novuPayload = {
            userName: name,   // Map 'name' from task payload to 'userName' for the template
            jobName: `Data Processing Job #${jobId}`, // Can include static text or transform data
          };

          await novu.trigger({
            workflowId: "inbox-test", // Use the appropriate workflow ID
            to: {
              subscriberId: userId,
              email: email,
            },
            payload: { // Pass the constructed payload to Novu
              ...novuPayload,
            },
          });
          
          console.log("Novu workflow triggered successfully with payload:", novuPayload);
          return novuPayload;

        } catch (error: unknown) {
          const errorMessage = error instanceof Error ? error.message : 'Unknown error triggering Novu';
          console.error("Failed to trigger Novu workflow with payload:", errorMessage, error);
          throw new Error(`Novu trigger failed: ${errorMessage}`);
        }
      },
      { maxAttempts: 2 } // Retry the task up to 2 times
    );
  },
});
```

### Key Points about Payloads

<Cards>
  <Card title="Trigger.dev Task Payload">
    This is the data your Trigger.dev task receives when it's invoked. It contains the context needed for the task to run.
  </Card>

  <Card title="Novu Trigger Payload">
    This is the data you *specifically send to Novu* via the `payload` property in the `novu.trigger()` call. It's used for populating variables in your notification templates.
  </Card>
</Cards>

## Implementation Examples

### AI Tasks

This example demonstrates how to build a reliable AI content generation system that keeps users informed throughout the process using Novu notifications.

```typescript

import { task, event, eventTrigger, retry } from "@trigger.dev/sdk/v3";
import { Novu } from "@novu/api";
import OpenAI from "openai";

// Initialize clients
const novu = new Novu({ 
  secretKey: 'ApiKey ' + process.env['NOVU_SECRET_KEY']
});

const openai = new OpenAI({
  apiKey: process.env['OPENAI_API_KEY'],
});

// Helper functions for prompt generation
function generateTextPrompt(theme: string, description: string) {
  return [
    { role: "system", content: "You are a creative writer crafting marketing content." },
    { role: "user", content: `Write a short, engaging paragraph about ${theme}. Key points: ${description}` }
  ];
}

function generateImagePrompt(theme: string, description: string) {
  return `Create a professional marketing image that represents ${theme}. Style: modern, clean. Details: ${description}`;
}

// Event trigger for content generation
export const contentGenerationRequested = event({
  id: "content-generation-requested",
  trigger: eventTrigger({
    name: "content.generation.requested",
  }),
});

// Notification task for successful completion
export const notifyContentReady = task({
  id: "notify-content-ready",
  run: async ({ 
    userId, 
    email, 
    theme, 
    contentId,
    contentPreview,
    imageUrl
  }: { 
    userId: string;
    email: string;
    theme: string;
    contentId: string;
    contentPreview: string;
    imageUrl: string;
  }) => {
    await retry.onThrow(
      async () => {
        try {
          await novu.trigger({
            workflowId: "ai-generation-completed",
            to: {
              subscriberId: userId,
              email: email,
            },
            payload: {
              userName: email.split('@')[0],
              theme: theme,
              contentId: contentId,
              contentPreview: contentPreview,
              imageUrl: imageUrl,
              viewUrl: `https://yourapp.com/content/${contentId}`
            },
          });
          
          console.log("Content ready notification sent successfully");
        } catch (error: unknown) {
          const errorMessage = error instanceof Error ? error.message : 'Unknown error';
          console.error("Failed to send content ready notification:", errorMessage);
          throw new Error(`Final notification failed: ${errorMessage}`);
        }
      },
      { maxAttempts: 3, factor: 2, minTimeoutInMs: 1000 }
    );
  },
});

// Error notification task
export const notifyError = task({
  id: "notify-error",
  run: async ({ 
    userId, 
    email, 
    theme, 
    errorMessage 
  }: { 
    userId: string;
    email: string;
    theme: string;
    errorMessage: string;
  }) => {
    await retry.onThrow(
      async () => {
        try {
          await novu.trigger({
            workflowId: "ai-generation-error",
            to: {
              subscriberId: userId,
              email: email,
            },
            payload: {
              userName: email.split('@')[0],
              theme: theme,
              errorMessage: errorMessage,
              supportEmail: "support@yourapp.com"
            },
          });
          
          console.log("Error notification sent successfully");
        } catch (error: unknown) {
          const errorMessage = error instanceof Error ? error.message : 'Unknown error';
          console.error("Failed to send error notification:", errorMessage);
          // Not rethrowing here to prevent infinite loops when error notifications fail
        }
      },
      { maxAttempts: 2 }
    );
  },
});

// Main AI content generation task
export const generateContent = task({
  id: "generate-content",
  retry: {
    maxAttempts: 3,
  },
  run: async ({ userId, email, theme, description }: { 
    userId: string;
    email: string;
    theme: string;
    description: string; 
  }) => {
    console.log(`Starting AI content generation for theme: ${theme}`);
    
    // Generate text content
    const textResult = await openai.chat.completions.create({
      model: "gpt-4",
      messages: [{ role: "user", content: description }]
    });

    if (!textResult.choices[0]?.message?.content) {
      throw new Error("No text content generated, retrying...");
    }
    
    // Generate image content
    const imageResult = await openai.images.generate({
      model: "dall-e-3",
      prompt: description,
      size: "1024x1024"
    });

    if (!imageResult.data[0]?.url) {
      throw new Error("No image generated, retrying...");
    }

    return {
      text: textResult.choices[0].message.content,
      imageUrl: imageResult.data[0].url,
    };
  },
});

// Main job that orchestrates the entire workflow
export const processContentRequest = task({
  id: "process-content-request",
  events: [contentGenerationRequested],
  run: async (payload: { 
    userId: string;
    email: string;
    theme: string;
    description: string;
  }) => {
    try {
      // Validate input
      if (!payload.theme || !payload.description) {
        throw new Error("Missing required parameters");
      }

      // Generate content
      const result = await generateContent.run(payload);
      
      const contentId = payload.userId + '-' + Date.now();
      
      // Send completion notification
      await notifyContentReady.run({ 
        userId,
        email,
        theme,
        contentId,
        contentPreview: result.text.substring(0, 100) + "...",
        imageUrl: result.imageUrl
      });
      
      return {
        success: true,
        contentId: contentId,
        ...result
      };
    } catch (error) {
      // If anything fails, ensure user gets notified
      const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
      console.error("Content generation failed:", errorMessage);
      
      await notifyError.run({
        userId,
        email,
        theme,
        errorMessage: errorMessage
      });
      
      return {
        success: false,
        error: errorMessage
      };
    }
  },
});

```

<Accordions>
  <Accordion title="Breakdown">
    **The Key Components**

    1. **The AI Generation Task**

    ```typescript
    export const generateContent = task({
    id: "generate-content",
    retry: { maxAttempts: 3 },
    run: async ({ userId, email, theme, description }) => {
      // Generate text using GPT-4
      const textResult = await openai.chat.completions.create({
        model: "gpt-4",
        messages: [{ role: "user", content: description }]
      });
      
      // Generate image using DALL-E 3
      const imageResult = await openai.images.generate({
        model: "dall-e-3",
        prompt: description,
        size: "1024x1024"
      });
      
      return {
        text: textResult.choices[0].message.content,
        imageUrl: imageResult.data[0].url,
      };
    },
    });
    ```

    <Callout>
      This task handles the actual AI content generation:

      * It creates text content using GPT-4
      * It creates an image using DALL-E 3
      * It has built-in retry logic (will try up to 3 times if it fails)
    </Callout>

    ***

    2. **The Two Notification Tasks**

    **Completion Notification**

    ```typescript
    export const notifyContentReady = task({
      id: "notify-content-ready",
      run: async ({ userId, email, theme, contentId, contentPreview, imageUrl }) => {
        await novu.trigger({
          workflowId: "ai-generation-completed",
          to: { subscriberId: userId, email: email },
          payload: {
            userName: email.split('@')[0],
            theme: theme,
            contentPreview: contentPreview,
            // Other details...
          },
        });
      },
    });
    ```

    **Error Notification**

    ```typescript
    export const notifyError = task({
      id: "notify-error",
      run: async ({ userId, email, theme, errorMessage }) => {
        await novu.trigger({
          workflowId: "ai-generation-error",
          to: { subscriberId: userId, email: email },
          payload: {
            userName: email.split('@')[0],
            theme: theme,
            errorMessage: errorMessage,
            // Other details...
          },
        });
      },
    });
    ```

    ***

    3. **The Main Workflow Orchestrator**

    ```typescript
    export const processContentRequest = task({
      id: "process-content-request",
      events: [contentGenerationRequested],
      run: async (payload) => {
        try {
          // 1. Generate the content
          const result = await generateContent.run(payload);
          
          // 2. Send success notification
          await notifyContentReady.run({
            userId: payload.userId,
            email: payload.email,
            theme: payload.theme,
            contentId: payload.userId + '-' + Date.now(),
            contentPreview: result.text.substring(0, 100) + "...",
            imageUrl: result.imageUrl
          });
          
          return { success: true, ...result };
        } catch (error) {
          // 3. Send error notification if anything fails
          await notifyError.run({
            userId: payload.userId,
            email: payload.email,
            theme: payload.theme,
            errorMessage: error instanceof Error ? error.message : "Unknown error occurred"
          });
          
          return { success: false, error: error instanceof Error ? error.message : "Unknown error occurred" };
        }
      },
    });
    ```

    <Callout>
      This task:

      * Responds to the content generation event
      * Tries to generate content
      * Sends appropriate notifications based on success or failure
    </Callout>

    ***

    **Notification Workflows You Would Need in Novu**

    1. **ai-generation-completed**: Sent when content is successfully generated

    Workflow `payload` variables: `{{userName}}`, `{{theme}}`, `{{contentPreview}}`, `{{imageUrl}}`, `{{viewUrl}}`

    2. **ai-generation-error**: Sent when content generation fails

    Workflow `payload` variables: `{{userName}}`, `{{theme}}`, `{{errorMessage}}`, `{{supportEmail}}`
  </Accordion>
</Accordions>

### Video processing

This example shows how to build a video transcription service that notifies users when their video has been transcribed or if an error occurs during processing.

```typescript

import { task, event, eventTrigger, retry } from "@trigger.dev/sdk/v3";
import { Novu } from "@novu/api";
import fs from "fs";
import { Deepgram } from "@deepgram/sdk";

// Initialize clients
const novu = new Novu({ 
  secretKey: 'ApiKey ' + process.env['NOVU_SECRET_KEY']
});

const deepgram = new Deepgram(process.env['DEEPGRAM_API_KEY']);

// Utility functions for video processing
async function downloadFile(url: string): Promise<string> {
  // Implementation of file download logic
  console.log(`Downloading file from ${url}`);
  // This would be the actual implementation to download a file
  return "/tmp/downloaded-video.mp4";
}

async function convertToWav(filePath: string): Promise<string> {
  // Implementation of conversion logic
  console.log(`Converting ${filePath} to WAV format`);
  // This would be the actual implementation to convert video to WAV
  return "/tmp/audio-extract.wav";
}

// Event trigger for transcription request
export const transcriptionRequested = event({
  id: "transcription-requested",
  trigger: eventTrigger({
    name: "video.transcription.requested",
  }),
});

// Notification task for successful transcription
export const notifyTranscriptionComplete = task({
  id: "notify-transcription-complete",
  run: async ({ 
    userId, 
    email, 
    videoName,
    transcriptionId,
    wordCount,
    duration
  }: { 
    userId: string;
    email: string;
    videoName: string;
    transcriptionId: string;
    wordCount: number;
    duration: string;
  }) => {
    await retry.onThrow(
      async () => {
        try {
          await novu.trigger({
            workflowId: "transcription-completed",
            to: {
              subscriberId: userId,
              email: email,
            },
            payload: {
              userName: email.split('@')[0],
              videoName: videoName,
              transcriptionId: transcriptionId,
              wordCount: wordCount,
              duration: duration,
              viewUrl: `https://yourapp.com/transcriptions/${transcriptionId}`
            },
          });
          
          console.log("Transcription complete notification sent successfully");
        } catch (error: unknown) {
          const errorMessage = error instanceof Error ? error.message : 'Unknown error';
          console.error("Failed to send transcription complete notification:", errorMessage);
          throw new Error(`Notification failed: ${errorMessage}`);
        }
      },
      { maxAttempts: 3, factor: 2, minTimeoutInMs: 1000 }
    );
  },
});

// Error notification task
export const notifyTranscriptionError = task({
  id: "notify-transcription-error",
  run: async ({ 
    userId, 
    email, 
    videoName, 
    errorMessage 
  }: { 
    userId: string;
    email: string;
    videoName: string;
    errorMessage: string;
  }) => {
    await retry.onThrow(
      async () => {
        try {
          await novu.trigger({
            workflowId: "transcription-error",
            to: {
              subscriberId: userId,
              email: email,
            },
            payload: {
              userName: email.split('@')[0],
              videoName: videoName,
              errorMessage: errorMessage,
              supportEmail: "support@yourapp.com"
            },
          });
          
          console.log("Transcription error notification sent successfully");
        } catch (error: unknown) {
          const errorMessage = error instanceof Error ? error.message : 'Unknown error';
          console.error("Failed to send error notification:", errorMessage);
          // Not rethrowing here to prevent infinite loops when error notifications fail
        }
      },
      { maxAttempts: 2 }
    );
  },
});

// Main transcription task
export const transcribeVideo = task({
  id: "transcribe",
  retry: {
    maxAttempts: 3,
  },
  machine: { preset: "large-2x" }, // Use larger machine for faster processing
  run: async (payload: { 
    id: string; 
    url: string;
    userId?: string;
    email?: string;
    videoName?: string;
  }) => {
    try {
      console.log(`Starting transcription for video: ${payload.videoName || payload.id}`);
      
      // Download and process the video
      const downloadedFile = await downloadFile(payload.url);
      const extractedAudio = await convertToWav(downloadedFile);

      // Transcribe using Deepgram
      const { result, error } = await deepgram.listen.prerecorded.transcribeFile(
        fs.createReadStream(extractedAudio),
        {
          model: "nova",
          language: "en-US",
          smart_format: true,
          diarize: true
        }
      );

      if (error || !result) {
        throw new Error(error || "Failed to transcribe video");
      }

      // Calculate some metadata from the result
      const wordCount = result.results?.utterances?.reduce((count, utterance) => count + utterance.words.length, 0) || 0;
      const duration = result.metadata?.duration 
        ? `${Math.floor(result.metadata.duration / 60)}:${Math.floor(result.metadata.duration % 60).toString().padStart(2, '0')}`
        : "Unknown";

      // Store in database
      const dbResult = await db.transcriptions.create(payload.id, result.results);
      
      return {
        transcriptionId: dbResult.id,
        wordCount,
        duration,
        transcript: result.results
      };
    } catch (error) {
      console.error("Error in transcription process:", error);
      throw error; // Rethrow to trigger retry
    }
  },
});

// Main job that orchestrates the entire workflow
export const processTranscriptionRequest = task({
  id: "process-transcription-request",
  events: [transcriptionRequested],
  run: async (payload: { 
    id: string;
    url: string;
    userId: string;
    email: string;
    videoName: string;
  }) => {
    try {
      // Validate input
      if (!payload.url) {
        throw new Error("Missing video URL");
      }

      // Transcribe video
      const result = await transcribeVideo.run(payload);
      
      // Send completion notification
      await notifyTranscriptionComplete.run({
        userId: payload.userId,
        email: payload.email,
        videoName: payload.videoName || "Your video",
        transcriptionId: result.transcriptionId,
        wordCount: result.wordCount,
        duration: result.duration
      });
      
      return {
        success: true,
        transcriptionId: result.transcriptionId,
        transcript: result.transcript
      };
    } catch (error) {
      // If anything fails, ensure user gets notified
      const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
      console.error("Transcription failed:", errorMessage);
      
      await notifyTranscriptionError.run({
        userId: payload.userId,
        email: payload.email,
        videoName: payload.videoName || "Your video",
        errorMessage: errorMessage
      });
      
      return {
        success: false,
        error: errorMessage
      };
    }
  },
});

// Type declaration for database operations (mock)
const db = {
  transcriptions: {
    create: async (id: string, results: unknown) => {
      console.log(`Storing transcription results for ${id} in database`);
      return { id, created: new Date() };
    }
  }
};

```

<Accordions>
  <Accordion title="Breakdown">
    **How It Works**

    * A user uploads a video for transcription
    * The system processes the video using Deepgram's API
    * The user receives a notification when transcription is complete or if an error occurs

    **The Key Components**

    1. **Transcription Task**

    The core of this workflow is the `transcribeVideo` task that handles the actual transcription:

    ```typescript
    export const transcribeVideo = task({
      id: "transcribe",
      retry: { maxAttempts: 3 },
      machine: { preset: "large-2x" }, // Use larger machine for faster processing
      run: async (payload) => {
        // Download the video file
        const downloadedFile = await downloadFile(payload.url);
        
        // Extract and convert audio to WAV format
        const extractedAudio = await convertToWav(downloadedFile);

        // Transcribe using Deepgram
        const { result, error } = await deepgram.listen.prerecorded.transcribeFile(
          fs.createReadStream(extractedAudio),
          { model: "nova" }
        );

        // Store results in database
        return await db.transcriptions.create(payload.id, result?.results);
      },
    });
    ```

    <Callout>
      **This task:**

      * Downloads the video file from a URL
      * Extracts audio and converts it to WAV format
      * Sends the audio to Deepgram for transcription
      * Stores the results in a database
    </Callout>

    ***

    2. **Notification Tasks**

    There are two notification tasks:

    **Success Notification**

    ```typescript
    export const notifyTranscriptionComplete = task({
      id: "notify-transcription-complete",
      run: async ({ userId, email, videoName, transcriptionId, wordCount, duration }) => {
        await novu.trigger({
          workflowId: "transcription-completed",
          to: { subscriberId: userId, email },
          payload: {
            userName: email.split('@')[0],
            videoName,
            wordCount,
            duration,
            viewUrl: `https://yourapp.com/transcriptions/${transcriptionId}`
          },
        });
      },
    });
    ```

    **Error Notification**

    ```typescript
    export const notifyTranscriptionError = task({
      id: "notify-transcription-error",
      run: async ({ userId, email, videoName, errorMessage }) => {
        await novu.trigger({
          workflowId: "transcription-error",
          to: { subscriberId: userId, email },
          payload: {
            userName: email.split('@')[0],
            videoName,
            errorMessage,
            supportEmail: "support@yourapp.com"
          },
        });
      },
    });
    ```

    ***

    3. **Main Workflow Orchestrator**

    ```typescript
    export const processTranscriptionRequest = task({
      id: "process-transcription-request",
      events: [transcriptionRequested],
      run: async (payload) => {
        try {
          // 1. Validate and transcribe video
          const result = await transcribeVideo.run(payload);
          
          // 2. Send success notification
          await notifyTranscriptionComplete.run({
            userId: payload.userId,
            email: payload.email,
            videoName: payload.videoName || "Your video",
            transcriptionId: result.transcriptionId,
            wordCount: result.wordCount,
            duration: result.duration
          });
          
          return { success: true, transcriptionId: result.transcriptionId };
        } catch (error) {
          // 3. Send error notification if anything fails
          await notifyTranscriptionError.run({
            userId: payload.userId,
            email: payload.email,
            videoName: payload.videoName || "Your video",
            errorMessage: error instanceof Error ? error.message : "Unknown error occurred"
          });
          
          return { success: false, error: error instanceof Error ? error.message : "Unknown error occurred" };
        }
      },
    });
    ```

    ***

    **Usage**

    1. **Create two notification workflows in Novu:**

    * transcription-completed with `payload` variables: `{{userName}}`, `{{videoName}}`, `{{wordCount}}`, `{{duration}}`, `{{viewUrl}}`
    * transcription-error with `payload` variables: `{{userName}}`, `{{videoName}}`, `{{errorMessage}}`, `{{supportEmail}}`

    2. **Trigger the workflow by sending an event:**

    ```typescript
    // Example of triggering the workflow
    await client.triggerEvent({
      name: "video.transcription.requested",
      payload: {
        id: "video-123",
        url: "https://storage.example.com/videos/meeting-recording.mp4",
        userId: "user-456",
        email: "user@example.com",
        videoName: "Weekly Team Meeting"
      }
    });
    ```

    **Example Notifications**

    Success notification email:

    <Callout>
      **Subject**: Your video "Weekly Team Meeting" has been transcribed

      Hello John,

      Good news! We've finished transcribing your video "Weekly Team Meeting".

      Details:

      * Duration: 32:15
      * Word count: 4,320

      You can view and edit your transcription here:
      [https://yourapp.com/transcriptions/transcript-789](https://yourapp.com/transcriptions/transcript-789)

      Thanks for using our service!
    </Callout>

    Error notification email:

    <Callout>
      **Subject**: Issue with transcribing "Weekly Team Meeting"

      Hello John,

      We encountered a problem while transcribing your video "Weekly Team Meeting".

      Error details: The audio quality was too low for accurate transcription.

      Please try uploading your video again with improved audio, or contact [support@yourapp.com](mailto:support@yourapp.com) if you need assistance.

      We apologize for the inconvenience.
    </Callout>

    This workflow provides a complete solution for transcribing videos and keeping users informed about the process status, all while handling errors gracefully.
  </Accordion>
</Accordions>

## Managing Subscribers

Before triggering notifications, Novu needs to know *who* to notify. That's where subscribers come in. A **subscriber** in Novu represents a user (or entity) that can receive notifications through one or more channels (in-app, email, SMS, etc.).

### When to Create Subscribers

Create or update a subscriber in Novu when:

<Steps>
  <Step>
    **New User Registration**

    When a new user signs up or is added to your system
  </Step>

  <Step>
    **Notification Eligibility**

    When a user becomes eligible to receive notifications
  </Step>

  <Step>
    **Data Updates**

    When you want to ensure subscriber metadata (name, phone, etc.) is up-to-date
  </Step>
</Steps>

<Callout type="info">
  If you trigger a workflow with a `subscriberId` that doesn't exist, Novu will auto-create the subscriber. However, doing it explicitly ensures full control over subscriber data.
</Callout>

### Subscriber Creation Example

```typescript
import { task, retry } from "@trigger.dev/sdk/v3";
import { Novu } from "@novu/api";

const novu = new Novu({
  secretKey: 'ApiKey ' + process.env["NOVU_SECRET_KEY"]
});

export const createSubscriberTask = task({
  id: "create-subscriber-task",
  run: async (payload: {
    userId: string;
    email?: string;
    firtName?: string;
    lastName?: string;
    phone?: string;
    locale?: string;
    avatar?: string;
    data?: object;
  }) => {
    const { userId, email, name, phone, avatar } = payload;

    // Split full name into first and last (fallback to empty string)
    const [firstName, lastName = ""] = name.split(" ");

    // Create subscriber object for Novu
    const subscriber = {
      subscriberId: userId,
      email,
      firstName,
      lastName,
      phone,
      locale,
      avatar,
      data,
    };

    await retry.onThrow(
      async () => {
        console.log("Creating Novu subscriber:", subscriber);

        const result = await novu.subscribers.create(subscriber);

        console.log("Subscriber created successfully:", result);
        return result;
      },
      {
        maxAttempts: 2,
      }
    );
  },
});
```

### Using Subscriber Data in Templates

Once you've added custom fields to a subscriber, you can use them in Novu templates using Handlebars:

<Tabs items={['Basic Fields', 'Contact Info', 'Custom Data']}>
  <Tab value="Basic Fields">Hi `{{subscriber.firstName}}`, welcome!</Tab>
  <Tab value="Contact Info">We'll reach you at `{{subscriber.phone}}` if needed.</Tab>
  <Tab value="Custom Data">Your account is set up under `{{subscriber.data.userName}}`.</Tab>
</Tabs>

## Best Practices

<Accordions>
  <Accordion title="Data Management Best Practices">
    As a best practice, maintain consistent subscriber identification:

    * Use your internal `userId` as the `subscriberId` in Novu
    * Keep this mapping consistent across all integrations
    * Store notification preferences and settings in your user model
    * Consider adding a reference field in your user table: `novuSubscriberId`

    Example user model:

    ```typescript
    interface User {
      id: string;
      email: string;
      novuSubscriberId: string; // Same as user.id
      notificationPreferences: {
        marketing: boolean;
        updates: boolean;
        // ... other preferences
      }
    }
    ```

    This ensures reliable user tracking and simplifies debugging notification issues.
  </Accordion>

  <Accordion title="Proactive Data Synchronization">
    Keep subscriber data synchronized by:

    * Updating Novu whenever user contact info changes
    * Implementing webhooks to handle notification delivery status
    * Setting up retry mechanisms for failed updates

    ```typescript
    // Example of proactive update
    async function updateUserProfile(userId: string, newData: UserUpdateData) {
      // Update your database
      await db.users.update(userId, newData);
      
      // Sync with Novu
      await novu.subscribers.update(userId, {
        email: newData.email,
        phone: newData.phone,
        data: { lastUpdated: new Date() }
      });
    }
    ```
  </Accordion>

  <Accordion title="Enriching Subscriber Data">
    Enhance notifications with contextual data:

    * Store user preferences, roles, and settings
    * Include relevant business logic flags
    * Add metadata for analytics and tracking

    ```typescript
    // Example of rich subscriber data
    await novu.subscribers.update(userId, {
      data: {
        role: 'admin',
        subscriptionTier: 'premium',
        features: ['advanced-analytics', 'priority-support']
      }
    });
    ```
  </Accordion>
</Accordions>
