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

Testing and Debugging Plugins

This page shows how to see what your plugin does, how to test it safely and how to fix each error.

Read the logs

Write log lines in your code:

ctx.log.info("Starting the sync.");
ctx.log.warn("The CRM answered slowly.");
ctx.log.error("The CRM refused the key.");

To read them, go to Application > Plugins, open your plugin and click Logs. The page shows the newest 200 lines, each with its level and hook.

Good to know:

  • Lines are kept for 30 days.
  • A line can have 2,000 characters.
  • Broadcast hides email addresses in logs: ada@example.com becomes a***@example.com. Other personal data is not hidden, so do not log it.
  • console.log does not reach this page. Use ctx.log.
  • A log line is saved when the call ends. It is saved also when your code throws, so log before the risky step.

Test before you deploy: drafts

A draft is a version that is not live. You can run its hooks against test data, and nothing real happens:

  • Subscribers are generated: person1@example.test to person25@example.test, with IDs 1 to 25.
  • Writes are simulated. Nothing changes.
  • There is no network. fetch answers only from the fixtures you pass.
  • Settings and secrets are only the ones you pass.
  • Your real data is never read.

To use drafts, you need a plugin developer token:

  1. Go to Application > Admin API Tokens.
  2. Create a token and turn on Plugin developer. It cannot have other permissions, and it expires within 30 days.

There are two ways to use it: with an AI agent or with the plugin kit.

With an AI agent

Connect your agent to Broadcast as MCP Server explains, with the plugin developer token. The agent then gets these tools:

Tool Does
plugin_api_reference Returns the plugin reference for the agent to read
plugins_list Lists plugins and versions
plugin_draft_create Starts a draft
plugin_draft_write_file, plugin_draft_read_file, plugin_draft_list_files, plugin_draft_delete_file Work with the draft’s files
plugin_draft_validate Checks the draft
plugin_draft_test Runs a hook with test data
plugin_logs Reads the log lines of test runs
plugin_draft_request_deploy Sends the draft to an admin for review

An agent cannot deploy. Only an admin can do that, in Application > Plugins.

With the plugin kit

The plugin kit is a folder with a command line tool. It needs Node 22 or later, and it has no other dependencies.

Warning

The kit is not on npm. npx broadcast-plugin and npm install broadcast-plugin do not work. Download the kit from your own Broadcast server at https://your-broadcast-server/plugin-kit/broadcast-plugin-kit.zip. It is the same version as the server. Unzip it, and run the tool from that folder.

From your plugin folder, run the tool with its path. Set your server and token first:

export BROADCAST_URL=https://your-broadcast-server
export BROADCAST_TOKEN=your-plugin-developer-token
node path/to/plugin_kit/cli/broadcast-plugin.mjs push

The output lists each file and ends with this:

Pushed. Next: broadcast-plugin validate, then broadcast-plugin test.

Then check the draft:

node path/to/plugin_kit/cli/broadcast-plugin.mjs validate
{
  "valid": true,
  "errors": [],
  "permissions": []
}

Save the test event in a file named args.json. The arguments are a list, with one item for each argument of the hook:

[{ "id": "evt_1", "type": "subscriber.created", "data": { "subscriber_id": 1 } }]

Run the hook:

node path/to/plugin_kit/cli/broadcast-plugin.mjs test --hook onEvent --args args.json
{
  "ok": true,
  "result": { "ok": true },
  "error": null,
  "ms": 284.941
}

Read the log lines of the test:

node path/to/plugin_kit/cli/broadcast-plugin.mjs logs --limit 5

When you are happy, send it for review with submit.

The kit has other commands too:

Command Does
init <name> Makes a new plugin folder. Add --template event-listener, scheduled-sync, settings-page or api-endpoint
build Turns src/main.js into dist/main.js. TypeScript, or code that imports other files, needs esbuild on your PATH
pack Writes the .broadcast-plugin file

Hook names for a test

Use these names with --hook or in plugin_draft_test. Some are not the same as the name in your code:

Hook Test name Example argument
Events onEvent { "id": "evt_1", "type": "subscriber.created", "data": { "subscriber_id": 1 } }
Schedules onSchedule { "name": "morning", "scheduled_for": "2026-11-02T09:00:00Z" }
Sequence steps sequenceStep { "name": "stop_if_tagged", "config": { "tag": "vip" }, "subscriber_id": 1 }
Merge tags mergeTags { "name": "tier", "subscribers": [{ "id": 1, "email": "person1@example.test" }] }
Liquid filters liquidFilter { "name": "format", "items": [{ "input": "5", "args": ["EUR"] }] }
Before send beforeSend { "broadcast_id": 1, "subject": "Hi", "subscribers": [{ "id": 1, "email": "person1@example.test" }] }
Public actions publicAction { "name": "rate", "value": "good", "subscriber_id": 1 }
API endpoints apiEndpoint { "name": "orders", "method": "POST", "query": {}, "body": {} }
AI tools aiTool { "name": "find_tags", "input": { "word": "vip" } }
Page data pageData { "page": 0, "input": {} }
Subscriber deleted onSubscriberDeleted { "subscriber_id": 1, "email": "person1@example.test" }

Fixtures: fake network answers

A test has no network. To test code that uses fetch, pass a list of answers. A URL that ends in * matches any address that starts the same way.

node path/to/plugin_kit/cli/broadcast-plugin.mjs test --hook onEvent --args args.json \
  --fixtures '[{"method":"POST","url":"https://hooks.slack.com/*","status":200,"body":"ok"}]' \
  --secrets '{"webhook_url":"https://hooks.slack.com/services/T0/B0/X"}'

If no fixture matches, fetch fails with No fixture for POST https://....

What drafts simulate

  • ctx.store.forSubscriber works on the 25 test people only. Another id answers not_found, as the live store does for a person outside the channel.
  • A tag or custom field that holds a topic is refused, as in a live call.
  • An event with webhook: true sends nothing. The test writes a log line that says what would go out.

What drafts cannot test

  • Pages do not open from a draft.
  • Merge tags, filters and links in a real email need a live plugin.

For these, deploy the plugin to a channel that has only your own test subscribers.

Check that it is really running

Use this list when a live plugin does nothing:

  1. Application > Plugins says the runtime is running.
  2. The list shows a version, not Not deployed.
  3. The channel shows On.
  4. The hook is in the manifest, and the function name matches it.
  5. The logs show a line for the call.

A plugin that failed too often shows Turned off after repeated errors, with the last error. Deploy a version to turn it on again.

Errors from a call

A failed call in the logs or test output has a code. A refused ctx call throws an error with the same code.

Code Meaning Fix
plugin_error Your code threw Read the message. The line that threw is in your code
timeout The call took longer than its limit, and the process was stopped Do less in one call. Look for a fetch with no answer
no_hook Broadcast called a hook that your code does not have Match the function name to the manifest name
load_failed The code did not load Run the code locally. Look for an error at the top level of the file
crashed The process ended, for example because it ran out of memory Use less memory. Each process has 64 MB
busy Too many calls waited, or the process took too long to start Broadcast tries events again. Use fewer calls at once
forbidden The plugin lacks a permission Add the permission to the manifest, and release a new version
not_found No such record in this channel Handle it. The person may be gone
invalid The arguments are wrong Read the message
rate_limited A limit was reached Slow down, or try at the next run
call_ended Your hook ended while a call was still open Add the missing await

These codes come from Broadcast, and they are not the plugin’s fault. They do not count toward the 20 failures: safe_mode, not_enabled, runtime_unavailable, busy and incompatible.

Messages you may see

Message Fix
The plugin may not use tags.add. Add "tags": "write" to the manifest
Reading past the first 100 subscribers needs subscribers:export. Add "subscribers": ["read", "export"], or read less
Creating subscribers needs subscribers:create. Add "subscribers": ["write", "create"]
A bulk call needs an idempotency_key. Add one. Build it from the run, not a random value, so that a retry uses the same key
A bulk call takes at most 1000 items. Split the list
The plugin declares no setting webhook_url. Add the setting to the manifest
Requires net access to "example.com:443", run again with the --allow-net flag Your manifest does not declare that host. Ignore the flag: add the host to permissions.network
"order.placed" is not an event this plugin declares. Add it to the top-level events key
the bundle must export default definePlugin({...}) Start your plugin with export default definePlugin({
the result is not JSON: ... Return plain data, with no functions or loops

Errors from an upload

Broadcast shows these when you upload a plugin or validate a draft. The text in <angle brackets> changes.

The package

Message Fix
Choose a .broadcast-plugin file. Pick a file
The package has no manifest.json. Run zip from inside the plugin folder
The package has no dist/main.js. Put your code at that exact path
<path> is not in an allowed folder (ui/, assets/, src/, vendor/). Remove the file, or move it. Allowed at the top: manifest.json, dist/main.js, README.md, LICENSE, package.json, deno.json, deno.lock
"<path>" is not an allowed file name. Remove hidden files such as .DS_Store. Names use letters, digits and . _ @ + -
manifest.json is not valid JSON: <detail> Fix the JSON
This is not a valid package file (<detail>). The file is not a zip file
The package is too large. Keep the zip file under 5 MB
The package has too many files. Use at most 500 files

The version

Message Fix
Version <x> is already installed. You uploaded the same file again
Version <x> is already installed with other code. A published version never changes: use a new version number. Raise version
Version <x> is older than the installed version <y>. Use rollback to go back to an earlier version. Raise version, or use Roll back
This plugin needs Broadcast <range>; this server runs <version>. Change requires.broadcast, or upgrade

Identity and requires

Message Fix
id "<id>" must look like publisher.name (lower case). Use local.my-plugin. Letters, digits and -
id <id>: a package that is not signed by the plugin directory must use an id that starts with local. Your own plugin must use local.
name is missing. Add name
version "<x>" is not a semantic version (for example 1.2.0). Use three numbers
requires.pluginApi must be one of 1. Use "pluginApi": "1" as a string
requires.broadcast must be a version range, for example ">=3.0.0 <4.0.0". Copy that range

Settings

Message Fix
settings must be an object. Use { }, not a list
Setting name "<name>" must be lower case letters, digits and _. Rename it. It must start with a letter
Setting <name> must be an object. Give it { "type": "string" }
Setting <name>: type must be one of string, text, url, integer, boolean, select. Use one of these
Setting <name>: a select needs options. Add an options list

Permissions

Message Fix
permissions must be an object. Use { }
Unknown permission <name>. Check the permission list. Basic things such as store need no declaration
The permission <name> does not exist for plugins: no plugin can have it. Remove it. No plugin can do this
elevated: <name> is not an elevated permission. Use network.private here
permissions.network must be a list. Use [ ]
network: "<host>" is not an allowed host. Use a public host name, for example api.example.com or api.example.com:8443. Do not use a URL with https://, an IP address, localhost or a private name
network: fromSetting "<name>" must name a setting of type url. Declare that setting with "type": "url"

Hooks

Message Fix
hooks must be an object. Use { }
Unknown hook <name>. Check the spelling. pageData needs no entry here
hooks.events: <name> is not an event Broadcast publishes. Use a name from Webhooks
hooks.schedules: each schedule needs a name (lower case) and a cron expression. Add name and cron
hooks.schedules.<name>: "<cron>" is not a cron expression. Check the five parts
hooks.schedules.<name>: runs more often than every 5 minutes. Use a longer gap
hooks.<key>: "<name>" must be lower case letters, digits and _. Rename it
hooks.aiTools: each tool must be an object with a name and a description. Use an object, not a string
hooks.aiTools.<name>: description is missing. Add description
hooks.aiTools.<name>: input_schema must be a JSON schema of type object. Use "type": "object"
hooks.apiEndpoints: each endpoint must be an object. Use an object
hooks.apiEndpoints.<name>: method must be GET or POST. Use upper case
hooks.apiEndpoints.<name>: permission must name an API token permission (for example subscribers_read). Use a token permission name
hooks.publicActions: <action> must be an object with a name of lower case letters, digits and _. Use { "name": "vote" }, not a string

Pages

Message Fix
ui.pages: each page must be an object. Use an object
ui.pages: slot must be one of nav, subscriberPanel, dashboardWidget. Use one of these
ui.pages: each page needs a title. Add title
ui.pages: "<path>" must be an .html file in ui/ in the package. Check the path, and pack the ui folder

Code

These start with dist/main.js:.

Message Fix
dist/main.js does not parse: <detail> Fix the syntax error
dist/main.js has no `export default definePlugin({...})`. Add it
The bundle imports another module. Bundle every dependency into dist/main.js. Remove import, or bundle with esbuild
The bundle re-exports another module. Bundle every dependency into dist/main.js. Remove export ... from
The bundle loads code at run time with import(). This is not allowed. Remove it
import.meta is not available to plugins. Remove it
eval() is not allowed., Function() is not allowed., new Function() is not allowed. Remove it
Line <n> is longer than 2000 characters. Minified code is not allowed. Do not minify. An admin must be able to read the code
The bundle check failed (<reason>). The server could not run the check. Ask the admin to check the plugin runtime

Events and segment rules have their own messages. They name the field and say what it needs. See Plugin Events and Segment Rules.

Turning a plugin on

Message Fix
Fill in these settings first: <names>. Fill in the required settings
Deploy a version of this plugin before you enable it. Deploy it
This plugin has secret settings, and this server has no encryption keys (ACTIVE_RECORD_ENCRYPTION_*). Ask the server admin to set the encryption keys

Last updated

Was this page helpful?

Thanks for your feedback!

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