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.