Build, package, and deploy Storyden plugins using Go. Use when creating new Storyden plugins, implementing event handlers, requesting API access, or packaging plugins for distribution.
This skill helps you build, test, and deploy Storyden plugins using Go and the official Storyden Go SDK.
Use this skill when you need to:
Before starting, ensure you have:
sd CLI installed and authenticated (see storyden-cli skill)The sd CLI (sd plugin --help) is the primary tool for scaffolding, running, packaging, installing, and managing plugins. Always consult the installed CLI for authoritative flag and argument docs:
sd plugin --help
sd plugin dev --help
sd plugin dev run --help
This quick start builds a minimal plugin that reacts to new thread replies with a fire emoji.
Scaffold with the sd CLI (creates manifest.yaml pre-filled with sane defaults):
sd plugin dev new reactbot
cd reactbot
go mod init example.com/reactbot
Or start from a fresh, empty directory manually:
mkdir reactbot
cd reactbot
go mod init example.com/reactbot
Create manifest.yaml:
id: reactbot
name: React Bot
author: your-name
description: React to every new thread reply with a fire emoji.
version: 1.0.0
command: "./reactbot"
events_consumed:
- EventThreadReplyCreated
access:
handle: reactbot
name: React Bot
permissions:
- CREATE_REACTION
If your Storyden host is Windows, use command: "./reactbot.exe".
Create main.go:
package main
import (
"context"
"errors"
"fmt"
"log/slog"
"os"
"os/signal"
"time"
"github.com/Southclaws/storyden/app/transports/http/openapi"
"github.com/Southclaws/storyden/lib/plugin/rpc"
"github.com/Southclaws/storyden/sdk/go/storyden"
)
const (
fireEmoji = "\U0001F525"
apiCallTimeout = 10 * time.Second
)
func main() {
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
slog.SetDefault(logger)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
plugin, err := storyden.New(ctx)
if err != nil {
logger.Error("failed to create plugin", slog.String("error", err.Error()))
os.Exit(1)
}
defer func() {
if err := plugin.Shutdown(); err != nil && !errors.Is(err, context.Canceled) {
logger.Warn("plugin shutdown returned error", slog.String("error", err.Error()))
}
}()
// Register event handlers before starting the runtime loop.
plugin.OnThreadReplyCreated(func(ctx context.Context, event *rpc.EventThreadReplyCreated) error {
timeoutCtx, cancel := context.WithTimeout(ctx, apiCallTimeout)
defer cancel()
client, err := plugin.BuildAPIClient(timeoutCtx)
if err != nil {
return fmt.Errorf("build api client: %w", err)
}
postID := openapi.PostIDParam(event.ReplyID.String())
resp, err := client.PostReactAddWithResponse(timeoutCtx, postID, openapi.PostReactAddJSONRequestBody{
Emoji: fireEmoji,
})
if err != nil {
return fmt.Errorf("create reaction: %w", err)
}
if resp.StatusCode() != 200 {
return fmt.Errorf("create reaction returned status %d", resp.StatusCode())
}
return nil
})
// Run connects to Storyden RPC and starts handling incoming messages/events.
if err := plugin.Run(ctx); err != nil {
if errors.Is(err, context.Canceled) {
return
}
logger.Error("plugin stopped", slog.String("error", err.Error()))
os.Exit(1)
}
}
Run go mod tidy once after writing your code:
go mod tidy
sd plugin dev run handles external plugin registration and injects STORYDEN_RPC_URL automatically — no manual UI steps required:
sd plugin dev run
It reads manifest.yaml, registers or updates the plugin as an external plugin on the current instance, then executes the manifest's command with STORYDEN_RPC_URL set.
To override the manifest command (e.g. run with go run . during development):
sd plugin dev run -- go run .
Useful flags:
--manifest/-m <path> — path to manifest YAML (default: manifest.yaml)--no-update — skip updating the existing external plugin manifest before running--instance-id <id> — target a specific existing plugin installationValidate the manifest and package structure before distributing:
sd plugin dev validate
Install directly to the connected Storyden instance (builds the package and uploads it in one step):
sd plugin dev install
If a supervised plugin with the same manifest id already exists it is updated in place; otherwise a new one is created. Use --dir if your project is not in the current directory.
To produce a portable zip for manual distribution instead:
sd plugin dev package
# or specify an output path:
sd plugin dev package --output reactbot.zip
The zip contains manifest.json (converted from manifest.yaml) and the binary. If you need to cross-compile first:
export CGO_ENABLED=0
export GOOS=linux
export GOARCH=amd64
go build -o reactbot main.go
sd plugin dev package --output reactbot.zip
If target host is Windows, build reactbot.exe and set manifest command to ./reactbot.exe.
All plugin management commands use a <plugin-instance-id> which is the UUID assigned when the plugin is registered. Use sd plugin list or sd plugin get to find it.
# List all plugins on the current instance
sd plugin list
sd plugin list --format json
sd plugin list --wide # includes version and description columns
# Inspect a specific plugin
sd plugin get <plugin-instance-id>
# Start / stop supervised plugins
sd plugin activate <plugin-instance-id>
sd plugin deactivate <plugin-instance-id>
# Stream live logs from a supervised plugin
sd plugin logs <plugin-instance-id>
# Delete a plugin
sd plugin delete <plugin-instance-id>
# Rotate the static RPC token for an external plugin
sd plugin token rotate <plugin-instance-id>
Always run
sd plugin --helporsd plugin <subcommand> --helpto get the version-matched docs for your installed CLI.
Event handler methods are generated on *storyden.Plugin (for example OnThreadReplyCreated, OnNodeCreated, OnReportUpdated).
Canonical references:
Plugins can define runtime configuration that admins edit in the UI:
configuration_schema:
fields:
- id: webhook_url
label: Webhook URL
description: Where to send event notifications
type: string
- id: enabled
label: Enable notifications
description: Toggle notification sending
type: boolean
- id: retry_count
label: Retry attempts
description: Number of retries for failed requests
type: number
access.permissionsevents_consumed that your SDK handler code does not implementmanifest.json in supervised plugin archivesplugin_id (external mode uses token-based URL)Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer