skills/openhands-api/SKILL.md
Reference skill for the OpenHands Cloud REST API (V1) and agent-server APIs, including how to start additional cloud or local backend conversations for fresh-context or delegated work.
npx skillsauth add openhands/extensions openhands-apiInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill documents the OpenHands Cloud API (V1), commonly used agent-server APIs, and small, easy-to-copy clients.
Windows PowerShell equivalents for the shell examples in this skill are in references/windows.md.
It is intentionally focused on common OpenHands API workflows:
https://app.all-hands.dev)./api/v1/....X-Session-API-Key.Use this skill when you need to:
Use Bearer auth:
Authorization: Bearer <OPENHANDS_CLOUD_API_KEY>OPENHANDS_CLOUD_API_KEYOPENHANDS_API_KEYUse session auth:
X-Session-API-Key: <session_api_key>How to obtain agent_server_url and session_api_key:
POST /api/v1/app-conversationsGET /api/v1/app-conversations?ids=<conversation_id>agent_server_url (or similar)session_api_key (or similar){agent_server_url}/api/...X-Session-API-Key: <session_api_key>Example (common field names; adjust to your deployment):
# using the minimal Python client (`OpenHandsAPI`)
conv = api.app_conversation_get(app_conversation_id)
session_api_key = conv.get("session_api_key")
conversation_url = conv.get("conversation_url", "")
# `conversation_url` often looks like: https://<runtime-host>/api/conversations/<id>
agent_server_url = conversation_url.rsplit("/api/conversations", 1)[0]
If those fields are not present on the conversation record, list/search sandboxes (GET /api/v1/sandboxes/search) and use the sandbox referenced by the conversation to locate the agent server URL + session key.
Use the local backend flow only for local Agent Canvas / agent-server development, such as agent-canvas, agent-canvas --backend-only, or npm run dev with ingress at http://localhost:8000. This calls the agent server directly with X-Session-API-Key. It is not an automation, and it is different from OpenHands Cloud delegation through POST /api/v1/app-conversations, which uses Bearer auth against the Cloud app API and may return asynchronous start-task records.
When Agent Canvas runs locally, the launcher uses LOCAL_BACKEND_API_KEY when it is set. Otherwise it generates and persists the session API key at ~/.openhands/agent-canvas/api-key.txt. Set OH_SESSION_API_KEY_PATH to override the persisted key path. Never print, log, or paste the actual key; use command substitution or an environment variable in examples and scripts.
LOCAL_AGENT_SERVER_URL="${LOCAL_AGENT_SERVER_URL:-http://localhost:8000}"
SESSION_API_KEY="${LOCAL_BACKEND_API_KEY:-$(cat "${OH_SESSION_API_KEY_PATH:-$HOME/.openhands/agent-canvas/api-key.txt}")}"
Check the local server before creating a backend conversation:
curl -sS "${LOCAL_AGENT_SERVER_URL}/server_info" \
-H "X-Session-API-Key: ${SESSION_API_KEY}"
Start a backend conversation with POST /api/conversations. Include the agent settings and workspace expected by that backend. Local agent-server calls use an explicit workspace such as {"kind": "LocalWorkspace", "working_dir": "/workspace"}; Cloud app-conversation delegation instead uses app-server fields such as selected_repository and selected_branch. If you are starting the conversation from an existing Agent Canvas session, pass through the current configured settings or encrypted settings rather than hard-coding secrets into scripts.
CONVERSATION_JSON=$(curl -sS -X POST "${LOCAL_AGENT_SERVER_URL}/api/conversations" \
-H "X-Session-API-Key: ${SESSION_API_KEY}" \
-H "Content-Type: application/json" \
-d @- <<'JSON'
{
"agent": {
"kind": "Agent",
"llm": {
"model": "your-model-provider/your-model-name",
"api_key": "**********"
},
"tools": [
{"name": "terminal"},
{"name": "file_editor"},
{"name": "task_tracker"}
]
},
"workspace": {"kind": "LocalWorkspace", "working_dir": "/workspace"},
"initial_message": {
"content": [{"text": "Summarize the current workspace."}],
"run": true
}
}
JSON
)
CONVERSATION_ID=$(python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' <<<"${CONVERSATION_JSON}")
printf 'Conversation: %s/api/conversations/%s\n' "${LOCAL_AGENT_SERVER_URL}" "${CONVERSATION_ID}"
Poll status and inspect recent events:
curl -sS "${LOCAL_AGENT_SERVER_URL}/api/conversations/${CONVERSATION_ID}" \
-H "X-Session-API-Key: ${SESSION_API_KEY}"
curl -sS "${LOCAL_AGENT_SERVER_URL}/api/conversations/${CONVERSATION_ID}/events/search?limit=20&sort_order=TIMESTAMP_DESC" \
-H "X-Session-API-Key: ${SESSION_API_KEY}"
If the same base URL serves the Agent Canvas UI, the browser route is:
printf '%s/conversations/%s\n' "${LOCAL_AGENT_SERVER_URL}" "${CONVERSATION_ID}"
The following are the main endpoints implemented in the minimal client:
GET /api/v1/users/me — validate auth and inspect current accountGET /api/v1/app-conversations/search?limit=... — list recent conversationsGET /api/v1/app-conversations?ids=... — fetch conversation records by id (batch)GET /api/v1/app-conversations/count — count conversationsPOST /api/v1/app-conversations — start a new conversation (creates a sandbox)GET /api/v1/app-conversations/start-tasks?ids=... — check async start-task statusGET /api/v1/conversation/{app_conversation_id}/events/search?limit=... — read conversation eventsGET /api/v1/conversation/{app_conversation_id}/events/count — count eventsGET /api/v1/sandboxes/search?limit=... — list sandboxesPOST /api/v1/sandboxes/{sandbox_id}/pause / .../resume — manage sandbox lifecycleGET /api/v1/app-conversations/{app_conversation_id}/download — download trajectory zipUse the Cloud API when you want a separate OpenHands conversation with its own fresh context window. This is useful for:
When you start a delegated Cloud conversation:
POST /api/v1/app-conversations.status is READY and you have an app_conversation_id.GET /api/v1/app-conversations?ids=....https://app.all-hands.dev/conversations/<app_conversation_id>.curl -X POST "https://app.all-hands.dev/api/v1/app-conversations" \
-H "Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"initial_message": {
"content": [{"type": "text", "text": "Investigate flaky tests in tests/test_api.py. Report the root cause and propose a fix."}]
},
"selected_repository": "owner/repo"
}'
If the response does not already include app_conversation_id, poll the start-task:
curl -s "https://app.all-hands.dev/api/v1/app-conversations/start-tasks?ids=${START_TASK_ID}" \
-H "Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY}"
Then check execution status:
curl -s "https://app.all-hands.dev/api/v1/app-conversations?ids=${APP_CONVERSATION_ID}" \
-H "Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY}"
from openhands_api import OpenHandsAPI
api = OpenHandsAPI() # prefers OPENHANDS_CLOUD_API_KEY
start = api.app_conversation_start(
initial_message=(
"Implement the requested dashboard component in src/dashboard.tsx. "
"Update any related tests and summarize the changes."
),
selected_repository="owner/repo",
selected_branch="main",
title="Dashboard component task",
)
ready = start
if not ready.get("app_conversation_id"):
ready = api.poll_start_task_until_ready(start["id"])
conversation_id = ready["app_conversation_id"]
print(f"Delegated conversation: {api.base_url}/conversations/{conversation_id}")
status = api.app_conversation_get(conversation_id)
print(status.get("sandbox_status"), status.get("execution_status"))
api.close()
execution_status == "running".GET /api/v1/app-conversations?ids=... when you already know their ids.Example:
items = api.app_conversations_search(limit=50).get("items", [])
running = [item for item in items if item.get("execution_status") == "running"]
if len(running) >= 5:
print("Wait for some delegated conversations to finish before starting more.")
app_conversation_id (common pitfall)In many deployments, POST /api/v1/app-conversations is asynchronous and returns a start-task object:
id is the start_task_idapp_conversation_id is the id you should use for conversation operations like:
GET /api/v1/app-conversations/{app_conversation_id}/downloadGET /api/v1/conversation/{app_conversation_id}/events/...If app_conversation_id is not present in the initial response, fetch it via:
GET /api/v1/app-conversations/start-tasks?ids=<start_task_id>If you pass a start_task_id to /download, you will get 404 Not Found.
These run against agent_server_url (not the app server):
POST {agent_server_url}/api/bash/execute_bash_commandGET {agent_server_url}/api/file/download/<absolute_path>POST {agent_server_url}/api/file/upload/<absolute_path> (multipart)GET {agent_server_url}/api/conversations/{conversation_id}/events/searchGET {agent_server_url}/api/conversations/{conversation_id}/events/countIf you need to know how many events a conversation has, you can:
GET /api/v1/conversation/{app_conversation_id}/events/countGET {agent_server_url}/api/conversations/{app_conversation_id}/events/countGET /api/v1/app-conversations/{app_conversation_id}/downloadevent_*.json filesDo not rely on the last event id to infer the total number of events.
In the agent-server API, event IDs are UUIDs (not monotonically increasing integers).
For common issues and solutions, see TROUBLESHOOTING.md.
Events returned by:
GET /api/v1/conversation/{id}/events/searchGET {agent_server_url}/api/conversations/{id}/events/search…share the same high-level shape.
Each event typically includes:
id (UUID)timestampkindsourceCommon kind values:
| kind | source (typical) | key fields (common) | purpose |
|---|---|---|---|
| ActionEvent | agent | tool_name, tool_call_id, action | tool call requested by the agent |
| ObservationEvent | environment | tool_name, tool_call_id, action_id, observation | tool result produced by the sandbox/environment |
| MessageEvent | user / assistant | message (or similar) | user/assistant chat messages |
| ConversationStateUpdateEvent | environment | key, value | state transitions/metadata |
Linking tool calls:
ActionEvent.tool_call_id == ObservationEvent.tool_call_idObservationEvent.action_id == ActionEvent.idExample (simplified):
{
"id": "<action-event-uuid>",
"kind": "ActionEvent",
"source": "agent",
"tool_name": "terminal",
"tool_call_id": "toolu_...",
"action": {"command": "ls"}
}
{
"id": "<observation-event-uuid>",
"kind": "ObservationEvent",
"source": "environment",
"tool_name": "terminal",
"tool_call_id": "toolu_...",
"action_id": "<action-event-uuid>",
"observation": {"exit_code": 0, "stdout": "..."}
}
These assume you're querying the app server endpoint. For agent-server queries, swap the URL base + use X-Session-API-Key.
curl -s "${BASE_URL:-https://app.all-hands.dev}/api/v1/conversation/${APP_CONVERSATION_ID}/events/search?limit=100" \
-H "Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY:-$OPENHANDS_API_KEY}" \
-H "Accept: application/json" | \
python3 - <<'PY'
import json, sys
items = (json.load(sys.stdin) or {}).get("items", [])
for i, e in enumerate(items):
print(f"{i:04d} {e.get('timestamp','')} {e.get('source','')} {e.get('kind','')}")
PY
curl -s "${BASE_URL:-https://app.all-hands.dev}/api/v1/conversation/${APP_CONVERSATION_ID}/events/search?limit=200" \
-H "Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY:-$OPENHANDS_API_KEY}" \
-H "Accept: application/json" | \
python3 - <<'PY'
import json, sys
items = (json.load(sys.stdin) or {}).get("items", [])
for i, e in enumerate(items):
if e.get("kind") == "ErrorEvent" or ("code" in e and "detail" in e):
print(i, e.get("kind"), e.get("code"), str(e.get("detail", ""))[:400])
PY
curl -s "${BASE_URL:-https://app.all-hands.dev}/api/v1/conversation/${APP_CONVERSATION_ID}/events/search?limit=200" \
-H "Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY:-$OPENHANDS_API_KEY}" \
-H "Accept: application/json" | \
python3 - <<'PY'
import json, sys
from collections import Counter
items = (json.load(sys.stdin) or {}).get("items", [])
action_ids = {e.get("id") for e in items if e.get("kind") == "ActionEvent"}
obs_action_ids = [e.get("action_id") for e in items if e.get("kind") == "ObservationEvent" and e.get("action_id")]
observed = set(obs_action_ids)
print("actions:", len(action_ids))
print("observations:", len(observed))
unmatched = action_ids - observed
print("unmatched actions:", list(unmatched)[:20] if unmatched else "none")
dups = [aid for aid, c in Counter(obs_action_ids).items() if c > 1]
print("duplicate observation action_ids:", list(dups)[:20] if dups else "none")
PY
# Copy `skills/openhands-api/scripts/openhands_api.py` into your project (e.g. as `openhands_api.py`),
# then import it normally:
from openhands_api import OpenHandsAPI
api = OpenHandsAPI() # prefers OPENHANDS_CLOUD_API_KEY
me = api.users_me()
print(me)
recent = api.app_conversations_search(limit=5)
print(recent)
api.close()
Search conversations:
export OPENHANDS_CLOUD_API_KEY="..."
python skills/openhands-api/scripts/openhands_api.py search-conversations --limit 5
Start a conversation from a prompt file:
python skills/openhands-api/scripts/openhands_api.py start-conversation \
--prompt-file skills/openhands-api/references/example_prompt.md \
--repo owner/repo \
--branch main
.../search endpoints with a small limit.X-Session-API-Key.See also:
skills/openhands-api/scripts/openhands_api.pyenyst/llm-playground → openhands-api-client-v1/scripts/cloud_api_v1.pyhttps://github.com/jpshackelford/.openhands/tree/main/skills/openhands-cloud-apiThis skill is aligned against the current OpenHands API docs and implementation:
OpenHands/docs/openhands/usage/cloud/cloud-api.mdxOpenHands/docs/openhands/usage/agent-canvas/backend-setup/local.mdxOpenHands/docs/sdk/arch/agent-server.mdxOpenHands/docs/openhands/usage/api/v1.mdxOpenHands/OpenHands/openhands/app_server/v1_router.pyOpenHands/OpenHands/openhands/app_server/app_conversation/app_conversation_router.pyOpenHands/OpenHands/openhands/app_server/app_conversation/app_conversation_models.pytools
Iterate on a GitHub pull request — drive it through CI, code review, and QA until it is merge-ready. Poll verification layers with `gh` CLI, diagnose and fix CI failures, address review feedback, retry flaky checks, push fixes, and repeat. The agent is the orchestration loop.
development
Guides technical explanations toward flowing, direct, conversational prose. This skill should be used for engineering chat, design discussion, architecture analysis, code-review explanations, and technical recommendations that should be concise without becoming fragmented or vague.
tools
This skill should be used when the user asks to "set up a Jira automation to create pull requests", "poll Jira for create-pr issues", "automatically create GitHub PRs from Jira tickets", "deploy a Jira issue-to-PR automation", "create a Jira to GitHub PR workflow", or mentions automating GitHub PR creation from a Jira label. Deploys a cron-based OpenHands automation that watches a Jira Cloud project for issues labeled with a configurable label (default: "create-pr") and spawns an agent conversation to create a GitHub pull request for each new issue found. The target GitHub repository is read from the body of the Jira ticket - no repo parameter is required at deploy time.
tools
This skill should be used when the user asks to "create an automation", "schedule a task", "set up a cron job", "webhook integration", "event-triggered automation", or mentions automations, scheduled tasks, cron scheduling, or webhook events in OpenHands Cloud.