agentleFS
OpenHandsOpenHands/extensions/skills/openhands-api/SKILL.md

openhands-api

Skill167 starsChanged 16 days agoMITView on GitHub

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.

  • Reads credentials
  • Sends data out

We split this skill into the rules it gives an agent

24 rules. About 0 are common, 16 are distinctive, and 8 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈16

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 8

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use this skill when you need to:

    line 26
  2. Distinctive:

    start or inspect OpenHands Cloud conversations from code

    line 28
  3. Distinctive:

    create separate Cloud conversations for parallel or background work

    line 31
  4. Distinctive:

    start or inspect conversations on a local Agent Canvas backend or local agent server

    line 33
  5. Distinctive:

    Start or fetch an app conversation via the app server (Bearer auth), e.g.:

    line 53
  6. Distinctive:

    Use those values to call the agent server directly:

    line 59
  7. Very specific:

    If those fields are not present on the conversation record, list/search sandboxes (GET /api/v1/sandboxes/search) and use the sandbox …

    line 77
  8. Distinctive:

    Check the local server before creating a backend conversation:

    line 90
  9. Distinctive:

    If the same base URL serves the Agent Canvas UI, the browser route is:

    line 139
  10. Distinctive:

    Use the Cloud API when you want a separate OpenHands conversation with its own fresh context window.

    line 163
  11. Distinctive:

    When you start a delegated Cloud conversation:

    line 173
  12. Distinctive:

    Write a self-contained task description.

    line 175
  13. Very specific:

    Start the new conversation with POST /api/v1/app-conversations.

    line 177
  14. Very specific:

    If the response does not already include app_conversation_id, poll the start-task:

    line 196
  15. Distinctive:

    Prefer 5 or fewer concurrently running delegated conversations.

    line 242
  16. Very specific:

    Before starting more, check recent conversations and count how many are still execution_status == "running".

    line 243
  17. Very specific:

    If app_conversation_id is not present in the initial response, fetch it via:

    line 265
  18. Very specific:

    If you pass a start_task_id to /download, you will get 404 Not Found.

    line 269
  19. Distinctive:

    If you need to know how many events a conversation has, you can:

    line 283
  20. Very specific:

    Do not rely on the last event id to infer the total number of events.

    line 293
  21. Distinctive:

    Start a conversation from a prompt file:

    line 438
  22. Very specific:

    Prefer .../search endpoints with a small limit.

    line 449
  23. Distinctive:

    Avoid loops that could generate many API calls.

    line 450
  24. Distinctive:

    Start conversations only when asked: it may create sandboxes and cost money.

    line 451

≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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:

  • 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.
  • Covers local Agent Canvas backend conversations: start or inspect conversations by calling a local agent server directly.

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
  • start or inspect conversations on a local Agent Canvas backend or local agent server

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:

  1. 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>
  2. 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)
  3. Use those values to call the agent server directly:
    • Base: {agent_server_url}/api/...
    • Header: 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.

Local Agent Canvas backend

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}"

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 account
  • GET /api/v1/app-conversations/search?limit=... — list recent conversations
  • GET /api/v1/app-conversations?ids=... — fetch conversation records by id (batch)
  • GET /api/v1/app-conversations/count — count conversations
  • POST /api/v1/app-conversations — start a new conversation (creates a sandbox)
  • GET /api/v1/app-conversations/start-tasks?ids=... — check async start-task status
  • GET /api/v1/conversation/{app_conversation_id}/events/search?limit=... — read conversation events
  • GET /api/v1/conversation/{app_conversation_id}/events/count — count events
  • GET /api/v1/sandboxes/search?limit=... — list sandboxes
  • POST /api/v1/sandboxes/{sandbox_id}/pause / .../resume — manage sandbox lifecycle
  • GET /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:

  1. Write a self-contained task description. Do not assume the new conversation has any context from the current one.
  2. Include the repository, branch, relevant file paths, constraints, and expected output.
  3. Start the new conversation with POST /api/v1/app-conversations.
  4. Poll the start-task until status is READY and you have an app_conversation_id.
  5. Monitor the delegated conversation via GET /api/v1/app-conversations?ids=....
  6. Share or store the conversation's conversation_ui_url from that response. It opens the conversation in Agent Canvas. If the server does not return it, use <base_url>/canvas/conversations/<app_conversation_id>, where <base_url> is the server you called the API on. Never share conversation_url; it is the sandbox's agent-server API, not a page users can open.

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 and get the link to share (conversation_ui_url):

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.app_conversation_ui_url(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:

  • id is the start_task_id
  • app_conversation_id is the id you should use for conversation operations like:
    • GET /api/v1/app-conversations/{app_conversation_id}/download
    • GET /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_command
  • GET {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/search
  • GET {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:

  1. App server count (fastest when working)
    • GET /api/v1/conversation/{app_conversation_id}/events/count
  2. Agent server count (reliable fallback)
    • GET {agent_server_url}/api/conversations/{app_conversation_id}/events/count
  3. Trajectory zip fallback (heavier, but still one call + gives full payloads)
    • GET /api/v1/app-conversations/{app_conversation_id}/download
    • Unzip and count event_*.json files

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)
  • timestamp
  • kind
  • source

Common kind values:

kindsource (typical)key fields (common)purpose
ActionEventagenttool_name, tool_call_id, actiontool call requested by the agent
ObservationEventenvironmenttool_name, tool_call_id, action_id, observationtool result produced by the sandbox/environment
MessageEventuser / assistantmessage (or similar)user/assistant chat messages
ConversationStateUpdateEventenvironmentkey, valuestate transitions/metadata

Linking tool calls:

  • ActionEvent.tool_call_id == ObservationEvent.tool_call_id
  • ObservationEvent.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 .../search endpoints with a small limit.
  • 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

Source of truth

This skill is aligned against the current OpenHands API docs and implementation:

  • OpenHands/docs/openhands/usage/cloud/cloud-api.mdx
  • OpenHands/docs/openhands/usage/agent-canvas/backend-setup/local.mdx
  • OpenHands/docs/sdk/arch/agent-server.mdx
  • OpenHands/docs/openhands/usage/api/v1.mdx
  • OpenHands/OpenHands/openhands/app_server/v1_router.py
  • OpenHands/OpenHands/openhands/app_server/app_conversation/app_conversation_router.py
  • OpenHands/OpenHands/openhands/app_server/app_conversation/app_conversation_models.py

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

Reviews can't be read right now.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.