Adding Commands
Plugins can expose commands for runtime interaction via the ze API.
Declaring Commands
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"}},
},
})
Handling Commands
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 string, data any, 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
}
})
Wire Format
Commands are delivered to the plugin as execute-command RPCs over the MuxConn connection. The wire format uses #<id> <verb> [<json>] framing.
Request (engine to plugin)
#17 ze-plugin-callback:execute-command {"serial":"abc123","command":"my-plugin status","args":[],"peer":""}
Success Response (plugin to engine)
#17 ok {"status":"done","data":"{\"status\":\"running\"}"}
Error Response (plugin to engine)
#17 error {"message":"execute-command not supported"}
Return Values
The OnExecuteCommand handler returns three values: (status string, data any, err error).
Success with Data
return "done", `{"count":42,"items":["a","b"]}`, nil
The SDK wraps this into an ExecuteCommandOutput and sends:
#17 ok {"status":"done","data":"{\"count\":42,\"items\":[\"a\",\"b\"]}"}
Success without Data
return "done", "", nil
Response:
#17 ok {"status":"done"}
Handler Error
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"}
ExecuteCommandInput Fields
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) |
Naming Conventions
| 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
Command Arguments
Arguments arrive in the args parameter of the handler:
p.OnExecuteCommand(func(serial, command string, args []string, peer string) (string, any, 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"
CommandDecl Fields
Commands are declared with these fields:
| Field | Type | Required | Purpose |
|---|---|---|---|
Name |
string | ✓ | Command name (e.g., "my-plugin status") |
Description |
string | ✕ | Human-readable description |
Args |
[]string | ✕ | Expected argument names (for help/completion) |
Completable |
bool | ✕ | Whether the command supports tab completion |
Complex Responses
Return structured JSON data for API consumers:
p.OnExecuteCommand(func(serial, command string, args []string, peer string) (string, any, 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").
Dispatching Commands to Other Plugins
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.
Help Text
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
}