# Your First Plugin

> A step-by-step tutorial. Start with an empty folder and finish with a Broadcast plugin that tags each new subscriber, with every file in full.

Source: https://sendbroadcast.net/docs/plugins-first-plugin

> Coming in Broadcast 3.0. Until then, this page applies only to the Upcoming (edge) build.

In this tutorial you build a plugin named **Signup tagger**. When a new person subscribes, the plugin adds a tag to that person. The admin chooses the tag.

It is a small plugin, but it uses the parts that every plugin uses: a manifest, a hook, a permission, a setting and a log line.

## What you need

- A Broadcast server, version 3.0 or later, and a user with sudo access. If you are not an admin, ask one to do steps 6 to 8.
- A text editor.
- A terminal with the `zip` command. macOS and Linux have it. On Windows, use WSL or Git Bash.

You do not need Node, npm or any install. A plugin is a few text files in a zip file.

Before you start, sign in to Broadcast and go to **Application > Plugins**. You should see **The plugin runtime is running**. If you see **The plugin runtime is not running or not installed**, plugins cannot run on this server yet. Everything else in Broadcast still works.

## Step 1: Make the folders

Run these commands:

```bash
mkdir -p signup-tagger/dist
cd signup-tagger
```

You now have an empty folder `signup-tagger`, with one folder `dist` in it. The name `dist` is fixed, because Broadcast looks there for your code.

## Step 2: Write the manifest

The manifest tells Broadcast what your plugin is. Make a file named `manifest.json` in `signup-tagger`, with this content:

```json
{
  "id": "local.signup-tagger",
  "name": "Signup tagger",
  "version": "1.0.0",
  "requires": { "broadcast": ">=3.0.0 <4.0.0", "pluginApi": "1" },
  "permissions": {
    "tags": "write"
  },
  "hooks": {
    "events": ["subscriber.created"]
  },
  "settings": {
    "tag_name": {
      "type": "string",
      "label": "Tag for new subscribers",
      "required": true
    }
  }
}
```

Here is what each part means:

| Part | Meaning |
|---|---|
| `id` | The name Broadcast uses inside. Your own plugins must start with `local.` |
| `name` | The name people see |
| `version` | The version of your plugin. Change it each time you release |
| `requires` | The versions of Broadcast and of the plugin API that this plugin works with. Copy this line as it is |
| `permissions` | What the plugin may do. Here: add and remove tags |
| `hooks` | When Broadcast calls your code. Here: when a subscriber is created |
| `settings` | Values the admin fills in. Here: the name of the tag |

Do not add comments to this file. JSON does not allow them.

## Step 3: Write the code

Make a file named `main.js` in the `dist` folder, with this content:

```javascript
export default definePlugin({
  async onEvent(event, ctx) {
    const tag = ctx.settings.get("tag_name");
    const subscriberId = event.data.subscriber_id;

    await ctx.broadcast.tags.add({ subscriber_id: subscriberId, tags: [tag] });

    ctx.log.info(`Added the tag ${tag} to subscriber ${subscriberId}.`);
  },
});
```

Read it line by line:

1. `export default definePlugin({ ... })` is how every plugin starts. `definePlugin` is ready for you. Do not import it.
2. `onEvent` is the hook. Broadcast calls it with two values: `event` says what happened, and `ctx` is your way to talk to Broadcast.
3. `ctx.settings.get("tag_name")` reads the setting.
4. `event.data.subscriber_id` is the ID of the new person.
5. `ctx.broadcast.tags.add(...)` asks Broadcast to add the tag. The `await` matters: without it, your function can end before the work is done.
6. `ctx.log.info(...)` writes a line that the admin can read.

Your folder now has two files:

```text
signup-tagger/
  manifest.json
  dist/
    main.js
```

## Step 4: Pack the plugin

A `.broadcast-plugin` file is a plain zip file with a different name. Run this command in the `signup-tagger` folder:

```bash
zip -r -X ../signup-tagger-1.0.0.broadcast-plugin manifest.json dist
```

You should see this:

```text
  adding: manifest.json (deflated 44%)
  adding: dist/ (stored 0%)
  adding: dist/main.js (deflated 36%)
```

The numbers can be different. You now have the file `signup-tagger-1.0.0.broadcast-plugin` next to your folder.

Warning

Run `zip` from inside the plugin folder, and do not use the **Compress** command in Finder. `manifest.json` must be at the top of the zip file, and Finder adds hidden files that Broadcast refuses.

## Step 5: Upload it

1. Go to **Application > Plugins**.
2. In **Upload a plugin**, choose your file.
3. Click **Upload**.

You should see the page **Signup tagger 1.0.0** and the message **Uploaded. Check the permissions, then deploy.**

If you see a red message on the plugins page, your package has a problem, and nothing was installed. Here are the ones you are most likely to see:

| Message | What to do |
|---|---|
| `The package has no manifest.json.` or `signup-tagger/manifest.json is not in an allowed folder` | You zipped the folder from the outside. Run `zip` from inside the folder, as in step 4 |
| `manifest.json is not valid JSON: ...` | Look for a missing comma, an extra comma after the last item, or a comment |
| `dist/main.js: dist/main.js does not parse: ...` | Your code has a syntax error. The message gives the line |
| `"__MACOSX/._manifest.json" is not an allowed file name.` | You used Finder. Use the `zip` command |

[Testing and Debugging Plugins](https://sendbroadcast.net/docs/plugins-testing#errors-from-an-upload) has the full list. Fix the problem, run `zip` again and upload again. First delete the old zip file, or `zip` adds to it.

## Step 6: Review and deploy

This is the page an admin sees for each new version. Read it from the top:

- A note says **Not from the plugin directory. Install it only if you trust where it came from.** This is normal for your own plugin.
- **Permissions** shows **Read tags** and **Change tags**, both marked **Standard**. You asked for `write`, and `write` includes `read`.
- **What it adds** shows `events: subscriber.created`.
- **Code** shows your files, so that the admin can read them.

Click **Deploy**. You should see **Version 1.0.0 is live.**

Deployed does not mean running. The plugin is live on the server, and it is still off in every channel.

## Step 7: Fill in the setting

1. On the plugin page, find **Channels**.
2. Click **Open** next to the channel you want to use.
3. Find the **Signup tagger** card and click **Settings and permissions**.
4. In **Tag for new subscribers**, type `new-subscriber`.
5. Click **Save settings**.

You should see **Settings saved.**

If you turn the plugin on first, you see **Fill in these settings first: tag_name.** The setting is `required` in your manifest, so Broadcast asks for it.

## Step 8: Turn it on

Click the switch on the **Signup tagger** card. You should see **The plugin is on in** and the name of your channel, and the card shows **On**.

## Step 9: See it work

1. Add a subscriber to that channel. Use a test address that you own.
2. Wait a few seconds, then open that subscriber.

You should see the tag `new-subscriber`.

Now read the log:

1. Go to **Application > Plugins** and click **Signup tagger**.
2. Click **Logs**.

You should see a line like this:

```text
info  onEvent  Added the tag new-subscriber to subscriber 12.
```

That is your plugin at work. You built it, packed it, reviewed it and ran it.

## If nothing happens

Check these in order:

1. **Is the plugin on in this channel?** The card must show **On**. Each channel has its own switch.
2. **Did you add the person to the same channel?**
3. **Is the person new?** The event `subscriber.created` runs once, when a subscriber is first created. Changing an existing subscriber does not run it.
4. **Is there a red line in the logs?** The message tells you what went wrong. For example, `The plugin may not use tags.add.` means the manifest does not ask for `tags: write`.
5. **Did you wait?** Events run as background jobs. Also, after you turn a plugin on, Broadcast can take up to 30 seconds to start sending it events.
6. **Does the plugin page say it was turned off?** A plugin that fails 20 times in a row is turned off for all channels. Fix the bug and deploy a new version.

## Step 10: Release a change

Now change the plugin. Change the log line in `dist/main.js`:

```javascript
ctx.log.info(`Tagged subscriber ${subscriberId} in the channel ${ctx.channel.name}.`);
```

A version number is a promise that the code does not change. So you must also change the version in `manifest.json`:

```json
"version": "1.0.1",
```

Then pack it with the new name:

```bash
zip -r -X ../signup-tagger-1.0.1.broadcast-plugin manifest.json dist
```

Upload the file. You see the review page again, this time with **Changes since 1.0.0**. It shows only what changed. Click **Deploy**.

If you forget to change the version, you see `Version 1.0.0 is already installed with other code. A published version never changes: use a new version number.`

The channel stays on and the setting stays saved. The next new subscriber runs your new code.

## Step 11: Add a page

A plugin can have its own page in Broadcast. [Signal](https://sendbroadcast.net/docs/signal), the UI kit, makes it easy. Add a page that shows how many people the plugin tagged.

Make a folder for it, in the `signup-tagger` folder:

```bash
mkdir ui
```

Change `manifest.json` to this. The new parts are the version and `ui`:

```json
{
  "id": "local.signup-tagger",
  "name": "Signup tagger",
  "version": "1.1.0",
  "requires": { "broadcast": ">=3.0.0 <4.0.0", "pluginApi": "1" },
  "permissions": {
    "tags": "write"
  },
  "hooks": {
    "events": ["subscriber.created"]
  },
  "settings": {
    "tag_name": {
      "type": "string",
      "label": "Tag for new subscribers",
      "required": true
    }
  },
  "ui": {
    "pages": [
      { "slot": "nav", "title": "Signup tagger", "path": "ui/index.html" }
    ]
  }
}
```

Change `dist/main.js` to this. It now counts each tag, and `pageData` gives the numbers to the page:

```javascript
export default definePlugin({
  async onEvent(event, ctx) {
    const tag = ctx.settings.get("tag_name");
    const subscriberId = event.data.subscriber_id;

    await ctx.broadcast.tags.add({ subscriber_id: subscriberId, tags: [tag] });
    await ctx.store.increment("tagged");

    ctx.log.info(`Added the tag ${tag} to subscriber ${subscriberId}.`);
  },

  async pageData(request, ctx) {
    return {
      tag: ctx.settings.get("tag_name"),
      tagged: (await ctx.store.get("tagged")) ?? 0,
    };
  },
});
```

Make `ui/index.html`. The two lines in `` load the kit from your server:

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Signup tagger</title>
    <link rel="stylesheet" href="/plugin-kit/v1/kit.css">
    <script type="module" src="/plugin-kit/v1/kit.js"></script>
    <script type="module" src="app.js"></script>
  </head>
  <body>
    <sg-page title="Signup tagger" description="Tags each new subscriber in this channel.">
      <sg-card id="summary"><sg-skeleton lines="2"></sg-skeleton></sg-card>
    </sg-page>
  </body>
</html>
```

Make `ui/app.js`:

```javascript
const summary = document.getElementById("summary");

function stat(label, value) {
  const node = document.createElement("sg-stat");
  node.setAttribute("label", label);
  node.setAttribute("value", value);
  return node;
}

try {
  const data = await sg.pageData();
  const grid = document.createElement("sg-grid");
  grid.append(stat("Tag", data.tag), stat("Subscribers tagged", data.tagged));
  summary.replaceChildren(grid);
} catch (error) {
  sg.toast(`The page could not load: ${error.message}`, { tone: "danger" });
}
```

Pack it, now with `ui`:

```bash
zip -r -X ../signup-tagger-1.1.0.broadcast-plugin manifest.json dist ui
```

Upload it and click **Deploy**. Then open **Plugins** in the sidebar and click **Signup tagger**. You should see the tag, and the number of subscribers tagged since this version.

## What you learned

- A plugin is a manifest and one JavaScript file in a zip file.
- The manifest declares everything. Code cannot use what the manifest does not ask for.
- An admin reviews and deploys each version, then turns the plugin on per channel.
- `ctx` is your one way to talk to Broadcast.

## Next steps

- [Signal Quick Start](https://sendbroadcast.net/docs/signal-quick-start) builds a richer page.
- [Plugin Core Concepts](https://sendbroadcast.net/docs/plugins-concepts) explains each idea you used here.
- [Testing and Debugging Plugins](https://sendbroadcast.net/docs/plugins-testing) shows how to test code before you deploy it.
- [Plugin Hooks](https://sendbroadcast.net/docs/plugins-hooks) shows what else your plugin can do.
