Plugin Launch Flow
This document traces the complete data flow for launching plugins in OpenHands, from the source marketplace through to agent execution. Each section shows the exact endpoints, payloads, and transformations.
This document traces the complete data flow for launching plugins in OpenHands, from the source marketplace through to agent execution. Each section shows the exact endpoints, payloads, and transformations.
This document traces the complete data flow for launching plugins in OpenHands, from the source marketplace through to agent execution. Each section shows the exact endpoints, payloads, and transformations.
Marketplace ──▶ Plugin Directory ──▶ Frontend /launch ──▶ App Server ──▶ Agent Server ──▶ SDK
(GitHub) (Index + UI) (Modal) (API) (in sandbox) (plugin loading)| Component | Responsibility | |-----------|---------------| | **Marketplace** | Source of truth for plugin catalog (GitHub repo) | | **Plugin Directory** | Index plugins from marketplace, serve browsing UI, construct launch URLs | | **Frontend** | Display confirmation modal, collect parameters, call API | | **App Server** | Validate request, create conversation, pass plugin specs to agent server | | **Agent Server** | Run inside sandbox, delegate plugin loading to SDK | | **SDK** | Fetch plugins, load contents, merge skills/hooks/MCP into agent |
---
**Source**: A GitHub repository (e.g., `github.com/OpenHands/plugin-marketplace`)
The marketplace is a GitHub repository containing a `marketplace.json` that indexes all available plugins.
{
"name": "OpenHands Plugin Marketplace",
"owner": {
"name": "OpenHands",
"email": "team@all-hands.dev"
},
"metadata": {
"description": "Official OpenHands plugin marketplace",
"pluginRoot": "plugins"
},
"plugins": [
{
"name": "city-weather",
"source": "github:jpshackelford/openhands-sample-plugins",
"ref": "main",
"repo_path": "plugins/city-weather",
"description": "Get current weather for any city",
"tags": ["weather", "utility"]
}
]
}Each plugin has a `plugin.json` in its `.claude-plugin/` directory. This file contains both official plugin manifest fields and optional directory-specific config fields:
{
"name": "city-weather",
"description": "Get current weather for any city",
"entry_command": "now",
"parameters": {
"city": {
"type": "string",
"description": "City name",
"required": true,
"default": "San Francisco"
}
},
"examples": [
{
"title": "Check Tokyo weather",
"prompt": "/city-weather:now Tokyo"
}
]
}**Output to Plugin Directory**: `marketplace.json` + individual `plugin.json` files
---
**Endpoints**:
Fetches and transforms the marketplace catalog.
**Request**: None (fetches from configured `MARKETPLACE_SOURCE`)
**Response**:
{
"plugins": [
{
"id": "city-weather",
"name": "city-weather",
"description": "Get current weather for any city",
"source": {
"source": "github",
"repo": "jpshackelford/openhands-sample-plugins",
"ref": "main",
"repo_path": "plugins/city-weather"
},
"tags": ["weather", "utility"]
}
]
}Fetches and returns the config fields from `plugin.json`.
**Request**: `GET /api/plugins/city-weather/config`
**Response** (200 OK):
{
"entry_command": "now",
"parameters": {
"city": {
"type": "string",
"description": "City name",
"required": true,
"default": "San Francisco"
}
},
"examples": [
{
"title": "Check Tokyo weather",
"prompt": "/city-weather:now Tokyo"
}
]
}**Output to Plugin Directory Client**: Plugin metadata + config
---
When user clicks "Launch", the client constructs a launch URL using `buildLaunchUrl()`.
From Plugin Directory Server APIs:
{
"name": "city-weather",
"source": {
"source": "github",
"repo": "jpshackelford/openhands-sample-plugins",
"ref": "main",
"repo_path": "plugins/city-weather"
}
} {
"entry_command": "now",
"parameters": {
"city": { "type": "string", "required": true, "default": "San Francisco" }
}
}**Launch URL**:
https://app.openhands.ai/launch?plugins=BASE64&message=%2Fcity-weather%3AnowWhere `plugins` (base64-decoded) contains:
[{
"source": "github:jpshackelford/openhands-sample-plugins",
"ref": "main",
"repo_path": "plugins/city-weather",
"parameters": {
"city": "San Francisco"
}
}]And `message` (URL-decoded) is:
/city-weather:now**Key point**: The `parameters` in the PluginSpec contain **default values** for pre-filling the launch modal form. The `message` contains only the slash command—the Frontend passes it through unchanged, and the App Server appends the parameter values as a formatted text block.
---
**Route**: `/launch?plugins=BASE64&message=/city-weather:now`
[PR #12699](https://github.com/OpenHands/OpenHands/pull/12699)
**Decoded**:
{
"plugins": [{
"source": "github:jpshackelford/openhands-sample-plugins",
"ref": "main",
"repo_path": "plugins/city-weather",
"parameters": { "city": "San Francisco" }
}],
"message": "/city-weather:now"
}The frontend displays a confirmation modal:
When user clicks "Start Conversation":
"parameters": { "city": "Tokyo" }POST /api/v1/app-conversations
Content-Type: application/json
Authorization: Bearer <user_token>
{
"plugins": [{
"source": "github:jpshackelford/openhands-sample-plugins",
"ref": "main",
"repo_path": "plugins/city-weather",
"parameters": {
"city": "Tokyo"
}
}],
"initial_message": {
"role": "user",
"content": [{"type": "text", "text": "/city-weather:now"}]
}
}**Summary of transformations**: | Field | Input (from URL) | Output (to API) | |-------|------------------|-----------------| | `plugins[].parameters` | Default values (`"San Francisco"`) | User's values (`"Tokyo"`) | | `initial_message.text` | Slash command (`/city-weather:now`) | Slash command unchanged (`/city-weather:now`) |
**Note**: The Frontend does NOT append parameter values to the message. Parameters are passed as structured data in `plugins[].parameters`. The App Server will append them to the message text (see Step 5).
---
**Endpoint**: `POST /api/v1/app-conversations`
[PR #12338](https://github.com/OpenHands/OpenHands/pull/12338)
{
"plugins": [{
"source": "github:jpshackelford/openhands-sample-plugins",
"ref": "main",
"repo_path": "plugins/city-weather",
"parameters": { "city": "Tokyo" }
}],
"initial_message": {
"role": "user",
"content": [{"type": "text", "text": "/city-weather:now"}]
}
}Note: The `initial_message.text` contains only the slash command—parameter values come separately in `plugins[].parameters`.
class PluginSpec(PluginSource):
"""Extends SDK's PluginSource with user-provided parameters."""
parameters: dict[str, Any] | None = None # User-provided values
class AppConversationStartRequest(BaseModel):
plugins: list[PluginSpec] | None = None
initial_message: SendMessageRequest | None = None
# ... other fields**Call stack** in `LiveStatusAppConversationService`:
# Original message: "/city-weather:now"
# Parameters: {"city": "Tokyo"}
# Result: "/city-weather:now\n\nPlugin Configuration Parameters:\n- city: Tokyo" sdk_plugins = [
PluginSource(
source=p.source, # "github:jpshackelford/openhands-sample-plugins"
ref=p.ref, # "main"
repo_path=p.repo_path # "plugins/city-weather"
)
# NOTE: p.parameters is NOT passed to SDK PluginSource!
for p in plugins
]StartConversationRequest(
plugins=[
PluginSource(
source="github:jpshackelford/openhands-sample-plugins",
ref="main",
repo_path="plugins/city-weather"
# NO parameters field - SDK PluginSource doesn't have it
)
],
initial_message=SendMessageRequest(
content=[
TextContent(
text="/city-weather:now\n\nPlugin Configuration Parameters:\n- city: Tokyo"
)
]
),
# ... other fields
)**⚠️ CRITICAL**: Plugin parameters are passed to the agent via **message text**, not via the `PluginSource` object. The SDK's `PluginSource` class only has `source`, `ref`, and `repo_path` fields.
**Note on message construction**: The original slash command `/city-weather:now` does NOT include the parameter value "Tokyo" inline. The parameter appears only in the formatted "Plugin Configuration Parameters" block appended by the App Server.
---
**Entry point**: `ConversationService.start_conversation()`
[SDK PR #1651](https://github.com/OpenHands/software-agent-sdk/pull/1651)
StartConversationRequest(
plugins=[
PluginSource(
source="github:jpshackelford/openhands-sample-plugins",
ref="main",
repo_path="plugins/city-weather"
)
],
initial_message=SendMessageRequest(
content=[
TextContent(
text="/city-weather:now\n\nPlugin Configuration Parameters:\n- city: Tokyo"
)
]
)
)**Call stack**:
LocalConversation(
agent=agent,
plugins=[PluginSource(...)], # Stored, not yet loaded
workspace=workspace,
# initial_message queued for processing
)---
**Trigger**: First `conversation.run()` or `conversation.send_message()`
[SDK PR #1647](https://github.com/OpenHands/software-agent-sdk/pull/1647)
[
PluginSource(
source="github:jpshackelford/openhands-sample-plugins",
ref="main",
repo_path="plugins/city-weather"
)
]**Call stack**:
Plugin(
name="city-weather",
path="/tmp/plugins/city-weather",
manifest=PluginManifest(
name="city-weather",
entry_command="now", # Read from plugin.json
commands={"now": Command(...)},
skills=[Skill(...)],
hooks={...},
mcp_servers={...}
)
# NOTE: No parameters field - parameters are in the message text
)---
The agent now has:
/city-weather:now
Plugin Configuration Parameters:
- city: TokyoWhen the agent processes the message:
**Note**: Parameters are NOT passed as structured data to the plugin. The agent reads them from the message text in the formatted "Plugin Configuration Parameters" block appended by the App Server.
---
| Step | Component | Input | Output | |------|-----------|-------|--------| | 1 | Marketplace | - | `marketplace.json` + `plugin.json` files | | 2 | Plugin Directory Server | Marketplace files | REST API responses with `entry_command`, `parameters` | | 3 | Plugin Directory Client | Plugin + Config | Launch URL: `plugins` (with defaults) + `message` (slash command only) | | 4 | OpenHands Frontend | URL query params | API call: `plugins` (with user values) + `message` (unchanged slash command) | | 5 | App Server | API request | `StartConversationRequest`: `PluginSource` (no params) + message (params in text) | | 6 | Agent Server | `StartConversationRequest` | `LocalConversation` with deferred plugins | | 7 | SDK | `PluginSource` list | Loaded `Plugin` objects with skills/hooks/MCP | | 8 | Agent | Initial message with params in text | Command execution |
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ Plugin Directory │ │ OpenHands Frontend │ │ App Server │
│ │ │ │ │ │
│ plugins[].params │────▶│ plugins[].params │────▶│ Appends params to │
│ = defaults │ │ = user values │ │ message as text │
│ │ │ (from form edit) │ │ block, then DROPS │
│ │ │ │ │ from PluginSource │
│ message = │ │ message = │ │ │
│ /cmd:entry │────▶│ /cmd:entry │────▶│ Final message: │
│ (no values) │ │ (unchanged!) │ │ /cmd:entry │
│ │ │ │ │ + params block │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘**Key insight**: The Frontend does NOT modify the message. It passes the slash command through unchanged and sends parameters as structured data in `plugins[].parameters`. The App Server is responsible for formatting parameters into the message text.
---
Plugins load **inside the sandbox** because:
The `entry_command` field contains only the command name (e.g., `"now"`), not the full slash command. This separation allows:
Parameters travel through the system as **structured data until the App Server**, where they are converted to text:
The SDK's `PluginSource` class intentionally does NOT have a `parameters` field. All parameter context is communicated to the agent via the initial message text, specifically in the "Plugin Configuration Parameters" block appended by the App Server.
---