Building

Lifecycle events

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

View as Markdown

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 right after the change that caused it is saved, then delivered by a background worker. If AgentParley restarts after that, the event is still delivered.
  • A handler that throws, times out or runs out of memory is logged and skipped; it is not run again for that event. Catch errors you can recover from inside the handler, and make the next event (sessionStarted, Re-run setup) able to finish the job.
  • If AgentParley itself can't deliver the event (the plugin runtime is unreachable, a database error), the delivery 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 (a redelivery after such an infrastructure failure). 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.*, host.cronJobs.* and host.models.list() (these work without a chat), host.oauth, host.llm and host.decide. The session events additionally get host.session.* for their chat, host.session.llm (the chat's agent model) and host.workspace.* (the chat's agent's files).

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.