Agent skill
openhands-api
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.
Install this agent skill to your Project
npx add-skill https://github.com/OpenHands/extensions/tree/main/skills/openhands-api
SKILL.md
This skill documents the OpenHands Cloud API (V1) and provides small, easy-to-copy clients.
It is intentionally focused on common OpenHands Cloud workflows:
- Defaults to OpenHands Cloud (
https://app.all-hands.dev). - Targets the V1 app server REST API under
/api/v1/.... - Includes a few agent server endpoints (inside a sandbox) that use
X-Session-API-Key. - Covers the multi-conversation delegation pattern: start separate Cloud conversations when you want fresh context windows or background work.
When to use this skill
Use this skill when you need to:
- start or inspect OpenHands Cloud conversations from code
- monitor async startup via start-task polling
- monitor execution status for long-running jobs
- create separate Cloud conversations for parallel or background work
- access sandbox agent-server endpoints once a conversation is running
Auth
App server (Cloud)
Use Bearer auth:
- Header:
Authorization: Bearer <OPENHANDS_CLOUD_API_KEY> - Preferred env var:
OPENHANDS_CLOUD_API_KEY - Backward-compatible env var:
OPENHANDS_API_KEY
Agent server (inside a sandbox)
Use session auth:
- Header:
X-Session-API-Key: <session_api_key>
How to obtain agent_server_url and session_api_key:
- Start or fetch an app conversation via the app server (Bearer auth), e.g.:
POST /api/v1/app-conversations- or
GET /api/v1/app-conversations?ids=<conversation_id>
- In the returned JSON, look for sandbox/runtime connection fields (names vary slightly by deployment/version). Common patterns:
- a sandbox object containing
agent_server_url(or similar) - a session key such as
session_api_key(or similar)
- a sandbox object containing
- Use those values to call the agent server directly:
- Base:
{agent_server_url}/api/... - Header:
X-Session-API-Key: <session_api_key>
- Base:
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.
Common V1 app server endpoints
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 zip
Delegating work with additional Cloud conversations
Use the Cloud API when you want a separate OpenHands conversation with its own fresh context window. This is useful for:
- background jobs that can run independently
- parallel investigations or implementation tasks
- long-running work where you want to keep the current conversation focused
- task-specific contexts, such as one conversation building a component while another runs tests
Delegation checklist
When you start a delegated Cloud conversation:
- Write a self-contained task description. Do not assume the new conversation has any context from the current one.
- Include the repository, branch, relevant file paths, constraints, and expected output.
- Start the new conversation with
POST /api/v1/app-conversations. - Poll the start-task until
statusisREADYand you have anapp_conversation_id. - Monitor the delegated conversation via
GET /api/v1/app-conversations?ids=.... - Share or store the Cloud URL:
https://app.all-hands.dev/conversations/<app_conversation_id>.
Minimal cURL flow
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}"
Minimal Python flow
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()
Parallelism guidance
- Prefer 5 or fewer concurrently running delegated conversations.
- Before starting more, check recent conversations and count how many are still
execution_status == "running". - Batch specific conversation lookups with
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.")
Start-task vs app_conversation_id (common pitfall)
In many deployments, POST /api/v1/app-conversations is asynchronous and returns a start-task object:
idis the start_task_idapp_conversation_idis 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.
Common agent server endpoints
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/count
Counting events (recommended approach)
If you need to know how many events a conversation has, you can:
- App server count (fastest when working)
GET /api/v1/conversation/{app_conversation_id}/events/count
- Agent server count (reliable fallback)
GET {agent_server_url}/api/conversations/{app_conversation_id}/events/count
- Trajectory zip fallback (heavier, but still one call + gives full payloads)
GET /api/v1/app-conversations/{app_conversation_id}/download- Unzip and count
event_*.jsonfiles
Do 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).
Troubleshooting
For common issues and solutions, see TROUBLESHOOTING.md.
Event structure (for debugging)
Events returned by:
- app server:
GET /api/v1/conversation/{id}/events/search - agent server:
GET {agent_server_url}/api/conversations/{id}/events/search
…share the same high-level shape.
Each event typically includes:
id(UUID)timestampkindsource
Common 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.id
Example (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": "..."}
}
Debugging one-liners (events)
These assume you're querying the app server endpoint. For agent-server queries, swap the URL base + use X-Session-API-Key.
Print a quick timeline
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
Find error-like events
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
Check tool-call matching (unmatched actions / duplicate observations)
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
Quick start (Python)
# 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()
CLI examples
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
Notes for AI agents extending this client
- Prefer
.../searchendpoints with a smalllimit. - Avoid loops that could generate many API calls.
- Start conversations only when asked: it may create sandboxes and cost money.
- For sandbox file operations and command execution, use the agent server endpoints with
X-Session-API-Key.
See also:
skills/openhands-api/scripts/openhands_api.py- The original inspiration client:
enyst/llm-playground→openhands-api-client-v1/scripts/cloud_api_v1.py - Troubleshooting content and real-world usage feedback →
https://github.com/jpshackelford/.openhands/tree/main/skills/openhands-cloud-api
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
releasenotes
Generate formatted changelogs from git history since the last release tag. Use when preparing release notes that categorize changes into breaking changes, features, fixes, and other sections.
bitbucket
Interact with Bitbucket repositories and pull requests using the BITBUCKET_TOKEN environment variable. Use when working with code hosted on Bitbucket or managing Bitbucket resources via API.
add-skill
Add an external skill from a GitHub repository to the current workspace. Use when users want to import, install, or add a skill from a GitHub URL (e.g., `/add-skill https://github.com/OpenHands/extensions/tree/main/skills/codereview` or "add the codereview skill from https://github.com/OpenHands/extensions/"). Handles fetching the skill files and placing them in .agents/skills/.
add-javadoc
Add comprehensive JavaDoc documentation to Java classes and methods. Use when documenting Java code, adding API documentation, or improving code documentation.
flarglebargle
A test skill that responds to the magic word "flarglebargle" with a compliment. Use for testing skill activation and trigger functionality.
codereview-roasted
Brutally honest code review in the style of Linus Torvalds, focusing on data structures, simplicity, and pragmatism. Use when you want critical, no-nonsense feedback that prioritizes engineering fundamentals over style preferences.
Didn't find tool you were looking for?