Plugin Lifecycle
Every plugin goes through the same four-phase lifecycle. Understanding what belongs in each phase keeps your plugins clean and prevents some common bugs — especially around goroutine leaks and resource cleanup.
The Four Hooks
All four methods receive *tako.Context and return error. A non-nil error from OnInit or OnActivate marks the plugin as failed and skips it for the rest of boot. Errors from OnDeactivate and OnDestroy are logged but don't stop other plugins from shutting down.
All four are wrapped in a recover() sandbox — a panic in your plugin won't crash the process.
OnInit — Setup Phase
Called for every plugin, regardless of type. This is where you:
- Resolve services from the container
- Register keybindings
- Register hook providers
- Register cleanup callbacks with
ctx.OnDestroy
func (p *Plugin) OnInit(ctx *tako.Context) error {
// ✅ Resolve dependencies
var logger contracts.Logger
if err := ctx.Container().Make(&logger); err != nil {
return fmt.Errorf("search: logger not available: %w", err)
}
p.logger = logger
// ✅ Register keybindings — no router boilerplate needed
ctx.Keys().Bind("ctrl+f", func() {
ctx.Overlay().ShowComponent(&SearchBox{})
})
// ✅ Register hook providers
ctx.Hooks().Multi("app.sidebar", func() any {
return p.renderSidebar()
})
// ✅ Register cleanup
ctx.OnDestroy(func() {
p.cleanup()
})
return nil
}Don't do in OnInit:
- Start goroutines (use
OnActivatefor that) - Subscribe to events (use
OnActivate— it respects the activation order) - Call services that haven't been registered yet
OnActivate — Running Phase
Called only for TypeHeadless plugins, after all plugins have completed OnInit. This is where you:
- Start background goroutines with
ctx.Spawn - Subscribe to events on the event bus
- Begin any ongoing work
func (p *Plugin) OnActivate(ctx *tako.Context) error {
// ✅ Start a background worker
ctx.Spawn(func(c context.Context) {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
p.refresh()
ctx.Emit("search:index-updated", nil)
case <-c.Done():
return // ← ALWAYS respect ctx.Done() to prevent goroutine leaks
}
}
})
// ✅ Subscribe to events
cancel := ctx.On("config:reloaded", func(e contracts.Event) {
p.reloadIndex()
})
p.cancelSub = cancel
// Alternative — scoped to ctx so it's auto-pruned on context cancellation:
// cancel = ctx.Subscribe(ctx, "config:reloaded", func(e contracts.Event) { ... })
return nil
}The key rule here: always select on ctx.Done() in your goroutines. When the app shuts down, the context is cancelled. If your goroutine doesn't check ctx.Done(), it will be leaked — ctx.Spawn tracks goroutines and waits for them during shutdown, so the app will hang until it times out.
OnDeactivate — Winding Down
Called during shutdown, in reverse boot order — the last plugin to boot is the first to deactivate. This mirrors the dependency relationship: if plugin B depends on plugin A, B shuts down before A.
This is where you:
- Cancel event subscriptions
- Release any locks or connections your plugin owns
- Signal background goroutines to stop (though
ctx.Spawngoroutines stop automatically via context cancellation)
func (p *Plugin) OnDeactivate(ctx *tako.Context) error {
// ✅ Cancel event subscriptions
if p.cancelSub != nil {
p.cancelSub()
p.cancelSub = nil
}
// ✅ Signal any channels
if p.stopChan != nil {
close(p.stopChan)
}
return nil
}OnDestroy — Final Cleanup
Called immediately after OnDeactivate, still in reverse order. This is your last chance to:
- Close file handles or network connections
- Flush any unsaved state to the KV store
- Release any OS resources
func (p *Plugin) OnDestroy(ctx *tako.Context) error {
// ✅ Save state before the KV store closes
if p.history != nil {
_ = ctx.Storage().Set("search.history", p.history)
}
// ✅ Close any resources your plugin owns
if p.index != nil {
p.index.Close()
}
return nil
}Tip: The KV Store's
Close()is called after plugin shutdown completes, so it's safe to write to storage inOnDestroy.
Shutdown Sequence
Each OnDeactivate and OnDestroy call is sandboxed — a panic in one plugin's teardown doesn't prevent the others from running.
Up next: 06.03 — Plugin Dependencies — a practical guide to declaring dependencies, handling missing plugins gracefully, and structuring multi-plugin apps.
