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