From botster
Use when creating or modifying a Botster Lua plugin with hooks, MCP tools, prompts, secrets, timers, HTTP, UI, or plugin.db persistence.
How this skill is triggered — by the user, by Claude, or both
Slash command
/botster:botster-customize-pluginThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Create reusable Botster behavior as a Lua plugin when it should be shared,
Create reusable Botster behavior as a Lua plugin when it should be shared, hot-reloaded, scoped, or distributed.
~/.botster/plugins/<name>/init.lua~/.botster-dev/plugins/<name>/init.lua<repo>/.botster/plugins/<name>/init.luaNew plugin directories may require one hub restart before hot-reload watches them. Existing plugin files hot-reload on save.
local hooks = require("hub.hooks")
local db = plugin.db{
version = 1,
models = {
events = {
id = true,
kind = { "text", required = true },
payload = { "text", required = true },
created_at = { "integer", required = true },
},
},
}
mcp.tool("my_tool", {
description = "Do one plugin action",
input_schema = {
type = "object",
properties = {
value = { type = "string" },
},
required = { "value" },
},
}, function(params, context)
db.events:insert{
kind = "my_tool",
payload = json.encode(params),
created_at = os.time(),
}
return { ok = true, session_uuid = context.session_uuid }
end)
return {}
Call plugin.db{} at plugin load time and capture the handle in a local. Use it
for plugin-owned durable state: queues, ledgers, workflow stages, sync cursors,
and audit records.
Do not persist PTY delivery mechanics in plugin DB. PTY probing and immediate delivery queues belong to runtime state.
Core primitives:
logjsonfsconfigsecretsEvent-driven primitives:
webrtctuisocketptyhubconnectionworktreeeventshttptimerwatchwebsocketaction_cablehub_discoveryhub_clientmcpupdatepushUse lib.session_actions for plugin-owned per-session affordances. Core
publishes action descriptors as session_action entities, and every client
invokes them through execute_session_action.
Action handlers run on the hub command path. They should validate, update
plugin-owned state, queue work, and return promptly. Do not resolve executables
or write generated config files directly in an action handler. Use
hub.prepare_plugin_command({ request_id, command, config_path?, config_contents?, context?, }) to offload PATH/filesystem preparation to the blocking worker pool, then
continue from events.on("plugin_command_prepared", ...).
Completion events include:
request_id — plugin token used to ignore stale completions.command and config_path on success.context — the opaque table passed by the plugin.error_kind — command_blank, command_missing,
config_write_failed, or task_failed.error — human-readable message.Store the active request token in plugin-owned state and drop completion events
whose request_id is no longer current. This covers disable/retry/reload races
without adding provider-specific behavior to core.
Hook observers:
agent_createdagent_deletedagent_lifecycle_pty_notification_rawpty_notificationpty_title_changedpty_cwd_changedpty_promptpty_inputclient_connectedclient_disconnectedafter_agent_createbefore_agent_closeafter_agent_closeshutdownHook interceptors:
before_agent_createbefore_agent_deletebefore_client_subscribefilter_agent_envExpose small, stable tools with clear schemas. Prefer structured return tables over prose strings when other tools or agents may consume the result.
Register prompts only for instructions that are genuinely reusable. Agent-side skills should carry static workflow guidance when possible.
Use lib.surfaces when a plugin needs a routable browser interface. Core owns
sidebar placement, so plugins should declare navigation metadata instead of
patching workspace layouts.
local surfaces = require("lib.surfaces")
surfaces.register("vault", {
label = "Vault",
icon = "book-open", -- Heroicons mini filename without .svg
nav = { section = "workspace", order = 25 },
sidebar = { surface = "vault_sidebar" },
routes = {
{ path = "/", render = vault_home },
{ path = "/sessions/:session_uuid", render = vault_session },
{ path = "/graph", layout = "fullscreen", render = vault_graph },
},
})
Set nav = false for routable utility/debug surfaces that should not appear
in the core Plugins section. Icon names are Heroicons mini filenames from
app/assets/svg/icons/heroicons/mini, without the .svg suffix.
When a plugin has its own session/workspace structure, declare a route-scoped sidebar instead of nesting navigation in the main page. Botster renders the plugin name with a back button, then mounts the named sidebar surface while the user is inside the plugin route:
surfaces.register("vault_sidebar", {
render = function()
return ui.stack{
ui.session_list{
visibility = "plugin",
owner_plugin = "vault",
surface = "vault",
},
}
end,
})
When a plugin owns sessions that should not appear as normal workspace sessions, spawn them with ownership metadata:
metadata = {
owner_plugin = "vault",
visibility = "plugin",
surface = "vault",
}
Then render them inside the plugin surface:
ui.session_list{
visibility = "plugin",
owner_plugin = "vault",
surface = "vault",
}
For terminal views inside a plugin route, use ui.session_terminal rather than
linking to the global /sessions/:session_uuid route:
ui.session_terminal{
session_uuid = state.params.session_uuid,
back = ctx.path("/"),
}
Notification URLs for plugin-owned sessions route through
/hubs/<hub>/<surface>/sessions/<session_uuid> when the session has
visibility = "plugin" and a matching surface/owner_plugin.
Use lib.plugin_assets plus ui.iframe when a plugin needs a fully custom
HTML/CSS/JS interface, such as a generated graph or drag-and-drop board. Do not
inject raw HTML into the parent Botster app.
local plugin_assets = require("lib.plugin_assets")
local board_url = plugin_assets.expose_file("kanban_board", "/path/to/board.html", {
content_type = "text/html",
})
plugin_assets.on_message("card.move", function(payload, ctx)
-- Validate payload and update plugin-owned state.
end)
ui.iframe{
src = board_url,
title = "Board",
sandbox = "allow-scripts",
bridge = { actions = { "card.move" } },
}
Register iframe/canvas/editor routes with layout = "fullscreen" so Botster
uses the same full-height, no-padding shell as terminal routes:
routes = {
{ path = "/board", layout = "fullscreen", render = render_board },
}
Iframe JavaScript can post:
window.parent.postMessage({
type: "botster.plugin_action",
action: "card.move",
payload: { card_id: "c1", to_column: "done", position: 2 },
}, "*")
Only actions declared in bridge.actions are forwarded to the hub. Keep the
iframe sandboxed by default and use self-contained HTML unless you also expose
supporting assets deliberately.
docs/lua/primitives.md — primitive APIs and execution model.docs/lua/hook-system.md — hook APIs, events, and Rust bridge callbacks.docs/lua/plugin-db.md — plugin.db{} schema, migrations, constraints.docs/lua/directory-structure.md — plugin paths and override order.docs/lua/hot-reload.md — plugin reload behavior.catalog/templates/plugins/ — working plugin templates.npx claudepluginhub tonksthebear/trybotster --plugin botsterGuides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.