# Lifecycle Hooks

> Run plugin code when a plugin is first turned on, when a new version is deployed or rolled back, and just before it is removed. Store migrations, retries and limits.

Source: https://sendbroadcast.net/docs/plugins-hook-lifecycle

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

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:

```json
{
  "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.

```javascript
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:

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

```javascript
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](https://sendbroadcast.net/docs/plugins-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](https://sendbroadcast.net/docs/plugins-reference#your-own-things).

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