/docs / plugins

Plugins Overview

A Plugin is a managed native binary that skills depend on. Instead of bundling a heavy binary inside every skill that needs it, publish the binary once as a plugin and let skills declare it as a dependency. Nebo downloads the plugin automatically when a skill that needs it is installed.

When to use a plugin vs. embedded binary

PatternUse whenExample
PluginMultiple skills share the same binary, or the binary is large (>5 MB)gws, ffmpeg
Embedded binaryOne skill bundles its own small binaryCustom tool specific to a single skill

You can use both patterns. A skill can embed a binary and declare plugin dependencies. The embedded binary takes precedence for the skill's own execution; plugin binaries are available to scripts via environment variables.

What a plugin can do

A plugin is a single native binary that can contribute multiple capabilities simultaneously. What makes a plugin a "connector" vs a "provider" vs a "tool plugin" is determined by which capabilities it declares in its manifest. A single plugin can declare all of them.

CapabilityManifest fieldWhat it doesRuntime
Toolscapabilities.tools[]Registers typed, schema-validated tools the agent can callRouted through STRAP PluginTool
Hookscapabilities.hooks[]Intercepts lifecycle events (tool execution, messages, memory, prompts)HookDispatcher with circuit breaker
Commandscapabilities.commands[]Registers /slash commands that bypass the LLM and execute directlyChat dispatch, 30s timeout
Routescapabilities.routes[]Handles HTTP endpoints (OAuth callbacks, webhooks)Proxied through catch-all handler
Providerscapabilities.providers[]Registers as an AI model provider (LLM, speech, image)PluginProvider with NDJSON streaming
Configcapabilities.configSchema[]Declares user-configurable settings rendered as a form in the UIValues injected as env vars
Eventsevents[]Produces events via long-running watch processes (NDJSON on stdout)Auto-emitted into EventBus
AuthauthDeclares an authentication flow (OAuth, API keys) with login/status/logoutHTTP endpoints + WebSocket events
PermissionspermissionsDeclares env var access, network needs, and max execution timeoutEnforced at exec time

See Plugin Capabilities for the full reference on each capability type.

How it works

The install flow is fully automatic. When a user installs a skill that needs a plugin, Nebo resolves and downloads the binary without any manual steps.

  • Publisher uploads a native binary to NeboAI for each platform
  • Publisher creates a skill with plugins: in SKILL.md frontmatter (or another plugin with dependencies: in plugin.json)
  • User installs the skill or plugin (via marketplace or install code)
  • Nebo detects the plugin dependency and downloads the binary silently
  • If the plugin declares its own dependencies[], those are installed recursively
  • Binary is stored locally at <data_dir>/nebo/plugins/<slug>/<version>/ (the Nebo data directory is platform-native — ~/Library/Application Support/Nebo/ on macOS, %APPDATA%\Nebo\ on Windows, ~/.local/share/nebo/ on Linux; NEBO_HOME overrides)
  • Plugin runtime data is stored at <data_dir>/appdata/plugins/<slug>/ -- physically separate from the binary, survives upgrades
  • Skill scripts access the binary via $${plugin.SLUG_BIN} template variable
  • If the plugin declares capabilities.tools[], typed tools are registered for the agent
User installs skill
  -> SKILL.md declares plugins: [{ name: "gws", version: ">=1.2.0" }]
  -> Nebo downloads gws binary for current platform
  -> Skill script runs with GWS_BIN=/path/to/gws

Declaring plugin dependencies

Add a plugins field to your SKILL.md frontmatter:

---
name: google-workspace
description: Manage Google Workspace -- Gmail, Calendar, Drive
plugins:
  - name: gws
    version: ">=1.2.0"
---

Dependency fields

FieldTypeDefaultDescription
namestringrequiredPlugin slug (must match the plugin's registered slug in NeboAI)
versionstring"*"Semver version range
optionalboolfalseIf true, the skill loads even if this plugin isn't installed

Version ranges

Version ranges follow semver conventions:

RangeMeaning
"*"Any version
">=1.2.0"1.2.0 or higher
"^1.0.0"Compatible with 1.x.x (>=1.0.0, <2.0.0)
"~1.2.0"Patch updates only (>=1.2.0, <1.3.0)
"=1.2.0"Exact version

Multiple dependencies

---
name: media-processor
description: Process and convert media files
plugins:
  - name: ffmpeg
    version: ">=5.0.0"
  - name: imagemagick
    version: ">=7.0.0"
    optional: true
---

The skill loads only if all required plugins resolve. Optional plugins are silently skipped if missing.

Alternative: requires block

Plugin manifests can also declare dependencies using a requires block, which supports the same fields:

requires:
  plugins:
    - name: gws
      version: ">=1.2.0"
      optional: false

Using plugin binaries in scripts

Plugin binaries are accessible in two ways depending on context.

In SKILL.md body (template variables)

Use $${plugin.SLUG_BIN} syntax. Nebo expands these at skill activation time. The slug is uppercased with hyphens replaced by underscores.

Plugin slugTemplate variableExpands to
gws$${plugin.GWS_BIN}<data_dir>/nebo/plugins/gws/1.2.3/gws
ffmpeg$${plugin.FFMPEG_BIN}<data_dir>/nebo/plugins/ffmpeg/2.0.0/ffmpeg
my-tool$${plugin.MY_TOOL_BIN}<data_dir>/nebo/plugins/my-tool/1.0.0/my-tool

In script files (environment variables)

When a script runs, plugin binaries are injected as environment variables using the {SLUG}_BIN naming convention:

Plugin slugEnvironment variable
gwsGWS_BIN
ffmpegFFMPEG_BIN
my-toolMY_TOOL_BIN

Additionally, NEBO_PLUGIN_DATA is set to <data_dir>/appdata/plugins/<slug>/ -- the persistent data directory for this plugin. Use this for caches, databases, and any state that should survive plugin upgrades.

Python example

#!/usr/bin/env python3
import os
import subprocess

gws_bin = os.environ["GWS_BIN"]
result = subprocess.run([gws_bin, "gmail", "list", "--limit", "10"], capture_output=True, text=True)
print(result.stdout)

TypeScript example

import { execSync } from "child_process";

const gwsBin = process.env.GWS_BIN!;
const output = execSync(`$${gwsBin} gmail list --limit 10`, { encoding: "utf-8" });
console.log(output);

Shell example

#!/bin/bash
$GWS_BIN gmail list --limit 10

The plugin.json manifest

Every plugin has a plugin.json manifest that describes the binary, its platforms, and optional capabilities like authentication and events. This file is stored alongside the binary on disk and cached in memory by the PluginStore.

{
  "id": "gws",
  "slug": "gws",
  "name": "Google Workspace CLI",
  "version": "1.2.3",
  "description": "Google Workspace integration for email, calendar, and drive",
  "author": "NeboAI Inc.",
  "platforms": {
    "darwin-arm64": {
      "binaryName": "gws",
      "sha256": "a1b2c3...",
      "signature": "base64...",
      "size": 45678900,
      "downloadUrl": "https://cdn.neboai.com/plugins/gws/1.2.3/darwin-arm64/gws"
    },
    "linux-amd64": {
      "binaryName": "gws",
      "sha256": "d4e5f6...",
      "signature": "base64...",
      "size": 42000000,
      "downloadUrl": "https://cdn.neboai.com/plugins/gws/1.2.3/linux-amd64/gws"
    }
  },
  "signingKeyId": "key-001",
  "envVar": "",
  "auth": { ... },
  "events": [ ... ],
  "dependencies": [ ... ],
  "capabilities": { ... }
}

Manifest fields

FieldTypeRequiredDescription
idstringYesUnique identifier -- use the plugin's slug. Deserialization fails without this field.
slugstringYesURL-safe slug. Must match what skills reference in plugins[].name
namestringYesHuman-readable display name
versionstringYesSemver version string
descriptionstringNoBrief description
authorstringNoPublisher name
platformsobjectYesMap of platform key to PlatformBinary
signingKeyIdstringNoED25519 signing key ID
envVarstringNoCustom env var name override. If empty, defaults to {SLUG}_BIN
authobjectNoAuthentication configuration
eventsarrayNoEvent declarations
dependenciesarrayNoPlugin-to-plugin dependencies
capabilitiesobjectNoStructured capability declarations
The id field is required for all plugins. Without it, PluginManifest deserialization fails and the plugin cannot be resolved. Use the slug as the id (e.g., "id": "gws").

PlatformBinary

Each entry in the platforms map describes the binary for one platform:

FieldTypeDescription
binaryNamestringFilename of the binary (e.g., "gws" or "gws.exe")
sha256stringSHA256 hex hash for integrity verification
signaturestringED25519 signature (base64)
sizenumberFile size in bytes
downloadUrlstringCDN URL or API path to download the binary

Authentication

Plugins that require user credentials (e.g., Google OAuth, API keys) can declare an auth block. Nebo provides HTTP endpoints and WebSocket events to drive the auth flow from the frontend.

{
  "auth": {
    "type": "oauth_cli",
    "label": "Google Account",
    "description": "Authenticate with your Google Workspace account.",
    "commands": {
      "login": "auth login",
      "status": "auth status",
      "logout": "auth logout"
    },
    "env": {
      "GOOGLE_CLIENT_ID": "...",
      "GOOGLE_CLIENT_SECRET": "..."
    }
  }
}

Auth fields

FieldTypeRequiredDescription
typestringYesAuth type identifier (e.g., "oauth_cli")
labelstringYesButton label in UI (e.g., "Google Account")
descriptionstringNoDescription shown to user during auth step
commands.loginstringYesCLI args appended to binary for login
commands.statusstringNoCLI args for status check. Exit code 0 = authenticated
commands.logoutstringNoCLI args to clear credentials
envobjectNoEnvironment variables injected when running auth commands

Auth HTTP endpoints

EndpointMethodDescription
/api/v1/pluginsGETReturns hasAuth and authLabel per plugin
/api/v1/plugins/{slug}/auth/statusGETRuns status command. Returns { "authenticated": bool }
/api/v1/plugins/{slug}/auth/loginPOSTSpawns login command in background. Returns { "started": true }
/api/v1/plugins/{slug}/auth/logoutPOSTRuns logout command synchronously

Auth WebSocket events

EventPayloadWhen
plugin_auth_started{ plugin, label }Login command spawned
plugin_auth_url{ plugin, url }OAuth URL discovered in output
plugin_auth_complete{ plugin }Login succeeded (exit code 0)
plugin_auth_error{ plugin, error }Login failed

The login flow is asynchronous. Nebo spawns the plugin's login command, scans stderr/stdout for OAuth URLs, broadcasts them to the frontend via WebSocket, and also attempts to open the URL as a server-side fallback.

Plugin events

Plugins can declare event-producing capabilities. When a long-running watch process outputs NDJSON to stdout, Nebo auto-emits each line into the EventBus. Other agents can subscribe to these events without knowing plugin internals.

{
  "events": [
    {
      "name": "email.new",
      "description": "Fires when a new email arrives in Gmail",
      "command": "gmail +watch --format ndjson --project {{gcp_project}}"
    },
    {
      "name": "calendar.event",
      "description": "Fires on calendar event changes",
      "command": "calendar +watch --format ndjson",
      "multiplexed": true
    }
  ]
}

Event fields

FieldTypeDefaultDescription
namestringrequiredEvent name. Prefixed with plugin slug at runtime (e.g., "gws.email.new")
descriptionstring""Human-readable description of what triggers this event
commandstringrequiredCLI args for the watch process. Supports {{key}} template substitution
multiplexedboolfalseIf true, NDJSON lines may contain an "event" field for multiplexing

NDJSON protocol

Watch processes output one JSON object per line to stdout.

Single event type (multiplexed: false) -- the entire line becomes the event payload, emitted under the declared event source:

{"messageId": "123", "from": "alice@example.com", "subject": "Hello"}

Multiplexed (multiplexed: true) -- each line may contain an "event" field that discriminates the event type. The field is stripped from the payload before emission:

{"event": "email.new", "messageId": "123", "from": "alice@example.com"}
{"event": "email.read", "messageId": "456"}

If a multiplexed line has no event field, the declared event name is used as fallback.

Agent watch triggers

Agents consume plugin events by declaring a watch trigger with event and command in agent.json. Always provide both -- event enables EventBus auto-emission, command is the fallback if the event isn't found in the manifest:

{
  "email-watcher": {
    "trigger": {
      "type": "watch",
      "plugin": "gws",
      "event": "email.new",
      "command": "gmail +watch --format ndjson",
      "restart_delay_secs": 5
    },
    "description": "React to new emails",
    "activities": [...]
  }
}

When event is set, the runtime first attempts to resolve the CLI command from the plugin's manifest. If the event is not found, the command field is used instead. Without either, the watcher is silently skipped.

Plugin-to-plugin dependencies

Plugins can depend on other plugins. For example, digest needs ffmpeg for media extraction, and nebo-office may need nebo-pdf for shared rendering. Declare dependencies in your plugin.json:

{
  "id": "digest",
  "slug": "digest",
  "version": "1.2.0",
  "dependencies": [
    { "name": "ffmpeg", "version": ">=5.0.0" },
    { "name": "imagemagick", "version": "*", "optional": true }
  ]
}

Each required dependency is resolved and installed recursively (same cascade as skill-to-plugin deps). Your plugin binary receives dependency binaries as environment variables: {DEP_SLUG}_BIN. The dependency cascade uses a visited set for cycle protection -- no infinite loops.