Protocol
Plugin Manifest
Nexting Developer Documentation | August 2026
1. Introduction
The OpenClaw Plugin system allows hardware devices to integrate with the OpenClaw AI platform as channel plugins. Each plugin declares the channels it provides and the configuration it requires via a manifest file.
Nexting is the reference implementation of a hardware channel plugin — a wearable input device that connects via BLE through an iPhone companion app to the OpenClaw Gateway and Agent remote-control path.
Version 0.2.0, July 2026. Plugins run as part of the OpenClaw Gateway process.
2. Manifest Format
Every plugin MUST include an openclaw.plugin.json file at its package root. This manifest declares the plugin identity, the channels it provides, and the JSON Schema for its configuration.
{
"id": "pinclaw",
"channels": ["pinclaw"],
"name": "Nexting",
"description": "Hardware voice interface channel for OpenClaw",
"channelConfigs": {
"pinclaw": {
"schema": {| Field | Type | Description |
|---|---|---|
id | string | Unique plugin identifier. Must match the exported plugin object's id. |
channels | string[] | Channel IDs this plugin provides. Each ID becomes a key under channels.* in openclaw.json. |
configSchema | object | JSON Schema describing the plugin's configuration shape. Used for validation at startup. |
3. Configuration Schema
The plugin reads its configuration from the channels.pinclaw section of openclaw.json. The TypeScript interface defines the available fields:
interface NextingAccountConfig {
enabled?: boolean; // default: true
wsPort?: number; // default: 18790
authToken?: string; // required for operation
relay?: { // Nexting Cloud relay connection
enabled: boolean;
token: string;
url?: string; // default: wss://api.nexting.ai
};
}| Field | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Whether this account is active. Set to false to disable without removing config. |
wsPort | number | 18790 | Port on which the plugin's WebSocket + HTTP server listens. |
authToken | string | (none) | Bearer token for device authentication. Account is not considered configured without it. |
relay | object | (none) | Nexting Cloud relay connection: { enabled, token, url? } — url defaults to wss://api.nexting.ai. |
Single Account Configuration
The simplest configuration uses a single default account. All fields are placed directly under channels.pinclaw:
{
"channels": {
"pinclaw": {
"enabled": true,
"wsPort": 18790,
"authToken": "your-secret-token"
}
}
}Multi-Account Configuration
To connect multiple devices, use the accounts object. Each key is a unique account ID, and each value is a NextingAccountConfig:
{
"channels": {
"pinclaw": {
"accounts": {
"device-1": { "wsPort": 18790, "authToken": "token-1" },
"device-2": { "wsPort": 18791, "authToken": "token-2" }
}
}
}
}accounts key is present, the plugin iterates its keys to discover account IDs. When absent, the plugin treats the top-level section as a single account with ID "default".4. Adapters
The plugin exposes five adapter interfaces that the Gateway uses to manage the plugin lifecycle, route messages, and query status.
Config Adapter
Responsible for discovering accounts, resolving their full configuration, and reporting their readiness.
| Method | Signature | Description |
|---|---|---|
listAccountIds | (cfg) => string[] | List all configured account IDs. Reads from accounts keys or returns ["default"]. |
resolveAccount | (cfg, accountId?) => ResolvedPinclawAccount | Resolve full account config with defaults applied. |
defaultAccountId | () => string | Returns "default". Used when no explicit account is specified. |
isConfigured | (account) => boolean | True if authToken is set and non-empty. |
isEnabled | (account) => boolean | True if enabled !== false. |
describeAccount | (account) => object | Returns { accountId, enabled, configured } summary. |
Outbound Adapter
Handles agent-to-device message delivery. The Gateway calls sendText when the AI produces a response that needs to reach a hardware device.
| Property | Value |
|---|---|
deliveryMode | "direct" — the plugin delivers straight to the device over its own WebSocket server. |
textChunkLimit | 4096 — maximum text length per delivery. |
The sendText method routes to NextingWsServer.sendToDevice(). If the server instance is unavailable (e.g., during startup), the message is dropped (an error is logged) and sendText returns without throwing.
{ channel: "pinclaw", messageId: string }Status Adapter
Tracks per-account runtime state. The Gateway queries this to display plugin health in dashboards.
{
accountId: string,
enabled: boolean,
configured: boolean,
running: boolean,
lastError: string | null
}Security Adapter
Controls which entities can send messages through the plugin channel.
| Property | Value |
|---|---|
DM Policy | "open" — any authenticated device can send messages. |
allowFrom | ["*"] — no sender restrictions beyond token authentication. |
allowFromPath resolves to channels.pinclaw.dm. in the config tree.Gateway Adapter
The startAccount method is the main lifecycle entry point. It runs as a long-lived process within the Gateway:
| Step | Action |
|---|---|
| 1 | Creates NextingWsServer with port, authToken, gateway URL, and gateway token. |
| 2 | Starts the HTTP + WebSocket server. |
| 3 | Connects to the Gateway via WebSocket RPC (Ed25519 authenticated). |
| 4 | Sets account status to running = true. |
| 5 | Blocks until the abort signal fires (keeps the gateway alive). |
| 6 | On abort: stops the server, clears status to running = false. |
5. Outbound Delivery
The full message routing flow from AI agent response to device delivery:
| Step | Description |
|---|---|
| 1 | Agent produces a text response. |
| 2 | Gateway calls outbound.sendText(). |
| 3 | Plugin checks if the target device has an active WebSocket connection. |
| 4 | If connected: send an agent_message via WebSocket. |
| 5 | If offline: the plugin drops the message (returns ok:false). |
| 6 | When the plugin itself is offline, the Nexting Cloud relay queues outbound messages in Postgres with a 24-hour TTL and drains them on reconnect. |
6. Tool Definitions
The plugin ships three server tools, auto-discovered from plugin/src/tools/ and injected into TOOLS.md as <server_tool> prompt tools.
generate_image
Generate an image from a text prompt using AI.
{
"name": "generate_image",
"description": "Generate an image from a text prompt using AI. Returns the image URL that can be displayed to the user via markdown.",
"parameters": {
"prompt": { "type": "string", "required": true, "description": "Detailed English description of the image to generate" },
"size": { "type": "string", "required": false, "description": "\"1024x1024\" (default), \"1792x1024\" (landscape), or \"1024x1792\" (portrait)" }
}
}generate_audio
Generate speech audio from text using OpenAI TTS.
{
"name": "generate_audio",
"description": "Generate speech audio from text using OpenAI TTS. Returns a URL to the audio file.",
"parameters": {
"text": { "type": "string", "required": true, "description": "The text to convert to speech" },
"voice": { "type": "string", "required": false, "description": "\"alloy\" (default), \"echo\", \"fable\", \"onyx\", \"nova\", or \"shimmer\"" },
"speed": { "type": "number", "required": false, "description": "Speed from 0.25 to 4.0 (default 1.0)" }
}
}memory_edit
Read, write, or list memory files.
{
"name": "memory_edit",
"description": "Read, write, or list memory files (~/clawd/memory/*.md).",
"parameters": {
"action": { "type": "string", "required": true, "description": "\"list\", \"read\", \"write\", or \"append\"" },
"filename": { "type": "string", "required": false, "description": "File name, e.g. \"2026-03-11.md\". Required for read/write/append." },
"content": { "type": "string", "required": false, "description": "Content to write or append. Required for write/append." }
}
}device_tools_register WebSocket message, and the agent invokes them through <device_tool> tags. Scheduling goes through the /cron/jobs REST API or the cron.* gateway RPC — there is no schedule tool.7. Inbound Events
The plugin receives inbound events from the client over its WebSocket connection. The main inbound message types are:
| Message type | Direction | Description |
|---|---|---|
text | Client → Plugin | User input (typically STT output) forwarded to the AI session. |
device_tools_register | Client → Plugin | Phone registers the device tools available on the hardware. |
context_update | Client → Plugin | Client context update (e.g. device state, locale). |
8. Permission Model
| Aspect | Policy |
|---|---|
| Token Authentication | Each device account requires an authToken. Connections and HTTP requests without a valid token are rejected. |
| DM Policy | "open" — any authenticated device can send direct messages. |
| Rate Limiting | No built-in rate limiting. Rely on AI provider limits for cost control. |
| WebSocket Auth Timeout | 10 seconds to authenticate after connection. Server closes with code 4001 if exceeded. |
| Connection Replacement | A new connection from the same deviceId replaces the old one. The previous WebSocket is closed with code 1000. |
9. Cron Integration
Cron jobs deliver scheduled messages to devices through the Gateway's cron system. The plugin server exposes REST endpoints whose cron-proxy forwards a cron.add RPC (announce: true, with an optional [DELIVERY:NOTIFY|SILENT|RESULT] tag prefix) to the gateway.
Cron Endpoints
| Method | Path | Description |
|---|---|---|
GET | /cron/jobs | List all cron jobs. |
POST | /cron/jobs | Create a new cron job. |
DELETE | /cron/jobs/:id | Delete a cron job by ID. |
POST | /cron/jobs/:id/toggle | Enable or disable a cron job. |
Create Job Request
{
"message": "Time to take a break!",
"at": "30m",
"channel": "pinclaw",
"deleteAfterRun": false
}When channel is "pinclaw", the cron-proxy forwards a cron.add RPC to the gateway with announce: true (optionally prefixed with a [DELIVERY:NOTIFY|SILENT|RESULT] tag). The job fires in the gateway, not in the plugin.
Delivery Flow
| Step | Description |
|---|---|
| 1 | Cron job fires in the gateway at the scheduled time. |
| 2 | The announce pipeline routes the result to the pinclaw channel. |
| 3 | Gateway calls outbound.sendText(), which routes to sendToDevice(). |
| 4 | If device is online: delivery via WebSocket. |
| 5 | If device is offline: the message is dropped. If the plugin itself is offline, the Nexting Cloud relay queues it in Postgres (24-hour TTL) and drains on reconnect. |
10. Lifecycle Hooks
startAccount(ctx)
The primary lifecycle hook. The Gateway calls this method for each enabled and configured account. The context object provides:
| Property | Type | Description |
|---|---|---|
ctx.account | ResolvedPinclawAccount | Fully resolved account config with defaults. |
ctx.cfg | object | Full OpenClaw configuration (openclaw.json). |
ctx.log | Logger | Scoped logger with info, warn, error methods. |
ctx.abortSignal | AbortSignal | Fired when the Gateway requests shutdown. |
ctx.setStatus | function | Updates the runtime status for this account. |
Startup Flow
1. Read gateway port from ctx.cfg.gateway.port (default: 18789)
2. Read gateway token from ctx.cfg.gateway.auth.token
3. Create NextingWsServer instance
4. Call server.start()
5. Connect Gateway WebSocket RPC (Ed25519 auth)
6. Set status: running = true
7. Await abort signal (blocks — keeps gateway alive)
8. On abort: server.stop(), running = falseGateway Auto-Reconnect
If the Gateway WebSocket disconnects, the plugin automatically reconnects after a 3-second delay. This ensures the RPC channel recovers from transient network issues without manual intervention.
stop() hook — shutdown is handled entirely via the AbortSignal. When the signal fires, the server is stopped, the WebSocket connections are closed, and the status is set to running = false.