Plugins can expose commands for runtime interaction via the ze API.
Commands are declared in the Registration struct passed to Run. The engine learns about them during Stage 1 (declare-registration).
err := p.Run(ctx, sdk.Registration{
Commands: []sdk.CommandDecl{
{Name: "my-plugin status", Description: "Show current status"},
{Name: "my-plugin check", Description: "Trigger immediate check", Args: []string{"target"}},
},
})Register a handler with OnExecuteCommand before calling Run. The handler receives the command serial, command name, arguments, and peer selector.
p.OnExecuteCommand(func(serial, command string, args []string, peer string) (status, data string, err error) {
switch command {
case "my-plugin status":
return "done", `{"status":"running","uptime":3600}`, nil
case "my-plugin check":
if len(args) < 1 {
return "error", "usage: my-plugin check <target>", nil
}
result := performCheck(args[0])
return "done", result, nil
default:
return "error", "unknown command: " + command, nil
}
})Commands are delivered to the plugin as execute-command RPCs over the MuxConn connection. The wire format uses #<id> <verb> [<json>] framing.
#17 ze-plugin-callback:execute-command {"serial":"abc123","command":"my-plugin status","args":[],"peer":""}
#17 ok {"status":"done","data":"{\"status\":\"running\"}"}
#17 error {"message":"execute-command not supported"}
The OnExecuteCommand handler returns three values: (status, data string, err error).
return "done", `{"count":42,"items":["a","b"]}`, nilThe SDK wraps this into an ExecuteCommandOutput and sends:
#17 ok {"status":"done","data":"{\"count\":42,\"items\":[\"a\",\"b\"]}"}
return "done", "", nilResponse:
#17 ok {"status":"done"}
If the handler returns a non-nil error, the SDK sends an error response:
return "", "", fmt.Errorf("operation failed: database timeout")Response:
#17 error {"message":"operation failed: database timeout"}
The engine sends these fields in the execute-command RPC:
| Field | Type | Purpose |
|---|---|---|
serial |
string | Correlation ID for the request |
command |
string | Command name (e.g., "my-plugin status") |
args |
[]string | Additional arguments (may be empty) |
peer |
string | Peer selector (may be empty) |
| Pattern | Example | Purpose |
|---|---|---|
<plugin> status |
acme-monitor status |
Get current state |
<plugin> stats |
acme-monitor stats |
Get metrics |
<plugin> <action> |
acme-monitor check |
Perform action |
<plugin> list |
acme-monitor list |
List items |
Rules:
- Start with plugin name to avoid conflicts
- Use kebab-case for multi-word commands
- Keep commands short and memorable
Arguments arrive in the args parameter of the handler:
p.OnExecuteCommand(func(serial, command string, args []string, peer string) (string, string, error) {
if command == "my-plugin get" {
if len(args) < 1 {
return "error", "usage: my-plugin get <key>", nil
}
key := args[0]
value := getValue(key)
return "done", value, nil
}
return "error", "unknown command", nil
})Invocation:
ze bgp run "my-plugin get config.timeout"
Commands are declared with these fields:
| Field | Type | Required | Purpose |
|---|---|---|---|
Name |
string | Yes | Command name (e.g., "my-plugin status") |
Description |
string | No | Human-readable description |
Args |
[]string | No | Expected argument names (for help/completion) |
Completable |
bool | No | Whether the command supports tab completion |
Return structured JSON data for API consumers:
p.OnExecuteCommand(func(serial, command string, args []string, peer string) (string, string, error) {
if command == "monitor metrics" {
data, _ := json.Marshal(struct {
Checks int `json:"checks"`
Failures int `json:"failures"`
LatencyMs float64 `json:"latency-ms"`
LastCheck string `json:"last-check"`
}{
Checks: state.checks,
Failures: state.failures,
LatencyMs: state.latency,
LastCheck: state.lastCheck.Format(time.RFC3339),
})
return "done", string(data), nil
}
return "error", "unknown command", nil
})Note: JSON keys use kebab-case per ze conventions ("latency-ms", not "latency_ms").
Plugins can invoke commands on other plugins through the engine's command dispatcher:
p.OnStarted(func(ctx context.Context) error {
status, data, err := p.DispatchCommand(ctx, "rib show-in ipv4/unicast")
if err != nil {
return err
}
fmt.Printf("rib response: status=%s data=%s\n", status, data)
return nil
})The engine routes the command by longest-match registry lookup and returns the full {status, data} response from the target handler.
Provide usage information via a dedicated command:
// In Registration:
sdk.CommandDecl{Name: "my-plugin help", Description: "Show available commands"},
// In handler:
if command == "my-plugin help" {
data, _ := json.Marshal(map[string]any{
"commands": []string{
"my-plugin status - Show current status",
"my-plugin check <target> - Trigger immediate check",
"my-plugin metrics - Show performance metrics",
},
})
return "done", string(data), nil
}