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.
| 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 |