Building

Lifecycle events

Run code when your plugin is installed, updated or removed, and when a chat starts, is cleared or deleted.

Lifecycle events are notifications: "you were just installed", "a chat just started". Unlike middleware there is nothing to transform or veto — your handler runs, returns nothing, and the platform moves on. They are where a plugin sets itself up, heals itself, and cleans up.

globalThis.events.<event> = async (context) => {
  // do the work; return nothing
};

Assigning the handler at the top level is all it takes — the event is discovered at publish time like any other capability.

The six events

Event Fires Sent to host.session
pluginInstalled your plugin was installed on an account (also on Re-run setup) your plugin only none — there is no chat
pluginUpdated the install moved to a different version (auto-update, accepted update, policy change, a new draft) your plugin only none
pluginUninstalled your plugin was uninstalled your plugin only none
sessionStarted a new interactive chat was created every plugin on the account that handles it that chat
sessionCleared a chat was cleared in place with /new every plugin on the account that handles it that (now empty) chat
sessionDeleted a chat was deleted every plugin on the account that handles it none usable — the chat is already gone

The session events fire for interactive chats only — not for scheduled runs, subagent sessions or agent-to-agent threads.

Contexts

pluginInstalled:   { installationId: string; pluginSlug: string; versionId: string; semVer: string }
pluginUpdated:     { installationId: string; previousVersionId: string; newVersionId: string;
                     previousSemVer: string; newSemVer: string }
pluginUninstalled: { installationId: string; pluginSlug: string }
sessionStarted:    { sessionId: string; agentId: string; sessionKind: string }   // "interactive" today
sessionCleared:    { sessionId: string; agentId: string }
sessionDeleted:    { sessionId: string; agentId: string }                         // the only record left

Delivery: at least once, never blocking

  • Each event is written to a durable outbox in the same transaction as the change that caused it, then delivered by a background worker. If AgentParley restarts in between, the event is still delivered.
  • A handler that throws or times out is retried with exponential backoff (30 s base, up to 8 attempts).
  • The action that caused the event is never held up by your handler: the install completes, the chat opens.

At least once means your handler can run more than once for the same occurrence. Write every handler to be idempotent — running it twice must leave the same result as running it once. For crews that's built in: host.agents.create is get-or-create by key.

What a handler can reach

The full host surface your permissions earn: host.fetch, host.settings, host.crypto, host.log, host.agents.* and host.models.list() (these work without a chat), host.llm and host.decide. The session events additionally get host.session.* for their chat, and host.session.llm (the chat's agent model).

Wall clock: 30 s per delivery attempt.

Patterns

Provision and heal a crew

The Oh My OpenAgents crew plugin uses three events to keep its seven agents correct — created on install, re-asserted after every update, and re-created if the user deleted one, the next time any chat starts:

// infrastructure/plugins/oh-my-openagents/index.ts
globalThis.events.pluginInstalled = async () => {
  await reconcileCrew();
};
globalThis.events.sessionStarted = async () => {
  await reconcileCrew();
};
globalThis.events.pluginUpdated = async () => {
  await reconcileCrew();
};

reconcileCrew lists the plugin's agents, creates missing ones by key, updates drifted ones and deletes retired ones. See Agent crews.

If the account didn't have room for every agent on install (plan limit), the owner can upgrade and press Re-run setup on the installed plugin's page — that sends pluginInstalled again.

Greet a new chat

Written for these docs — post a first message into every new interactive chat:

globalThis.events.sessionStarted = async () => {
  await host.session!.message.post({ text: "Tip: type /orchestrate <goal> to put the whole crew on it." });
};

Remember this can run twice; a greeting posted twice is harmless, but a counter incremented twice is not.

Clean up external state

pluginUninstalled is where to revoke a webhook you registered with a vendor or delete a remote record. Your plugin's agents, channels, live connections and stored secrets are removed by the platform itself on uninstall — you don't need to delete them.

Events vs. the session-clear middleware

sessionClearing (middleware) fires before /new deletes anything, so you can still read the transcript. sessionCleared (event) fires after, durably. Use the hook to read, the event to react.