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

Plugin Reference

This page lists everything in one place. To learn the ideas, start with Plugin Core Concepts.

Package files

A .broadcast-plugin file is a zip file.

Path Required Holds
manifest.json Yes The manifest
dist/main.js Yes The code that runs
ui/... No HTML, CSS and JavaScript of pages
assets/... No Images and fonts
src/... No Your source files, for reference
vendor/... No Copied dependencies, for reference
README.md, LICENSE No Text for the reader
package.json, deno.json, deno.lock No Build files, for reference

Rules for dist/main.js:

  • One ES module with export default definePlugin({ ... }). definePlugin is a global.
  • No import, export ... from, import() or import.meta. Put all code in this file.
  • No eval, Function() or new Function().
  • No line longer than 2,000 characters, so no minified code.
  • JavaScript only. Compile TypeScript first.

Manifest fields

Field Required Rule
id Yes publisher.name. Lower case letters, digits and -. A plugin without a directory signature must use local.
name Yes The name people see
version Yes Semantic version, such as 1.2.0 or 1.2.0-beta.1
requires.broadcast Yes A version range, such as >=3.0.0 <4.0.0
requires.pluginApi Yes "1"
permissions No See Permissions
hooks No See Hooks
settings No See Settings
events No Declared events with value, properties and webhook. See Plugin Events and Segment Rules
segmentRules No See Plugin Events and Segment Rules
ui.pages No A list of { "slot", "title", "path" }. See Plugin Pages and Signal

Icon and listing

Both are optional. They go through review with your code, and the signature covers them.

{
  "icon": "assets/icon.png",
  "listing": {
    "features": ["One short line per feature"],
    "support": "help@example.com",
    "privacy": "https://example.com/privacy",
    "changelog": "What changed in this version."
  }
}
Field Rule
icon A PNG in assets/, exactly 512 x 512 pixels, 256 KB at most
listing.features Up to 3 lines, each up to 80 characters
listing.support An email address or an https address
listing.privacy An https address
listing.changelog Text, up to 2000 characters, for this version

SVG is not accepted. Leave the corners square, because Broadcast rounds them. Use no words in the icon, and do not copy another brand.

A plugin with no icon shows its first letter on a colour tile.

Hooks

Key Value
events A list of event names
schedules A list of { "name", "cron" }
sequenceSteps A list of names, or { "name", "label", "settings" }
mergeTags A list of names, or { "name", "output": "html" }
liquidFilters A list of names, or { "name", "output": "html" }
aiTools A list of { "name", "description", "input_schema", "readOnly" }
apiEndpoints A list of { "name", "method", "permission" }. method is GET or POST
publicActions A list of { "name" }
beforeSend true
onSubscriberDeleted true

A name has 1 to 40 characters: lower case letters, digits and _, starting with a letter.

In a map hook, an entry is a function, or an object with a run function. mergeTags and liquidFilters can also be one function for all names.

Settings

Field Meaning
type Required. See below
label The text next to the field
secret true stores it as a secret. Read it with ctx.settings.secret
required true means it must have a value before the plugin can be turned on
options For select: a list of strings
Type Form field Your code gets
string One line A string
text Several lines A string
url One line A string. Broadcast does not check the form
integer One line A string, such as "5". Broadcast does not check the form
boolean Yes or No "true" or "false", as strings
select A list A string

ctx

Your own things

Call Answer
ctx.channel { id, name }
ctx.settings.get(name) The value, or undefined. No await
await ctx.settings.secret(name) The value, or null
await ctx.store.get(key) The value, or null
await ctx.store.set(key, value) true
await ctx.store.delete(key) true if a row was deleted
await ctx.store.increment(key, by = 1) The new number. Atomic. by is a whole number and can be negative
await ctx.store.list(prefix) A list of { key, value }, at most the first 100 rows
ctx.store.forSubscriber(id) A store with the same five functions, for one person
ctx.log.info(...), .warn(...), .error(...) Nothing. No await. Never throws

Subscribers

Call Permission Answer
subscribers.get(id) read A subscriber. Throws not_found
subscribers.find(email) read A subscriber, or null
subscribers.list({ limit, cursor, updated_since }) read { items, next_cursor }
subscribers.update({ id, first_name, last_name, custom_data }) write A subscriber
subscribers.bulkUpsert({ items, idempotency_key }) write One result for each item

All are under ctx.broadcast.

A subscriber has these fields: id, email, first_name, last_name, is_active, subscribed_at, unsubscribed_at, confirmed_at, tags, custom_data, source, signup_ref, created_at and updated_at.

Good to know:

  • Without a limit, subscribers.list gives up to 100 people.
  • A limit over 100 or a cursor needs the export permission. Then one page can have 1,000 people.
  • update merges custom_data with the old value. It cannot change the email or consent.
  • A custom_data key that holds a topic is refused with forbidden, in update and bulkUpsert. See Topics.
  • bulkUpsert items have email, first_name, last_name, custom_data and tags. It updates people who exist. To create people, it needs create, and a suppressed address is stored as inactive.

Tags

Call Permission Answer
tags.list() read Tag names, up to 1,000
tags.add({ subscriber_id, tags }) write The person’s tags
tags.remove({ subscriber_id, tags }) write The person’s tags
tags.bulkAdd({ subscriber_ids, tags }) write One result for each ID
tags.bulkRemove({ subscriber_ids, tags }) write One result for each ID

Topics

A topic is a subscriber’s consent, and it is stored in a tag or a custom_data key. No plugin can change it. If a tag matches a topic tag of the channel, without regard to case, Broadcast refuses the write with the code forbidden:

  • tags.add and tags.remove throw, and change nothing. This is also true if only one tag in the list is a topic.
  • tags.bulkAdd, tags.bulkRemove and subscribers.bulkUpsert refuse that item, and go on with the others. The item has "ok": false and the code.

Catch the error if the rest of your work must go on:

try {
  await ctx.broadcast.tags.add({ subscriber_id: id, tags: [tag] });
} catch (error) {
  if (error.code !== "forbidden") throw error;
  ctx.log.warn(`Tag ${tag} was refused.`);
}

Tag changes are safe to repeat.

Broadcasts, sequences and segments

Call Permission Answer
broadcasts.get(id), sequences.get(id), segments.get(id) read on that area A summary. Throws not_found
broadcasts.list({ limit, offset }), also sequences and segments read on that area { items }, up to 100

Summaries have no email bodies and no recipient lists.

Area Fields
Broadcast id, name, subject, status, scheduled_send_at, sent_at, total_recipients, sent_count, tags
Sequence id, name, active, init_tag, created_at
Segment id, name, description, subscribers_count, created_at

Events

Call Answer
events.record({ name, subscriber_id or email, value, properties, occurred_at, idempotency_key }) { ok }, or { ok: false, error }
events.bulkRecord({ items, idempotency_key }) One result for each item

fetch

fetch(url, options) works as usual, for declared hosts only. The call goes through Broadcast’s network gate, which refuses private addresses and does not follow redirects.

Permissions

Basic

Every plugin has these. Do not declare them: its own settings, storage, logs and events, and the channel name.

With no subscribers permission, a plugin’s event.data holds only fields that are not personal. See Events Hook.

Standard

Manifest Allows The admin sees
"subscribers": "read" get, find, list (first 100) Read subscribers
"subscribers": "write" read, plus update and bulkUpsert of existing people Change subscribers
"tags": "read" tags.list Read tags
"tags": "write" read, plus tag changes Change tags
"broadcasts": "read" Read broadcasts Read broadcasts
"sequences": "read" Read sequences Read sequences
"segments": "read" Read segments Read segments
"network": ["api.example.com"] Exact hosts. Port 443, unless you add one such as api.example.com:8443 Internet access

Sensitive

Manifest Allows The admin sees
"subscribers": ["read", "export"] Reading past the first 100 Copy your whole list
"subscribers": ["write", "create"] bulkUpsert can create people, up to 5,000 an hour Add new subscribers
"network": ["*.example.com"] One level of subdomains Internet access to many sites
"network": [{ "fromSetting": "x" }] The host of the URL in setting x, which must have type url Internet access to an address you enter
"output": "html" Merge tags and filters can return HTML, which is cleaned HTML in emails
hooks.beforeSend Added by the hook Change emails before they are sent
hooks.apiEndpoints Added by the hook Its own API
hooks.publicActions Added by the hook Links anyone can click in emails
"webhook": true on a declared event An admin can send the event to a webhook endpoint Its events in webhooks

Elevated

An admin must turn these on for each channel.

Manifest Allows The admin sees
"elevated": ["network.private"] One host on a private network, which the admin enters Access to one private address
"elevated": ["email:sender"] Reserved. No hook uses it yet Change the sender

Never available

sql, files, env, process, subscribers:resubscribe, suppressions:write, email:recipients and email:unsubscribe. If you declare one, the manifest is refused.

Network hosts

A host must be a public DNS name with at least two parts. These are refused: IP addresses, localhost, and names that end in .local, .localhost, .internal, .lan, .home, .corp, .intranet, .test, .invalid, .example or .arpa. Write the host only, with no https://.

Limits

Time

Limit Value
A normal hook call 10 seconds
Merge tags, Liquid filters, before send, public actions 5 seconds
Failures in a row before a plugin is turned off 20
Retries: events, subscriber deleted Up to 5 attempts
Retries: schedules Up to 3 attempts
Retries: sequence steps Up to 5 attempts
Shortest gap between schedule runs 5 minutes
A process with no calls stops after 5 minutes

Size

Limit Value
Send-time batch 100 people
Merge tag or filter value 10,000 characters
Before send: subject 998 characters
Before send: each HTML addition 10,000 characters
Public action: value 200 characters
Public action: title 200 characters
Public action: message 1,000 characters
API endpoint body 1 MB
Bulk call 1,000 items
Idempotency answers are kept for 1 day

Rates

Limit Value
Public action clicks for one link 5 a minute
Public action clicks from one IP address 60 a minute
Calls at once for one plugin 4
Calls that can wait in line 50
New subscribers from one plugin 5,000 an hour

Storage

Limit Value
Store value 256 KB
Store total, for each plugin and channel 10 MB
Store key 200 characters
store.list 100 rows
Plugin events, for each plugin and channel 2,000,000
Linked events are kept for 2 years
Events with no subscriber are kept for 90 days
Segment rules 10
where filters in a rule 5

Memory

Limit Value
Heap of one plugin process 64 MB
All plugins, by default 256 MB

Logs

Limit Value
Line length 2,000 characters
Lines saved from one call 100
Lines are kept for 30 days

Packages

Limit Value
Package 5 MB
Package, unpacked 20 MB
One file 5 MB
Files 500
manifest.json 256 KB

Draft tests

Limit Value
Generated subscribers 25
Fixtures 100
One fixture body 1 MB
Plugin developer token Expires within 30 days

Last updated

Was this page helpful?

Thanks for your feedback!

Thanks for letting us know. We'll work on improving this page.