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

Lifecycle Hooks

Use these hooks to run code at three moments in the life of your plugin:

Function When it runs What you get
onInstall The first time the plugin is turned on in a channel { version }
onUpgrade After a new version is deployed, or after a rollback, once in each channel where the plugin is on { from_version, to_version, rollback }
onUninstall Just before the plugin is removed, once in each channel where the plugin is on { version }

Declare each one with true in the manifest. Without it, your function is never called:

{
  "hooks": {
    "onInstall": true,
    "onUpgrade": true,
    "onUninstall": true
  }
}

Any other value than true is refused when you upload the plugin.

onInstall

Broadcast calls onInstall the first time an admin turns the plugin on in a channel. Use it to set default rows in the store, or to register a webhook with another service.

export default definePlugin({
  async onInstall({ version }, ctx) {
    // Safe to repeat: set only what is not there yet.
    if (await ctx.store.get("schema") === null) {
      await ctx.store.set("schema", 1);
    }
    ctx.log.info(`Installed ${version} in ${ctx.channel.name}.`);
  },
});

Good to know:

  • It runs once for each channel, the first time only. If the admin turns the plugin off and on again, it does not run again.
  • version is the version that was on when the plugin was turned on, such as "1.2.0".

onUpgrade

Broadcast calls onUpgrade after an admin deploys a new version, and after a rollback to an older version. It runs once in each channel where the plugin is on, with the new code. This is the place to migrate your store rows to a new shape.

Field Holds
from_version The version that was live before, such as "1.0.0"
to_version The version that is live now, such as "1.1.0"
rollback true when an admin rolled back. to_version is then older than from_version

It does not run for the first deploy of a plugin, and it does not run when the version stays the same.

The version that is live now decides. Broadcast calls onUpgrade only if that version declares it. On a rollback, that is the older version, so an older version without onUpgrade gets no call.

Full example: migrate store rows

Version 1.0.0 stored each poll as a number of votes. Version 1.1.0 stores an object with the votes and the time of the last vote. This onUpgrade changes every old row, and pages through the store with ctx.store.list so that no row is missed:

export default definePlugin({
  async onUpgrade({ from_version, to_version, rollback }, ctx) {
    if (rollback) {
      ctx.log.info(`Rolled back from ${from_version} to ${to_version}. Nothing to migrate.`);
      return;
    }

    let migrated = 0;
    let page = await ctx.store.list({ prefix: "poll:" });
    while (page.length > 0) {
      for (const { key, value } of page) {
        // Safe to repeat: a row that is already an object is skipped.
        if (typeof value === "number") {
          await ctx.store.set(key, { votes: value, last_vote_at: null });
          migrated += 1;
        }
      }
      page = await ctx.store.list({ prefix: "poll:", cursor: page.at(-1).key });
    }

    ctx.log.info(`Upgraded from ${from_version} to ${to_version}. Migrated ${migrated} polls.`);
  },
});

A rollback runs the older code, and that code may not read the new shape. If you change the shape of your data, keep the old code able to read it, or write the old shape back when rollback is true.

onUninstall

Broadcast calls onUninstall when an admin removes the plugin, just before the plugin, its settings and its store are deleted. Your store and secrets are still there, so you can read them. Use it to clean up outside Broadcast, for example to delete a webhook that onInstall registered with another service.

export default definePlugin({
  async onUninstall({ version }, ctx) {
    const hookId = await ctx.store.get("crm_webhook_id");
    if (hookId === null) return;

    const apiKey = await ctx.settings.secret("api_key");
    const response = await fetch(`https://api.example-crm.com/v1/webhooks/${hookId}`, {
      method: "DELETE",
      headers: { authorization: `Bearer ${apiKey}` },
    });
    if (!response.ok && response.status !== 404) {
      throw new Error(`The CRM answered ${response.status}`);
    }
  },
});

Good to know:

  • It has 5 seconds, and it runs once. It is not tried again.
  • It runs only when the plugin is removed. Turning the plugin off in a channel does not call it.
  • It runs only in the channels where the plugin is on at that time.

When onUninstall fails

A failure does not stop the removal. The plugin and its data are deleted, and Broadcast writes the failure to the plugin audit log. To see it, go to Application > Plugins and click Audit log. The row shows the action uninstall_hook_failed, the channel, and the error code and message, such as {"code":"timeout","message":"..."}.

The work in the other service did not happen, so the admin must do it by hand. Write a clear error message, so that the admin knows what to clean up.

Retries and limits

Hook Time limit When it fails
onInstall 30 seconds Tried again, up to 5 times
onUpgrade 30 seconds Tried again, up to 5 times
onUninstall 5 seconds Not tried again. Written to the audit log

onInstall and onUpgrade run in a background job. Delivery is “at least once”: a retry or a restart can run the same call again. Make both safe to repeat. Check what is already there before you write, as the examples do.

A failed onInstall or onUpgrade counts toward the 20 failures in a row that turn a plugin off. See Plugin Hooks.

Common mistakes

You migrate only the first 100 rows. One ctx.store.list call returns at most 100 rows. Page through the store with the last key as the cursor, as the example does. See ctx.store.list.

Your migration is not safe to repeat. If the job runs twice, a migration that adds to a number adds twice. Check the shape of each row before you change it.

You expect onUpgrade in every channel. It runs only where the plugin is on at the time of the deploy. A channel where the plugin was off gets no call. If the admin turns it on later, it has old rows. Make your code read the old shape too, or migrate a row when you read it.

You expect onInstall each time the plugin is turned on. It runs the first time only.

You do slow work in onUninstall. It has 5 seconds and one try. Do one quick call, and throw with a clear message if it fails.

You forget the manifest. Each function needs its true in hooks. Without it, Broadcast does not call it.

Last updated

Was this page helpful?

Thanks for your feedback!

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