Actions
Server functions your plugin UI can call.
An action is a function that runs on the server and is called from your plugin's components. Use one whenever the browser should not be trusted with the work: API keys, database reads, anything privileged. Unlike a command, an action is not typed into chat and produces no message.
Register an Action
// src/server/index.ts
import { Permission, type PluginContext } from "@sharkord/plugin-sdk";
import type { TPlugin } from "../types";
const onLoad = (ctx: PluginContext<TPlugin>) => {
ctx.actions.register({
name: "sum",
description: "Adds two numbers.",
requires: Permission.SEND_MESSAGES,
executes: async (invoker, payload) => payload.a + payload.b,
});
};With a contract passed to PluginContext, name has to be one of its actions keys, and the payload and return type of executes follow from it.
requires works exactly as it does for commands: a default an admin can override, not a guarantee. Enforce anything that matters with ctx.permissions.userCan inside executes.
A plugin can register at most 100 actions.
Call It From the Client
// src/client/sum-button.tsx
import { createCallAction } from "@sharkord/plugin-sdk/client";
import { memo, useCallback } from "react";
import type { TPlugin } from "../types";
const callAction = createCallAction<TPlugin>();
const SumButton = memo(() => {
const onClick = useCallback(async () => {
const result = await callAction("sum", { a: 1, b: 2 });
console.log(result);
}, []);
return <button onClick={onClick}>Calculate</button>;
});
export { SumButton };createCallAction<TPlugin>() is called once at module scope, not inside a component. It works out which plugin it belongs to from the URL its own bundle was served from, so it only runs in plugin client code Sharkord loaded.
Rules of the Road
- The caller needs the
USE_PLUGINSpermission, plus whatever the action's access rule resolves to. - Handlers time out after 30 seconds.
- Calls are rate limited (60 per minute per user by default) and written to the activity log with their payload.
- The
invokerargument is the same one commands get, withsourcealwaysapiand noparentMessageIdormessageId, since nothing was typed into a channel. ItschannelIdis whichever channel the caller had open, and the server checks they can see it before your handler runs. - Nothing validates the payload. An action declares no argument shape, so unlike command arguments it arrives exactly as the caller sent it. The contract is compile-time typing only.
- The invoker is built by the server, so it is the one thing in the call you can trust: validate the payload yourself.
See Client SDK for the rest of what your UI code can reach, and Data for what an action can read and write once it is running.