skills/openhands-api/SKILL.md
Reference skill for the OpenHands Cloud REST API (V1), including how to start additional cloud conversations for fresh-context or delegated work. Use when you need to automate common OpenHands Cloud actions; don't use for general sandbox/dev tasks unrelated to the OpenHands API.
npx skillsauth add openhands/skills 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) and provides small, easy-to-copy clients.
It is intentionally focused on common OpenHands Cloud 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.
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 V1 docs and implementation:
OpenHands/docs/openhands/usage/cloud/cloud-api.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
Create an automation that generates an async standup digest from Slack. Searches selected channels for messages since the previous workday, groups updates by project, highlights blockers and decisions, and posts a summary to a target channel.
tools
Create an automation that writes a recurring research brief. Uses Tavily MCP for web research and Notion MCP to publish the final brief with executive summary, implications, and source citations.
tools
Create an automation that triages new Linear issues. Inspects the issue title, description, team, customer, priority, and recent related issues via Linear MCP. Suggests labels, priority, likely owner, duplicates, and posts a clarifying comment.
tools
Create an automation that drafts incident retrospectives. Gathers incident-channel messages from Slack, collects linked tickets and follow-ups from Linear, and publishes a retrospective draft to Notion with a timeline, impact summary, root-cause hypotheses, and action items.