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.

openclaw.plugin.json57 lines
{
  "id": "pinclaw",
  "channels": ["pinclaw"],
  "name": "Nexting",
  "description": "Hardware voice interface channel for OpenClaw",
  "channelConfigs": {
    "pinclaw": {
      "schema": {
FieldTypeDescription
idstringUnique plugin identifier. Must match the exported plugin object's id.
channelsstring[]Channel IDs this plugin provides. Each ID becomes a key under channels.* in openclaw.json.
configSchemaobjectJSON 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:

NextingAccountConfig (from types.ts)
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
  };
}
FieldTypeDefaultDescription
enabledbooleantrueWhether this account is active. Set to false to disable without removing config.
wsPortnumber18790Port on which the plugin's WebSocket + HTTP server listens.
authTokenstring(none)Bearer token for device authentication. Account is not considered configured without it.
relayobject(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:

openclaw.json — single account
{
  "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:

openclaw.json — multi-account
{
  "channels": {
    "pinclaw": {
      "accounts": {
        "device-1": { "wsPort": 18790, "authToken": "token-1" },
        "device-2": { "wsPort": 18791, "authToken": "token-2" }
      }
    }
  }
}
When the 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.

MethodSignatureDescription
listAccountIds(cfg) => string[]List all configured account IDs. Reads from accounts keys or returns ["default"].
resolveAccount(cfg, accountId?) => ResolvedPinclawAccountResolve full account config with defaults applied.
defaultAccountId() => stringReturns "default". Used when no explicit account is specified.
isConfigured(account) => booleanTrue if authToken is set and non-empty.
isEnabled(account) => booleanTrue if enabled !== false.
describeAccount(account) => objectReturns { 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.

PropertyValue
deliveryMode"direct" — the plugin delivers straight to the device over its own WebSocket server.
textChunkLimit4096 — 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.

sendText return shape
{ channel: "pinclaw", messageId: string }

Status Adapter

Tracks per-account runtime state. The Gateway queries this to display plugin health in dashboards.

Account status snapshot
{
  accountId: string,
  enabled: boolean,
  configured: boolean,
  running: boolean,
  lastError: string | null
}

Security Adapter

Controls which entities can send messages through the plugin channel.

PropertyValue
DM Policy"open" — any authenticated device can send messages.
allowFrom["*"] — no sender restrictions beyond token authentication.
All devices with valid tokens can send messages. Token-based authentication provides the primary access control. The 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:

StepAction
1Creates NextingWsServer with port, authToken, gateway URL, and gateway token.
2Starts the HTTP + WebSocket server.
3Connects to the Gateway via WebSocket RPC (Ed25519 authenticated).
4Sets account status to running = true.
5Blocks until the abort signal fires (keeps the gateway alive).
6On abort: stops the server, clears status to running = false.

5. Outbound Delivery

The full message routing flow from AI agent response to device delivery:

StepDescription
1Agent produces a text response.
2Gateway calls outbound.sendText().
3Plugin checks if the target device has an active WebSocket connection.
4If connected: send an agent_message via WebSocket.
5If offline: the plugin drops the message (returns ok:false).
6When 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.

generate_image
{
  "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.

generate_audio
{
  "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.

memory_edit
{
  "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." }
  }
}
Hardware interaction goes the other way: the phone registers device tools via the 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 typeDirectionDescription
textClient → PluginUser input (typically STT output) forwarded to the AI session.
device_tools_registerClient → PluginPhone registers the device tools available on the hardware.
context_updateClient → PluginClient context update (e.g. device state, locale).

8. Permission Model

AspectPolicy
Token AuthenticationEach 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 LimitingNo built-in rate limiting. Rely on AI provider limits for cost control.
WebSocket Auth Timeout10 seconds to authenticate after connection. Server closes with code 4001 if exceeded.
Connection ReplacementA 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

MethodPathDescription
GET/cron/jobsList all cron jobs.
POST/cron/jobsCreate a new cron job.
DELETE/cron/jobs/:idDelete a cron job by ID.
POST/cron/jobs/:id/toggleEnable or disable a cron job.

Create Job Request

POST /cron/jobs
{
  "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

StepDescription
1Cron job fires in the gateway at the scheduled time.
2The announce pipeline routes the result to the pinclaw channel.
3Gateway calls outbound.sendText(), which routes to sendToDevice().
4If device is online: delivery via WebSocket.
5If 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:

PropertyTypeDescription
ctx.accountResolvedPinclawAccountFully resolved account config with defaults.
ctx.cfgobjectFull OpenClaw configuration (openclaw.json).
ctx.logLoggerScoped logger with info, warn, error methods.
ctx.abortSignalAbortSignalFired when the Gateway requests shutdown.
ctx.setStatusfunctionUpdates the runtime status for this account.

Startup Flow

startAccount execution 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 = false

Gateway 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.

The plugin operates as a long-running process within the Gateway. It does not have a separate 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.