Two hooks carry a plugin's whole lifecycle today: @plugin.on_ready for setup and your event handlers for everything else.
| Decorator | Fires when |
|---|---|
| @plugin.on_ready | Running locally with yourbot dev: once per process start. In production (plugins run in pool mode): once per (worker, server), right before that server's first event reaches you. Either way it runs before your handlers, which makes it the place for per-server init like seeding KV defaults. |
| Event handlers | Everything else. @plugin.on_event(...), @plugin.on_slash_command(...) and friends; see the Build tab. |
@plugin.on_ready
def ready(ctx: Context):
# Runs before this server's first event - seed defaults once.
if ctx.kv.get("welcome_channel") is None:
ctx.kv.set("welcome_channel", "")
ctx.log("ready on server " + ctx.server_id)
plugin.run() # required - starts the event loop and blocks forever.
@plugin.on_install / on_enable / on_disable / on_uninstall fire as advisory signals: the platform delivers them best-effort when a worker for your plugin is live (a cold start can drop one), with a tenant-scoped ctx that keeps your approved capabilities — on_uninstall can run its ctx.sql cleanup. Treat them as optimizations, never as the only path: put required init in on_ready, and design your state so leftover keys are harmless if an uninstall signal is missed.
You must call plugin.run() at the bottom of __main__.py. Without it the process exits before the SDK connects to the runner.