---
source_url: https://www.pubnub.com/docs/use-cases/sports-media-entertainment/automated-polling
title: Open a poll automatically when fans react
updated_at: 2026-09-30T07:20:08.000Z
---

# Open a poll automatically when fans react

## Documentation index

To discover more PubNub resources:

1. Fetch [PubNub's llms.txt](https://www.pubnub.com/llms-full.txt) for a list of available pages in Markdown format.
2. Identify relevant URLs from that index.
3. Fetch the target pages.

Do not assume a path exists, always check the index first.

PubNub [Illuminate](https://www.pubnub.com/docs/analytics/decisions/overview.md) watches a stream of messages, computes metrics from them, and runs an action when a metric crosses a condition you set. In this tutorial, fan reactions published to `game.stream-reactions` feed an Illuminate metric. When that metric crosses a threshold, an Illuminate Decision publishes a trigger to `game.poll-triggers`. Your own poll service then opens a poll on `game.new-poll` in response, with no one pressing a button.

Illuminate is a paid add-on. You can complete the reaction-publishing and poll-service code on this page without it. The Decision half, where Illuminate watches for the burst and fires the trigger, requires Illuminate to be active on your account.

## What you'll build

Fans tap reactions in `react.js`, which publishes each one to `game.stream-reactions`. An Illuminate metric counts those reactions, and when the count crosses the threshold you set, an Illuminate Decision publishes a trigger to `game.poll-triggers`. Your `poll-service.js` subscribes to that channel and publishes a poll to `game.new-poll`, but only if it has not opened one in the last 60 seconds. Fans subscribed to `game.new-poll` then see the poll appear, with no one pressing a button.

```mermaid
sequenceDiagram
    participant Fans as Fan clients
    participant Reactions as game.stream-reactions
    participant Illuminate
    participant Triggers as game.poll-triggers
    participant Service as poll-service.js
    participant NewPoll as game.new-poll

    Fans->>Reactions: react.js publishes reaction
    Reactions->>Illuminate: count reactions
    Illuminate->>Triggers: threshold crossed, publish trigger
    Triggers->>Service: trigger with reaction
    Service->>NewPoll: publish poll, at most one per 60 s
    NewPoll->>Fans: poll appears
```

## Before you begin

You need:

* Node.js 22 or later.
* A PubNub account and your own keyset, with Message Persistence enabled, as set up in [Run a live poll during a match](https://www.pubnub.com/docs/use-cases/sports-media-entertainment/live-polls.md). If you already have `game.new-poll` publishing working from that tutorial, you can skip ahead to [Receive the trigger in your poll service](#receive-the-trigger-in-your-poll-service) and reuse it.
* Illuminate available on your account. Illuminate runs on a two-week free trial. Continuing to use it after the trial requires a paid plan. See [Availability and access](https://www.pubnub.com/docs/analytics/decisions/overview.md#availability-and-access).

## Set up the project

Create a new directory and install the PubNub SDK:

```bash
mkdir automated-polling-tutorial
cd automated-polling-tutorial
npm init -y
npm install pubnub
```

Add `"type": "module"` to `package.json`, since this tutorial's code uses `import`.

Create three files:

* `react.js`, which publishes a fan's reaction.
* `poll-service.js`, which receives Illuminate's trigger and opens a poll.
* `send-trigger.js`, a small script that publishes a trigger message by hand, standing in for Illuminate while you build and test `poll-service.js`.

Add this to `react.js`:

import PubNub from 'pubnub';
const pubnub = new PubNub({  publishKey: 'YOUR_PUBLISH_KEY',  subscribeKey: 'YOUR_SUBSCRIBE_KEY',  userId: 'fan-42',});

Add this to `poll-service.js`:

import PubNub from 'pubnub';
const pubnub = new PubNub({  publishKey: 'YOUR_PUBLISH_KEY',  subscribeKey: 'YOUR_SUBSCRIBE_KEY',  userId: 'poll-service',});

Add this to `send-trigger.js`:

import PubNub from 'pubnub';
const pubnub = new PubNub({  publishKey: 'YOUR_PUBLISH_KEY',  subscribeKey: 'YOUR_SUBSCRIBE_KEY',  userId: 'match-service',});

Replace `YOUR_PUBLISH_KEY` and `YOUR_SUBSCRIBE_KEY` in all three files with the keys from your keyset.

## Publish what fans are reacting to

Add this to `react.js`:

```javascript
await pubnub.publish({
  channel: 'game.stream-reactions',
  message: { reaction: '🔥' },
  customMessageType: 'reaction',
});
```

Every tap publishes one small message. A reaction like this is also a good candidate for a signal instead of a message, since you never need to look one up later. This tutorial keeps it a message because Illuminate reads it the same way either source publishes it. For the difference and when to choose each, refer to [Send different message types](https://www.pubnub.com/docs/pub-sub/publish/send-different-message-types.md#send-a-signal-instead-of-a-message).

## Configure the reaction-count metric in Illuminate

An Illuminate [Business Object](https://www.pubnub.com/docs/analytics/decisions/business-objects.md) reads reactions published to `game.stream-reactions`. New Business Objects pre-map `Channel` and `Message Type`. Add a `Reaction` field with the `String` type before activation.

| Field | Source | Mapped automatically? |
| --- | --- | --- |
| Channel | `$.message.channel` | Yes, pre-mapped |
| Message Type | `$.message.body.type` | Yes, pre-mapped |
| Reaction | `$.message.body.reaction` | No, add and map it |

Use a metric with **Function** set to `Count`, **Dimension** set to `Reaction`, and **Period** set to `1 minute`. Filter for `Channel Equals game.stream-reactions` and `Message Type Equals reaction`. The dimension keeps each reaction's count separate.

Activate the Business Object after mapping `Reaction`. You cannot add data fields after activation. For the console steps, see [Create a Business Object](https://www.pubnub.com/docs/analytics/decisions/create-business-objects.md) and [Metrics](https://www.pubnub.com/docs/analytics/decisions/business-objects.md#metrics).

## Decide when a poll is worth opening

A [Decision](https://www.pubnub.com/docs/analytics/decisions.md) evaluates that metric and fires an action when it crosses a threshold. Create one that uses your reaction-count metric as its condition source, with a rule such as **Count of Reactions greater than or equal to 15**.

How many reactions in a minute justify opening a poll is your call, not a fixed product threshold. Fifteen is a starting point. Watch how your own audience reacts, and raise or lower it from there.

Add a **Send Message** action to the rule, targeting **Channel ID** `game.poll-triggers` with a **Body** of `{"reaction": "${Reaction}"}`. `${Reaction}` injects whichever emoji's count crossed the threshold, so your poll service learns which reaction fired it. Follow [Create a Decision](https://www.pubnub.com/docs/analytics/decisions/create-decisions.md) for the console steps to build and activate a rule like this one.

You don't need this Decision active yet to continue. The rest of this tutorial builds and tests the code that receives its trigger, using a stand-in message that matches the same shape.

## Open the poll a trigger asks for

Build the poll first, so the trigger handler has something to call. Add this to `poll-service.js`:

```javascript
const pollsByReaction = new Map([
  [
    '\u{1F621}',
    {
      title: 'Which team is playing dirtiest?',
      options: [
        { id: 1, text: 'Home team' },
        { id: 2, text: 'Away team' },
      ],
    },
  ],
  [
    '\u{1F389}',
    {
      title: 'Whose fans are celebrating hardest?',
      options: [
        { id: 1, text: 'Home team' },
        { id: 2, text: 'Away team' },
      ],
    },
  ],
]);

async function openPollForReaction(reaction = '') {
  const template = pollsByReaction.get(reaction);

  if (!template) {
    console.log('no poll is defined for', reaction);
    return;
  }

  try {
    const response = await pubnub.publish({
      channel: 'game.new-poll',
      message: {
        id: `poll-${Date.now()}`,
        title: template.title,
        durationSeconds: 30,
        options: template.options,
      },
      customMessageType: 'poll-opened',
      storeInHistory: true,
    });
    console.log('triggered poll published at timetoken:', response.timetoken);
  } catch (error) {
    const status = error instanceof Error && 'status' in error ? error.status : undefined;
    console.error(
      `Publishing the triggered poll failed: ${error}${status ? ` Additional information: ${status}` : ''}`,
    );
  }
}
```

`pollsByReaction` maps each reaction your Decision can report to the poll you want that reaction to open. `openPollForReaction` looks up the template and publishes it to `game.new-poll` with `storeInHistory: true`, the same channel and pattern [Run a live poll during a match](https://www.pubnub.com/docs/use-cases/sports-media-entertainment/live-polls.md) covers. A reaction with no template logs and returns, so an unexpected emoji can't open an empty poll.

## Stop one burst from opening ten polls

A burst of reactions crosses your Decision's threshold again and again while it lasts, and every crossing fires the action. Without a guard, one burst opens a poll, then another, then another, seconds apart.

Illuminate can limit this on its own side. Set the Send Message action's execution limit to **Once per interval** instead of the default **Always**. Doing this makes Illuminate fire it at most once per interval, regardless of how many times the rule matches. Refer to [Action execution limit](https://www.pubnub.com/docs/analytics/decisions.md#action-execution-limit) for the setting.

Add a guard in your own service too. It protects `poll-service.js` regardless of how the Decision is configured, including while you're still testing with a trigger you send by hand. Add this to `poll-service.js`:

```javascript
const minimumMillisecondsBetweenPolls = 60000;
let lastPollOpenedAt = 0;

function shouldOpenPoll() {
  const now = Date.now();

  if (now - lastPollOpenedAt < minimumMillisecondsBetweenPolls) {
    console.log('that trigger arrived inside the cooldown window, so no new poll opened');
    return false;
  }

  lastPollOpenedAt = now;
  return true;
}
```

`shouldOpenPoll()` returns `true` at most once every 60 seconds, and records when it last did so. When it blocks a trigger, it logs that it did, so a poll that never appeared is never a silent failure.

## Receive the trigger in your poll service

Now wire the two together. Add this to `poll-service.js`:

```javascript
const triggerSubscription = pubnub.channel('game.poll-triggers').subscription({ receivePresenceEvents: false });

triggerSubscription.onMessage = (event) => {
  const trigger = event.message;
  const rawReaction =
    typeof trigger === 'object' && trigger !== null && !Array.isArray(trigger) && 'reaction' in trigger
      ? trigger.reaction
      : undefined;

  if (typeof rawReaction !== 'string') return;

  const reaction = rawReaction;

  console.log('open a poll because fans keep tapping', reaction);

  if (shouldOpenPoll()) {
    void openPollForReaction(reaction);
  }
};

triggerSubscription.subscribe();
```

`pubnub.channel('game.poll-triggers').subscription()` scopes a subscription to that one channel, and `onMessage` fires with the `reaction` field your Decision's `Body` sent. `subscribe()` activates it. The handler logs every trigger it receives, then opens a poll only if the cooldown allows one. So the log tells you both whether a trigger arrived and whether it did anything.

## Run it

Add this to `send-trigger.js`, after its client setup:

```javascript
const reaction = process.argv[2] || '🎉';

await pubnub.publish({
  channel: 'game.poll-triggers',
  message: { reaction },
  customMessageType: 'poll-trigger',
});

console.log('sent a trigger for', reaction);
```

In one terminal, start the poll service:

```bash
node poll-service.js
```

It prints nothing and waits. The service has no work until a trigger arrives.

In a second terminal, send one by hand:

```bash
node send-trigger.js 🎉
```

In the `poll-service.js` terminal, you should see the trigger arrive and the poll go out:

```text
open a poll because fans keep tapping 🎉
triggered poll published at timetoken: 17123456789011111
```

That publish also started the 60-second cooldown. Send a second trigger straight away, while the cooldown is still running:

```bash
node send-trigger.js 🎉
```

This time the trigger arrives but opens nothing:

```text
open a poll because fans keep tapping 🎉
that trigger arrived inside the cooldown window, so no new poll opened
```

That is the guard doing its job. Wait a full 60 seconds from the first poll, send a third trigger, and it publishes again.

`poll-service.js` keeps running because its subscription holds the connection open. Press **Ctrl+C** to stop it.

## What happened

1. `send-trigger.js` published a message to `game.poll-triggers`, standing in for the Send Message action your Illuminate Decision publishes for real once it's active.
2. `poll-service.js`'s subscription to `game.poll-triggers` received it and read the `reaction` field.
3. The guard allowed the first trigger, because nothing had opened a poll yet, and `poll-service.js` published a poll to `game.new-poll`. That is the same channel and shape [Run a live poll during a match](https://www.pubnub.com/docs/use-cases/sports-media-entertainment/live-polls.md) covers.
4. The guard blocked the second trigger, because it arrived inside the 60-second cooldown the first poll had begun, and said so in the log.

Once your Business Object, metric, and Decision are active, `game.stream-reactions` traffic from real fans replaces `send-trigger.js`, and the rest of this flow runs unchanged.

## Next steps

* [Run a live poll during a match](https://www.pubnub.com/docs/use-cases/sports-media-entertainment/live-polls.md). The poll-opening, voting, and results flow this tutorial's poll service builds on.
* [Illuminate](https://www.pubnub.com/docs/analytics/decisions/overview.md). How Business Objects, Decisions, and Dashboards fit together.
* [Dashboards](https://www.pubnub.com/docs/analytics/decisions/dashboards.md). Chart the reaction-count metric and the Decision's triggered actions together.
* [Rate limiting](https://www.pubnub.com/docs/design-patterns/rate-limiting.md). Throttle or shard `game.stream-reactions` itself if reaction volume grows large enough to need it.

Last updated at: 2026-09-30T07:20:08.000Z
