docs/platform/concepts/topics.mdx
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.
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:
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:
to field accepts the topic’s key.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>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:
This makes topics especially useful for modeling dynamic relationships in your application.
Topics are designed for high-volume, high-efficiency use cases:
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.
As mentioned above, a workflow can be triggered to a topic in the same way as it is triggered to subscribers.
To trigger a workflow to a topic, use to field with type Topic and topicKey.
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"},
))
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,
),
),
);
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();
to field also accepts array of topics. This is useful when you want to trigger a workflow to multiple topics at once.
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"},
],
))
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,
),
],
),
);
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();
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.
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",
))
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',
),
);
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();
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>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>