| Attribute | Type |
|---|---|
| ctx.server_id | str — Discord server (guild) ID |
| ctx.plugin_id | str — your plugin's id |
| ctx.version | str — installed version string |
| ctx.capabilities | set[str] — capabilities the user approved |
| ctx.discord | Discord REST surface |
| ctx.interaction | Interaction response surface |
| ctx.kv | Per-server KV store |
| ctx.sql | Sandboxed SQL (capability) |
| ctx.ephemeral | Redis-backed counters / cooldowns / flags |
| ctx.http | Outbound HTTP via proxy |
| ctx.ws | Persistent WebSocket connections via the platform broker (capability) |
| ctx.secrets | Encrypted secret storage (capability) |
| ctx.metrics | Time-series metrics |
| ctx.request_id | str — correlation ID for the current interaction ("" outside one) |
| ctx.log(message, *, level, tags, **extra) | Structured plugin log |
| ctx.has_capability(name) | bool — capability check |
ctx.discord
| Method | Capability |
|---|---|
send_message(*, channel_id, content="", embeds=None, components=None, files=None) → returns {"message_id", "channel_id"} — files is a list of {"filename", "data_b64"}, up to 3 attachments | discord:send_message |
edit_message(*, channel_id, message_id, content=None, embeds=None, components=None) — None leaves components unchanged, [] clears them | discord:edit_message |
| delete_message(*, channel_id, message_id) | discord:delete_message |
| bulk_delete_messages(*, channel_id, message_ids) | discord:delete_message |
| add_reaction(*, channel_id, message_id, emoji) | discord:add_reaction |
| pin_message(*, channel_id, message_id) | discord:send_message |
| unpin_message(*, channel_id, message_id) | discord:send_message |
| get_messages(*, channel_id, limit=50, before=None, after=None) | discord:read |
iter_messages(*, channel_id, batch_size=50, before=None, after=None) v0.6.1 — generator that pages through full channel history (newest→oldest, or oldest→newest with after=) | discord:read |
| get_guild() | discord:read |
| get_channel(*, channel_id) | discord:read |
| list_channels() | discord:read |
| get_member(*, user_id) | discord:read |
| list_members(*, role_id=None, limit=100, after=None) | discord:read |
| search_members(query, *, limit=25) | discord:read |
| list_roles() | discord:read |
| create_channel(*, name, channel_type=0, category_id=None, topic=None, user_limit=None) | discord:manage_channels |
| edit_channel(*, channel_id, name=None, topic=None, user_limit=None) | discord:manage_channels |
| delete_channel(*, channel_id) | discord:manage_channels |
| set_channel_permissions(*, channel_id, target_id, allow="0", deny="0", target_type=0) | discord:manage_channels |
| delete_channel_permission(*, channel_id, target_id) | discord:manage_channels |
| create_thread(*, channel_id, name, thread_type=11, auto_archive_duration=1440) | discord:manage_channels |
| edit_thread(*, thread_id, archived=None, locked=None, name=None, auto_archive_duration=None) | discord:manage_channels |
| timeout_member(*, user_id, duration_seconds, reason="") | discord:moderate_members |
| set_nickname(*, user_id, nickname=None) | discord:moderate_members |
| kick_member(*, user_id, reason="") | discord:kick_members |
| ban_member(*, user_id, reason="", delete_message_seconds=0) | discord:ban_members |
| unban_member(*, user_id) | discord:ban_members |
| add_role(*, user_id, role_id, reason="") | discord:manage_roles |
| remove_role(*, user_id, role_id, reason="") | discord:manage_roles |
| add_role_bulk / remove_role_bulk / timeout_bulk / kick_bulk | (matches single-action capability; each call takes at most 25 user_ids — extras are silently dropped, so chunk big lists yourself) |
| create_webhook(*, channel_id, name) | discord:manage_webhooks |
| execute_webhook(*, webhook_id, webhook_token, content="", embeds=None, username=None, avatar_url=None) | discord:manage_webhooks |
| delete_webhook(*, webhook_id) | discord:manage_webhooks |
edit_message, delete_message and bulk_delete_messages only operate on messages your plugin sent — attempts on other messages are rejected, and a bulk call fails closed on the whole batch if any ID isn't plugin-owned. Ownership records expire after 24 hours (each successful edit refreshes the clock), so design flows that edit long-lived messages — leaderboard boards, reaction-role posts — to re-send rather than edit once the message is a day old.
Silent clamps: arguments outside Discord's ranges are clamped or truncated rather than raising — bulk_delete_messages takes at most 100 IDs, timeout_member caps at 28 days, ban_member's delete_message_seconds at 7 days, set_nickname at 32 chars, and get_messages / list_members / search_members clamp their limit. If an exact value matters, validate before you call.
ctx.ws
Requires proxy:websocket. Connections are named; the platform's broker holds the socket. Full guide on the Storage & I/O tab.
| Method | Use |
|---|---|
| ensure(name, url, *, secret_auth=None, auth=None, subscribe=None, binary=False) | Idempotently open (or confirm) the named connection. Returns {"conn_id", "name", "state"}. subscribe frames are re-sent after every reconnect. |
| send(name, data) | One frame: str sends text, bytes sends binary. |
| close(name) | Hang up the named connection. |
| allow_host(host) / revoke_host(host) | Authorize a WebSocket destination a server admin supplies at setup time; call it from a slash handler run by a member with Admin/Manage Server, or from a dashboard handler with a manager viewer. |
ctx.interaction
All four methods require the interaction:respond capability (auto-added when your manifest declares slash_commands).
| Method | Use |
|---|---|
| respond(*, content="", embeds=None, components=None, ephemeral=False, allowed_mentions=None, update_message=False) | First reply (within 3s of receiving the interaction). allowed_mentions mirrors Discord's API field — e.g. {"parse": []} suppresses all pings. update_message=True (component handlers only, v0.7.0) edits the message the component sits on instead of sending a new reply. |
| defer(*, ephemeral=False) | "I'm working on it" — buys you up to 15 minutes for a followup |
| followup(*, content="", embeds=None, components=None, ephemeral=False, allowed_mentions=None) | Reply after a defer (or send additional messages). Returns {"message_id", "channel_id"} so you can edit the message later. |
| send_modal(*, title, custom_id, fields=None) | Open a modal — use as the FIRST response, not after a defer |
ctx.kv / ctx.sql / ctx.ephemeral / ctx.http / ctx.metrics
Full method signatures live on the Storage & I/O tab: KV, SQL, Ephemeral, HTTP (including secret-backed Authorization injection via secret_auth=), Metrics.
Plugin decorators
| Decorator | Handler signature |
|---|---|
| @plugin.on_ready | (ctx) — per (worker, server) before that server's first event in pool mode; once per boot locally |
| @plugin.on_event(event_type) | (ctx, event) |
| @plugin.on_slash_command(name) | (ctx, event) |
| @plugin.on_component(custom_id=None, *, prefix=None) | (ctx, event) — exactly one of exact custom_id or prefix match |
| @plugin.on_modal_submit(custom_id) | (ctx, event) |
| @plugin.on_ws_message(name) | (ctx, frame) — {"name", "conn_id", "data", "binary"}, binary data base64. "prefix:*" wildcard matches by prefix; exact names win |
| @plugin.on_ws_open(name) | (ctx) |
| @plugin.on_ws_close(name) | (ctx, info) — {"reason", "will_reconnect"} |
| @plugin.on_dashboard(method_name) | (ctx, params) → dict |
| @plugin.schedule(seconds) | (ctx) — local yourbot dev only; does not run in pool mode (marketplace plugins), see Pool mode |
| @plugin.cron(spec) | (ctx) — 5-field UTC cron string. Runs in production when declared in the manifest "cron" array (5-minute floor, 5 entries max); see Cron schedules |
| @plugin.on_install / on_enable / on_disable / on_uninstall | (ctx) — advisory signals, delivered best-effort when a worker is live, with a tenant-scoped ctx that keeps your approved capabilities. Required init still belongs in on_ready |
Event payload reference
Typed payload shapes for the 16 core events live in yourbot_sdk.events (MessageCreate, MemberJoin, …); the four gateway-only events (reaction_clear, thread_create, guild_join, guild_remove) are plain dicts. Defaults produced by yourbot_sdk.testing.make_event:
make_event("message_create", content="hi")
# {message_id, channel_id, guild_id, author_id, author_username, author_bot,
# content, timestamp}
make_event("interaction_create", command_name="ban", custom_id="")
# {interaction_id, interaction_type, guild_id, channel_id, user_id,
# command_name, custom_id, modal_values}
make_event("reaction_add", emoji="🎉")
# {message_id, channel_id, user_id, emoji, guild_id}
WebSocket handlers receive their own shapes (not events): on_ws_message gets {"name", "conn_id", "data", "binary"} (binary data base64, frames per connection arrive in order) and on_ws_close gets {"reason", "will_reconnect"}.
Exceptions reference
See the full table under Error handling on the Production tab.