← Dokumentation  /  Entwicklerhandbuch

Erstelle ein Plugin

Die vollständige SDK-Referenz für Plugin-Entwickler — Sandboxed Python, als ZIP-Datei versendet, über den Marketplace verteilt.

These docs are publicly viewable. To publish a plugin you'll need a free YourBot account: sign in to access the Dev Portal.
SDK v0.8.4

Plugins sind isolierte Python-Prozesse. Schreibe einen Handler, deklariere deine Fähigkeiten, lade ein Zip hoch — wir führen es in einem abgesicherten Docker-Container aus und übertragen Discord-Events per JSON-RPC.

Discord-Events

Verwende @plugin.on_event(name) für rohe Discord-Events. Sechzehn Event-Typen werden an Plugins weitergeleitet:

EreignisPayload-Highlights
message_createmessage_id, channel_id, author_id, author_bot, content, created_at
message_editmessage_id, channel_id, content
message_deletemessage_id, channel_id
member_joinuser_id, username, guild_id
member_leaveuser_id, username, guild_id
member_updateuser_id, roles
reaction_addmessage_id, channel_id, user_id, emoji
reaction_removemessage_id, channel_id, user_id, emoji
voice_state_updateuser_id, before_channel_id, after_channel_id, self_mute, self_deaf
interaction_createinteraction_id, interaction_type, command_name, options, custom_id, values, modal_values
channel_createchannel_id, name, type
channel_deletechannel_id, name
channel_updatechannel_id, name
role_createrole_id, name
role_deleterole_id, name
role_updaterole_id, name

Field availability can differ slightly by delivery path (for example author_bot), so read optional fields defensively with event.get(...).

Hinweis: Für Slash-Befehle, Button-Klicks und Modal-Eingaben verwende die dedizierten Dekoratoren unten — sie sind einfacher als das Durchsuchen von interaction_create.

Message text is capability-gated: without events:message_content the whole payload is reduced to IDs and timestamps — content, author fields and attachments are omitted (interaction fields like options and custom_id are always preserved). The events still fire either way. All 16 payloads also ship typed shapes you can import for autocomplete: from yourbot_sdk.events import MessageCreate, MemberJoin, …

Zustellkanal

  • Delivered events arrive at least once: your handler can be called twice for the same event, so make side effects idempotent or guard with ctx.ephemeral.dedup(…). Under extreme load the platform sheds gateway events rather than buffering them, so don't build flows that assume every Discord event reaches you.
  • If a handler raises, the event is marked failed and is not retried. The full traceback lands in your plugin logs.
  • 5 consecutive crashes within 5 minutes quarantines the plugin (see the Production tab).
  • Handlers for the same server run on a small thread pool and can execute concurrently. Module-level state shared across handlers must be thread-safe (use threading.Lock).

Slash-Befehle

Zwei Schritte: Deklariere den Befehl in manifest.json, dann verarbeite ihn mit @plugin.on_slash_command. Die Plattform synchronisiert die Befehlsregistrierung automatisch mit Discord, wenn deine Version veröffentlicht wird.

Fähigkeits-Kurzweg: Die Deklaration von slash_commands fügt automatisch interaction:respond zu capabilities_required hinzu — du musst es nicht selbst aufführen. Die explizite Deklaration im folgenden Beispiel ist harmlos, aber redundant.

Name rules: command names must be unique within your manifest and may not use names reserved by built-in YourBot plugins (see the definitive list; yourbot validate checks it locally). Uploads that break these rules are rejected at publish time. If another installed plugin already uses one of your names on a server, the plugin installed first keeps the name there and yours is skipped.

Interaction timing. The platform acknowledges each slash command for you before your handler runs (users see a "thinking…" placeholder), and your respond() fills it in — you have up to 15 minutes, not Discord's raw 3 seconds. The exception is modals: a modal must be the first response, so a command that opens one must set "defer_on_dispatch": false in its manifest entry and then call send_modal within 3 seconds.

Deklaration in manifest.json

{
  "id": "my_plugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "capabilities_required": ["interaction:respond"],
  "slash_commands": [
    {
      "name": "greet",
      "description": "Say hello to someone",
      "options": [
        {
          "name": "user",
          "description": "Who to greet",
          "type": 6,
          "required": true
        },
        {
          "name": "message",
          "description": "Custom greeting",
          "type": 3,
          "required": false
        },
        {
          "name": "tone",
          "description": "How formal?",
          "type": 3,
          "required": false,
          "choices": [
            {"name": "Casual", "value": "casual"},
            {"name": "Formal", "value": "formal"}
          ]
        }
      ]
    }
  ]
}

Discord-Optionstypen, die du häufig verwenden wirst:

TypBedeutung
3Zeichenkette
4Ganzzahl
5Boolescher Wert
6Benutzer (wird zu einer Benutzer-ID aufgelöst)
7Kanal
8Rolle
10Zahl (Gleitkomma)

Handler

@plugin.on_slash_command("greet")
def greet(ctx: Context, event: dict):
    options = {opt["name"]: opt["value"] for opt in event.get("options", [])}
    target_user_id = options.get("user")
    custom_msg = options.get("message", "Hey there!")

    ctx.interaction.respond(
        content=f"<@{target_user_id}> {custom_msg}",
    )

Because the platform already acknowledged the interaction, respond() here edits the "thinking…" placeholder — take the time you need (up to 15 minutes). Additional messages go through followup().

Autocomplete is not forwarded. The platform does not currently deliver Discord autocomplete interactions to plugins, so declare static choices on your options (see the tone option above) instead of autocomplete handlers.

Schaltflächen & Menüs

Komponentenklassen erstellen Discord-Komponenten-JSON für dich. Sie verwenden Zeichenkettenwerte, keine Enumerationen.

Button

from yourbot_sdk import Button, ActionRow

Button(
    label="Click me",
    custom_id="btn_hi",
    style="primary",   # "primary" | "secondary" | "success" | "danger" | "link"
    emoji="🎉",
    disabled=False,
)

# Link buttons use url instead of custom_id
Button(label="Docs", url="https://yourbot.gg/dev/docs", style="link")

SelectMenu

from yourbot_sdk import SelectMenu, SelectOption, ActionRow

menu = SelectMenu(
    custom_id="pick_role",
    options=[
        SelectOption(label="Gamer", value="role_gamer", description="Get pinged for game nights"),
        SelectOption(label="Music",  value="role_music"),
        SelectOption(label="Art",    value="role_art", emoji="🎨"),
    ],
    placeholder="Pick your interests…",
    min_values=1,
    max_values=2,
)

ActionRow

Discord zeigt Komponenten in Zeilen an. Verpacke eine oder mehrere Komponenten in einer ActionRow und übergib sie als components=[…]:

@plugin.on_slash_command("menu")
def menu_cmd(ctx: Context, event: dict):
    row = ActionRow(
        Button("Yes", custom_id="yes", style="success"),
        Button("No",  custom_id="no",  style="danger"),
    )
    ctx.interaction.respond(content="Pick one:", components=[row])

@plugin.on_component("yes")
def yes_clicked(ctx: Context, event: dict):
    ctx.interaction.respond(content="✅", ephemeral=True)

@plugin.on_component("no")
def no_clicked(ctx: Context, event: dict):
    ctx.interaction.respond(content="❌", ephemeral=True)

Dynamic custom_ids: prefix matching

@plugin.on_component("exact_id") matches the whole string. When your custom_id encodes state (page numbers, vote targets), register with prefix= instead and parse the rest yourself. Exactly one of custom_id= or prefix= must be given.

@plugin.on_component(prefix="page:")
def handle_page(ctx: Context, event: dict):
    cid = event["custom_id"]            # e.g. "page:next:5"
    _, direction, index = cid.split(":", 2)
    ...

Buttons posted by an older version of your plugin stay clickable on Discord forever. If you encode state in custom_id, embed a schema version too (e.g. "v2:vote:42") so stale buttons can be detected and politely refused.

Updating a message in place v0.7.0

Inside a component handler, pass update_message=True to respond() to edit the message the button or menu is attached to (game boards, pagination, live leaderboards) instead of sending a new reply:

@plugin.on_component(prefix="counter:")
def bump(ctx: Context, event: dict):
    count = int(event["custom_id"].split(":")[1]) + 1
    ctx.interaction.respond(
        content=f"Count: **{count}**",
        components=[ActionRow(Button("+1", custom_id=f"counter:{count}"))],
        update_message=True,
    )
  • Component interactions only. The platform rejects it for slash commands and modal submits.
  • The fields you pass replace the message's current content/embeds/components, so pass everything the updated message should contain.
  • ephemeral is ignored (the message keeps its original visibility). You can call it repeatedly within the 15-minute interaction window.

Modaldialoge

Modals erfassen Texteingaben vom Benutzer. Öffne eines mit ctx.interaction.send_modal und handle die Übermittlung mit @plugin.on_modal_submit:

Required manifest flag: Discord only accepts a modal as the first response, and the platform normally acknowledges slash commands for you before your handler runs. A command that opens a modal must opt out of that acknowledgement on its slash_commands entry:

{ "name": "feedback", "description": "Send feedback", "defer_on_dispatch": false }

With the flag set, call send_modal promptly (within Discord's 3-second window). Without it the modal is rejected with an API error.

from yourbot_sdk import TextInput

@plugin.on_slash_command("feedback")
def feedback_cmd(ctx: Context, event: dict):
    ctx.interaction.send_modal(
        title="Send Feedback",
        custom_id="feedback_form",
        fields=[
            TextInput(
                label="Subject",
                custom_id="subject",
                style="short",          # "short" | "paragraph"
                placeholder="Quick summary",
                required=True,
                max_length=100,
            ),
            TextInput(
                label="Details",
                custom_id="details",
                style="paragraph",
                required=False,
                max_length=2000,
            ),
        ],
    )

@plugin.on_modal_submit("feedback_form")
def feedback_submitted(ctx: Context, event: dict):
    values = event["modal_values"]   # {"subject": "...", "details": "..."}
    ctx.kv.set(f"feedback:{event['interaction_id']}", values)
    ctx.interaction.respond(content="Thanks for the feedback!", ephemeral=True)

Hintergrundaufgaben

How schedules reach production: declare each @plugin.cron task in your manifest's "cron" array (see below) and the platform fires it per installed server — UTC, a 5-minute floor, up to 5 entries. @plugin.schedule (fixed intervals) runs only in local yourbot dev; for interval-style work in production use a cron spec or the event-driven pattern on Pool mode.

@plugin.schedule(seconds) führt eine Koroutine in einem festen Intervall aus, beginnend nach Ablauf des ersten Intervalls. Das Mindestintervall beträgt 30 Sekunden.

@plugin.schedule(300)   # every 5 minutes
def heartbeat(ctx: Context):
    count = ctx.kv.increment("heartbeats")
    ctx.log(f"heartbeat #{count}")

Runs never overlap — the next interval is counted from when the previous run finishes, so a slow handler delays later runs rather than stacking them.

Cron-Zeitpläne

@plugin.cron(spec) führt eine Aufgabe nach einem 5-Feld-Cron-Zeitplan in UTC aus. Verwende dies, wenn eine Aufgabe zu einer bestimmten Uhrzeit ausgelöst werden soll (z. B. Montag 09:00) anstatt in einem festen Intervall seit dem Start.

Spec-Format: "minute hour day-of-month month day-of-week" — jedes Feld unterstützt *, */N, N, N,M,…, N-M, N-M/S. Wochentag ist 0=Sonntag bis 6=Samstag.

@plugin.cron("0 9 * * 1")        # every Monday at 09:00 UTC
def weekly_report(ctx: Context):
    ctx.log("Sending weekly report")

@plugin.cron("*/15 * * * *")     # every 15 minutes
def refresh_cache(ctx: Context):
    ctx.kv.increment("cache_refreshes")

@plugin.cron("30 0 1 * *")       # 00:30 UTC on the 1st of each month
def monthly_rollup(ctx: Context):
    ...

To run in production, each task also needs a manifest entry whose name matches the function (the platform reads schedules from the manifest, not your code):

"cron": [
  {"spec": "0 9 * * 1", "name": "weekly_report"},
  {"spec": "*/15 * * * *", "name": "refresh_cache"}
]
  • Specs firing more often than every 5 minutes are rejected at upload, and a manifest carries at most 5 cron entries. yourbot validate applies the same checks and warns when a decorated task has no matching manifest entry (it would never run in production).
  • Delivery rides the normal event pipeline per installed server: your handler gets a tenant-scoped ctx, delivery is at-least-once (rare replays possible — keep handlers idempotent or guard with ctx.ephemeral.dedup), and a missed tick is not replayed.
  • Ungültige Specs lösen bei der Registrierung einen ValueError aus — der Fehler ist beim Start sichtbar, nicht erst beim ersten verpassten Ausführungszeitpunkt.

SDK v0.8.4 · Zuletzt aktualisiert Juli 2026 · Zurück zum Entwicklerportal