Back to Novu

Topics

docs/platform/concepts/topics.mdx

3.18.016.0 KB
Original Source

In Novu, a topic is a way to group subscribers under a shared label so that you can broadcast a message to many users with a single workflow trigger. This fan-out mechanism removes the need to manage individual subscriber IDs for each notification, streamlining scenarios like product announcements, feature rollouts, or status updates.

Topics identifiers

Each topic is uniquely identified by a topic key, a permanent, internal reference in your system. You can also assign a name to help describe its purpose, for example, "Task Updates" or "Comment Thread Subscribers".

The topicKey must be unique and cannot be changed after creation; Novu enforces this uniqueness behind the scenes. Example: task:taskId or post:postId.

Common use cases for topics:

  • Notifying all members of a specific team or project
  • Messaging users who commented on a post
  • Updating subscribers to a specific product or feature

How topics fit into Novu's model

Novu’s notification system is built around workflows, which are triggered for one or more recipients. Topics act as a special type of recipient, representing a group of subscribers instead of an individual.

When you trigger a workflow using a topic:

  • The to field accepts the topic’s key.
  • Novu looks up all subscribers assigned to the topic.
  • A separate workflow event is created for each subscriber.

Topics let you target large groups with a single API call, removing the need to loop through subscribers or send multiple workflow triggers.

<Note> Topics don’t replace individual targeting. You can still trigger workflows for specific subscribers or arrays of subscribers when needed. </Note>

Dynamic and decoupled grouping

Once a topic is created, you can assign or remove subscribers at any time. Subscribers don’t need to know they belong to a topic, and it doesn’t affect their personal notification preferences.

Topics are dynamic and reflect real-time states. For example, a topic might include:

  • Users who are currently watching a post
  • Team members assigned to a project

This makes topics especially useful for modeling dynamic relationships in your application.

Scalability and limits

Topics are designed for high-volume, high-efficiency use cases:

  • Each topic supports up to 100,000 subscribers.
  • A separate workflow event is created for each subscriber when a workflow is triggered to the topic

Autogenerated topics

Novu supports on-the-fly topic creation. If you attempt to add subscribers to a topic key that doesn't exist for the selected environment and organization, Novu will automatically create a new topic named Autogenerated-<TOPIC_KEY> and assign the subscribers to it. This auto-generated topic can then be renamed and managed just like any other topic created manually.

<Note> Topics can be managed from [Novu dashboard](https://dashboard.novu.co/topics) or using [Topics APIs](/api-reference/topics/topic-schema) </Note>

Trigger workflow

As mentioned above, a workflow can be triggered to a topic in the same way as it is triggered to subscribers.

To a single topic

To trigger a workflow to a topic, use to field with type Topic and topicKey.

<Tabs> <Tab title="Node.js"> ```ts import { Novu } from "@novu/api"

const novu = new Novu({ secretKey: "<YOUR_SECRET_KEY_HERE>", });

await novu.trigger({ to: { type: "Topic", topicKey: "topic_key" }, workflowId: "workflow_identifier", });

  </Tab>
  <Tab title="Python">
```python
import os
import novu_py
from novu_py import Novu

with Novu(secret_key=os.getenv("NOVU_SECRET_KEY", "")) as novu:
    novu.trigger(trigger_event_request_dto=novu_py.TriggerEventRequestDto(
        workflow_id="workflow_identifier",
        to={"type": "Topic", "topic_key": "topic_key"},
    ))
</Tab> <Tab title="Go"> ```go import ( "context" "os"
novugo "github.com/novuhq/novu-go"
"github.com/novuhq/novu-go/models/components"

)

s := novugo.New(novugo.WithSecurity(os.Getenv("NOVU_SECRET_KEY")))

res, err := s.Trigger(context.Background(), components.TriggerEventRequestDto{ WorkflowID: "workflow_identifier", To: components.CreateToTopicPayloadDto(components.TopicPayloadDto{ Type: components.TriggerRecipientsTypeEnumTopic, TopicKey: "topic_key", }), }, nil)

  </Tab>
  <Tab title="PHP">
```php
use novu;
use novu\Models\Components;

$sdk = novu\Novu::builder()
    ->setSecurity('<YOUR_SECRET_KEY_HERE>')
    ->build();

$sdk->trigger(
    triggerEventRequestDto: new Components\TriggerEventRequestDto(
        workflowId: 'workflow_identifier',
        to: new Components\TopicPayloadDto(
            topicKey: 'topic_key',
            type: Components\TriggerRecipientsTypeEnum::Topic,
        ),
    ),
);
</Tab> <Tab title=".NET"> ```csharp using Novu; using Novu.Models.Components;

var sdk = new NovuSDK(secretKey: "<YOUR_SECRET_KEY_HERE>");

await sdk.TriggerAsync(triggerEventRequestDto: new TriggerEventRequestDto() { WorkflowId = "workflow_identifier", To = To.CreateTopicPayloadDto(new TopicPayloadDto() { Type = TriggerRecipientsTypeEnum.Topic, TopicKey = "topic_key", }), });

  </Tab>
  <Tab title="Java">
```java
import co.novu.Novu;
import co.novu.models.components.*;

Novu novu = Novu.builder()
    .secretKey("<YOUR_SECRET_KEY_HERE>")
    .build();

novu.trigger()
    .body(TriggerEventRequestDto.builder()
        .workflowId("workflow_identifier")
        .to(To2.of(TopicPayloadDto.builder()
            .type(TriggerRecipientsTypeEnum.TOPIC)
            .topicKey("topic_key")
            .build()))
        .build())
    .call();
</Tab> <Tab title="cURL"> ```bash curl -L -g -X POST 'https://api.novu.co/v1/events/trigger' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: ApiKey <NOVU_SECRET_KEY>' \ -d '{ "name": "workflow_identifier", "to": { "type": "Topic", "topicKey": "topic_key" } }' ``` </Tab> </Tabs>

To multiple topics

to field also accepts array of topics. This is useful when you want to trigger a workflow to multiple topics at once.

<Tabs> <Tab title="Node.js"> ```ts import { Novu } from "@novu/api"

const novu = new Novu({ secretKey: "<YOUR_SECRET_KEY_HERE>", });

await novu.trigger({ to: [ { type: "Topic", topicKey: "topic_key_1" }, { type: "Topic", topicKey: "topic_key_2" } ], workflowId: "workflow_identifier", });

  </Tab>
  <Tab title="Python">
```python
import os
import novu_py
from novu_py import Novu

with Novu(secret_key=os.getenv("NOVU_SECRET_KEY", "")) as novu:
    novu.trigger(trigger_event_request_dto=novu_py.TriggerEventRequestDto(
        workflow_id="workflow_identifier",
        to=[
            {"type": "Topic", "topic_key": "topic_key_1"},
            {"type": "Topic", "topic_key": "topic_key_2"},
        ],
    ))
</Tab> <Tab title="Go"> ```go import ( "context" "os"
novugo "github.com/novuhq/novu-go"
"github.com/novuhq/novu-go/models/components"

)

s := novugo.New(novugo.WithSecurity(os.Getenv("NOVU_SECRET_KEY")))

res, err := s.Trigger(context.Background(), components.TriggerEventRequestDto{ WorkflowID: "workflow_identifier", To: components.CreateToArrayOfTo1([]components.To1{ components.CreateTo1TopicPayloadDto(components.TopicPayloadDto{ Type: components.TriggerRecipientsTypeEnumTopic, TopicKey: "topic_key_1", }), components.CreateTo1TopicPayloadDto(components.TopicPayloadDto{ Type: components.TriggerRecipientsTypeEnumTopic, TopicKey: "topic_key_2", }), }), }, nil)

  </Tab>
  <Tab title="PHP">
```php
use novu;
use novu\Models\Components;

$sdk = novu\Novu::builder()
    ->setSecurity('<YOUR_SECRET_KEY_HERE>')
    ->build();

$sdk->trigger(
    triggerEventRequestDto: new Components\TriggerEventRequestDto(
        workflowId: 'workflow_identifier',
        to: [
            new Components\TopicPayloadDto(
                topicKey: 'topic_key_1',
                type: Components\TriggerRecipientsTypeEnum::Topic,
            ),
            new Components\TopicPayloadDto(
                topicKey: 'topic_key_2',
                type: Components\TriggerRecipientsTypeEnum::Topic,
            ),
        ],
    ),
);
</Tab> <Tab title=".NET"> ```csharp using Novu; using Novu.Models.Components; using System.Collections.Generic;

var sdk = new NovuSDK(secretKey: "<YOUR_SECRET_KEY_HERE>");

await sdk.TriggerAsync(triggerEventRequestDto: new TriggerEventRequestDto() { WorkflowId = "workflow_identifier", To = To.CreateArrayOfTo1(new List<To1> { To1.CreateTopicPayloadDto(new TopicPayloadDto() { Type = TriggerRecipientsTypeEnum.Topic, TopicKey = "topic_key_1", }), To1.CreateTopicPayloadDto(new TopicPayloadDto() { Type = TriggerRecipientsTypeEnum.Topic, TopicKey = "topic_key_2", }), }), });

  </Tab>
  <Tab title="Java">
```java
import co.novu.Novu;
import co.novu.models.components.*;
import java.util.List;

Novu novu = Novu.builder()
    .secretKey("<YOUR_SECRET_KEY_HERE>")
    .build();

novu.trigger()
    .body(TriggerEventRequestDto.builder()
        .workflowId("workflow_identifier")
        .to(To2.of(List.of(
            To1.of(TopicPayloadDto.builder()
                .type(TriggerRecipientsTypeEnum.TOPIC)
                .topicKey("topic_key_1")
                .build()),
            To1.of(TopicPayloadDto.builder()
                .type(TriggerRecipientsTypeEnum.TOPIC)
                .topicKey("topic_key_2")
                .build()))))
        .build())
    .call();
</Tab> <Tab title="cURL"> ```bash curl -L -g -X POST 'https://api.novu.co/v1/events/trigger' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: ApiKey <NOVU_SECRET_KEY>' \ -d '{ "name": "workflow_identifier", "to": [ { "type": "Topic", "topicKey": "topic_key_1" }, { "type": "Topic", "topicKey": "topic_key_2" } ] }' ``` </Tab> </Tabs>

Exclude actor

When a workflow is triggered to a topic, notification is sent to all subscribers present in the topic. However, you can exclude a specific subscriber from receiving the notification by using the actor field. Here actor is the subscriberId of the subscriber you want to exclude.

<Tabs> <Tab title="Node.js"> ```ts import { Novu } from "@novu/api"

const novu = new Novu({ secretKey: "<YOUR_SECRET_KEY_HERE>", });

await novu.trigger({ to: [{ type: "Topic", topicKey: "topic_key_1" }], workflowId: "workflow_identifier", actor: "actor_subscriber_Id" });

  </Tab>
  <Tab title="Python">
```python
import os
import novu_py
from novu_py import Novu

with Novu(secret_key=os.getenv("NOVU_SECRET_KEY", "")) as novu:
    novu.trigger(trigger_event_request_dto=novu_py.TriggerEventRequestDto(
        workflow_id="workflow_identifier",
        to=[{"type": "Topic", "topic_key": "topic_key_1"}],
        actor="actor_subscriber_Id",
    ))
</Tab> <Tab title="Go"> ```go import ( "context" "os"
novugo "github.com/novuhq/novu-go"
"github.com/novuhq/novu-go/models/components"

)

s := novugo.New(novugo.WithSecurity(os.Getenv("NOVU_SECRET_KEY")))

res, err := s.Trigger(context.Background(), components.TriggerEventRequestDto{ WorkflowID: "workflow_identifier", To: components.CreateToArrayOfTo1([]components.To1{ components.CreateTo1TopicPayloadDto(components.TopicPayloadDto{ Type: components.TriggerRecipientsTypeEnumTopic, TopicKey: "topic_key_1", }), }), Actor: components.CreateActorStr("actor_subscriber_Id"), }, nil)

  </Tab>
  <Tab title="PHP">
```php
use novu;
use novu\Models\Components;

$sdk = novu\Novu::builder()
    ->setSecurity('<YOUR_SECRET_KEY_HERE>')
    ->build();

$sdk->trigger(
    triggerEventRequestDto: new Components\TriggerEventRequestDto(
        workflowId: 'workflow_identifier',
        to: [
            new Components\TopicPayloadDto(
                topicKey: 'topic_key_1',
                type: Components\TriggerRecipientsTypeEnum::Topic,
            ),
        ],
        actor: 'actor_subscriber_Id',
    ),
);
</Tab> <Tab title=".NET"> ```csharp using Novu; using Novu.Models.Components; using System.Collections.Generic;

var sdk = new NovuSDK(secretKey: "<YOUR_SECRET_KEY_HERE>");

await sdk.TriggerAsync(triggerEventRequestDto: new TriggerEventRequestDto() { WorkflowId = "workflow_identifier", To = To.CreateArrayOfTo1(new List<To1> { To1.CreateTopicPayloadDto(new TopicPayloadDto() { Type = TriggerRecipientsTypeEnum.Topic, TopicKey = "topic_key_1", }), }), Actor = Actor.CreateStr("actor_subscriber_Id"), });

  </Tab>
  <Tab title="Java">
```java
import co.novu.Novu;
import co.novu.models.components.*;
import java.util.List;

Novu novu = Novu.builder()
    .secretKey("<YOUR_SECRET_KEY_HERE>")
    .build();

novu.trigger()
    .body(TriggerEventRequestDto.builder()
        .workflowId("workflow_identifier")
        .to(To2.of(List.of(
            To1.of(TopicPayloadDto.builder()
                .type(TriggerRecipientsTypeEnum.TOPIC)
                .topicKey("topic_key_1")
                .build()))))
        .actor(TriggerEventRequestDtoActor.of("actor_subscriber_Id"))
        .build())
    .call();
</Tab> <Tab title="cURL"> ```bash curl -L -g -X POST 'https://api.novu.co/v1/events/trigger' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: ApiKey <NOVU_SECRET_KEY>' \ -d '{ "name": "workflow_identifier", "to": [ { "type": "Topic", "topicKey": "topic_key_1" } ], "actor": "actor_subscriber_Id" }' ``` </Tab> </Tabs>

Explore the topics APIs

These are commonly used topics APIs. Explore all topics APIs on topics api reference page.

<Columns cols={2}> <Card title="Create a topic API" href="/api-reference/topics/create-a-topic" description="Create a new topic with name and topicKey" /> <Card title="Update a topic API" href="/api-reference/topics/update-a-topic" description="Update existing topic using topicKey" /> <Card title="Create topic subscription API" href="/api-reference/topics/create-topic-subscriptions" description="Add multiple subscribers into a topic" /> <Card title="List topic subscriptions API" href="/api-reference/topics/list-topic-subscriptions" description="List all subscribers present in a single topic" /> <Card title="List all topics API" href="/api-reference/topics/list-all-topics" description="List all topics in an environment" /> <Card title="Check subscriber subscription API" href="/api-reference/topics/check-topic-subscriber" description="Check if a subscriber is present in a topic" /> </Columns>

Frequently asked questions

Below are some of the most frequently asked questions about topics:

<AccordionGroup> <Accordion title="Do topics override subscriber preferences?"> No. Topics only define delivery groups. Each subscriber's individual notification preferences remain intact. </Accordion> <Accordion title="Is a topic like a mailing list?"> Conceptually, yes. Topics group users to receive shared messages. However, topics are more dynamic and integrated into your application logic. </Accordion> <Accordion title="Can a subscriber belong to multiple topics?"> Yes. Subscribers can be members of any number of topics. This allows overlapping targeting strategies (for example, `task-updates`, `project-X`, and `admin-notifications`). </Accordion> <Accordion title="Can I reuse the same topic key across environments?"> Topic keys must be unique within each [environment](/platform/developer/environments). You can reuse the same key (for example, `feature-release`) in different environments like staging and production. </Accordion> <Accordion title="What happens if I trigger a workflow to a topic with no subscribers?"> The workflow is processed, but no notifications are delivered and, hence, this trigger is not counted for billing. </Accordion> </AccordionGroup>