@openhands/extensions 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/pypi-publish.yml +104 -0
- package/.github/workflows/tests.yml +6 -0
- package/.release-please-manifest.json +1 -1
- package/AGENTS.md +1 -1
- package/README.md +34 -4
- package/integrations/README.md +25 -9
- package/integrations/catalog/airtable.json +16 -10
- package/integrations/catalog/apify.json +0 -2
- package/integrations/catalog/asana.json +33 -0
- package/integrations/catalog/atlassian.json +5 -8
- package/integrations/catalog/bitbucket.json +34 -0
- package/integrations/catalog/box.json +34 -0
- package/integrations/catalog/brave-search.json +1 -2
- package/integrations/catalog/browser-mcp.json +0 -3
- package/integrations/catalog/canva.json +31 -0
- package/integrations/catalog/clickhouse.json +1 -2
- package/integrations/catalog/cloudflare-bindings.json +5 -8
- package/integrations/catalog/cloudflare-browser-rendering.json +5 -8
- package/integrations/catalog/cloudflare-builds.json +6 -9
- package/integrations/catalog/cloudflare-docs.json +4 -7
- package/integrations/catalog/cloudflare-observability.json +5 -8
- package/integrations/catalog/confluence.json +33 -0
- package/integrations/catalog/datadog.json +34 -0
- package/integrations/catalog/deepwiki.json +3 -7
- package/integrations/catalog/discord.json +34 -0
- package/integrations/catalog/elevenlabs.json +27 -28
- package/integrations/catalog/everything.json +0 -2
- package/integrations/catalog/exa.json +0 -2
- package/integrations/catalog/fetch.json +0 -2
- package/integrations/catalog/figma.json +35 -9
- package/integrations/catalog/filesystem.json +0 -3
- package/integrations/catalog/firecrawl.json +0 -2
- package/integrations/catalog/git.json +0 -3
- package/integrations/catalog/github.json +34 -10
- package/integrations/catalog/gmail.json +33 -0
- package/integrations/catalog/google-calendar.json +33 -0
- package/integrations/catalog/google-docs.json +33 -0
- package/integrations/catalog/google-drive.json +33 -0
- package/integrations/catalog/google-sheets.json +33 -0
- package/integrations/catalog/hubspot.json +34 -0
- package/integrations/catalog/huggingface.json +1 -2
- package/integrations/catalog/intercom.json +34 -0
- package/integrations/catalog/jira.json +33 -0
- package/integrations/catalog/kagi.json +2 -3
- package/integrations/catalog/linear.json +24 -16
- package/integrations/catalog/memory.json +0 -2
- package/integrations/catalog/microsoft-outlook.json +33 -0
- package/integrations/catalog/microsoft-teams.json +33 -0
- package/integrations/catalog/miro.json +42 -0
- package/integrations/catalog/monday.json +42 -0
- package/integrations/catalog/mongodb.json +1 -2
- package/integrations/catalog/neon.json +0 -2
- package/integrations/catalog/netlify.json +33 -0
- package/integrations/catalog/notion.json +30 -10
- package/integrations/catalog/obsidian.json +1 -3
- package/integrations/catalog/okta.json +33 -0
- package/integrations/catalog/onedrive.json +33 -0
- package/integrations/catalog/ordinal.json +33 -0
- package/integrations/catalog/paypal.json +5 -8
- package/integrations/catalog/playwright.json +0 -2
- package/integrations/catalog/posthog.json +38 -0
- package/integrations/catalog/quickbooks.json +86 -0
- package/integrations/catalog/redis.json +1 -3
- package/integrations/catalog/resend.json +1 -2
- package/integrations/catalog/salesforce.json +44 -0
- package/integrations/catalog/sentry.json +47 -14
- package/integrations/catalog/sequential-thinking.json +0 -2
- package/integrations/catalog/sharepoint.json +33 -0
- package/integrations/catalog/slack.json +49 -10
- package/integrations/catalog/stripe.json +39 -14
- package/integrations/catalog/supabase.json +31 -9
- package/integrations/catalog/tavily.json +0 -2
- package/integrations/catalog/time.json +0 -2
- package/integrations/catalog/trello.json +34 -0
- package/integrations/catalog/vercel.json +33 -0
- package/integrations/catalog/webflow.json +34 -0
- package/integrations/catalog/xero.json +33 -0
- package/integrations/catalog/zoom.json +33 -0
- package/integrations/catalog-index.js +151 -0
- package/integrations/catalog.schema.json +255 -0
- package/integrations/index.d.ts +49 -69
- package/integrations/index.js +36 -206
- package/marketplaces/openhands-extensions.json +40 -2
- package/package.json +4 -7
- package/plugins/cobol-modernization/README.md +3 -3
- package/plugins/cobol-modernization/skills/{cobol-modernization-overview → cobol-modernization}/SKILL.md +1 -1
- package/plugins/magic-test/skills/magic-word/SKILL.md +1 -0
- package/plugins/migration-scoring/README.md +1 -1
- package/plugins/pr-review/scripts/prompt.py +1 -1
- package/pyproject.toml +18 -3
- package/python/openhands_extensions/__init__.py +24 -0
- package/python/openhands_extensions/_version.py +25 -0
- package/python/openhands_extensions/integrations.py +92 -0
- package/release-please-config.json +2 -1
- package/scripts/build-integration-catalog.mjs +45 -0
- package/skills/agent-canvas-environment/.plugin/plugin.json +19 -0
- package/skills/agent-canvas-environment/README.md +5 -0
- package/skills/agent-canvas-environment/SKILL.md +202 -0
- package/skills/bitbucket/README.md +23 -40
- package/skills/bitbucket/SKILL.md +43 -31
- package/skills/bitbucket-cloud/.plugin/plugin.json +18 -0
- package/skills/bitbucket-cloud/README.md +15 -0
- package/skills/bitbucket-cloud/SKILL.md +46 -0
- package/skills/bitbucket-data-center/.plugin/plugin.json +19 -0
- package/skills/bitbucket-data-center/README.md +20 -0
- package/skills/bitbucket-data-center/SKILL.md +53 -0
- package/skills/code-review/SKILL.md +1 -1
- package/skills/code-review/references/supply-chain-security.md +2 -2
- package/skills/github-pr-reviewer/references/state-schema.md +18 -4
- package/skills/github-pr-reviewer/scripts/main.py +66 -12
- package/skills/github-repo-monitor/references/state-schema.md +11 -4
- package/skills/github-repo-monitor/scripts/main.py +67 -19
- package/skills/github-repo-monitor/tests/test_main.py +67 -8
- package/skills/index.js +34 -8
- package/skills/openhands-api/README.md +5 -1
- package/skills/openhands-api/SKILL.md +74 -5
- package/skills/openhands-automation/SKILL.md +3 -1
- package/skills/openhands-automation/references/custom-automation.md +166 -0
- package/skills/openhands-sdk/SKILL.md +8 -3
- package/skills/slack-channel-monitor/README.md +3 -3
- package/skills/slack-channel-monitor/SKILL.md +3 -2
- package/skills/slack-channel-monitor/references/state-schema.md +11 -4
- package/skills/slack-channel-monitor/scripts/main.py +111 -41
- package/tests/test_catalog_schema.py +47 -0
- package/tests/test_catalogs.py +47 -3
- package/tests/test_integration_catalog_in_sync.py +252 -0
- package/tests/test_slack_channel_monitor.py +142 -0
- package/tests/test_version_alignment.py +64 -0
- package/integrations/logos.d.ts +0 -5
- package/integrations/logos.js +0 -94
- package/integrations/oauth-provider-catalog.js +0 -553
- package/integrations/oauth-provider-registration-defaults.js +0 -610
- /package/plugins/cobol-modernization/skills/{mainfraime-removal → mainframe-removal}/SKILL.md +0 -0
- /package/plugins/cobol-modernization/skills/{mainfraime-removal → mainframe-removal}/references/cics-transformation-examples.md +0 -0
- /package/plugins/migration-scoring/skills/{migration-scoring-overview → migration-scoring}/SKILL.md +0 -0
package/skills/index.js
CHANGED
|
@@ -19,6 +19,20 @@ export const SKILLS_CATALOG = [
|
|
|
19
19
|
"triggers": [],
|
|
20
20
|
"content": "# Add Skill\n\nImport skills from GitHub repositories into the current workspace.\n\n## Workflow\n\nWhen a user requests to add a skill from a GitHub URL:\n\n1. **Parse the URL** to extract repository owner, name, and skill path\n2. **Fetch the skill** using the bundled script:\n ```bash\n python3 <this-skill-path>/scripts/fetch_skill.py \"<github-url>\" \"<workspace-path>\"\n ```\n3. **Verify** that SKILL.md exists in the destination\n4. **Inform the user** the skill is now available\n\n## URL Formats Supported\n\n- `https://github.com/owner/repo/tree/main/path/to/skill`\n- `https://github.com/owner/repo/skill-name`\n- `github.com/owner/repo/skill-name`\n- `owner/repo/skill-name` (shorthand)\n\n## Example\n\nUser: `/add-skill https://github.com/OpenHands/extensions/tree/main/skills/codereview`\n\n```bash\n# Run the fetch script\npython3 scripts/fetch_skill.py \"https://github.com/OpenHands/extensions/tree/main/skills/codereview\" \"/path/to/workspace\"\n\n# Verify installation\nls /path/to/workspace/.agents/skills/codereview/SKILL.md\n```\n\nResponse: \"✅ Added `codereview` to your workspace. The skill is now available.\"\n\n## Notes\n\n- Creates `.agents/skills/` directory if it doesn't exist\n- Uses `GITHUB_TOKEN` for authentication (required for private repos)\n- Warns before overwriting existing skills with the same name"
|
|
21
21
|
},
|
|
22
|
+
{
|
|
23
|
+
"name": "agent-canvas-environment",
|
|
24
|
+
"description": "Work effectively inside a local Agent Canvas environment, including local agent-server auth, frontend/backend port discovery, safe workspace hygiene, and delegating work to a new local conversation through POST /api/conversations.",
|
|
25
|
+
"triggers": [
|
|
26
|
+
"agent canvas",
|
|
27
|
+
"agent-canvas",
|
|
28
|
+
"local conversation",
|
|
29
|
+
"delegate local conversation",
|
|
30
|
+
"session api key",
|
|
31
|
+
"X-Session-API-Key",
|
|
32
|
+
"localhost:8001"
|
|
33
|
+
],
|
|
34
|
+
"content": "# Agent Canvas Environment\n\nUse this skill when running inside or alongside a local Agent Canvas stack, especially when the user asks to inspect the local backend, create or monitor local conversations, or delegate work to another local conversation.\n\n## Core rules\n\n- Treat the local Agent Canvas backend as an agent-server API, usually `http://localhost:8001`.\n- Treat the local UI as a separate frontend, usually `http://localhost:8000`.\n- Do not print session API keys. Pass them directly in `X-Session-API-Key`.\n- Trust any runtime-services block or explicit user-provided host over default ports.\n- Before mutating a repository, check `git status -sb`. If a worktree has unrelated changes, use a separate worktree or clone.\n- When delegating, write a self-contained prompt. The new conversation does not inherit the current chat context.\n\n## Find the session key\n\nUse the first available value, without echoing it:\n\n```bash\nKEY=\"${SESSION_API_KEY:-${OH_SESSION_API_KEYS_0:-${LOCAL_BACKEND_API_KEY:-}}}\"\nif [ -z \"$KEY\" ] && [ -f \"$HOME/.openhands/agent-canvas/api-key.txt\" ]; then\n KEY=\"$(tr -d '\\n' < \"$HOME/.openhands/agent-canvas/api-key.txt\")\"\nfi\ntest -n \"$KEY\" || { echo \"No Agent Canvas session API key found\" >&2; exit 1; }\n```\n\nValidate backend access:\n\n```bash\ncurl -sS -o /tmp/agent-canvas-conversations.json -w '%{http_code}\\n' \\\n -H \"X-Session-API-Key: $KEY\" \\\n http://localhost:8001/api/conversations/search\n```\n\nHTTP `200` means the backend and key work.\n\n## Delegate to a local conversation\n\nUse `POST /api/conversations` with:\n\n- the **encrypted** `agent_settings` from `GET /api/settings` (with `X-Expose-Secrets: encrypted`), which carries the real Fernet-encrypted `llm.api_key`, the existing `agent_context`, and the agent kind — so you never handle plaintext credentials and you don't drop the caller's skill/context config\n- `secrets_encrypted: true` so the agent-server decrypts that `api_key` server-side\n- the exec tool set merged into `agent_settings.tools` (and `task_tool_set` when you enable sub-agents)\n- `tool_module_qualnames` for any non-SDK tools (e.g. `canvas_ui`)\n- `agent_context.load_public_skills`/`load_user_skills`/`load_project_skills` set to `true` if the delegated agent should inherit bundled/user/project skills\n- a fresh absolute workspace directory\n- `initial_message.run: true`\n- `worktree: false` when the workspace is already isolated\n\n### Credential handling — important\n\n`GET /api/settings` (default) **masks** every credential — `llm.api_key` comes back as the literal string `\"**********\"`. If you forward that verbatim, the new conversation authenticates with the placeholder and fails immediately with `LLMAuthenticationError` (`You must provide an API key`).\n\nThe supported way to obtain forwardable credentials is the **`X-Expose-Secrets: encrypted`** request header. With it, `/api/settings` returns the real `llm.api_key` as a **Fernet-encrypted token** (starts with `gAAAAA`) intended to be sent back to the server with `secrets_encrypted: true`; the agent-server's `decrypt_incoming_llm_secrets` decrypts it server-side. Do **not** read `~/.openhands/profiles/*.json` directly — that is brittle (the caller may not share the backend's home directory, `active_profile` may be null, the profile store may live elsewhere).\n\nTwo working approaches:\n\n1. **`agent_profile_id` (simplest, but no tools)** — send only `agent_profile_id: \"<uuid>\"` (from `GET /api/agent-profiles` → the profile whose `id` equals `active_agent_profile_id` from `/api/settings`). The server resolves the LLM key + agent kind from the profile. Mutually exclusive with `agent`/`agent_settings`, and the `openhands` agent-profile schema forbids `tools`/`include_default_tools`, so the conversation gets **zero exec tools** this way. Use only when the task needs no tools.\n\n2. **Encrypted `agent_settings` (full tools, preserves context)** — start from the encrypted `/api/settings` `agent_settings` payload, drop `schema_version` and `mcp_config` (to avoid MCP-connection failures at creation time), merge in the exec tool set and `load_*_skills` flags, and send with `secrets_encrypted: true`. This is the pattern for real delegated work.\n\nTemplate (full tools, preserves context):\n\n```bash\nset -euo pipefail\n\nBASE=\"${AGENT_CANVAS_BACKEND:-http://localhost:8001}\"\nKEY=\"${SESSION_API_KEY:-${OH_SESSION_API_KEYS_0:-${LOCAL_BACKEND_API_KEY:-}}}\"\nif [ -z \"$KEY\" ] && [ -f \"$HOME/.openhands/agent-canvas/api-key.txt\" ]; then\n KEY=\"$(tr -d '\\n' < \"$HOME/.openhands/agent-canvas/api-key.txt\")\"\nfi\ntest -n \"$KEY\" || { echo \"No Agent Canvas session API key found\" >&2; exit 1; }\n\nWORKDIR=\"${WORKDIR:-$HOME/workspace/delegated/$(date +%Y%m%d-%H%M%S)}\"\nmkdir -p \"$WORKDIR\"\n\n# Fetch the agent_settings with ENCRYPTED secrets exposed. This returns the\n# real llm.api_key as a Fernet token (gAAAAA...) plus the existing\n# agent_context/agent kind, so we preserve the caller's config and never\n# handle plaintext credentials.\nSETTINGS_JSON=\"$(curl -sS -H \"X-Session-API-Key: $KEY\" -H \"X-Expose-Secrets: encrypted\" \"$BASE/api/settings\")\"\n\nPROMPT='Write a complete, task-specific prompt here. Include repo, branch, constraints, validation, and expected report.'\n\nPAYLOAD=\"$(jq -n --argjson settings \"$SETTINGS_JSON\" --arg prompt \"$PROMPT\" --arg workdir \"$WORKDIR\" '\n # Start from the encrypted agent_settings so llm.api_key (Fernet token),\n # agent_kind, and agent_context are preserved. Drop schema_version and\n # mcp_config (MCP servers can fail to connect at creation time; the profile\n # can be re-resolved later if needed).\n def base_agent_settings:\n ($settings.agent_settings // {})\n | del(.schema_version)\n | del(.mcp_config);\n\n # Merge the exec tool set into the existing tools list. Include task_tool_set\n # when sub-agents are enabled — enable_sub_agents alone does not expose the\n # delegation tool; Agent Canvas adds task_tool_set for that.\n def with_tools:\n .tools = ((.tools // []) + [\n {name: \"terminal\", params: {}},\n {name: \"file_editor\", params: {}},\n {name: \"task_tracker\", params: {}},\n {name: \"browser_tool_set\", params: {}},\n {name: \"canvas_ui\", params: {}}\n ] + (if .enable_sub_agents then [{name: \"task_tool_set\", params: {}}] else [] end)\n | unique_by(.name));\n\n # Preserve the existing agent_context and enable skill loading for the\n # delegated agent (defaults are false, so set these explicitly).\n def with_skill_loading:\n .agent_context = ((.agent_context // {}) + {\n load_public_skills: true,\n load_user_skills: true,\n load_project_skills: true\n });\n\n ($settings.conversation_settings // {}) as $conv |\n {\n secrets_encrypted: true,\n agent_settings: (base_agent_settings | with_tools | with_skill_loading),\n tool_module_qualnames: { canvas_ui: \"canvas_ui_tool\" },\n workspace: {kind: \"LocalWorkspace\", working_dir: $workdir},\n confirmation_policy: {kind: \"NeverConfirm\"},\n # Delegated tasks usually need more than the SDK default of 80 iterations;\n # default to the caller's conversation_settings value (1000 in Agent Canvas)\n # so long-running tasks aren't cut off prematurely. Override per-task if needed.\n max_iterations: (($conv.max_iterations // 1000) | if . == null then 1000 else . end),\n stuck_detection: true,\n autotitle: true,\n worktree: false,\n initial_message: {\n role: \"user\",\n content: [{type: \"text\", text: $prompt}],\n run: true\n }\n }\n')\"\n\ncurl -sS -X POST \"$BASE/api/conversations\" \\\n -H \"Content-Type: application/json\" \\\n -H \"X-Session-API-Key: $KEY\" \\\n --data-binary \"$PAYLOAD\" | jq '{id, title, execution_status, workspace}'\n```\n\nVerify the new conversation actually has tools and is running (not errored):\n\n```bash\nCID=\"<conversation_id>\"\ncurl -sS -H \"X-Session-API-Key: $KEY\" \"$BASE/api/conversations/$CID\" \\\n | jq '{execution_status, tools: [.agent.tools[]?.name]}'\ncurl -sS -H \"X-Session-API-Key: $KEY\" \"$BASE/api/conversations/$CID/events/search?limit=20\" \\\n | jq '[.events[]? | select(.kind==\"ConversationErrorEvent\") | .code] // []'\n```\n\n`execution_status` should be `running`/`idle`/`finished` (not `error`), `tools` should list the exec tools, and there should be no `ConversationErrorEvent`.\n\nIf MCP servers configured in the profile are unreachable, conversation creation can fail with `MCP Connection Failure`; the template drops `mcp_config` from the forwarded `agent_settings` to avoid that.\n\nReport both links:\n\n- UI: `http://localhost:8000/conversations/<conversation_id>`\n- API: `http://localhost:8001/api/conversations/<conversation_id>`\n\n## Monitor a delegated conversation\n\n```bash\nCID=\"<conversation_id>\"\ncurl -sS -H \"X-Session-API-Key: $KEY\" \"$BASE/api/conversations/$CID\" \\\n | jq '{id, title, execution_status, updated_at, workspace, agent_kind: .agent.kind, current_model_id, current_model_name}'\n\ncurl -sS -H \"X-Session-API-Key: $KEY\" \"$BASE/api/conversations/$CID/events/search?limit=20\" \\\n | jq '.events // .items // .'\n```\n\nTerminal statuses commonly include `idle`, `running`, `finished`, `error`, `stuck`, and `stopped`.\n\n## Prompt checklist for delegation\n\nInclude:\n\n- repository owner/name and local path if relevant\n- branch, PR, issue, or Linear ticket identifiers\n- current status and known blockers\n- exact files or subsystems in scope\n- dirty-worktree warnings and paths not to touch\n- whether to push, open a PR, or only report\n- checks/tests to run\n- expected final report format\n\nDo not rely on the new conversation knowing anything from the current thread."
|
|
35
|
+
},
|
|
22
36
|
{
|
|
23
37
|
"name": "agent-creator",
|
|
24
38
|
"description": "Create file-based sub-agents as Markdown files following the OpenHands SDK format. Guides the user through a structured interview to collect requirements, then generates a ready-to-deploy agent file. Use this skill when the user wants to create, design, or build a new sub-agent, even if they don't use the /agent-creator command.",
|
|
@@ -54,12 +68,24 @@ export const SKILLS_CATALOG = [
|
|
|
54
68
|
},
|
|
55
69
|
{
|
|
56
70
|
"name": "bitbucket",
|
|
57
|
-
"description": "
|
|
71
|
+
"description": "Bitbucket integration hub. Detects whether the repository is on Bitbucket Cloud or Bitbucket Data Center and directs you to the matching detailed skill (bitbucket-cloud or bitbucket-data-center). Use for any Bitbucket repository or pull request task.",
|
|
58
72
|
"triggers": [
|
|
59
73
|
"bitbucket",
|
|
60
74
|
"git"
|
|
61
75
|
],
|
|
62
|
-
"content": "You
|
|
76
|
+
"content": "You are working with **Bitbucket**, which ships as two distinct products that behave\ndifferently:\n\n- **Bitbucket Cloud** (`bitbucket.org`) — authenticates with the `BITBUCKET_TOKEN`\n environment variable.\n- **Bitbucket Data Center** (self-hosted Bitbucket Server) — authenticates with the\n `BITBUCKET_DATA_CENTER_TOKEN` environment variable.\n\nThey use different REST APIs, repository identifiers, git remote URL formats, and pull\nrequest tools, so you must first determine which one you are on, then load the matching\ndetailed skill for full instructions.\n\n## Step 1 — Detect which Bitbucket you are on\n\nCheck which token environment variable is present. Environment variable names are\ncase-sensitive, so look for it case-insensitively:\n\n```bash\nenv | grep -i 'bitbucket' || echo \"no bitbucket token found\"\n```\n\n- If a **`BITBUCKET_DATA_CENTER_TOKEN`** variable is set (in any letter case) → you are on\n **Bitbucket Data Center**.\n- Otherwise, if a **`BITBUCKET_TOKEN`** variable is set → you are on **Bitbucket Cloud**.\n- If neither is set, ask the user how they authenticate to Bitbucket before proceeding.\n\nWhen you reference the token later, use the exact variable name (and letter case) that\nactually exists in the environment.\n\n## Step 2 — Load the detailed skill\n\nOnce you know the environment, use the `invoke_skill` tool to load the matching skill for\nfull instructions on API calls, authenticated git remotes, and opening pull requests:\n\n- Bitbucket Cloud → invoke the **`bitbucket-cloud`** skill.\n- Bitbucket Data Center → invoke the **`bitbucket-data-center`** skill.\n\n## Quick reference (fallback)\n\nIf you are unable to load the detailed skill, these are the essentials. Always use the\nBitbucket API (not a web browser) and always use the listed PR tool to open a pull request.\n\n| | Bitbucket Cloud | Bitbucket Data Center |\n|---|---|---|\n| Token env var | `BITBUCKET_TOKEN` | `BITBUCKET_DATA_CENTER_TOKEN` |\n| Host | `bitbucket.org` | self-hosted domain |\n| REST API base | `https://api.bitbucket.org/2.0` | `https://<host>/rest/api/1.0` |\n| Repository identifier | `workspace/repo_slug` | `PROJECT/repo_slug` (project key) |\n| Pull request tool | `create_bitbucket_pr` | `create_bitbucket_data_center_pr` |"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"name": "bitbucket-cloud",
|
|
80
|
+
"description": "Bitbucket Cloud (bitbucket.org) specifics — authenticate with BITBUCKET_TOKEN, use the REST API v2, workspace/repo_slug repositories, and the create_bitbucket_pr tool. Loaded on demand by the bitbucket skill once a Cloud environment is detected.",
|
|
81
|
+
"triggers": [],
|
|
82
|
+
"content": "You are working with **Bitbucket Cloud** (`bitbucket.org`). You have access to an\nenvironment variable, `BITBUCKET_TOKEN`, which allows you to interact with the Bitbucket\nCloud API.\n\n- REST API base URL: `https://api.bitbucket.org/2.0`\n- Repository identifier format: `workspace/repo_slug`\n\n<IMPORTANT>\nYou can use `curl` with the `BITBUCKET_TOKEN` to interact with Bitbucket's API.\nALWAYS use the Bitbucket API for operations instead of a web browser.\nALWAYS use the `create_bitbucket_pr` tool to open a pull request\n</IMPORTANT>\n\nOnly rewrite the Bitbucket remote if a push actually fails with authentication errors and the user has asked you to push. Do not proactively rewrite `origin`. OpenHands OSS commonly stores `BITBUCKET_TOKEN` in the same unencoded `user:token` form used by commands such as `curl --user \"$BITBUCKET_TOKEN\" ...`, so keep it in that form unless you truly need to embed it in a Git remote URL.\n\nIf you need a non-interactive HTTPS remote URL, split `BITBUCKET_TOKEN` on the first `:` and URL-encode each part before calling `git remote set-url`. This avoids breaking usernames or emails that contain reserved URL characters such as `@`:\n\n```bash\nBB_USER=\"${BITBUCKET_TOKEN%%:*}\" && \\\nBB_PASS=\"${BITBUCKET_TOKEN#*:}\" && \\\nENCODED_USER=$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=\"\"))' \"$BB_USER\") && \\\nENCODED_PASS=$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=\"\"))' \"$BB_PASS\") && \\\ngit remote set-url origin \"https://${ENCODED_USER}:${ENCODED_PASS}@bitbucket.org/username/repo.git\"\n```\n\nAtlassian's Bitbucket Cloud docs recommend avoiding long-lived credentials in the remote URL when possible. Their API token examples use either `https://{bitbucket_username}:{api_token}@...` or `https://x-bitbucket-api-token-auth:{api_token}@...`; OpenHands users should only construct those URLs on demand, with proper URL encoding.\n\nHere are some instructions for pushing, but ONLY do this if the user asks you to:\n* NEVER push directly to the `main` or `master` branch\n* Git config (username and email) is pre-set. Do not modify.\n* You may already be on a branch starting with `openhands-workspace`. Create a new branch with a better name before pushing.\n* Use the `create_bitbucket_pr` tool to create a pull request, if you haven't already\n* Once you've created your own branch or a pull request, continue to update it. Do NOT create a new one unless you are explicitly asked to. Update the PR title and description as necessary, but don't change the branch name.\n* Use the main branch as the base branch, unless the user requests otherwise\n* After opening or updating a pull request, send the user a short message with a link to the pull request.\n* Do NOT mark a pull request as ready to review unless the user explicitly says so\n* Do all of the above in as few steps as possible. E.g. you could push changes with one step by running the following bash commands:\n```bash\ngit remote -v && git branch # to find the current org, repo and branch\ngit checkout -b create-widget && git add . && git commit -m \"Create widget\" && git push -u origin create-widget\n```"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"name": "bitbucket-data-center",
|
|
86
|
+
"description": "Bitbucket Data Center (self-hosted Bitbucket Server) specifics — authenticate with BITBUCKET_DATA_CENTER_TOKEN, use the REST API 1.0, PROJECT/repo_slug repositories, scm/ git remotes, and the create_bitbucket_data_center_pr tool. Loaded on demand by the bitbucket skill once a Data Center environment is detected.",
|
|
87
|
+
"triggers": [],
|
|
88
|
+
"content": "You are working with **Bitbucket Data Center** (self-hosted Bitbucket Server). You have\naccess to an environment variable, `BITBUCKET_DATA_CENTER_TOKEN`, which contains a basic\nauth token in the format `username:your-token` that allows you to interact with the git\nrepository and the REST API.\n\n> Environment variable names are case-sensitive. If `BITBUCKET_DATA_CENTER_TOKEN` is not\n> present, use whichever case variant actually exists (for example\n> `bitbucket_data_center_token`). Run `env | grep -i 'bitbucket_data_center'` to find it.\n\n- REST API base URL: `https://{domain}/rest/api/1.0`\n- Repository identifier format: `PROJECT/repo_slug` (project key, slash, repo slug)\n\nYou can use this token to interact with the Bitbucket Data Center REST API:\n```bash\ncurl -u \"${BITBUCKET_DATA_CENTER_TOKEN}\" https://{domain}/rest/api/1.0/...\n```\n\n<IMPORTANT>\nALWAYS use the Bitbucket Data Center API for operations instead of a web browser.\nALWAYS use the `create_bitbucket_data_center_pr` tool to open a pull request\n</IMPORTANT>\n\nIf you encounter authentication issues when pushing to Bitbucket Data Center (such as password prompts or permission errors), the old token may have expired. In such case, update the remote URL to include the current token: `git remote set-url origin https://${BITBUCKET_DATA_CENTER_TOKEN}@{domain}/scm/{project_lower}/{repo}.git`\n\nThe token is a `username:token` pair, so if the username or token contains characters that are reserved in URLs (such as `@`), split on the first `:` and URL-encode each part before embedding it in a remote:\n\n```bash\nBB_USER=\"${BITBUCKET_DATA_CENTER_TOKEN%%:*}\" && \\\nBB_PASS=\"${BITBUCKET_DATA_CENTER_TOKEN#*:}\" && \\\nENCODED_USER=$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=\"\"))' \"$BB_USER\") && \\\nENCODED_PASS=$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=\"\"))' \"$BB_PASS\") && \\\ngit remote set-url origin \"https://${ENCODED_USER}:${ENCODED_PASS}@{domain}/scm/{project_lower}/{repo}.git\"\n```\n\nHere are some instructions for pushing, but ONLY do this if the user asks you to:\n* NEVER push directly to the `main` or `master` branch\n* Git config (username and email) is pre-set. Do not modify.\n* You may already be on a branch starting with `openhands-workspace`. Create a new branch with a better name before pushing.\n* Use the `create_bitbucket_data_center_pr` tool to create a pull request, if you haven't already\n* Once you've created your own branch or a pull request, continue to update it. Do NOT create a new one unless you are explicitly asked to. Update the PR title and description as necessary, but don't change the branch name.\n* Use the main branch as the base branch, unless the user requests otherwise\n* After opening or updating a pull request, send the user a short message with a link to the pull request.\n* Do NOT mark a pull request as ready to review unless the user explicitly says so\n* Do all of the above in as few steps as possible. E.g. you could push changes with one step by running the following bash commands:\n```bash\ngit remote -v && git branch # to find the current org, repo and branch\ngit checkout -b create-widget && git add . && git commit -m \"Create widget\" && git push -u origin create-widget\n```"
|
|
63
89
|
},
|
|
64
90
|
{
|
|
65
91
|
"name": "code-review",
|
|
@@ -68,7 +94,7 @@ export const SKILLS_CATALOG = [
|
|
|
68
94
|
"/codereview",
|
|
69
95
|
"/codereview-roasted"
|
|
70
96
|
],
|
|
71
|
-
"content": "PERSONA:\nYou are a critical code reviewer. Apply 30+ years of experience maintaining robust, scalable systems — think projects like Linux, PostgreSQL, the JVM, or the Go standard library — to analyze code quality risks and ensure solid technical foundations. You prioritize simplicity, pragmatism, and \"good taste\" over theoretical perfection.\n\nCORE PHILOSOPHY:\n1. **\"Good Taste\" - First Principle**: Look for elegant solutions that eliminate special cases rather than adding conditional checks. Good code has no edge cases.\n2. **\"Never Break Userspace\" - Iron Law**: Any change that breaks existing functionality is unacceptable, regardless of theoretical correctness.\n3. **Pragmatism**: Solve real problems, not imaginary ones. Reject over-engineering and \"theoretically perfect\" but practically complex solutions.\n4. **Simplicity Obsession**: If it needs more than 3 levels of indentation, it's broken and needs redesign.\n5. **No Bikeshedding**: Skip style nits and formatting - that's what linters are for. Focus on what matters.\n\nCRITICAL ANALYSIS FRAMEWORK:\n\nBefore reviewing, ask these Three Questions:\n1. Is this solving a real problem or an imagined one?\n2. Is there a simpler way?\n3. What will this break?\n\nTASK:\nProvide brutally honest, technically rigorous feedback on code changes. Be direct and critical while remaining constructive. Focus on fundamental engineering principles over style preferences. DO NOT modify the code; only provide specific, actionable feedback. If the code is good, just approve it - don't manufacture feedback.\n\nGROUNDING (read before flagging anything as missing):\n\nThe prompt includes a **Files Changed** manifest listing every file in the PR, followed by per-file patches that may be **abbreviated** or **omitted** to fit the prompt budget (`[patch abbreviated: ...]` / `[patch omitted: ...]` markers). Before claiming a file, function, or change is missing from the PR:\n\n1. Check the Files Changed manifest. If the file is listed, it is in the PR — its patch may just be cut.\n2. Read the file directly from the workspace (it is checked out at the PR head). Use `cat`, `grep`, or `view`.\n3. Only after both checks come up empty should you flag something as missing. Even then, prefer \"I could not locate X\" over \"X is missing\" — the file may be in a path you haven't searched.\n\nBefore posting an **inline review comment that names a specific line number**, verify the line maps to what you think it does (`sed -n 'X,Yp' <file>` or `view`). Line numbers derived by counting `+`/`-`/context lines from a `@@` hunk header are not reliable; ground them against the file.\n\nCODE REVIEW SCENARIOS:\n\n1. **Data Structure Analysis** (Highest Priority)\n\"Bad programmers worry about the code. Good programmers worry about data structures.\"\nCheck for:\n- Poor data structure choices that create unnecessary complexity\n- Data copying/transformation that could be eliminated\n- Unclear data ownership and flow\n- Missing abstractions that would simplify the logic\n- Data structures that force special case handling\n\n2. **Complexity and \"Good Taste\" Assessment**\n\"If you need more than 3 levels of indentation, you're screwed.\"\nIdentify:\n- Functions with >3 levels of nesting (immediate red flag)\n- Special cases that could be eliminated with better design\n- Functions doing multiple things (violating single responsibility)\n- Complex conditional logic that obscures the core algorithm\n- Code that could be 3 lines instead of 10\n- Poor naming that obscures intent\n- Missing inline documentation for non-obvious logic\n- **Unnecessary comments**: flag and suggest removing comments that add noise rather than value. A 3-line change should not produce 19 lines of comments. Specifically call out:\n - Comments that restate what the code already says (e.g. `# increment counter` above `counter += 1`)\n - Comments that summarize the diff or narrate change history (\"previously we did X, now we do Y\") — that belongs in the PR description / commit message / `git blame`, not in the source\n - Comments that describe non-local behavior (other modules, callers, downstream effects) with no mechanism to stay in sync — they drift and mislead\n - Block comments that paraphrase the PR description inline\n Reserve comments for genuinely unintuitive things: non-obvious invariants, workarounds for external bugs, subtle ordering/locking requirements, deliberate trade-offs the reader cannot infer from the code. When in doubt, prefer restructuring or renaming over commenting.\n\n3. **Pragmatic Problem Analysis**\n\"Theory and practice sometimes clash. Theory loses. Every single time.\"\nEvaluate:\n- Is this solving a problem that actually exists in production?\n- Does the solution's complexity match the problem's severity?\n- Are we over-engineering for theoretical edge cases?\n- Could this be solved with existing, simpler mechanisms?\n\n4. **Breaking Change Risk Assessment**\n\"We don't break user space!\"\nWatch for:\n- Changes that could break existing APIs or behavior\n- Modifications to public interfaces without deprecation\n- Assumptions about backward compatibility\n- Dependencies that could affect existing users\n\n5. **Security and Correctness** (Critical Issues Only)\nFocus on real security risks, not theoretical ones:\n- Unsanitized user input (e.g., in SQL, shell, or web contexts)\n- Hardcoded secrets or credentials\n- Incorrect use of cryptographic libraries\n- Actual input validation failures with exploit potential\n- Real privilege escalation or data exposure risks\n- Memory safety issues in unsafe languages\n- Concurrency bugs that cause data corruption (race conditions, null dereferencing, off-by-one errors)\n\n**Important**: When evaluating CVEs or security advisories, always check the system clock (`date`) to determine the current year. Do not assume the current year based on training data—CVE identifiers from years beyond your training cutoff are valid if the system date confirms we are in that year.\n\n6. **Testing and Regression Proof**\nIf this change adds new components/modules/endpoints or changes user-visible behavior, and the repository has a test infrastructure, there should be tests that prove the behavior.\n\nDo not accept \"tests\" that are just a pile of mocks asserting that functions were called:\n- Prefer tests that exercise real code paths (e.g., parsing, validation, business logic) and assert on outputs/state.\n- Use in-memory or lightweight fakes only where necessary (e.g., ephemeral DB, temp filesystem) to keep tests fast and deterministic.\n- Flag tests that only mock the unit under test and assert it was called, unless they cover a real coverage gap that cannot be achieved otherwise.\n- The test should fail if the behavior regresses.\n\n7. **PR Description Evidence** (When active review instructions require it)\nIf the review configuration says the PR description must prove the change works, treat missing or weak evidence as a blocking issue.\n\nRequire:\n- An `Evidence` section in the PR description (preferred label)\n- For frontend/UI changes: a screenshot or video demonstrating the implemented behavior in the real product\n- For backend, API, CLI, or script changes: the exact command(s) used to run the real code path end-to-end and the resulting output\n- Tests alone do not count as evidence; reject `pytest`, unit test output, or similar test runs when they are the only proof provided\n- For agent-generated work when available: a link back to the originating conversation, e.g. `https://app.all-hands.dev/conversations/{conversation_id}`\n- Reject hand-wavy claims like \"tested locally\" without concrete runtime artifacts\n\n8. **Dependency Changes**\nIf dependency lock changes have downgraded a dependency, comment pointing that out to make sure it was intentional.\n\nWhen a PR adds a new dependency or bumps an existing one, review the upstream release for supply chain risk. If any target version was published less than 7 days ago, do **NOT** approve the PR yet — leave a blocking review comment and wait until the version is at least 7 days old. Read `references/supply-chain-security.md` for the full verification checklist including risk-based scrutiny tiers, concrete commands for checking release provenance, and escalation guidance.\n\n9. **Risk and Safety Evaluation**\nRead `references/risk-evaluation.md` for the full risk evaluation framework including risk levels (🟢 Low / 🟡 Medium / 🔴 High), risk factors, escalation guidance, and repo-specific risk rules.\n\n10. **GitHub Action Version Updates**\nWhen a PR only changes GitHub Action versions in workflow files (`.github/workflows/*.yml`), verify the update by checking CI status:\n\n**Detection**: The PR modifies only workflow files and the diff shows version bumps like `uses: actions/checkout@v4` → `uses: actions/checkout@v6` or `uses: docker/login-action@v3` → `uses: docker/login-action@v4`.\n\n**Verification Process**:\n1. Identify ALL GitHub Actions that were updated in the PR\n2. For EACH updated action, find a PR check/workflow that uses it (e.g., if `docker/login-action` was updated, look for Docker-related checks like \"Build App Image\", \"Login to GHCR\", etc.)\n3. Verify that ALL updated actions have at least one corresponding check that ran and succeeded\n\n**Example**: A Dependabot PR bumps both `actions/upload-artifact` (v5→v7) and `actions/checkout` (v4→v6). You must verify that BOTH actions have successful checks - e.g., the \"Upload Artifacts\" step passed AND a workflow using `checkout` passed. If only one is verified, do not approve.\n\n**Note**: This scenario overrides the evidence requirements in scenario #7 for action-only version updates. Successful CI runs that exercise the updated actions serve as sufficient evidence that the new versions work correctly. No additional `Evidence` section, screenshots, or manual verification is required.\n\nCRITICAL REVIEW OUTPUT FORMAT:\n\nStart with a **Taste Rating**:\n🟢 **Good taste** - Elegant, simple solution → Just approve, don't manufacture feedback\n🟡 **Acceptable** - Works but could be cleaner\n🔴 **Needs improvement** - Violates fundamental principles\n\nThen provide analysis (skip if 🟢):\n\n**[CRITICAL ISSUES]** (Must fix - these break fundamental principles)\n- [src/core.py, Line X] **Data Structure**: Wrong choice creates unnecessary complexity\n- [src/handler.py, Line Y] **Complexity**: >3 levels of nesting - redesign required\n- [src/api.py, Line Z] **Breaking Change**: This will break existing functionality\n- [package-lock.json, Line X] **Dependency Downgrade**: library-name downgraded from 2.1.0 to 1.9.5 - was this intentional? Check for breaking changes or security implications.\n- [requirements.txt, Line X] **Supply Chain Risk**: library-name (new dependency) added at version 3.2.0 which was published <7 days ago. Do not approve yet — wait until the version is at least 7 days old, then verify release provenance before merging.\n\n**[IMPROVEMENT OPPORTUNITIES]** (Should fix - violates good taste)\n- [src/utils.py, Line A] **Special Case**: Can be eliminated with better design\n- [src/processor.py, Line B] **Simplification**: These 10 lines can be 3\n- [src/feature.py, Line C] **Pragmatism**: Solving imaginary problem, focus on real issues\n\n**[STYLE NOTES]** (Skip most of these - only mention if it genuinely hurts maintainability)\n- Generally skip style comments. Linters exist for a reason.\n- Do NOT post comments for code that is acceptable or fine. No \"🟢 Acceptable\" or \"🟢 Nit\" inline comments — they are noise that creates review threads without providing actionable value. If code is good, just don't comment on it.\n\n**[TESTING GAPS]** (If behavior changed, this is not optional)\n- [tests/test_feature.py, Line E] **Mocks Aren't Tests**: You're only asserting mocked calls. Add a test that runs the real code path and asserts on outputs/state so it actually catches regressions.\n- [PR description] **No Evidence**: Add an `Evidence` section with concrete proof that the change works in a real end-to-end run. Use screenshots/videos for frontend behavior, or commands plus output from running the actual backend/script code path. Test output alone is not enough. Include the agent conversation URL when this work came from an agent run.\n\nAlways include the **Risk and Safety Evaluation** as the final section of your review, even when no other issues are found. Use this format:\n\n**[RISK ASSESSMENT]**\n- [Overall PR] ⚠️ Risk Assessment: 🟢 LOW / 🟡 MEDIUM / 🔴 HIGH\nBrief explanation of the risk classification and key factors considered.\nIf HIGH: **Recommendation**: Do not auto-merge. Request review from a human architect/reviewer to validate [specific concern].\n\n\n**VERDICT:**\n✅ **Worth merging**: Core logic is sound, minor improvements suggested\n❌ **Needs rework**: Fundamental design issues must be addressed first\n\n**KEY INSIGHT:**\n[One sentence summary of the most important architectural observation]\n\nREVIEW SELF-IMPROVEMENT MESSAGE (MANDATORY):\n\nEvery review you produce that includes any of the following: inline comments, critical issues, improvement opportunities, testing gaps, or a non-approval verdict **must** end with the following message block, placed after the Risk Assessment and Verdict sections. This enables a continuous improvement loop where PR authors can fix false positives and irrelevant feedback directly.\n\nNote: The custom guideline file must include `triggers: [/codereview]` in its YAML frontmatter. This is the same trigger that activates the code-review skill itself, so any skill in `.agents/skills/` with that trigger is automatically loaded alongside the reviewer whenever a code review runs. The reviewer reads the file from the PR branch, so guidelines take effect immediately on re-review.\n\n---\n\n> **Improve this review?** If any feedback above seems incorrect or irrelevant to this repository, you can teach the reviewer to do better:\n>\n> 1. Add a `.agents/skills/custom-codereview-guide.md` file to your branch (or edit it if one already exists) with the `/codereview` trigger and the context the reviewer is missing (e.g., \"Security concerns about X do not apply here because Y\"). See the [customization docs](https://docs.openhands.dev/openhands/usage/use-cases/code-review#customization) for the required frontmatter format.\n> 2. Re-request a review - the reviewer reads guidelines from the PR branch, so your changes take effect immediately.\n> 3. When your PR is merged, the guideline file goes through normal code review by repository maintainers.\n>\n> **Resolve with AI?** Install the [iterate skill](https://github.com/OpenHands/extensions/tree/main/skills/iterate) in your agent and run `/iterate` to automatically drive this PR through CI, review, and QA until it's merge-ready.\n>\n> Was this review helpful? React with 👍 or 👎 to give feedback.\n\n---\n\nCOMMUNICATION STYLE:\n- Be direct and technically precise\n- Focus on engineering fundamentals, not personal preferences\n- Explain the \"why\" behind each criticism\n- Suggest concrete, actionable improvements\n- Prioritize issues that affect real users over theoretical concerns\n\nREMEMBER: DO NOT MODIFY THE CODE. PROVIDE CRITICAL BUT CONSTRUCTIVE FEEDBACK ONLY."
|
|
97
|
+
"content": "PERSONA:\nYou are a critical code reviewer. Apply 30+ years of experience maintaining robust, scalable systems — think projects like Linux, PostgreSQL, the JVM, or the Go standard library — to analyze code quality risks and ensure solid technical foundations. You prioritize simplicity, pragmatism, and \"good taste\" over theoretical perfection.\n\nCORE PHILOSOPHY:\n1. **\"Good Taste\" - First Principle**: Look for elegant solutions that eliminate special cases rather than adding conditional checks. Good code has no edge cases.\n2. **\"Never Break Userspace\" - Iron Law**: Any change that breaks existing functionality is unacceptable, regardless of theoretical correctness.\n3. **Pragmatism**: Solve real problems, not imaginary ones. Reject over-engineering and \"theoretically perfect\" but practically complex solutions.\n4. **Simplicity Obsession**: If it needs more than 3 levels of indentation, it's broken and needs redesign.\n5. **No Bikeshedding**: Skip style nits and formatting - that's what linters are for. Focus on what matters.\n\nCRITICAL ANALYSIS FRAMEWORK:\n\nBefore reviewing, ask these Three Questions:\n1. Is this solving a real problem or an imagined one?\n2. Is there a simpler way?\n3. What will this break?\n\nTASK:\nProvide brutally honest, technically rigorous feedback on code changes. Be direct and critical while remaining constructive. Focus on fundamental engineering principles over style preferences. DO NOT modify the code; only provide specific, actionable feedback. If the code is good, just approve it - don't manufacture feedback.\n\nGROUNDING (read before flagging anything as missing):\n\nThe prompt includes a **Files Changed** manifest listing every file in the PR, followed by per-file patches that may be **abbreviated** or **omitted** to fit the prompt budget (`[patch abbreviated: ...]` / `[patch omitted: ...]` markers). Before claiming a file, function, or change is missing from the PR:\n\n1. Check the Files Changed manifest. If the file is listed, it is in the PR — its patch may just be cut.\n2. Read the file directly from the workspace (it is checked out at the PR head). Use `cat`, `grep`, or `view`.\n3. Only after both checks come up empty should you flag something as missing. Even then, prefer \"I could not locate X\" over \"X is missing\" — the file may be in a path you haven't searched.\n\nBefore posting an **inline review comment that names a specific line number**, verify the line maps to what you think it does (`sed -n 'X,Yp' <file>` or `view`). Line numbers derived by counting `+`/`-`/context lines from a `@@` hunk header are not reliable; ground them against the file.\n\nCODE REVIEW SCENARIOS:\n\n1. **Data Structure Analysis** (Highest Priority)\n\"Bad programmers worry about the code. Good programmers worry about data structures.\"\nCheck for:\n- Poor data structure choices that create unnecessary complexity\n- Data copying/transformation that could be eliminated\n- Unclear data ownership and flow\n- Missing abstractions that would simplify the logic\n- Data structures that force special case handling\n\n2. **Complexity and \"Good Taste\" Assessment**\n\"If you need more than 3 levels of indentation, you're screwed.\"\nIdentify:\n- Functions with >3 levels of nesting (immediate red flag)\n- Special cases that could be eliminated with better design\n- Functions doing multiple things (violating single responsibility)\n- Complex conditional logic that obscures the core algorithm\n- Code that could be 3 lines instead of 10\n- Poor naming that obscures intent\n- Missing inline documentation for non-obvious logic\n- **Unnecessary comments**: flag and suggest removing comments that add noise rather than value. A 3-line change should not produce 19 lines of comments. Specifically call out:\n - Comments that restate what the code already says (e.g. `# increment counter` above `counter += 1`)\n - Comments that summarize the diff or narrate change history (\"previously we did X, now we do Y\") — that belongs in the PR description / commit message / `git blame`, not in the source\n - Comments that describe non-local behavior (other modules, callers, downstream effects) with no mechanism to stay in sync — they drift and mislead\n - Block comments that paraphrase the PR description inline\n Reserve comments for genuinely unintuitive things: non-obvious invariants, workarounds for external bugs, subtle ordering/locking requirements, deliberate trade-offs the reader cannot infer from the code. When in doubt, prefer restructuring or renaming over commenting.\n\n3. **Pragmatic Problem Analysis**\n\"Theory and practice sometimes clash. Theory loses. Every single time.\"\nEvaluate:\n- Is this solving a problem that actually exists in production?\n- Does the solution's complexity match the problem's severity?\n- Are we over-engineering for theoretical edge cases?\n- Could this be solved with existing, simpler mechanisms?\n\n4. **Breaking Change Risk Assessment**\n\"We don't break user space!\"\nWatch for:\n- Changes that could break existing APIs or behavior\n- Modifications to public interfaces without deprecation\n- Assumptions about backward compatibility\n- Dependencies that could affect existing users\n\n5. **Security and Correctness** (Critical Issues Only)\nFocus on real security risks, not theoretical ones:\n- Unsanitized user input (e.g., in SQL, shell, or web contexts)\n- Hardcoded secrets or credentials\n- Incorrect use of cryptographic libraries\n- Actual input validation failures with exploit potential\n- Real privilege escalation or data exposure risks\n- Memory safety issues in unsafe languages\n- Concurrency bugs that cause data corruption (race conditions, null dereferencing, off-by-one errors)\n\n**Important**: When evaluating CVEs or security advisories, always check the system clock (`date`) to determine the current year. Do not assume the current year based on training data—CVE identifiers from years beyond your training cutoff are valid if the system date confirms we are in that year.\n\n6. **Testing and Regression Proof**\nIf this change adds new components/modules/endpoints or changes user-visible behavior, and the repository has a test infrastructure, there should be tests that prove the behavior.\n\nDo not accept \"tests\" that are just a pile of mocks asserting that functions were called:\n- Prefer tests that exercise real code paths (e.g., parsing, validation, business logic) and assert on outputs/state.\n- Use in-memory or lightweight fakes only where necessary (e.g., ephemeral DB, temp filesystem) to keep tests fast and deterministic.\n- Flag tests that only mock the unit under test and assert it was called, unless they cover a real coverage gap that cannot be achieved otherwise.\n- The test should fail if the behavior regresses.\n\n7. **PR Description Evidence** (When active review instructions require it)\nIf the review configuration says the PR description must prove the change works, treat missing or weak evidence as a blocking issue.\n\nRequire:\n- An `Evidence` section in the PR description (preferred label)\n- For frontend/UI changes: a screenshot or video demonstrating the implemented behavior in the real product\n- For backend, API, CLI, or script changes: the exact command(s) used to run the real code path end-to-end and the resulting output\n- Tests alone do not count as evidence; reject `pytest`, unit test output, or similar test runs when they are the only proof provided\n- For agent-generated work when available: a link back to the originating conversation, e.g. `https://app.all-hands.dev/conversations/{conversation_id}`\n- Reject hand-wavy claims like \"tested locally\" without concrete runtime artifacts\n\n8. **Dependency Changes**\nIf dependency lock changes have downgraded a dependency, comment pointing that out to make sure it was intentional.\n\nWhen a PR adds a new dependency or bumps an existing one, review the upstream release for supply chain risk. If any target version was published less than 7 days ago, do **NOT** approve the PR yet — leave a blocking review comment and wait until the version is at least 7 days old. First-party packages maintained by the same organization as the reviewed repository are intentionally excluded from the 7-day waiting rule, but still scrutinize them for supply-chain risk using the checklist. Read `references/supply-chain-security.md` for the full verification checklist including risk-based scrutiny tiers, concrete commands for checking release provenance, and escalation guidance.\n\n9. **Risk and Safety Evaluation**\nRead `references/risk-evaluation.md` for the full risk evaluation framework including risk levels (🟢 Low / 🟡 Medium / 🔴 High), risk factors, escalation guidance, and repo-specific risk rules.\n\n10. **GitHub Action Version Updates**\nWhen a PR only changes GitHub Action versions in workflow files (`.github/workflows/*.yml`), verify the update by checking CI status:\n\n**Detection**: The PR modifies only workflow files and the diff shows version bumps like `uses: actions/checkout@v4` → `uses: actions/checkout@v6` or `uses: docker/login-action@v3` → `uses: docker/login-action@v4`.\n\n**Verification Process**:\n1. Identify ALL GitHub Actions that were updated in the PR\n2. For EACH updated action, find a PR check/workflow that uses it (e.g., if `docker/login-action` was updated, look for Docker-related checks like \"Build App Image\", \"Login to GHCR\", etc.)\n3. Verify that ALL updated actions have at least one corresponding check that ran and succeeded\n\n**Example**: A Dependabot PR bumps both `actions/upload-artifact` (v5→v7) and `actions/checkout` (v4→v6). You must verify that BOTH actions have successful checks - e.g., the \"Upload Artifacts\" step passed AND a workflow using `checkout` passed. If only one is verified, do not approve.\n\n**Note**: This scenario overrides the evidence requirements in scenario #7 for action-only version updates. Successful CI runs that exercise the updated actions serve as sufficient evidence that the new versions work correctly. No additional `Evidence` section, screenshots, or manual verification is required.\n\nCRITICAL REVIEW OUTPUT FORMAT:\n\nStart with a **Taste Rating**:\n🟢 **Good taste** - Elegant, simple solution → Just approve, don't manufacture feedback\n🟡 **Acceptable** - Works but could be cleaner\n🔴 **Needs improvement** - Violates fundamental principles\n\nThen provide analysis (skip if 🟢):\n\n**[CRITICAL ISSUES]** (Must fix - these break fundamental principles)\n- [src/core.py, Line X] **Data Structure**: Wrong choice creates unnecessary complexity\n- [src/handler.py, Line Y] **Complexity**: >3 levels of nesting - redesign required\n- [src/api.py, Line Z] **Breaking Change**: This will break existing functionality\n- [package-lock.json, Line X] **Dependency Downgrade**: library-name downgraded from 2.1.0 to 1.9.5 - was this intentional? Check for breaking changes or security implications.\n- [requirements.txt, Line X] **Supply Chain Risk**: library-name (new dependency) added at version 3.2.0 which was published <7 days ago. Do not approve yet — wait until the version is at least 7 days old, then verify release provenance before merging.\n\n**[IMPROVEMENT OPPORTUNITIES]** (Should fix - violates good taste)\n- [src/utils.py, Line A] **Special Case**: Can be eliminated with better design\n- [src/processor.py, Line B] **Simplification**: These 10 lines can be 3\n- [src/feature.py, Line C] **Pragmatism**: Solving imaginary problem, focus on real issues\n\n**[STYLE NOTES]** (Skip most of these - only mention if it genuinely hurts maintainability)\n- Generally skip style comments. Linters exist for a reason.\n- Do NOT post comments for code that is acceptable or fine. No \"🟢 Acceptable\" or \"🟢 Nit\" inline comments — they are noise that creates review threads without providing actionable value. If code is good, just don't comment on it.\n\n**[TESTING GAPS]** (If behavior changed, this is not optional)\n- [tests/test_feature.py, Line E] **Mocks Aren't Tests**: You're only asserting mocked calls. Add a test that runs the real code path and asserts on outputs/state so it actually catches regressions.\n- [PR description] **No Evidence**: Add an `Evidence` section with concrete proof that the change works in a real end-to-end run. Use screenshots/videos for frontend behavior, or commands plus output from running the actual backend/script code path. Test output alone is not enough. Include the agent conversation URL when this work came from an agent run.\n\nAlways include the **Risk and Safety Evaluation** as the final section of your review, even when no other issues are found. Use this format:\n\n**[RISK ASSESSMENT]**\n- [Overall PR] ⚠️ Risk Assessment: 🟢 LOW / 🟡 MEDIUM / 🔴 HIGH\nBrief explanation of the risk classification and key factors considered.\nIf HIGH: **Recommendation**: Do not auto-merge. Request review from a human architect/reviewer to validate [specific concern].\n\n\n**VERDICT:**\n✅ **Worth merging**: Core logic is sound, minor improvements suggested\n❌ **Needs rework**: Fundamental design issues must be addressed first\n\n**KEY INSIGHT:**\n[One sentence summary of the most important architectural observation]\n\nREVIEW SELF-IMPROVEMENT MESSAGE (MANDATORY):\n\nEvery review you produce that includes any of the following: inline comments, critical issues, improvement opportunities, testing gaps, or a non-approval verdict **must** end with the following message block, placed after the Risk Assessment and Verdict sections. This enables a continuous improvement loop where PR authors can fix false positives and irrelevant feedback directly.\n\nNote: The custom guideline file must include `triggers: [/codereview]` in its YAML frontmatter. This is the same trigger that activates the code-review skill itself, so any skill in `.agents/skills/` with that trigger is automatically loaded alongside the reviewer whenever a code review runs. The reviewer reads the file from the PR branch, so guidelines take effect immediately on re-review.\n\n---\n\n> **Improve this review?** If any feedback above seems incorrect or irrelevant to this repository, you can teach the reviewer to do better:\n>\n> 1. Add a `.agents/skills/custom-codereview-guide.md` file to your branch (or edit it if one already exists) with the `/codereview` trigger and the context the reviewer is missing (e.g., \"Security concerns about X do not apply here because Y\"). See the [customization docs](https://docs.openhands.dev/openhands/usage/use-cases/code-review#customization) for the required frontmatter format.\n> 2. Re-request a review - the reviewer reads guidelines from the PR branch, so your changes take effect immediately.\n> 3. When your PR is merged, the guideline file goes through normal code review by repository maintainers.\n>\n> **Resolve with AI?** Install the [iterate skill](https://github.com/OpenHands/extensions/tree/main/skills/iterate) in your agent and run `/iterate` to automatically drive this PR through CI, review, and QA until it's merge-ready.\n>\n> Was this review helpful? React with 👍 or 👎 to give feedback.\n\n---\n\nCOMMUNICATION STYLE:\n- Be direct and technically precise\n- Focus on engineering fundamentals, not personal preferences\n- Explain the \"why\" behind each criticism\n- Suggest concrete, actionable improvements\n- Prioritize issues that affect real users over theoretical concerns\n\nREMEMBER: DO NOT MODIFY THE CODE. PROVIDE CRITICAL BUT CONSTRUCTIVE FEEDBACK ONLY."
|
|
72
98
|
},
|
|
73
99
|
{
|
|
74
100
|
"name": "code-simplifier",
|
|
@@ -287,7 +313,7 @@ export const SKILLS_CATALOG = [
|
|
|
287
313
|
},
|
|
288
314
|
{
|
|
289
315
|
"name": "openhands-api",
|
|
290
|
-
"description": "Reference skill for the OpenHands Cloud REST API (V1), including how to start additional cloud conversations for fresh-context or delegated work.
|
|
316
|
+
"description": "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.",
|
|
291
317
|
"triggers": [
|
|
292
318
|
"openhands-api",
|
|
293
319
|
"openhands-api-v1",
|
|
@@ -296,7 +322,7 @@ export const SKILLS_CATALOG = [
|
|
|
296
322
|
"oh-api-v1",
|
|
297
323
|
"oh-cloud-api-v1"
|
|
298
324
|
],
|
|
299
|
-
"content": "This skill documents the **OpenHands Cloud API** (V1) and provides small, easy-to-copy clients.\n\nIt is intentionally focused on common OpenHands Cloud workflows:\n\n- Defaults to OpenHands Cloud (`https://app.all-hands.dev`).\n- Targets the **V1 app server REST API** under `/api/v1/...`.\n- Includes a few **agent server** endpoints (inside a sandbox) that use `X-Session-API-Key`.\n- Covers the **multi-conversation delegation pattern**: start separate Cloud conversations when you want fresh context windows or background work.\n\n## When to use this skill\n\nUse this skill when you need to:\n\n- start or inspect OpenHands Cloud conversations from code\n- monitor async startup via start-task polling\n- monitor execution status for long-running jobs\n- create separate Cloud conversations for parallel or background work\n- access sandbox agent-server endpoints once a conversation is running\n\n## Auth\n\n### App server (Cloud)\n\nUse Bearer auth:\n\n- Header: `Authorization: Bearer <OPENHANDS_CLOUD_API_KEY>`\n- Preferred env var: `OPENHANDS_CLOUD_API_KEY`\n- Backward-compatible env var: `OPENHANDS_API_KEY`\n\n### Agent server (inside a sandbox)\n\nUse session auth:\n\n- Header: `X-Session-API-Key: <session_api_key>`\n\nHow to obtain `agent_server_url` and `session_api_key`:\n\n1. Start or fetch an app conversation via the app server (Bearer auth), e.g.:\n - `POST /api/v1/app-conversations`\n - or `GET /api/v1/app-conversations?ids=<conversation_id>`\n2. In the returned JSON, look for sandbox/runtime connection fields (names vary slightly by deployment/version). Common patterns:\n - a sandbox object containing `agent_server_url` (or similar)\n - a session key such as `session_api_key` (or similar)\n3. Use those values to call the agent server directly:\n - Base: `{agent_server_url}/api/...`\n - Header: `X-Session-API-Key: <session_api_key>`\n\nExample (common field names; adjust to your deployment):\n\n```python\n# using the minimal Python client (`OpenHandsAPI`)\nconv = api.app_conversation_get(app_conversation_id)\n\nsession_api_key = conv.get(\"session_api_key\")\nconversation_url = conv.get(\"conversation_url\", \"\")\n\n# `conversation_url` often looks like: https://<runtime-host>/api/conversations/<id>\nagent_server_url = conversation_url.rsplit(\"/api/conversations\", 1)[0]\n```\n\n\nIf 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.\n\n## Common V1 app server endpoints\n\nThe following are the main endpoints implemented in the minimal client:\n\n- `GET /api/v1/users/me` — validate auth and inspect current account\n- `GET /api/v1/app-conversations/search?limit=...` — list recent conversations\n- `GET /api/v1/app-conversations?ids=...` — fetch conversation records by id (batch)\n- `GET /api/v1/app-conversations/count` — count conversations\n- `POST /api/v1/app-conversations` — start a new conversation (creates a sandbox)\n- `GET /api/v1/app-conversations/start-tasks?ids=...` — check async start-task status\n- `GET /api/v1/conversation/{app_conversation_id}/events/search?limit=...` — read conversation events\n- `GET /api/v1/conversation/{app_conversation_id}/events/count` — count events\n- `GET /api/v1/sandboxes/search?limit=...` — list sandboxes\n- `POST /api/v1/sandboxes/{sandbox_id}/pause` / `.../resume` — manage sandbox lifecycle\n- `GET /api/v1/app-conversations/{app_conversation_id}/download` — download trajectory zip\n\n## Delegating work with additional Cloud conversations\n\nUse the Cloud API when you want a **separate OpenHands conversation** with its own fresh context window.\nThis is useful for:\n\n- background jobs that can run independently\n- parallel investigations or implementation tasks\n- long-running work where you want to keep the current conversation focused\n- task-specific contexts, such as one conversation building a component while another runs tests\n\n### Delegation checklist\n\nWhen you start a delegated Cloud conversation:\n\n1. Write a **self-contained task description**. Do not assume the new conversation has any context from the current one.\n2. Include the **repository**, branch, relevant file paths, constraints, and expected output.\n3. Start the new conversation with `POST /api/v1/app-conversations`.\n4. Poll the start-task until `status` is `READY` and you have an `app_conversation_id`.\n5. Monitor the delegated conversation via `GET /api/v1/app-conversations?ids=...`.\n6. Share or store the Cloud URL: `https://app.all-hands.dev/conversations/<app_conversation_id>`.\n\n### Minimal cURL flow\n\n```bash\ncurl -X POST \"https://app.all-hands.dev/api/v1/app-conversations\" \\\n -H \"Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"initial_message\": {\n \"content\": [{\"type\": \"text\", \"text\": \"Investigate flaky tests in tests/test_api.py. Report the root cause and propose a fix.\"}]\n },\n \"selected_repository\": \"owner/repo\"\n }'\n```\n\nIf the response does not already include `app_conversation_id`, poll the start-task:\n\n```bash\ncurl -s \"https://app.all-hands.dev/api/v1/app-conversations/start-tasks?ids=${START_TASK_ID}\" \\\n -H \"Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY}\"\n```\n\nThen check execution status:\n\n```bash\ncurl -s \"https://app.all-hands.dev/api/v1/app-conversations?ids=${APP_CONVERSATION_ID}\" \\\n -H \"Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY}\"\n```\n\n### Minimal Python flow\n\n```python\nfrom openhands_api import OpenHandsAPI\n\napi = OpenHandsAPI() # prefers OPENHANDS_CLOUD_API_KEY\n\nstart = api.app_conversation_start(\n initial_message=(\n \"Implement the requested dashboard component in src/dashboard.tsx. \"\n \"Update any related tests and summarize the changes.\"\n ),\n selected_repository=\"owner/repo\",\n selected_branch=\"main\",\n title=\"Dashboard component task\",\n)\n\nready = start\nif not ready.get(\"app_conversation_id\"):\n ready = api.poll_start_task_until_ready(start[\"id\"])\n\nconversation_id = ready[\"app_conversation_id\"]\nprint(f\"Delegated conversation: {api.base_url}/conversations/{conversation_id}\")\n\nstatus = api.app_conversation_get(conversation_id)\nprint(status.get(\"sandbox_status\"), status.get(\"execution_status\"))\n\napi.close()\n```\n\n### Parallelism guidance\n\n- Prefer **5 or fewer** concurrently running delegated conversations.\n- Before starting more, check recent conversations and count how many are still `execution_status == \"running\"`.\n- Batch specific conversation lookups with `GET /api/v1/app-conversations?ids=...` when you already know their ids.\n\nExample:\n\n```python\nitems = api.app_conversations_search(limit=50).get(\"items\", [])\nrunning = [item for item in items if item.get(\"execution_status\") == \"running\"]\nif len(running) >= 5:\n print(\"Wait for some delegated conversations to finish before starting more.\")\n```\n\n\n### Start-task vs `app_conversation_id` (common pitfall)\n\nIn many deployments, `POST /api/v1/app-conversations` is **asynchronous** and returns a **start-task** object:\n\n- `id` is the **start_task_id**\n- `app_conversation_id` is the id you should use for conversation operations like:\n - `GET /api/v1/app-conversations/{app_conversation_id}/download`\n - `GET /api/v1/conversation/{app_conversation_id}/events/...`\n\nIf `app_conversation_id` is not present in the initial response, fetch it via:\n\n- `GET /api/v1/app-conversations/start-tasks?ids=<start_task_id>`\n\nIf you pass a **start_task_id** to `/download`, you will get `404 Not Found`.\n\n## Common agent server endpoints\n\nThese run against `agent_server_url` (not the app server):\n\n- `POST {agent_server_url}/api/bash/execute_bash_command`\n- `GET {agent_server_url}/api/file/download/<absolute_path>`\n- `POST {agent_server_url}/api/file/upload/<absolute_path>` (multipart)\n- `GET {agent_server_url}/api/conversations/{conversation_id}/events/search`\n- `GET {agent_server_url}/api/conversations/{conversation_id}/events/count`\n\n### Counting events (recommended approach)\n\nIf you need to know how many events a conversation has, you can:\n\n1. **App server count (fastest when working)**\n - `GET /api/v1/conversation/{app_conversation_id}/events/count`\n2. **Agent server count (reliable fallback)**\n - `GET {agent_server_url}/api/conversations/{app_conversation_id}/events/count`\n3. **Trajectory zip fallback (heavier, but still one call + gives full payloads)**\n - `GET /api/v1/app-conversations/{app_conversation_id}/download`\n - Unzip and count `event_*.json` files\n\nDo **not** rely on the last event `id` to infer the total number of events.\nIn the agent-server API, event IDs are UUIDs (not monotonically increasing integers).\n\n## Troubleshooting\n\nFor common issues and solutions, see [TROUBLESHOOTING.md](references/TROUBLESHOOTING.md).\n\n## Event structure (for debugging)\n\nEvents returned by:\n\n- app server: `GET /api/v1/conversation/{id}/events/search`\n- agent server: `GET {agent_server_url}/api/conversations/{id}/events/search`\n\n…share the same high-level shape.\n\nEach event typically includes:\n\n- `id` (UUID)\n- `timestamp`\n- `kind`\n- `source`\n\nCommon `kind` values:\n\n| kind | source (typical) | key fields (common) | purpose |\n|---|---|---|---|\n| `ActionEvent` | `agent` | `tool_name`, `tool_call_id`, `action` | tool call requested by the agent |\n| `ObservationEvent` | `environment` | `tool_name`, `tool_call_id`, `action_id`, `observation` | tool result produced by the sandbox/environment |\n| `MessageEvent` | `user` / `assistant` | `message` (or similar) | user/assistant chat messages |\n| `ConversationStateUpdateEvent` | `environment` | `key`, `value` | state transitions/metadata |\n\nLinking tool calls:\n\n- `ActionEvent.tool_call_id` == `ObservationEvent.tool_call_id`\n- `ObservationEvent.action_id` == `ActionEvent.id`\n\nExample (simplified):\n\n```json\n{\n \"id\": \"<action-event-uuid>\",\n \"kind\": \"ActionEvent\",\n \"source\": \"agent\",\n \"tool_name\": \"terminal\",\n \"tool_call_id\": \"toolu_...\",\n \"action\": {\"command\": \"ls\"}\n}\n```\n\n```json\n{\n \"id\": \"<observation-event-uuid>\",\n \"kind\": \"ObservationEvent\",\n \"source\": \"environment\",\n \"tool_name\": \"terminal\",\n \"tool_call_id\": \"toolu_...\",\n \"action_id\": \"<action-event-uuid>\",\n \"observation\": {\"exit_code\": 0, \"stdout\": \"...\"}\n}\n```\n\n## Debugging one-liners (events)\n\nThese assume you're querying the **app server** endpoint. For agent-server queries, swap the URL base + use `X-Session-API-Key`.\n\n### Print a quick timeline\n\n```bash\ncurl -s \"${BASE_URL:-https://app.all-hands.dev}/api/v1/conversation/${APP_CONVERSATION_ID}/events/search?limit=100\" \\\n -H \"Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY:-$OPENHANDS_API_KEY}\" \\\n -H \"Accept: application/json\" | \\\npython3 - <<'PY'\nimport json, sys\nitems = (json.load(sys.stdin) or {}).get(\"items\", [])\nfor i, e in enumerate(items):\n print(f\"{i:04d} {e.get('timestamp','')} {e.get('source','')} {e.get('kind','')}\")\nPY\n```\n\n### Find error-like events\n\n```bash\ncurl -s \"${BASE_URL:-https://app.all-hands.dev}/api/v1/conversation/${APP_CONVERSATION_ID}/events/search?limit=200\" \\\n -H \"Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY:-$OPENHANDS_API_KEY}\" \\\n -H \"Accept: application/json\" | \\\npython3 - <<'PY'\nimport json, sys\nitems = (json.load(sys.stdin) or {}).get(\"items\", [])\nfor i, e in enumerate(items):\n if e.get(\"kind\") == \"ErrorEvent\" or (\"code\" in e and \"detail\" in e):\n print(i, e.get(\"kind\"), e.get(\"code\"), str(e.get(\"detail\", \"\"))[:400])\nPY\n```\n\n### Check tool-call matching (unmatched actions / duplicate observations)\n\n```bash\ncurl -s \"${BASE_URL:-https://app.all-hands.dev}/api/v1/conversation/${APP_CONVERSATION_ID}/events/search?limit=200\" \\\n -H \"Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY:-$OPENHANDS_API_KEY}\" \\\n -H \"Accept: application/json\" | \\\npython3 - <<'PY'\nimport json, sys\nfrom collections import Counter\nitems = (json.load(sys.stdin) or {}).get(\"items\", [])\naction_ids = {e.get(\"id\") for e in items if e.get(\"kind\") == \"ActionEvent\"}\nobs_action_ids = [e.get(\"action_id\") for e in items if e.get(\"kind\") == \"ObservationEvent\" and e.get(\"action_id\")]\nobserved = set(obs_action_ids)\nprint(\"actions:\", len(action_ids))\nprint(\"observations:\", len(observed))\nunmatched = action_ids - observed\nprint(\"unmatched actions:\", list(unmatched)[:20] if unmatched else \"none\")\ndups = [aid for aid, c in Counter(obs_action_ids).items() if c > 1]\nprint(\"duplicate observation action_ids:\", list(dups)[:20] if dups else \"none\")\nPY\n```\n\n\n## Quick start (Python)\n\n```python\n# Copy `skills/openhands-api/scripts/openhands_api.py` into your project (e.g. as `openhands_api.py`),\n# then import it normally:\nfrom openhands_api import OpenHandsAPI\n\napi = OpenHandsAPI() # prefers OPENHANDS_CLOUD_API_KEY\n\nme = api.users_me()\nprint(me)\n\nrecent = api.app_conversations_search(limit=5)\nprint(recent)\n\napi.close()\n```\n\n## CLI examples\n\nSearch conversations:\n\n```bash\nexport OPENHANDS_CLOUD_API_KEY=\"...\"\npython skills/openhands-api/scripts/openhands_api.py search-conversations --limit 5\n```\n\nStart a conversation from a prompt file:\n\n```bash\npython skills/openhands-api/scripts/openhands_api.py start-conversation \\\n --prompt-file skills/openhands-api/references/example_prompt.md \\\n --repo owner/repo \\\n --branch main\n```\n\n## Notes for AI agents extending this client\n\n- Prefer `.../search` endpoints with a small `limit`.\n- Avoid loops that could generate many API calls.\n- Start conversations only when asked: it may create sandboxes and cost money.\n- For sandbox file operations and command execution, use the agent server endpoints with `X-Session-API-Key`.\n\nSee also:\n- `skills/openhands-api/scripts/openhands_api.py`\n- The original inspiration client: `enyst/llm-playground` → `openhands-api-client-v1/scripts/cloud_api_v1.py`\n- Troubleshooting content and real-world usage feedback → `https://github.com/jpshackelford/.openhands/tree/main/skills/openhands-cloud-api`\n\n## Source of truth\n\nThis skill is aligned against the current V1 docs and implementation:\n\n- `OpenHands/docs/openhands/usage/cloud/cloud-api.mdx`\n- `OpenHands/docs/openhands/usage/api/v1.mdx`\n- `OpenHands/OpenHands/openhands/app_server/v1_router.py`\n- `OpenHands/OpenHands/openhands/app_server/app_conversation/app_conversation_router.py`\n- `OpenHands/OpenHands/openhands/app_server/app_conversation/app_conversation_models.py`"
|
|
325
|
+
"content": "This skill documents the **OpenHands Cloud API** (V1), commonly used **agent-server APIs**, and small, easy-to-copy clients.\n\nIt is intentionally focused on common OpenHands API workflows:\n\n- Defaults to OpenHands Cloud (`https://app.all-hands.dev`).\n- Targets the **V1 app server REST API** under `/api/v1/...`.\n- Includes a few **agent server** endpoints (inside a sandbox) that use `X-Session-API-Key`.\n- Covers the **multi-conversation delegation pattern**: start separate Cloud conversations when you want fresh context windows or background work.\n- Covers **local Agent Canvas backend conversations**: start or inspect conversations by calling a local agent server directly.\n\n## When to use this skill\n\nUse this skill when you need to:\n\n- start or inspect OpenHands Cloud conversations from code\n- monitor async startup via start-task polling\n- monitor execution status for long-running jobs\n- create separate Cloud conversations for parallel or background work\n- access sandbox agent-server endpoints once a conversation is running\n- start or inspect conversations on a local Agent Canvas backend or local agent server\n\n## Auth\n\n### App server (Cloud)\n\nUse Bearer auth:\n\n- Header: `Authorization: Bearer <OPENHANDS_CLOUD_API_KEY>`\n- Preferred env var: `OPENHANDS_CLOUD_API_KEY`\n- Backward-compatible env var: `OPENHANDS_API_KEY`\n\n### Agent server (inside a sandbox)\n\nUse session auth:\n\n- Header: `X-Session-API-Key: <session_api_key>`\n\nHow to obtain `agent_server_url` and `session_api_key`:\n\n1. Start or fetch an app conversation via the app server (Bearer auth), e.g.:\n - `POST /api/v1/app-conversations`\n - or `GET /api/v1/app-conversations?ids=<conversation_id>`\n2. In the returned JSON, look for sandbox/runtime connection fields (names vary slightly by deployment/version). Common patterns:\n - a sandbox object containing `agent_server_url` (or similar)\n - a session key such as `session_api_key` (or similar)\n3. Use those values to call the agent server directly:\n - Base: `{agent_server_url}/api/...`\n - Header: `X-Session-API-Key: <session_api_key>`\n\nExample (common field names; adjust to your deployment):\n\n```python\n# using the minimal Python client (`OpenHandsAPI`)\nconv = api.app_conversation_get(app_conversation_id)\n\nsession_api_key = conv.get(\"session_api_key\")\nconversation_url = conv.get(\"conversation_url\", \"\")\n\n# `conversation_url` often looks like: https://<runtime-host>/api/conversations/<id>\nagent_server_url = conversation_url.rsplit(\"/api/conversations\", 1)[0]\n```\n\n\nIf 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.\n\n### Local Agent Canvas backend\n\nUse 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.\n\nWhen 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.\n\n```bash\nLOCAL_AGENT_SERVER_URL=\"${LOCAL_AGENT_SERVER_URL:-http://localhost:8000}\"\nSESSION_API_KEY=\"${LOCAL_BACKEND_API_KEY:-$(cat \"${OH_SESSION_API_KEY_PATH:-$HOME/.openhands/agent-canvas/api-key.txt}\")}\"\n```\n\nCheck the local server before creating a backend conversation:\n\n```bash\ncurl -sS \"${LOCAL_AGENT_SERVER_URL}/server_info\" \\\n -H \"X-Session-API-Key: ${SESSION_API_KEY}\"\n```\n\nStart 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.\n\n```bash\nCONVERSATION_JSON=$(curl -sS -X POST \"${LOCAL_AGENT_SERVER_URL}/api/conversations\" \\\n -H \"X-Session-API-Key: ${SESSION_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d @- <<'JSON'\n{\n \"agent\": {\n \"kind\": \"Agent\",\n \"llm\": {\n \"model\": \"your-model-provider/your-model-name\",\n \"api_key\": \"**********\"\n },\n \"tools\": [\n {\"name\": \"terminal\"},\n {\"name\": \"file_editor\"},\n {\"name\": \"task_tracker\"}\n ]\n },\n \"workspace\": {\"kind\": \"LocalWorkspace\", \"working_dir\": \"/workspace\"},\n \"initial_message\": {\n \"content\": [{\"text\": \"Summarize the current workspace.\"}],\n \"run\": true\n }\n}\nJSON\n)\nCONVERSATION_ID=$(python3 -c 'import json,sys; print(json.load(sys.stdin)[\"id\"])' <<<\"${CONVERSATION_JSON}\")\nprintf 'Conversation: %s/api/conversations/%s\\n' \"${LOCAL_AGENT_SERVER_URL}\" \"${CONVERSATION_ID}\"\n```\n\nPoll status and inspect recent events:\n\n```bash\ncurl -sS \"${LOCAL_AGENT_SERVER_URL}/api/conversations/${CONVERSATION_ID}\" \\\n -H \"X-Session-API-Key: ${SESSION_API_KEY}\"\n\ncurl -sS \"${LOCAL_AGENT_SERVER_URL}/api/conversations/${CONVERSATION_ID}/events/search?limit=20&sort_order=TIMESTAMP_DESC\" \\\n -H \"X-Session-API-Key: ${SESSION_API_KEY}\"\n```\n\nIf the same base URL serves the Agent Canvas UI, the browser route is:\n\n```bash\nprintf '%s/conversations/%s\\n' \"${LOCAL_AGENT_SERVER_URL}\" \"${CONVERSATION_ID}\"\n```\n\n## Common V1 app server endpoints\n\nThe following are the main endpoints implemented in the minimal client:\n\n- `GET /api/v1/users/me` — validate auth and inspect current account\n- `GET /api/v1/app-conversations/search?limit=...` — list recent conversations\n- `GET /api/v1/app-conversations?ids=...` — fetch conversation records by id (batch)\n- `GET /api/v1/app-conversations/count` — count conversations\n- `POST /api/v1/app-conversations` — start a new conversation (creates a sandbox)\n- `GET /api/v1/app-conversations/start-tasks?ids=...` — check async start-task status\n- `GET /api/v1/conversation/{app_conversation_id}/events/search?limit=...` — read conversation events\n- `GET /api/v1/conversation/{app_conversation_id}/events/count` — count events\n- `GET /api/v1/sandboxes/search?limit=...` — list sandboxes\n- `POST /api/v1/sandboxes/{sandbox_id}/pause` / `.../resume` — manage sandbox lifecycle\n- `GET /api/v1/app-conversations/{app_conversation_id}/download` — download trajectory zip\n\n## Delegating work with additional Cloud conversations\n\nUse the Cloud API when you want a **separate OpenHands conversation** with its own fresh context window.\nThis is useful for:\n\n- background jobs that can run independently\n- parallel investigations or implementation tasks\n- long-running work where you want to keep the current conversation focused\n- task-specific contexts, such as one conversation building a component while another runs tests\n\n### Delegation checklist\n\nWhen you start a delegated Cloud conversation:\n\n1. Write a **self-contained task description**. Do not assume the new conversation has any context from the current one.\n2. Include the **repository**, branch, relevant file paths, constraints, and expected output.\n3. Start the new conversation with `POST /api/v1/app-conversations`.\n4. Poll the start-task until `status` is `READY` and you have an `app_conversation_id`.\n5. Monitor the delegated conversation via `GET /api/v1/app-conversations?ids=...`.\n6. Share or store the Cloud URL: `https://app.all-hands.dev/conversations/<app_conversation_id>`.\n\n### Minimal cURL flow\n\n```bash\ncurl -X POST \"https://app.all-hands.dev/api/v1/app-conversations\" \\\n -H \"Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"initial_message\": {\n \"content\": [{\"type\": \"text\", \"text\": \"Investigate flaky tests in tests/test_api.py. Report the root cause and propose a fix.\"}]\n },\n \"selected_repository\": \"owner/repo\"\n }'\n```\n\nIf the response does not already include `app_conversation_id`, poll the start-task:\n\n```bash\ncurl -s \"https://app.all-hands.dev/api/v1/app-conversations/start-tasks?ids=${START_TASK_ID}\" \\\n -H \"Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY}\"\n```\n\nThen check execution status:\n\n```bash\ncurl -s \"https://app.all-hands.dev/api/v1/app-conversations?ids=${APP_CONVERSATION_ID}\" \\\n -H \"Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY}\"\n```\n\n### Minimal Python flow\n\n```python\nfrom openhands_api import OpenHandsAPI\n\napi = OpenHandsAPI() # prefers OPENHANDS_CLOUD_API_KEY\n\nstart = api.app_conversation_start(\n initial_message=(\n \"Implement the requested dashboard component in src/dashboard.tsx. \"\n \"Update any related tests and summarize the changes.\"\n ),\n selected_repository=\"owner/repo\",\n selected_branch=\"main\",\n title=\"Dashboard component task\",\n)\n\nready = start\nif not ready.get(\"app_conversation_id\"):\n ready = api.poll_start_task_until_ready(start[\"id\"])\n\nconversation_id = ready[\"app_conversation_id\"]\nprint(f\"Delegated conversation: {api.base_url}/conversations/{conversation_id}\")\n\nstatus = api.app_conversation_get(conversation_id)\nprint(status.get(\"sandbox_status\"), status.get(\"execution_status\"))\n\napi.close()\n```\n\n### Parallelism guidance\n\n- Prefer **5 or fewer** concurrently running delegated conversations.\n- Before starting more, check recent conversations and count how many are still `execution_status == \"running\"`.\n- Batch specific conversation lookups with `GET /api/v1/app-conversations?ids=...` when you already know their ids.\n\nExample:\n\n```python\nitems = api.app_conversations_search(limit=50).get(\"items\", [])\nrunning = [item for item in items if item.get(\"execution_status\") == \"running\"]\nif len(running) >= 5:\n print(\"Wait for some delegated conversations to finish before starting more.\")\n```\n\n\n### Start-task vs `app_conversation_id` (common pitfall)\n\nIn many deployments, `POST /api/v1/app-conversations` is **asynchronous** and returns a **start-task** object:\n\n- `id` is the **start_task_id**\n- `app_conversation_id` is the id you should use for conversation operations like:\n - `GET /api/v1/app-conversations/{app_conversation_id}/download`\n - `GET /api/v1/conversation/{app_conversation_id}/events/...`\n\nIf `app_conversation_id` is not present in the initial response, fetch it via:\n\n- `GET /api/v1/app-conversations/start-tasks?ids=<start_task_id>`\n\nIf you pass a **start_task_id** to `/download`, you will get `404 Not Found`.\n\n## Common agent server endpoints\n\nThese run against `agent_server_url` (not the app server):\n\n- `POST {agent_server_url}/api/bash/execute_bash_command`\n- `GET {agent_server_url}/api/file/download/<absolute_path>`\n- `POST {agent_server_url}/api/file/upload/<absolute_path>` (multipart)\n- `GET {agent_server_url}/api/conversations/{conversation_id}/events/search`\n- `GET {agent_server_url}/api/conversations/{conversation_id}/events/count`\n\n### Counting events (recommended approach)\n\nIf you need to know how many events a conversation has, you can:\n\n1. **App server count (fastest when working)**\n - `GET /api/v1/conversation/{app_conversation_id}/events/count`\n2. **Agent server count (reliable fallback)**\n - `GET {agent_server_url}/api/conversations/{app_conversation_id}/events/count`\n3. **Trajectory zip fallback (heavier, but still one call + gives full payloads)**\n - `GET /api/v1/app-conversations/{app_conversation_id}/download`\n - Unzip and count `event_*.json` files\n\nDo **not** rely on the last event `id` to infer the total number of events.\nIn the agent-server API, event IDs are UUIDs (not monotonically increasing integers).\n\n## Troubleshooting\n\nFor common issues and solutions, see [TROUBLESHOOTING.md](references/TROUBLESHOOTING.md).\n\n## Event structure (for debugging)\n\nEvents returned by:\n\n- app server: `GET /api/v1/conversation/{id}/events/search`\n- agent server: `GET {agent_server_url}/api/conversations/{id}/events/search`\n\n…share the same high-level shape.\n\nEach event typically includes:\n\n- `id` (UUID)\n- `timestamp`\n- `kind`\n- `source`\n\nCommon `kind` values:\n\n| kind | source (typical) | key fields (common) | purpose |\n|---|---|---|---|\n| `ActionEvent` | `agent` | `tool_name`, `tool_call_id`, `action` | tool call requested by the agent |\n| `ObservationEvent` | `environment` | `tool_name`, `tool_call_id`, `action_id`, `observation` | tool result produced by the sandbox/environment |\n| `MessageEvent` | `user` / `assistant` | `message` (or similar) | user/assistant chat messages |\n| `ConversationStateUpdateEvent` | `environment` | `key`, `value` | state transitions/metadata |\n\nLinking tool calls:\n\n- `ActionEvent.tool_call_id` == `ObservationEvent.tool_call_id`\n- `ObservationEvent.action_id` == `ActionEvent.id`\n\nExample (simplified):\n\n```json\n{\n \"id\": \"<action-event-uuid>\",\n \"kind\": \"ActionEvent\",\n \"source\": \"agent\",\n \"tool_name\": \"terminal\",\n \"tool_call_id\": \"toolu_...\",\n \"action\": {\"command\": \"ls\"}\n}\n```\n\n```json\n{\n \"id\": \"<observation-event-uuid>\",\n \"kind\": \"ObservationEvent\",\n \"source\": \"environment\",\n \"tool_name\": \"terminal\",\n \"tool_call_id\": \"toolu_...\",\n \"action_id\": \"<action-event-uuid>\",\n \"observation\": {\"exit_code\": 0, \"stdout\": \"...\"}\n}\n```\n\n## Debugging one-liners (events)\n\nThese assume you're querying the **app server** endpoint. For agent-server queries, swap the URL base + use `X-Session-API-Key`.\n\n### Print a quick timeline\n\n```bash\ncurl -s \"${BASE_URL:-https://app.all-hands.dev}/api/v1/conversation/${APP_CONVERSATION_ID}/events/search?limit=100\" \\\n -H \"Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY:-$OPENHANDS_API_KEY}\" \\\n -H \"Accept: application/json\" | \\\npython3 - <<'PY'\nimport json, sys\nitems = (json.load(sys.stdin) or {}).get(\"items\", [])\nfor i, e in enumerate(items):\n print(f\"{i:04d} {e.get('timestamp','')} {e.get('source','')} {e.get('kind','')}\")\nPY\n```\n\n### Find error-like events\n\n```bash\ncurl -s \"${BASE_URL:-https://app.all-hands.dev}/api/v1/conversation/${APP_CONVERSATION_ID}/events/search?limit=200\" \\\n -H \"Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY:-$OPENHANDS_API_KEY}\" \\\n -H \"Accept: application/json\" | \\\npython3 - <<'PY'\nimport json, sys\nitems = (json.load(sys.stdin) or {}).get(\"items\", [])\nfor i, e in enumerate(items):\n if e.get(\"kind\") == \"ErrorEvent\" or (\"code\" in e and \"detail\" in e):\n print(i, e.get(\"kind\"), e.get(\"code\"), str(e.get(\"detail\", \"\"))[:400])\nPY\n```\n\n### Check tool-call matching (unmatched actions / duplicate observations)\n\n```bash\ncurl -s \"${BASE_URL:-https://app.all-hands.dev}/api/v1/conversation/${APP_CONVERSATION_ID}/events/search?limit=200\" \\\n -H \"Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY:-$OPENHANDS_API_KEY}\" \\\n -H \"Accept: application/json\" | \\\npython3 - <<'PY'\nimport json, sys\nfrom collections import Counter\nitems = (json.load(sys.stdin) or {}).get(\"items\", [])\naction_ids = {e.get(\"id\") for e in items if e.get(\"kind\") == \"ActionEvent\"}\nobs_action_ids = [e.get(\"action_id\") for e in items if e.get(\"kind\") == \"ObservationEvent\" and e.get(\"action_id\")]\nobserved = set(obs_action_ids)\nprint(\"actions:\", len(action_ids))\nprint(\"observations:\", len(observed))\nunmatched = action_ids - observed\nprint(\"unmatched actions:\", list(unmatched)[:20] if unmatched else \"none\")\ndups = [aid for aid, c in Counter(obs_action_ids).items() if c > 1]\nprint(\"duplicate observation action_ids:\", list(dups)[:20] if dups else \"none\")\nPY\n```\n\n\n## Quick start (Python)\n\n```python\n# Copy `skills/openhands-api/scripts/openhands_api.py` into your project (e.g. as `openhands_api.py`),\n# then import it normally:\nfrom openhands_api import OpenHandsAPI\n\napi = OpenHandsAPI() # prefers OPENHANDS_CLOUD_API_KEY\n\nme = api.users_me()\nprint(me)\n\nrecent = api.app_conversations_search(limit=5)\nprint(recent)\n\napi.close()\n```\n\n## CLI examples\n\nSearch conversations:\n\n```bash\nexport OPENHANDS_CLOUD_API_KEY=\"...\"\npython skills/openhands-api/scripts/openhands_api.py search-conversations --limit 5\n```\n\nStart a conversation from a prompt file:\n\n```bash\npython skills/openhands-api/scripts/openhands_api.py start-conversation \\\n --prompt-file skills/openhands-api/references/example_prompt.md \\\n --repo owner/repo \\\n --branch main\n```\n\n## Notes for AI agents extending this client\n\n- Prefer `.../search` endpoints with a small `limit`.\n- Avoid loops that could generate many API calls.\n- Start conversations only when asked: it may create sandboxes and cost money.\n- For sandbox file operations and command execution, use the agent server endpoints with `X-Session-API-Key`.\n\nSee also:\n- `skills/openhands-api/scripts/openhands_api.py`\n- The original inspiration client: `enyst/llm-playground` → `openhands-api-client-v1/scripts/cloud_api_v1.py`\n- Troubleshooting content and real-world usage feedback → `https://github.com/jpshackelford/.openhands/tree/main/skills/openhands-cloud-api`\n\n## Source of truth\n\nThis skill is aligned against the current OpenHands API docs and implementation:\n\n- `OpenHands/docs/openhands/usage/cloud/cloud-api.mdx`\n- `OpenHands/docs/openhands/usage/agent-canvas/backend-setup/local.mdx`\n- `OpenHands/docs/sdk/arch/agent-server.mdx`\n- `OpenHands/docs/openhands/usage/api/v1.mdx`\n- `OpenHands/OpenHands/openhands/app_server/v1_router.py`\n- `OpenHands/OpenHands/openhands/app_server/app_conversation/app_conversation_router.py`\n- `OpenHands/OpenHands/openhands/app_server/app_conversation/app_conversation_models.py`"
|
|
300
326
|
},
|
|
301
327
|
{
|
|
302
328
|
"name": "openhands-automation",
|
|
@@ -315,7 +341,7 @@ export const SKILLS_CATALOG = [
|
|
|
315
341
|
"issue automation",
|
|
316
342
|
"/automation:create"
|
|
317
343
|
],
|
|
318
|
-
"content": "# OpenHands Automations\n\nCreate and manage automations that run inside an OpenHands agent server — triggered by cron schedules or webhook events (GitHub, custom services).\n\n## Automation Creation Process\nThe agent must follow these steps when creating an automation:\n* Quickly check that you can access the correct automations backend using the auth mechanism below\n* Quickly check that you can access any necessary integrations (e.g. GitHub, Slack); if access fails, inform the user and stop\n* Ask the user for any necessary information, e.g. if you need the name of a Slack channel or GitHub repo to proceed\n* Write the code or prompt that will be sent to the automations backend _inside the current workspace_\n* Show the code to the user with the `canvas_ui` tool if available, otherwise present it in a fenced code block in your reply\n* Message the user with a concise summary of how the automation will behave, and ask if they are ready to deploy it\n\n## Architecture\n\nTwo components work together to run automations:\n\n**Automation Service** (API at `OPENHANDS_HOST/api/automation/v1`)\nManages the *when*: holds automation definitions, schedules cron-triggered runs, dispatches webhook-triggered runs, and receives completion callbacks to mark runs as done. This is the API you call to create, update, and manage automations.\n\n**Agent Server** (accessible as `AGENT_SERVER_URL` inside script runs)\nManages the *what*: the runtime environment where automation scripts execute and where conversations (AI agent interactions with tools, bash, file editing, etc.) run. When a run is triggered, the automation service uploads the automation's tarball to the agent server, which unpacks and runs the entrypoint script. The script connects back to the agent server using `AGENT_SERVER_URL` and a session API key to start, monitor, and stop conversations.\n\nThe agent server typically runs inside a **sandbox** (a Docker or Kubernetes container). Some deployments use sandboxless mode, where the agent server runs directly on a host.\n\n**Key environment variables:**\n\n| Variable | Availability | Description |\n|---|---|---|\n| `RUNTIME_URL` | Ambient in cloud environments | Public-facing URL of the **agent server** sandbox. Use this to determine whether external webhook delivery is possible — if unset or local, webhooks cannot be received. The automation service may run at a separate URL (see Determining the API Host). |\n| `AGENT_SERVER_URL` | Injected into scripts at run time only | Internal URL of the agent server. Available inside script execution context; **not** an ambient environment variable outside of a running script. |\n| `OPENHANDS_HOST` | Shell convention only — set manually | Base URL for the automation service API. **Not a real environment variable.** Set it from the `<HOST>` system-prompt value, or default to `https://app.all-hands.dev`. Used in all `curl` examples throughout this skill. |\n\n> **⚠️ CRITICAL — Agent behavior rules:**\n>\n> 0. **Does this task need an LLM at all? Check first.** Before picking a preset, ask whether the task actually requires reasoning, judgment, summarization, or open-ended tool use. If it is fully deterministic — fixed data transforms, scheduled HTTP calls, healthcheck pings, file rotation, picking from a known list, posting a templated message — an LLM-driven preset is overkill. Every run will consume LLM tokens, which adds up fast at high frequencies (every 5 min ≈ 288 runs/day). Surface the trade-off to the user and offer the custom-script path (see `references/custom-automation.md`) as the cheaper, more reliable option. Be especially careful for cron schedules tighter than hourly.\n>\n> **Instant-recognition patterns — these are always deterministic, never use an LLM preset:**\n> - \"post a quote / message / fact every N minutes\" (rotating from a list)\n> - \"send a scheduled reminder / standup / digest\"\n> - \"ping a health-check URL on a schedule\"\n> - \"post to Slack / webhook every N minutes\"\n> - Any task where the full output could be written as a static template right now\n>\n> 1. **For LLM-appropriate work, default to preset endpoints.** They handle all SDK boilerplate, tarball packaging, and upload automatically:\n> - **Prompt preset** (`POST /v1/preset/prompt`) — for tasks expressed as a natural language prompt that benefit from agent reasoning\n> - **Plugin preset** (`POST /v1/preset/plugin`) — when plugins with skills, MCP configs, or commands are needed\n> 2. **Do not silently create custom scripts.** Do not generate Python code, `setup.sh` files, or tarball uploads without user consent. But *do* proactively recommend the custom path (per rule 0) when the task is deterministic or high-frequency — surface the option and let the user choose.\n> 3. **If neither preset is the right fit**, do NOT silently fall back to custom automation. Instead, explain the available options to the user:\n> - **Prompt preset** — natural language prompt execution (LLM-driven)\n> - **Plugin preset** — load plugins with extended capabilities (skills, MCP, hooks, commands)\n> - **Custom script** — full control over code, with or without LLM; point them to `references/custom-automation.md`\n> - Let the user choose which approach to use.\n> 4. **Only create custom scripts after the user agrees to that path.** Refer to `references/custom-automation.md` for the full reference.\n> 5. **Before suggesting event-triggered (webhook) automations, check whether the deployment is publicly reachable.** Check `RUNTIME_URL`. Webhooks require an internet-accessible URL so that external services (GitHub, Slack, Linear, etc.) can deliver events to the automation service. If `RUNTIME_URL` is unset, empty, or resolves to a local or private address (`localhost`, `127.0.0.1`, `0.0.0.0`, or any RFC 1918 range: `10.x.x.x`, `192.168.x.x`, `172.16–31.x.x`), the service cannot receive inbound webhook traffic from the public internet. In that case:\n> - **Recommend a cron-based polling automation instead.** Have the automation run on a schedule and call the external service's API (e.g., the GitHub REST API) to check for new events since the last run.\n> - Explain the limitation clearly to the user: \"Because this is a local deployment, external services can't reach the webhook endpoint. I'll set up a polling automation using a cron schedule instead.\"\n\n### No-LLM Script Helpers\n\nWhen building a deterministic custom script, these two stdlib-only functions are required. Copy them verbatim — they use `AGENT_SERVER_URL` and `SESSION_API_KEY` injected by the automation service.\n\n```python\nimport json, os, urllib.request\n\ndef get_secret(name):\n \"\"\"Fetch a named secret stored in the agent server.\"\"\"\n url = os.environ.get(\"AGENT_SERVER_URL\", \"\").rstrip(\"/\")\n key = os.environ.get(\"SESSION_API_KEY\") or os.environ.get(\"OH_SESSION_API_KEYS_0\", \"\")\n with urllib.request.urlopen(urllib.request.Request(\n f\"{url}/api/settings/secrets/{name}\", headers={\"X-Session-API-Key\": key}\n )) as r:\n return r.read().decode().strip()\n\ndef fire_callback(status=\"COMPLETED\", error=None):\n \"\"\"Signal run completion. MUST be called on every exit path — success AND error.\"\"\"\n url = os.environ.get(\"AUTOMATION_CALLBACK_URL\", \"\")\n if not url: return\n body = {\"status\": status, \"run_id\": os.environ.get(\"AUTOMATION_RUN_ID\", \"\")}\n if error: body[\"error\"] = error\n try:\n urllib.request.urlopen(urllib.request.Request(url, data=json.dumps(body).encode(), headers={\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {os.environ.get('AUTOMATION_CALLBACK_API_KEY', '')}\",\n }))\n except Exception as e: print(f\"Callback error: {e}\")\n```\n\nEntrypoint must be `python3 main.py` (no `setup.sh` needed). Wrap your main logic in `try/except` and call `fire_callback(\"FAILED\", str(e))` in the except block.\n\n---\n\n## Authentication\n\nAll requests require Bearer authentication:\n\n```bash\n-H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n```\n\n## API Endpoints\n\n### Determining the API Host\n\n**Before making API calls, determine the correct host:**\n\nThe automation service may run at a different URL from the agent server. In the examples throughout this skill, `${OPENHANDS_HOST}` is a shell-variable convention for the automation service base URL — it is **not** a real environment variable. Set it from context before running any curl command:\n\n- Look for a `<HOST>` value in the system prompt. If present, use that URL.\n- Otherwise default to `https://app.all-hands.dev`.\n\n```bash\nOPENHANDS_HOST=\"https://app.all-hands.dev\" # replace with <HOST> if provided\n```\n\n\n### Automation Endpoints\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/api/automation/v1/preset/prompt` | POST | **Create automation from a prompt (recommended)** |\n| `/api/automation/v1/preset/plugin` | POST | **Create automation with plugins** |\n| `/api/automation/v1` | GET | List automations |\n| `/api/automation/v1/{id}` | GET | Get automation details |\n| `/api/automation/v1/{id}` | PATCH | Update automation |\n| `/api/automation/v1/{id}` | DELETE | Delete automation |\n| `/api/automation/v1/{id}/dispatch` | POST | Trigger a run manually |\n| `/api/automation/v1/{id}/runs` | GET | List automation runs |\n\n### Custom Webhook Endpoints\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/api/automation/v1/webhooks` | POST | Register a custom webhook source |\n| `/api/automation/v1/webhooks` | GET | List all custom webhooks |\n| `/api/automation/v1/webhooks/{id}` | GET | Get webhook details |\n| `/api/automation/v1/webhooks/{id}` | PATCH | Update webhook settings |\n| `/api/automation/v1/webhooks/{id}` | DELETE | Delete a webhook |\n| `/api/automation/v1/webhooks/{id}/rotate-secret` | POST | Rotate signing secret |\n\n---\n\n## Trigger Types\n\nAutomations support two trigger types:\n\n| Trigger Type | Use Case |\n|--------------|----------|\n| **Cron** | Run on a schedule (daily, weekly, hourly, etc.) |\n| **Event** | Run when a webhook event occurs (GitHub PR opened, issue commented, etc.) — **requires a publicly reachable deployment** |\n\n---\n\n## Creating Automations\n\nTwo preset endpoints simplify automation creation by handling SDK boilerplate, tarball packaging, and upload automatically:\n\n1. **Prompt Preset** — Execute a natural language prompt (simple tasks)\n2. **Plugin Preset** — Load plugins with skills, MCP configs, and commands (extended capabilities)\n\n---\n\n### Prompt Preset\n\nUse the **preset/prompt endpoint** for simple automations. Provide a natural language prompt describing the task.\n\n#### How It Works\n\n1. Send a prompt describing the task (e.g., \"Generate a weekly status report\")\n2. The automation service generates a Python script that: fetches LLM config and secrets from the agent server, starts an AI agent conversation with your prompt, and sends a completion callback when done\n3. The script is packaged as a tarball and the automation is registered; on each trigger, the automation service uploads the tarball to the agent server, which unpacks and runs the script inside its environment\n\n#### Request\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"My Automation Name\",\n \"prompt\": \"What the automation should do\",\n \"trigger\": {\n \"type\": \"cron\",\n \"schedule\": \"0 9 * * *\",\n \"timezone\": \"UTC\"\n }\n }'\n```\n\n#### Request Fields\n\n| Field | Required | Description |\n|-------|----------|-------------|\n| `name` | Yes | Name of the automation (1-500 characters) |\n| `prompt` | Yes | Natural language instructions (1-50,000 characters) |\n| `trigger` | Yes | Trigger configuration — either `cron` or `event` (see below) |\n| `timeout` | No | Max execution time in seconds (default: system maximum) |\n| `repos` | No | Repositories to clone (see [Repository Cloning](#repository-cloning)) |\n\n**Cron Trigger Fields:**\n\n| Field | Required | Description |\n|-------|----------|-------------|\n| `trigger.type` | Yes | `\"cron\"` |\n| `trigger.schedule` | Yes | Cron expression (5 fields: min hour day month weekday) |\n| `trigger.timezone` | No | IANA timezone (default: `\"UTC\"`) |\n\n**Event Trigger Fields:**\n\n| Field | Required | Description |\n|-------|----------|-------------|\n| `trigger.type` | Yes | `\"event\"` |\n| `trigger.source` | Yes | Event source: `\"github\"` or custom webhook source name |\n| `trigger.on` | Yes | Event key pattern(s) to match (see Event Keys below) |\n| `trigger.filter` | No | JMESPath expression for payload filtering (see Filter Expressions below) |\n\n#### Prompt Tips\n\nWrite the prompt as an instruction to an AI agent. The prompt executes inside a sandbox with full tool access (bash, file editing, etc.), the user's configured LLM, stored secrets, and MCP server integrations. Examples:\n\n- `\"Generate a weekly status report summarizing the team's GitHub activity and post it to Slack\"`\n- `\"Check the production API health endpoint every hour and alert if it returns non-200\"`\n- `\"Pull the latest data from our analytics API and update the dashboard spreadsheet\"`\n\n#### Cron Schedule\n\n| Field | Values | Description |\n|-------|--------|-------------|\n| Minute | 0-59 | Minute of the hour |\n| Hour | 0-23 | Hour of the day (24-hour) |\n| Day | 1-31 | Day of the month |\n| Month | 1-12 | Month of the year |\n| Weekday | 0-6 | Day of week (0=Sun, 6=Sat) |\n\nCommon schedules: `0 9 * * *` (daily 9 AM), `0 9 * * 1-5` (weekdays 9 AM), `0 9 * * 1` (Mondays 9 AM), `0 0 1 * *` (first of month), `*/15 * * * *` (every 15 min), `0 */6 * * *` (every 6 hours).\n\n#### Response (HTTP 201)\n\n```json\n{\n \"id\": \"123e4567-e89b-12d3-a456-426614174000\",\n \"name\": \"My Automation Name\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 9 * * *\", \"timezone\": \"UTC\"},\n \"enabled\": true,\n \"created_at\": \"2025-03-25T10:00:00Z\"\n}\n```\n\n#### Prompt Preset Examples\n\n**Daily report:**\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Daily Report\",\n \"prompt\": \"Generate a daily status report and save it to a file in the workspace\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 9 * * 1-5\", \"timezone\": \"America/New_York\"}\n }'\n```\n\n**Weekly cleanup:**\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Weekly Cleanup\",\n \"prompt\": \"Clean up temporary files older than 7 days and send a summary of what was removed\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 2 * * 0\", \"timezone\": \"UTC\"},\n \"timeout\": 300\n }'\n```\n\n---\n\n## Polling as a Webhook Alternative\n\nWhen the deployment cannot receive inbound webhook traffic (see rule 5), use a cron-triggered automation that calls the external service’s API on a schedule to check for new events.\n\n### Polling vs. Webhooks at a Glance\n\n| | Webhooks (Event trigger) | Polling (Cron trigger) |\n|---|---|---|\n| **Requires public URL** | Yes | No — works locally |\n| **Latency** | Near-instant | Up to one poll interval |\n| **API calls** | Only on real events | Every poll interval |\n| **Best for** | Cloud / public deployments | Local or private deployments |\n\n---\n\n## Event-Triggered Automations (Webhooks)\n\nEvent-triggered automations run when a webhook event occurs — like a GitHub PR being opened, an issue receiving a comment, or a custom service sending a notification.\n\n### Built-in Integrations\n\n**GitHub** is a built-in integration — no webhook registration needed. Just create automations with `\"source\": \"github\"`.\n\n### GitHub Event Keys\n\nEvents use the format `{event_type}.{action}` or just `{event_type}` (for events without actions like `push`).\n\n| Event Type | Event Keys | Description |\n|------------|------------|-------------|\n| `pull_request` | `pull_request.opened`, `pull_request.closed`, `pull_request.synchronize`, `pull_request.labeled`, `pull_request.unlabeled`, `pull_request.reopened`, `pull_request.edited`, `pull_request.ready_for_review` | PR activity |\n| `issues` | `issues.opened`, `issues.closed`, `issues.reopened`, `issues.labeled`, `issues.unlabeled`, `issues.edited`, `issues.assigned` | Issue activity |\n| `issue_comment` | `issue_comment.created`, `issue_comment.edited`, `issue_comment.deleted` | Comments on issues/PRs |\n| `push` | `push` | Code pushed to a branch |\n| `release` | `release.published`, `release.created`, `release.released`, `release.prereleased` | Release activity |\n| `pull_request_review` | `pull_request_review.submitted`, `pull_request_review.edited`, `pull_request_review.dismissed` | PR review activity |\n\n**Wildcards:** Use `*` to match any action — e.g., `pull_request.*` matches all PR events.\n\n**Multiple patterns:** The `on` field can be a string or array — e.g., `[\"push\", \"pull_request.opened\"]`.\n\n### Filter Expressions (JMESPath)\n\nFilters let you match events based on payload content using JMESPath expressions.\n\n#### Available Functions\n\n| Function | Description | Example |\n|----------|-------------|---------|\n| `glob(str, pattern)` | Wildcard pattern matching | `glob(repository.full_name, 'myorg/*')` |\n| `icontains(str, substr)` | Case-insensitive substring | `icontains(comment.body, '@openhands')` |\n| `contains(array, value)` | Array contains value | `contains(pull_request.labels[].name, 'bug')` |\n| `regex(str, pattern)` | Regular expression match | `regex(ref, '^refs/tags/v\\\\d+')` |\n| `starts_with(str, prefix)` | String starts with | `starts_with(ref, 'refs/heads/')` |\n| `ends_with(str, suffix)` | String ends with | `ends_with(ref, '/main')` |\n| `lower(str)` / `upper(str)` | Case conversion | `lower(sender.login) == 'admin'` |\n\n#### Boolean Operators\n\n- `&&` — AND\n- `||` — OR \n- `!` — NOT\n\n#### Filter Examples\n\n```javascript\n// Exact match on label name\n\"contains(pull_request.labels[].name, 'openhands')\"\n\n// Case-insensitive mention in comment\n\"icontains(comment.body, '@openhands')\"\n\n// Match specific repository\n\"repository.full_name == 'myorg/myrepo'\"\n\n// Match any repo in an org\n\"glob(repository.full_name, 'myorg/*')\"\n\n// PR with 'bug' label in any org repo\n\"glob(repository.full_name, 'myorg/*') && contains(pull_request.labels[].name, 'bug')\"\n\n// Push to main or release branches\n\"glob(ref, 'refs/heads/main') || glob(ref, 'refs/heads/release/*')\"\n\n// Issue opened by a specific user\n\"sender.login == 'dependabot[bot]'\"\n\n// Not a draft PR\n\"!pull_request.draft\"\n```\n\n---\n\n### Event-Triggered Examples\n\n#### GitHub: Respond to @openhands mentions in comments\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"OpenHands Mention Responder\",\n \"prompt\": \"Analyze the issue or PR context and provide a helpful response to the user'\\''s question. The comment body and context are available in the event payload.\",\n \"trigger\": {\n \"type\": \"event\",\n \"source\": \"github\",\n \"on\": \"issue_comment.created\",\n \"filter\": \"icontains(comment.body, '\\''@openhands'\\'')\"\n },\n \"timeout\": 300\n }'\n```\n\n#### GitHub: Auto-review PRs with the \"openhands\" label\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Auto Review PRs\",\n \"prompt\": \"Review this pull request for code quality, potential bugs, and best practices. Provide constructive feedback.\",\n \"trigger\": {\n \"type\": \"event\",\n \"source\": \"github\",\n \"on\": \"pull_request.labeled\",\n \"filter\": \"contains(pull_request.labels[].name, '\\''openhands'\\'')\"\n }\n }'\n```\n\n#### GitHub: Run tests on push to main\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Run Tests on Main\",\n \"prompt\": \"Clone the repository and run the test suite. Report any failures.\",\n \"trigger\": {\n \"type\": \"event\",\n \"source\": \"github\",\n \"on\": \"push\",\n \"filter\": \"ref == '\\''refs/heads/main'\\''\"\n }\n }'\n```\n\n#### GitHub: Triage new issues in specific repos\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Issue Triage Bot\",\n \"prompt\": \"Analyze this new issue and suggest appropriate labels. If it looks like a bug, try to identify the root cause.\",\n \"trigger\": {\n \"type\": \"event\",\n \"source\": \"github\",\n \"on\": \"issues.opened\",\n \"filter\": \"glob(repository.full_name, '\\''myorg/*'\\'')\"\n }\n }'\n```\n\n#### GitHub: Respond to multiple event types\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"PR Activity Bot\",\n \"prompt\": \"Process the PR event and take appropriate action based on the event type.\",\n \"trigger\": {\n \"type\": \"event\",\n \"source\": \"github\",\n \"on\": [\"pull_request.opened\", \"pull_request.synchronize\", \"pull_request.ready_for_review\"]\n }\n }'\n```\n\n---\n\n## Custom Webhooks\n\nFor services other than GitHub (Linear, Stripe, Slack, etc.), register a custom webhook first.\n\n> **Agent behavior:**\n> - **Always provide the curl request** to the user — do not attempt to register webhooks yourself.\n> - **Ask the user:** \"Do you have a webhook signing secret from [service], or should the system generate one?\"\n> - If they have one → include `webhook_secret` in the request\n> - If not → omit it; the response will contain a generated secret they must configure in their service\n\n### Register a Custom Webhook\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/webhooks\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Linear Issues\",\n \"source\": \"linear\",\n \"event_key_expr\": \"type\",\n \"signature_header\": \"Linear-Signature\",\n \"webhook_secret\": \"your-linear-webhook-secret\"\n }'\n```\n\n#### Webhook Fields\n\n| Field | Required | Description |\n|-------|----------|-------------|\n| `name` | Yes | Human-readable name for the webhook |\n| `source` | Yes | Unique source identifier (lowercase, alphanumeric with hyphens, 1-50 chars) |\n| `event_key_expr` | No | JMESPath expression to extract event type from payload (default: `\"type\"`) |\n| `signature_header` | No | HTTP header containing HMAC signature (default: `\"X-Signature-256\"`) |\n| `webhook_secret` | No | Signing secret — provide your own (from the external service) or let the system generate one |\n\n#### Response\n\n```json\n{\n \"id\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"webhook_url\": \"https://app.all-hands.dev/v1/events/{org_id}/linear\",\n \"source\": \"linear\",\n \"enabled\": true\n}\n```\n\n**Note:** When you provide your own `webhook_secret`, it won't be echoed back in the response. If you don't provide one, the system generates a secret and returns it once — store it securely.\n\n### Manage Custom Webhooks\n\n```bash\n# List all webhooks\ncurl \"${OPENHANDS_HOST}/api/automation/v1/webhooks\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n\n# Update a webhook\ncurl -X PATCH \"${OPENHANDS_HOST}/api/automation/v1/webhooks/{webhook_id}\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"enabled\": false}'\n\n# Rotate the signing secret\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/webhooks/{webhook_id}/rotate-secret\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n\n# Delete a webhook\ncurl -X DELETE \"${OPENHANDS_HOST}/api/automation/v1/webhooks/{webhook_id}\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n```\n\n### Custom Webhook Example: Linear\n\nLinear sends webhooks with:\n- Signature header: `Linear-Signature`\n- Event type in payload: `type` field (e.g., `Issue`, `Comment`, `Project`)\n- Action in payload: `action` field (e.g., `create`, `update`, `remove`)\n\n```bash\n# 1. Register the Linear webhook\n# - Get your webhook signing secret from Linear's webhook settings\n# - Use \"Linear-Signature\" as the signature header\n# - Use \"type\" to extract the event type from the payload\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/webhooks\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Linear Issues\",\n \"source\": \"linear\",\n \"event_key_expr\": \"type\",\n \"signature_header\": \"Linear-Signature\",\n \"webhook_secret\": \"lin_wh_xxxxxxxxxxxxx\"\n }'\n\n# Response includes webhook_url — configure this in Linear:\n# Settings → API → Webhooks → New webhook → paste the webhook_url\n\n# 2. Create an automation for new Linear issues\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Triage New Linear Issues\",\n \"prompt\": \"A new issue was created in Linear. Analyze the issue title and description, suggest appropriate labels, and add a comment with initial triage notes.\",\n \"trigger\": {\n \"type\": \"event\",\n \"source\": \"linear\",\n \"on\": \"Issue\",\n \"filter\": \"action == '\\''create'\\''\"\n }\n }'\n\n# 3. Create an automation for high-priority issue updates\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"High Priority Issue Alert\",\n \"prompt\": \"A high-priority issue was updated. Review the changes and notify the team if action is needed.\",\n \"trigger\": {\n \"type\": \"event\",\n \"source\": \"linear\",\n \"on\": \"Issue\",\n \"filter\": \"action == '\\''update'\\'' && data.priority == `1`\"\n }\n }'\n```\n\n### Common Signature Headers by Service\n\n| Service | Signature Header | Event Key Expression |\n|---------|-----------------|---------------------|\n| Linear | `Linear-Signature` | `type` |\n| Stripe | `Stripe-Signature` | `type` |\n| Slack | `X-Slack-Signature` | `type` |\n| Twilio | `X-Twilio-Signature` | `type` |\n| Generic | `X-Signature-256` | `type` |\n\n---\n\n### Plugin Preset\n\nUse the **preset/plugin endpoint** when you need to load one or more plugins that provide extended capabilities like skills, MCP configurations, hooks, and commands.\n\n> **💡 Finding plugins:** Browse the [OpenHands/extensions](https://github.com/OpenHands/extensions) repository for available skills and plugins. When given a broad use case, check this directory first to see if something already exists that fits your needs.\n\n#### How It Works\n\n1. Specify one or more plugins (from GitHub repos, git URLs, or monorepo subdirectories)\n2. Provide a prompt that can invoke plugin commands (e.g., `/plugin-name:command`)\n3. The service generates SDK boilerplate that loads all plugins at runtime, creates a conversation with plugin capabilities, and executes the prompt\n4. The service packages everything into a tarball, uploads it, and creates the automation\n\n#### Request\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/plugin\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"My Plugin Automation\",\n \"plugins\": [\n {\"source\": \"github:owner/repo\", \"ref\": \"v1.0.0\"},\n {\"source\": \"github:owner/another-plugin\"}\n ],\n \"prompt\": \"Use the plugin commands to perform the task\",\n \"trigger\": {\n \"type\": \"cron\",\n \"schedule\": \"0 9 * * 1\",\n \"timezone\": \"UTC\"\n }\n }'\n```\n\n#### Request Fields\n\n| Field | Required | Description |\n|-------|----------|-------------|\n| `name` | Yes | Name of the automation (1-500 characters) |\n| `plugins` | Yes | List of plugin sources (at least one required) |\n| `plugins[].source` | Yes | Plugin source: `github:owner/repo`, git URL, or local path |\n| `plugins[].ref` | No | Git ref: branch, tag, or commit SHA |\n| `plugins[].repo_path` | No | Subdirectory path for monorepos |\n| `prompt` | Yes | Instructions for the automation (1-50,000 characters) |\n| `trigger` | Yes | Trigger configuration — either `cron` or `event` (same as Prompt Preset) |\n| `timeout` | No | Max execution time in seconds (default: system maximum) |\n| `repos` | No | Repositories to clone (see [Repository Cloning](#repository-cloning)) |\n\n#### Plugin Source Formats\n\n| Format | Example | Description |\n|--------|---------|-------------|\n| GitHub shorthand | `github:owner/repo` | Fetches from GitHub |\n| Git URL | `https://github.com/owner/repo.git` | Any git repository |\n| With ref | `{\"source\": \"github:owner/repo\", \"ref\": \"v1.0.0\"}` | Specific branch/tag/commit |\n| Monorepo | `{\"source\": \"github:org/monorepo\", \"repo_path\": \"plugins/my-plugin\"}` | Subdirectory in repo |\n\n#### Response (HTTP 201)\n\n```json\n{\n \"id\": \"123e4567-e89b-12d3-a456-426614174000\",\n \"name\": \"My Plugin Automation\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 9 * * 1\", \"timezone\": \"UTC\"},\n \"enabled\": true,\n \"created_at\": \"2025-03-25T10:00:00Z\"\n}\n```\n\n#### Plugin Preset Examples\n\n**Single plugin with version:**\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/plugin\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Code Review Automation\",\n \"plugins\": [\n {\"source\": \"github:owner/code-review-plugin\", \"ref\": \"v2.0.0\"}\n ],\n \"prompt\": \"Review all Python files in the repository for code quality issues\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 9 * * 1-5\", \"timezone\": \"UTC\"}\n }'\n```\n\n**Multiple plugins:**\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/plugin\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Security Scan Automation\",\n \"plugins\": [\n {\"source\": \"github:owner/security-scanner\"},\n {\"source\": \"github:owner/report-generator\", \"ref\": \"main\"}\n ],\n \"prompt\": \"Run a security scan on the codebase and generate a report\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 2 * * 0\", \"timezone\": \"UTC\"},\n \"timeout\": 600\n }'\n```\n\n**Monorepo plugin:**\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/plugin\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Style Guide Enforcement\",\n \"plugins\": [\n {\"source\": \"github:company/monorepo\", \"repo_path\": \"plugins/style-guide\", \"ref\": \"main\"}\n ],\n \"prompt\": \"Check all files against the company style guide\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 8 * * 1\", \"timezone\": \"America/Los_Angeles\"}\n }'\n```\n\n---\n\n## Repository Cloning\n\nBoth presets support an optional `repos` field to clone repositories into the sandbox before execution. Cloned repos have their skills (AGENTS.md, `.agents/skills/`) automatically loaded.\n\n### Repo Source Formats\n\n| Format | Example | Description |\n|--------|---------|-------------|\n| Full URL | `\"https://github.com/owner/repo\"` | Provider auto-detected |\n| Full URL + ref | `{\"url\": \"https://github.com/owner/repo\", \"ref\": \"main\"}` | With branch/tag/SHA |\n| Short URL | `{\"url\": \"owner/repo\", \"provider\": \"github\"}` | Requires `provider` field |\n\n**Supported providers:** `github`, `gitlab`, `bitbucket`\n\n> **Note:** Short URLs (`owner/repo`) require an explicit `provider` field. Full URLs auto-detect the provider.\n\n### Examples\n\n**Single repo (full URL):**\n```json\n{\n \"repos\": [\"https://github.com/OpenHands/openhands-cli\"]\n}\n```\n\n**Multiple repos with refs:**\n```json\n{\n \"repos\": [\n {\"url\": \"https://github.com/owner/repo1\", \"ref\": \"main\"},\n {\"url\": \"https://gitlab.com/owner/repo2\", \"ref\": \"v1.0.0\"}\n ]\n}\n```\n\n**Short URL with provider:**\n```json\n{\n \"repos\": [\n {\"url\": \"owner/repo\", \"provider\": \"github\", \"ref\": \"main\"}\n ]\n}\n```\n\n### Complete Automation Example\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Analyze Codebase\",\n \"prompt\": \"Analyze the openhands-cli codebase and generate a summary report\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 9 * * 1\"},\n \"repos\": [\n {\"url\": \"https://github.com/OpenHands/openhands-cli\", \"ref\": \"main\"}\n ]\n }'\n```\n\n---\n\n## Managing Automations\n\n### List Automations\n\n```bash\ncurl \"${OPENHANDS_HOST}/api/automation/v1?limit=20\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n```\n\n### Get / Update / Delete\n\n```bash\n# Get details\ncurl \"${OPENHANDS_HOST}/api/automation/v1/{automation_id}\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n\n# Update (fields: name, trigger, enabled, timeout)\ncurl -X PATCH \"${OPENHANDS_HOST}/api/automation/v1/{automation_id}\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"enabled\": false}'\n\n# Delete\ncurl -X DELETE \"${OPENHANDS_HOST}/api/automation/v1/{automation_id}\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n```\n\n### Trigger and Monitor Runs\n\n```bash\n# Manually trigger a run\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/{automation_id}/dispatch\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n\n# List runs\ncurl \"${OPENHANDS_HOST}/api/automation/v1/{automation_id}/runs?limit=20\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n```\n\nRun status values: `PENDING` (waiting for dispatch), `RUNNING` (in progress), `COMPLETED` (success), `FAILED` (check `error_detail`).\n\n---\n\n## Run Lifecycle\n\nWhen a run completes, the automation service receives a callback and marks the run done. Any conversations started during the run remain accessible in the OpenHands UI — users can view the history and continue interacting. The agent server persists until it times out or is manually deleted.\n\nThe automation script itself controls when the callback fires (signalling completion). For simple synchronous scripts this happens naturally on exit. For scripts that start asynchronous conversations, the callback should be deferred until the conversation reaches an idle state (see `references/custom-automation.md` for patterns).\n\n---\n\n## Choosing the Right Preset\n\nPick based on **what the task needs**, not just **what is technically possible**. An LLM-driven preset can do almost anything, so \"the preset can satisfy this\" is not by itself a good reason to pick it — every run costs tokens and sandbox time.\n\n| Use Case | Recommended |\n|----------|-------------|\n| Reasoning, summarization, triage, code review, or open-ended tool use | **Prompt Preset** |\n| Needs plugin commands / skills / MCP configs / hooks | **Plugin Preset** |\n| Compare plugin versions or configurations across runs | **Plugin Preset with A/B testing** — see `references/ab-testing.md` |\n| **Deterministic task** (fixed data + scheduled action, e.g. healthcheck, Slack notification, rotating from a known list) — especially if it runs frequently | **Custom script, no LLM** — see `references/custom-automation.md#deterministic-script-no-llm` |\n| Custom Python dependencies, multi-file project, or direct SDK lifecycle control | **Custom script with SDK** — see `references/custom-automation.md#sdk-based-scripts` |\n\nThe **prompt preset** is the right default for genuinely agent-shaped work — anything that benefits from reasoning over context, calling tools dynamically, or producing a non-templated output. Use the **plugin preset** when you need extended capabilities from plugins (skills, MCP configurations, hooks, commands).\n\n**Watch for deterministic, high-frequency patterns.** Requests like \"send a daily standup reminder\", \"ping a healthcheck URL every minute\", \"post a random quote every 5 minutes\", or \"rotate a fact-of-the-day message\" do not need an LLM. Surface this to the user explicitly with a rough cost framing (e.g. \"this schedule will invoke your LLM ~288 times/day\") before defaulting to a preset. As a rule of thumb, any cron tighter than hourly deserves a deliberate \"should this really be agent-driven?\" check.\n\n**When neither preset is the right fit** (deterministic task, custom Python dependencies, non-Python entrypoint, multi-file project structure, direct SDK lifecycle control), explain the options to the user and let them decide. Do not attempt custom automation without explicit user agreement. If they choose the custom route, refer to `references/custom-automation.md`.\n\n## Reference Files\n\n- **`references/custom-automation.md`** — Detailed guide for custom automations: tarball uploads, code structure (SDK and no-LLM), environment variables, validation rules, and complete examples. Consult this whenever you need to evaluate or recommend the custom path (including for deterministic / cost-sensitive tasks per rule 0). Only *implement* a custom automation after the user agrees to that path.\n- **`references/ab-testing.md`** — A/B testing for plugin automations: defining variants with weights, experiment configuration, variant selection logic, observability via conversation tags, and complete examples. Consult this when a user wants to compare plugin versions or configurations."
|
|
344
|
+
"content": "# OpenHands Automations\n\nCreate and manage automations that run inside an OpenHands agent server — triggered by cron schedules or webhook events (GitHub, custom services).\n\n## Automation Creation Process\nThe agent must follow these steps when creating an automation:\n* Quickly check that you can access the correct automations backend using the auth mechanism below\n* Quickly check that you can access any necessary integrations (e.g. GitHub, Slack); if access fails, inform the user and stop\n* Ask the user for any necessary information, e.g. if you need the name of a Slack channel or GitHub repo to proceed\n* Write the code or prompt that will be sent to the automations backend _inside the current workspace_\n* Show the code to the user with the `canvas_ui` tool if available, otherwise present it in a fenced code block in your reply\n* Message the user with a concise summary of how the automation will behave, and ask if they are ready to deploy it\n\n## Architecture\n\nTwo components work together to run automations:\n\n**Automation Service** (API at `OPENHANDS_HOST/api/automation/v1`)\nManages the *when*: holds automation definitions, schedules cron-triggered runs, dispatches webhook-triggered runs, and receives completion callbacks to mark runs as done. This is the API you call to create, update, and manage automations.\n\n**Agent Server** (accessible as `AGENT_SERVER_URL` inside script runs)\nManages the *what*: the runtime environment where automation scripts execute and where conversations (AI agent interactions with tools, bash, file editing, etc.) run. When a run is triggered, the automation service uploads the automation's tarball to the agent server, which unpacks and runs the entrypoint script. The script connects back to the agent server using `AGENT_SERVER_URL` and a session API key to start, monitor, and stop conversations.\n\nThe agent server typically runs inside a **sandbox** (a Docker or Kubernetes container). Some deployments use sandboxless mode, where the agent server runs directly on a host.\n\n**Key environment variables:**\n\n| Variable | Availability | Description |\n|---|---|---|\n| `RUNTIME_URL` | Ambient in cloud environments | Public-facing URL of the **agent server** sandbox. Use this to determine whether external webhook delivery is possible — if unset or local, webhooks cannot be received. The automation service may run at a separate URL (see Determining the API Host). |\n| `AGENT_SERVER_URL` | Injected into scripts at run time only | Internal URL of the agent server. Available inside script execution context; **not** an ambient environment variable outside of a running script. |\n| `OPENHANDS_HOST` | Shell convention only — set manually | Base URL for the automation service API. **Not a real environment variable.** Set it from the `<HOST>` system-prompt value, or default to `https://app.all-hands.dev`. Used in all `curl` examples throughout this skill. |\n\n> **⚠️ CRITICAL — Agent behavior rules:**\n>\n> 0. **Does this task need an LLM at all? Check first.** Before picking a preset, ask whether the task actually requires reasoning, judgment, summarization, or open-ended tool use. If it is fully deterministic — fixed data transforms, scheduled HTTP calls, healthcheck pings, file rotation, picking from a known list, posting a templated message — an LLM-driven preset is overkill. Every run will consume LLM tokens, which adds up fast at high frequencies (every 5 min ≈ 288 runs/day). Surface the trade-off to the user and offer the custom-script path (see `references/custom-automation.md`) as the cheaper, more reliable option. Be especially careful for cron schedules tighter than hourly.\n>\n> **Instant-recognition patterns — these are always deterministic, never use an LLM preset:**\n> - \"post a quote / message / fact every N minutes\" (rotating from a list)\n> - \"send a scheduled reminder / standup / digest\"\n> - \"ping a health-check URL on a schedule\"\n> - \"post to Slack / webhook every N minutes\"\n> - Any task where the full output could be written as a static template right now\n>\n> 1. **For LLM-appropriate work, default to preset endpoints.** They handle all SDK boilerplate, tarball packaging, and upload automatically:\n> - **Prompt preset** (`POST /v1/preset/prompt`) — for tasks expressed as a natural language prompt that benefit from agent reasoning\n> - **Plugin preset** (`POST /v1/preset/plugin`) — when plugins with skills, MCP configs, or commands are needed\n> 2. **Do not silently create custom scripts.** Do not generate Python code, `setup.sh` files, or tarball uploads without user consent. But *do* proactively recommend the custom path (per rule 0) when the task is deterministic or high-frequency — surface the option and let the user choose.\n> 3. **If neither preset is the right fit**, do NOT silently fall back to custom automation. Instead, explain the available options to the user:\n> - **Prompt preset** — natural language prompt execution (LLM-driven)\n> - **Plugin preset** — load plugins with extended capabilities (skills, MCP, hooks, commands)\n> - **Custom script** — full control over code, with or without LLM; point them to `references/custom-automation.md`\n> - Let the user choose which approach to use.\n> 4. **Only create custom scripts after the user agrees to that path.** Refer to `references/custom-automation.md` for the full reference.\n> 5. **Before suggesting event-triggered (webhook) automations, check whether the deployment is publicly reachable.** Check `RUNTIME_URL`. Webhooks require an internet-accessible URL so that external services (GitHub, Slack, Linear, etc.) can deliver events to the automation service. If `RUNTIME_URL` is unset, empty, or resolves to a local or private address (`localhost`, `127.0.0.1`, `0.0.0.0`, or any RFC 1918 range: `10.x.x.x`, `192.168.x.x`, `172.16–31.x.x`), the service cannot receive inbound webhook traffic from the public internet. In that case:\n> - **Recommend a cron-based polling automation instead.** Have the automation run on a schedule and call the external service's API (e.g., the GitHub REST API) to check for new events since the last run.\n> - Explain the limitation clearly to the user: \"Because this is a local deployment, external services can't reach the webhook endpoint. I'll set up a polling automation using a cron schedule instead.\"\n\n### No-LLM Script Helpers\n\nWhen building a deterministic custom script, these two stdlib-only functions are required. Copy them verbatim — they use `AGENT_SERVER_URL` and `SESSION_API_KEY` injected by the automation service.\n\n```python\nimport json, os, urllib.request\n\ndef get_secret(name):\n \"\"\"Fetch a named secret stored in the agent server.\"\"\"\n url = os.environ.get(\"AGENT_SERVER_URL\", \"\").rstrip(\"/\")\n key = os.environ.get(\"SESSION_API_KEY\") or os.environ.get(\"OH_SESSION_API_KEYS_0\", \"\")\n with urllib.request.urlopen(urllib.request.Request(\n f\"{url}/api/settings/secrets/{name}\", headers={\"X-Session-API-Key\": key}\n )) as r:\n return r.read().decode().strip()\n\ndef fire_callback(status=\"COMPLETED\", error=None):\n \"\"\"Signal run completion. MUST be called on every exit path — success AND error.\"\"\"\n url = os.environ.get(\"AUTOMATION_CALLBACK_URL\", \"\")\n if not url: return\n body = {\"status\": status, \"run_id\": os.environ.get(\"AUTOMATION_RUN_ID\", \"\")}\n if error: body[\"error\"] = error\n try:\n urllib.request.urlopen(urllib.request.Request(url, data=json.dumps(body).encode(), headers={\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {os.environ.get('AUTOMATION_CALLBACK_API_KEY', '')}\",\n }))\n except Exception as e: print(f\"Callback error: {e}\")\n```\n\nEntrypoint must be `python3 main.py` (no `setup.sh` needed). Wrap your main logic in `try/except` and call `fire_callback(\"FAILED\", str(e))` in the except block.\n\n**State persistence between runs** — polling automations that track a \"last processed\" timestamp or active conversation IDs must use the built-in KV store rather than local files. Local files are lost when a run ends on a cloud pod. The KV store is available when `AUTOMATION_KV_TOKEN` is injected into the run environment. See `references/custom-automation.md#state-persistence-kv-store` for ready-to-copy `kv_get` / `kv_set` / `load_state` / `save_state` helpers.\n\n---\n\n## Authentication\n\nAll requests require Bearer authentication:\n\n```bash\n-H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n```\n\n## API Endpoints\n\n### Determining the API Host\n\n**Before making API calls, determine the correct host:**\n\nThe automation service may run at a different URL from the agent server. In the examples throughout this skill, `${OPENHANDS_HOST}` is a shell-variable convention for the automation service base URL — it is **not** a real environment variable. Set it from context before running any curl command:\n\n- Look for a `<HOST>` value in the system prompt. If present, use that URL.\n- Otherwise default to `https://app.all-hands.dev`.\n\n```bash\nOPENHANDS_HOST=\"https://app.all-hands.dev\" # replace with <HOST> if provided\n```\n\n\n### Automation Endpoints\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/api/automation/v1/preset/prompt` | POST | **Create automation from a prompt (recommended)** |\n| `/api/automation/v1/preset/plugin` | POST | **Create automation with plugins** |\n| `/api/automation/v1` | GET | List automations |\n| `/api/automation/v1/{id}` | GET | Get automation details |\n| `/api/automation/v1/{id}` | PATCH | Update automation |\n| `/api/automation/v1/{id}` | DELETE | Delete automation |\n| `/api/automation/v1/{id}/dispatch` | POST | Trigger a run manually |\n| `/api/automation/v1/{id}/runs` | GET | List automation runs |\n\n### Custom Webhook Endpoints\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `/api/automation/v1/webhooks` | POST | Register a custom webhook source |\n| `/api/automation/v1/webhooks` | GET | List all custom webhooks |\n| `/api/automation/v1/webhooks/{id}` | GET | Get webhook details |\n| `/api/automation/v1/webhooks/{id}` | PATCH | Update webhook settings |\n| `/api/automation/v1/webhooks/{id}` | DELETE | Delete a webhook |\n| `/api/automation/v1/webhooks/{id}/rotate-secret` | POST | Rotate signing secret |\n\n---\n\n## Trigger Types\n\nAutomations support two trigger types:\n\n| Trigger Type | Use Case |\n|--------------|----------|\n| **Cron** | Run on a schedule (daily, weekly, hourly, etc.) |\n| **Event** | Run when a webhook event occurs (GitHub PR opened, issue commented, etc.) — **requires a publicly reachable deployment** |\n\n---\n\n## Creating Automations\n\nTwo preset endpoints simplify automation creation by handling SDK boilerplate, tarball packaging, and upload automatically:\n\n1. **Prompt Preset** — Execute a natural language prompt (simple tasks)\n2. **Plugin Preset** — Load plugins with skills, MCP configs, and commands (extended capabilities)\n\n---\n\n### Prompt Preset\n\nUse the **preset/prompt endpoint** for simple automations. Provide a natural language prompt describing the task.\n\n#### How It Works\n\n1. Send a prompt describing the task (e.g., \"Generate a weekly status report\")\n2. The automation service generates a Python script that: fetches LLM config and secrets from the agent server, starts an AI agent conversation with your prompt, and sends a completion callback when done\n3. The script is packaged as a tarball and the automation is registered; on each trigger, the automation service uploads the tarball to the agent server, which unpacks and runs the script inside its environment\n\n#### Request\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"My Automation Name\",\n \"prompt\": \"What the automation should do\",\n \"trigger\": {\n \"type\": \"cron\",\n \"schedule\": \"0 9 * * *\",\n \"timezone\": \"UTC\"\n }\n }'\n```\n\n#### Request Fields\n\n| Field | Required | Description |\n|-------|----------|-------------|\n| `name` | Yes | Name of the automation (1-500 characters) |\n| `prompt` | Yes | Natural language instructions (1-50,000 characters) |\n| `trigger` | Yes | Trigger configuration — either `cron` or `event` (see below) |\n| `timeout` | No | Max execution time in seconds (default: system maximum) |\n| `repos` | No | Repositories to clone (see [Repository Cloning](#repository-cloning)) |\n\n**Cron Trigger Fields:**\n\n| Field | Required | Description |\n|-------|----------|-------------|\n| `trigger.type` | Yes | `\"cron\"` |\n| `trigger.schedule` | Yes | Cron expression (5 fields: min hour day month weekday) |\n| `trigger.timezone` | No | IANA timezone (default: `\"UTC\"`) |\n\n**Event Trigger Fields:**\n\n| Field | Required | Description |\n|-------|----------|-------------|\n| `trigger.type` | Yes | `\"event\"` |\n| `trigger.source` | Yes | Event source: `\"github\"` or custom webhook source name |\n| `trigger.on` | Yes | Event key pattern(s) to match (see Event Keys below) |\n| `trigger.filter` | No | JMESPath expression for payload filtering (see Filter Expressions below) |\n\n#### Prompt Tips\n\nWrite the prompt as an instruction to an AI agent. The prompt executes inside a sandbox with full tool access (bash, file editing, etc.), the user's configured LLM, stored secrets, and MCP server integrations. Examples:\n\n- `\"Generate a weekly status report summarizing the team's GitHub activity and post it to Slack\"`\n- `\"Check the production API health endpoint every hour and alert if it returns non-200\"`\n- `\"Pull the latest data from our analytics API and update the dashboard spreadsheet\"`\n\n#### Cron Schedule\n\n| Field | Values | Description |\n|-------|--------|-------------|\n| Minute | 0-59 | Minute of the hour |\n| Hour | 0-23 | Hour of the day (24-hour) |\n| Day | 1-31 | Day of the month |\n| Month | 1-12 | Month of the year |\n| Weekday | 0-6 | Day of week (0=Sun, 6=Sat) |\n\nCommon schedules: `0 9 * * *` (daily 9 AM), `0 9 * * 1-5` (weekdays 9 AM), `0 9 * * 1` (Mondays 9 AM), `0 0 1 * *` (first of month), `*/15 * * * *` (every 15 min), `0 */6 * * *` (every 6 hours).\n\n#### Response (HTTP 201)\n\n```json\n{\n \"id\": \"123e4567-e89b-12d3-a456-426614174000\",\n \"name\": \"My Automation Name\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 9 * * *\", \"timezone\": \"UTC\"},\n \"enabled\": true,\n \"created_at\": \"2025-03-25T10:00:00Z\"\n}\n```\n\n#### Prompt Preset Examples\n\n**Daily report:**\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Daily Report\",\n \"prompt\": \"Generate a daily status report and save it to a file in the workspace\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 9 * * 1-5\", \"timezone\": \"America/New_York\"}\n }'\n```\n\n**Weekly cleanup:**\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Weekly Cleanup\",\n \"prompt\": \"Clean up temporary files older than 7 days and send a summary of what was removed\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 2 * * 0\", \"timezone\": \"UTC\"},\n \"timeout\": 300\n }'\n```\n\n---\n\n## Polling as a Webhook Alternative\n\nWhen the deployment cannot receive inbound webhook traffic (see rule 5), use a cron-triggered automation that calls the external service’s API on a schedule to check for new events.\n\n### Polling vs. Webhooks at a Glance\n\n| | Webhooks (Event trigger) | Polling (Cron trigger) |\n|---|---|---|\n| **Requires public URL** | Yes | No — works locally |\n| **Latency** | Near-instant | Up to one poll interval |\n| **API calls** | Only on real events | Every poll interval |\n| **Best for** | Cloud / public deployments | Local or private deployments |\n\n---\n\n## Event-Triggered Automations (Webhooks)\n\nEvent-triggered automations run when a webhook event occurs — like a GitHub PR being opened, an issue receiving a comment, or a custom service sending a notification.\n\n### Built-in Integrations\n\n**GitHub** is a built-in integration — no webhook registration needed. Just create automations with `\"source\": \"github\"`.\n\n### GitHub Event Keys\n\nEvents use the format `{event_type}.{action}` or just `{event_type}` (for events without actions like `push`).\n\n| Event Type | Event Keys | Description |\n|------------|------------|-------------|\n| `pull_request` | `pull_request.opened`, `pull_request.closed`, `pull_request.synchronize`, `pull_request.labeled`, `pull_request.unlabeled`, `pull_request.reopened`, `pull_request.edited`, `pull_request.ready_for_review` | PR activity |\n| `issues` | `issues.opened`, `issues.closed`, `issues.reopened`, `issues.labeled`, `issues.unlabeled`, `issues.edited`, `issues.assigned` | Issue activity |\n| `issue_comment` | `issue_comment.created`, `issue_comment.edited`, `issue_comment.deleted` | Comments on issues/PRs |\n| `push` | `push` | Code pushed to a branch |\n| `release` | `release.published`, `release.created`, `release.released`, `release.prereleased` | Release activity |\n| `pull_request_review` | `pull_request_review.submitted`, `pull_request_review.edited`, `pull_request_review.dismissed` | PR review activity |\n\n**Wildcards:** Use `*` to match any action — e.g., `pull_request.*` matches all PR events.\n\n**Multiple patterns:** The `on` field can be a string or array — e.g., `[\"push\", \"pull_request.opened\"]`.\n\n### Filter Expressions (JMESPath)\n\nFilters let you match events based on payload content using JMESPath expressions.\n\n#### Available Functions\n\n| Function | Description | Example |\n|----------|-------------|---------|\n| `glob(str, pattern)` | Wildcard pattern matching | `glob(repository.full_name, 'myorg/*')` |\n| `icontains(str, substr)` | Case-insensitive substring | `icontains(comment.body, '@openhands')` |\n| `contains(array, value)` | Array contains value | `contains(pull_request.labels[].name, 'bug')` |\n| `regex(str, pattern)` | Regular expression match | `regex(ref, '^refs/tags/v\\\\d+')` |\n| `starts_with(str, prefix)` | String starts with | `starts_with(ref, 'refs/heads/')` |\n| `ends_with(str, suffix)` | String ends with | `ends_with(ref, '/main')` |\n| `lower(str)` / `upper(str)` | Case conversion | `lower(sender.login) == 'admin'` |\n\n#### Boolean Operators\n\n- `&&` — AND\n- `||` — OR \n- `!` — NOT\n\n#### Filter Examples\n\n```javascript\n// Exact match on label name\n\"contains(pull_request.labels[].name, 'openhands')\"\n\n// Case-insensitive mention in comment\n\"icontains(comment.body, '@openhands')\"\n\n// Match specific repository\n\"repository.full_name == 'myorg/myrepo'\"\n\n// Match any repo in an org\n\"glob(repository.full_name, 'myorg/*')\"\n\n// PR with 'bug' label in any org repo\n\"glob(repository.full_name, 'myorg/*') && contains(pull_request.labels[].name, 'bug')\"\n\n// Push to main or release branches\n\"glob(ref, 'refs/heads/main') || glob(ref, 'refs/heads/release/*')\"\n\n// Issue opened by a specific user\n\"sender.login == 'dependabot[bot]'\"\n\n// Not a draft PR\n\"!pull_request.draft\"\n```\n\n---\n\n### Event-Triggered Examples\n\n#### GitHub: Respond to @openhands mentions in comments\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"OpenHands Mention Responder\",\n \"prompt\": \"Analyze the issue or PR context and provide a helpful response to the user'\\''s question. The comment body and context are available in the event payload.\",\n \"trigger\": {\n \"type\": \"event\",\n \"source\": \"github\",\n \"on\": \"issue_comment.created\",\n \"filter\": \"icontains(comment.body, '\\''@openhands'\\'')\"\n },\n \"timeout\": 300\n }'\n```\n\n#### GitHub: Auto-review PRs with the \"openhands\" label\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Auto Review PRs\",\n \"prompt\": \"Review this pull request for code quality, potential bugs, and best practices. Provide constructive feedback.\",\n \"trigger\": {\n \"type\": \"event\",\n \"source\": \"github\",\n \"on\": \"pull_request.labeled\",\n \"filter\": \"contains(pull_request.labels[].name, '\\''openhands'\\'')\"\n }\n }'\n```\n\n#### GitHub: Run tests on push to main\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Run Tests on Main\",\n \"prompt\": \"Clone the repository and run the test suite. Report any failures.\",\n \"trigger\": {\n \"type\": \"event\",\n \"source\": \"github\",\n \"on\": \"push\",\n \"filter\": \"ref == '\\''refs/heads/main'\\''\"\n }\n }'\n```\n\n#### GitHub: Triage new issues in specific repos\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Issue Triage Bot\",\n \"prompt\": \"Analyze this new issue and suggest appropriate labels. If it looks like a bug, try to identify the root cause.\",\n \"trigger\": {\n \"type\": \"event\",\n \"source\": \"github\",\n \"on\": \"issues.opened\",\n \"filter\": \"glob(repository.full_name, '\\''myorg/*'\\'')\"\n }\n }'\n```\n\n#### GitHub: Respond to multiple event types\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"PR Activity Bot\",\n \"prompt\": \"Process the PR event and take appropriate action based on the event type.\",\n \"trigger\": {\n \"type\": \"event\",\n \"source\": \"github\",\n \"on\": [\"pull_request.opened\", \"pull_request.synchronize\", \"pull_request.ready_for_review\"]\n }\n }'\n```\n\n---\n\n## Custom Webhooks\n\nFor services other than GitHub (Linear, Stripe, Slack, etc.), register a custom webhook first.\n\n> **Agent behavior:**\n> - **Always provide the curl request** to the user — do not attempt to register webhooks yourself.\n> - **Ask the user:** \"Do you have a webhook signing secret from [service], or should the system generate one?\"\n> - If they have one → include `webhook_secret` in the request\n> - If not → omit it; the response will contain a generated secret they must configure in their service\n\n### Register a Custom Webhook\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/webhooks\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Linear Issues\",\n \"source\": \"linear\",\n \"event_key_expr\": \"type\",\n \"signature_header\": \"Linear-Signature\",\n \"webhook_secret\": \"your-linear-webhook-secret\"\n }'\n```\n\n#### Webhook Fields\n\n| Field | Required | Description |\n|-------|----------|-------------|\n| `name` | Yes | Human-readable name for the webhook |\n| `source` | Yes | Unique source identifier (lowercase, alphanumeric with hyphens, 1-50 chars) |\n| `event_key_expr` | No | JMESPath expression to extract event type from payload (default: `\"type\"`) |\n| `signature_header` | No | HTTP header containing HMAC signature (default: `\"X-Signature-256\"`) |\n| `webhook_secret` | No | Signing secret — provide your own (from the external service) or let the system generate one |\n\n#### Response\n\n```json\n{\n \"id\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"webhook_url\": \"https://app.all-hands.dev/v1/events/{org_id}/linear\",\n \"source\": \"linear\",\n \"enabled\": true\n}\n```\n\n**Note:** When you provide your own `webhook_secret`, it won't be echoed back in the response. If you don't provide one, the system generates a secret and returns it once — store it securely.\n\n### Manage Custom Webhooks\n\n```bash\n# List all webhooks\ncurl \"${OPENHANDS_HOST}/api/automation/v1/webhooks\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n\n# Update a webhook\ncurl -X PATCH \"${OPENHANDS_HOST}/api/automation/v1/webhooks/{webhook_id}\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"enabled\": false}'\n\n# Rotate the signing secret\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/webhooks/{webhook_id}/rotate-secret\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n\n# Delete a webhook\ncurl -X DELETE \"${OPENHANDS_HOST}/api/automation/v1/webhooks/{webhook_id}\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n```\n\n### Custom Webhook Example: Linear\n\nLinear sends webhooks with:\n- Signature header: `Linear-Signature`\n- Event type in payload: `type` field (e.g., `Issue`, `Comment`, `Project`)\n- Action in payload: `action` field (e.g., `create`, `update`, `remove`)\n\n```bash\n# 1. Register the Linear webhook\n# - Get your webhook signing secret from Linear's webhook settings\n# - Use \"Linear-Signature\" as the signature header\n# - Use \"type\" to extract the event type from the payload\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/webhooks\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Linear Issues\",\n \"source\": \"linear\",\n \"event_key_expr\": \"type\",\n \"signature_header\": \"Linear-Signature\",\n \"webhook_secret\": \"lin_wh_xxxxxxxxxxxxx\"\n }'\n\n# Response includes webhook_url — configure this in Linear:\n# Settings → API → Webhooks → New webhook → paste the webhook_url\n\n# 2. Create an automation for new Linear issues\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Triage New Linear Issues\",\n \"prompt\": \"A new issue was created in Linear. Analyze the issue title and description, suggest appropriate labels, and add a comment with initial triage notes.\",\n \"trigger\": {\n \"type\": \"event\",\n \"source\": \"linear\",\n \"on\": \"Issue\",\n \"filter\": \"action == '\\''create'\\''\"\n }\n }'\n\n# 3. Create an automation for high-priority issue updates\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"High Priority Issue Alert\",\n \"prompt\": \"A high-priority issue was updated. Review the changes and notify the team if action is needed.\",\n \"trigger\": {\n \"type\": \"event\",\n \"source\": \"linear\",\n \"on\": \"Issue\",\n \"filter\": \"action == '\\''update'\\'' && data.priority == `1`\"\n }\n }'\n```\n\n### Common Signature Headers by Service\n\n| Service | Signature Header | Event Key Expression |\n|---------|-----------------|---------------------|\n| Linear | `Linear-Signature` | `type` |\n| Stripe | `Stripe-Signature` | `type` |\n| Slack | `X-Slack-Signature` | `type` |\n| Twilio | `X-Twilio-Signature` | `type` |\n| Generic | `X-Signature-256` | `type` |\n\n---\n\n### Plugin Preset\n\nUse the **preset/plugin endpoint** when you need to load one or more plugins that provide extended capabilities like skills, MCP configurations, hooks, and commands.\n\n> **💡 Finding plugins:** Browse the [OpenHands/extensions](https://github.com/OpenHands/extensions) repository for available skills and plugins. When given a broad use case, check this directory first to see if something already exists that fits your needs.\n\n#### How It Works\n\n1. Specify one or more plugins (from GitHub repos, git URLs, or monorepo subdirectories)\n2. Provide a prompt that can invoke plugin commands (e.g., `/plugin-name:command`)\n3. The service generates SDK boilerplate that loads all plugins at runtime, creates a conversation with plugin capabilities, and executes the prompt\n4. The service packages everything into a tarball, uploads it, and creates the automation\n\n#### Request\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/plugin\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"My Plugin Automation\",\n \"plugins\": [\n {\"source\": \"github:owner/repo\", \"ref\": \"v1.0.0\"},\n {\"source\": \"github:owner/another-plugin\"}\n ],\n \"prompt\": \"Use the plugin commands to perform the task\",\n \"trigger\": {\n \"type\": \"cron\",\n \"schedule\": \"0 9 * * 1\",\n \"timezone\": \"UTC\"\n }\n }'\n```\n\n#### Request Fields\n\n| Field | Required | Description |\n|-------|----------|-------------|\n| `name` | Yes | Name of the automation (1-500 characters) |\n| `plugins` | Yes | List of plugin sources (at least one required) |\n| `plugins[].source` | Yes | Plugin source: `github:owner/repo`, git URL, or local path |\n| `plugins[].ref` | No | Git ref: branch, tag, or commit SHA |\n| `plugins[].repo_path` | No | Subdirectory path for monorepos |\n| `prompt` | Yes | Instructions for the automation (1-50,000 characters) |\n| `trigger` | Yes | Trigger configuration — either `cron` or `event` (same as Prompt Preset) |\n| `timeout` | No | Max execution time in seconds (default: system maximum) |\n| `repos` | No | Repositories to clone (see [Repository Cloning](#repository-cloning)) |\n\n#### Plugin Source Formats\n\n| Format | Example | Description |\n|--------|---------|-------------|\n| GitHub shorthand | `github:owner/repo` | Fetches from GitHub |\n| Git URL | `https://github.com/owner/repo.git` | Any git repository |\n| With ref | `{\"source\": \"github:owner/repo\", \"ref\": \"v1.0.0\"}` | Specific branch/tag/commit |\n| Monorepo | `{\"source\": \"github:org/monorepo\", \"repo_path\": \"plugins/my-plugin\"}` | Subdirectory in repo |\n\n#### Response (HTTP 201)\n\n```json\n{\n \"id\": \"123e4567-e89b-12d3-a456-426614174000\",\n \"name\": \"My Plugin Automation\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 9 * * 1\", \"timezone\": \"UTC\"},\n \"enabled\": true,\n \"created_at\": \"2025-03-25T10:00:00Z\"\n}\n```\n\n#### Plugin Preset Examples\n\n**Single plugin with version:**\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/plugin\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Code Review Automation\",\n \"plugins\": [\n {\"source\": \"github:owner/code-review-plugin\", \"ref\": \"v2.0.0\"}\n ],\n \"prompt\": \"Review all Python files in the repository for code quality issues\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 9 * * 1-5\", \"timezone\": \"UTC\"}\n }'\n```\n\n**Multiple plugins:**\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/plugin\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Security Scan Automation\",\n \"plugins\": [\n {\"source\": \"github:owner/security-scanner\"},\n {\"source\": \"github:owner/report-generator\", \"ref\": \"main\"}\n ],\n \"prompt\": \"Run a security scan on the codebase and generate a report\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 2 * * 0\", \"timezone\": \"UTC\"},\n \"timeout\": 600\n }'\n```\n\n**Monorepo plugin:**\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/plugin\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Style Guide Enforcement\",\n \"plugins\": [\n {\"source\": \"github:company/monorepo\", \"repo_path\": \"plugins/style-guide\", \"ref\": \"main\"}\n ],\n \"prompt\": \"Check all files against the company style guide\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 8 * * 1\", \"timezone\": \"America/Los_Angeles\"}\n }'\n```\n\n---\n\n## Repository Cloning\n\nBoth presets support an optional `repos` field to clone repositories into the sandbox before execution. Cloned repos have their skills (AGENTS.md, `.agents/skills/`) automatically loaded.\n\n### Repo Source Formats\n\n| Format | Example | Description |\n|--------|---------|-------------|\n| Full URL | `\"https://github.com/owner/repo\"` | Provider auto-detected |\n| Full URL + ref | `{\"url\": \"https://github.com/owner/repo\", \"ref\": \"main\"}` | With branch/tag/SHA |\n| Short URL | `{\"url\": \"owner/repo\", \"provider\": \"github\"}` | Requires `provider` field |\n\n**Supported providers:** `github`, `gitlab`, `bitbucket`\n\n> **Note:** Short URLs (`owner/repo`) require an explicit `provider` field. Full URLs auto-detect the provider.\n\n### Examples\n\n**Single repo (full URL):**\n```json\n{\n \"repos\": [\"https://github.com/OpenHands/openhands-cli\"]\n}\n```\n\n**Multiple repos with refs:**\n```json\n{\n \"repos\": [\n {\"url\": \"https://github.com/owner/repo1\", \"ref\": \"main\"},\n {\"url\": \"https://gitlab.com/owner/repo2\", \"ref\": \"v1.0.0\"}\n ]\n}\n```\n\n**Short URL with provider:**\n```json\n{\n \"repos\": [\n {\"url\": \"owner/repo\", \"provider\": \"github\", \"ref\": \"main\"}\n ]\n}\n```\n\n### Complete Automation Example\n\n```bash\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Analyze Codebase\",\n \"prompt\": \"Analyze the openhands-cli codebase and generate a summary report\",\n \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 9 * * 1\"},\n \"repos\": [\n {\"url\": \"https://github.com/OpenHands/openhands-cli\", \"ref\": \"main\"}\n ]\n }'\n```\n\n---\n\n## Managing Automations\n\n### List Automations\n\n```bash\ncurl \"${OPENHANDS_HOST}/api/automation/v1?limit=20\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n```\n\n### Get / Update / Delete\n\n```bash\n# Get details\ncurl \"${OPENHANDS_HOST}/api/automation/v1/{automation_id}\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n\n# Update (fields: name, trigger, enabled, timeout)\ncurl -X PATCH \"${OPENHANDS_HOST}/api/automation/v1/{automation_id}\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"enabled\": false}'\n\n# Delete\ncurl -X DELETE \"${OPENHANDS_HOST}/api/automation/v1/{automation_id}\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n```\n\n### Trigger and Monitor Runs\n\n```bash\n# Manually trigger a run\ncurl -X POST \"${OPENHANDS_HOST}/api/automation/v1/{automation_id}/dispatch\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n\n# List runs\ncurl \"${OPENHANDS_HOST}/api/automation/v1/{automation_id}/runs?limit=20\" \\\n -H \"Authorization: Bearer ${OPENHANDS_API_KEY}\"\n```\n\nRun status values: `PENDING` (waiting for dispatch), `RUNNING` (in progress), `COMPLETED` (success), `FAILED` (check `error_detail`).\n\n---\n\n## Run Lifecycle\n\nWhen a run completes, the automation service receives a callback and marks the run done. Any conversations started during the run remain accessible in the OpenHands UI — users can view the history and continue interacting. The agent server persists until it times out or is manually deleted.\n\nThe automation script itself controls when the callback fires (signalling completion). For simple synchronous scripts this happens naturally on exit. For scripts that start asynchronous conversations, the callback should be deferred until the conversation reaches an idle state (see `references/custom-automation.md` for patterns).\n\n---\n\n## Choosing the Right Preset\n\nPick based on **what the task needs**, not just **what is technically possible**. An LLM-driven preset can do almost anything, so \"the preset can satisfy this\" is not by itself a good reason to pick it — every run costs tokens and sandbox time.\n\n| Use Case | Recommended |\n|----------|-------------|\n| Reasoning, summarization, triage, code review, or open-ended tool use | **Prompt Preset** |\n| Needs plugin commands / skills / MCP configs / hooks | **Plugin Preset** |\n| Compare plugin versions or configurations across runs | **Plugin Preset with A/B testing** — see `references/ab-testing.md` |\n| **Deterministic task** (fixed data + scheduled action, e.g. healthcheck, Slack notification, rotating from a known list) — especially if it runs frequently | **Custom script, no LLM** — see `references/custom-automation.md#deterministic-script-no-llm` |\n| Custom Python dependencies, multi-file project, or direct SDK lifecycle control | **Custom script with SDK** — see `references/custom-automation.md#sdk-based-scripts` |\n\nThe **prompt preset** is the right default for genuinely agent-shaped work — anything that benefits from reasoning over context, calling tools dynamically, or producing a non-templated output. Use the **plugin preset** when you need extended capabilities from plugins (skills, MCP configurations, hooks, commands).\n\n**Watch for deterministic, high-frequency patterns.** Requests like \"send a daily standup reminder\", \"ping a healthcheck URL every minute\", \"post a random quote every 5 minutes\", or \"rotate a fact-of-the-day message\" do not need an LLM. Surface this to the user explicitly with a rough cost framing (e.g. \"this schedule will invoke your LLM ~288 times/day\") before defaulting to a preset. As a rule of thumb, any cron tighter than hourly deserves a deliberate \"should this really be agent-driven?\" check.\n\n**When neither preset is the right fit** (deterministic task, custom Python dependencies, non-Python entrypoint, multi-file project structure, direct SDK lifecycle control), explain the options to the user and let them decide. Do not attempt custom automation without explicit user agreement. If they choose the custom route, refer to `references/custom-automation.md`.\n\n## Reference Files\n\n- **`references/custom-automation.md`** — Detailed guide for custom automations: tarball uploads, code structure (SDK and no-LLM), state persistence via the KV store, environment variables, validation rules, and complete examples. Consult this whenever you need to evaluate or recommend the custom path (including for deterministic / cost-sensitive tasks per rule 0). Only *implement* a custom automation after the user agrees to that path.\n- **`references/ab-testing.md`** — A/B testing for plugin automations: defining variants with weights, experiment configuration, variant selection logic, observability via conversation tags, and complete examples. Consult this when a user wants to compare plugin versions or configurations."
|
|
319
345
|
},
|
|
320
346
|
{
|
|
321
347
|
"name": "openhands-sdk",
|
|
@@ -327,7 +353,7 @@ export const SKILLS_CATALOG = [
|
|
|
327
353
|
"agent-sdk",
|
|
328
354
|
"/sdk"
|
|
329
355
|
],
|
|
330
|
-
"content": "# OpenHands Software Agent SDK\n\nAll SDK documentation lives at <https://docs.openhands.dev/sdk>.\n\nFor the full topic index, fetch <https://docs.openhands.dev/llms.txt> and read\nthe \"OpenHands Software Agent SDK\" section.\n\n## Quick reference\n\nInstall: `pip install openhands-sdk openhands-tools`\n\n```python\nimport os\n\nfrom openhands.sdk import LLM, Agent, Conversation, Tool\nfrom openhands.tools.file_editor import FileEditorTool\nfrom openhands.tools.task_tracker import TaskTrackerTool\nfrom openhands.tools.terminal import TerminalTool\n\n\nllm = LLM(\n model=os.getenv(\"LLM_MODEL\", \"gpt-5.5\"),\n api_key=os.getenv(\"LLM_API_KEY\"),\n base_url=os.getenv(\"LLM_BASE_URL\", None),\n)\n\nagent = Agent(\n llm=llm,\n tools=[\n Tool(name=TerminalTool.name),\n Tool(name=FileEditorTool.name),\n Tool(name=TaskTrackerTool.name),\n ],\n)\n\ncwd = os.getcwd()\nconversation = Conversation(agent=agent, workspace=cwd)\n\nconversation.send_message(\"Write 3 facts about the current project into FACTS.txt.\")\nconversation.run()\nprint(\"All done!\")\n```\n\n## Core classes (`openhands.sdk`)\n\n| Class | Purpose |\n|---|---|\n| [`Agent`](https://docs.openhands.dev/sdk/arch/agent.md) | Reasoning-action loop |\n| [`Condenser`](https://docs.openhands.dev/sdk/arch/condenser.md) | Conversation history compression system |\n| [`Conversation`](https://docs.openhands.dev/sdk/arch/conversation.md) | Conversation orchestration system |\n| [`Event`](https://docs.openhands.dev/sdk/arch/events.md) | Typed event framework |\n| [`LLM`](https://docs.openhands.dev/sdk/arch/llm.md) | Provider-agnostic language model interface |\n| [`SecurityAnalyzer`](https://docs.openhands.dev/sdk/arch/security.md) | Action security analysis and validation |\n| [`Skill`](https://docs.openhands.dev/sdk/arch/skill.md) | Reusable prompt system |\n| [`Tool / ToolDefinition`](https://docs.openhands.dev/sdk/arch/tool-system.md) | Action-observation tool framework |\n| [`Workspace`](https://docs.openhands.dev/sdk/arch/workspace.md) | Execution environment abstraction |\n\n## API reference\n\n[`openhands.sdk.agent`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.agent.md), [`openhands.sdk.conversation`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.conversation.md), [`openhands.sdk.event`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.event.md), [`openhands.sdk.llm`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.llm.md), [`openhands.sdk.security`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.security.md), [`openhands.sdk.tool`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.tool.md), [`openhands.sdk.utils`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.utils.md), [`openhands.sdk.workspace`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.workspace.md)\n\n## Guides\n\n- [ACP Agent](https://docs.openhands.dev/sdk/guides/agent-acp.md): Delegate to an ACP-compatible server (Claude Code, Gemini CLI, etc.) instead of calling an LLM directly.\n- [Agent Settings](https://docs.openhands.dev/sdk/guides/agent-settings.md): Configure, serialize, and recreate agents from structured settings.\n- [Agent Skills & Context](https://docs.openhands.dev/sdk/guides/skill.md): Skills add specialized behaviors, domain knowledge, and context-aware triggers to your agent through structured prompts.\n- [API-based Sandbox](https://docs.openhands.dev/sdk/guides/agent-server/api-sandbox.md): Connect to hosted API-based agent server for fully managed infrastructure.\n- [Apptainer Sandbox](https://docs.openhands.dev/sdk/guides/agent-server/apptainer-sandbox.md): Run agent server in rootless Apptainer containers for HPC and shared computing environments.\n- [Ask Agent Questions](https://docs.openhands.dev/sdk/guides/convo-ask-agent.md): Get sidebar replies from the agent during conversation execution without interrupting the main flow.\n- [Assign Reviews](https://docs.openhands.dev/sdk/guides/github-workflows/assign-reviews.md): Automate PR management with intelligent reviewer assignment and workflow notifications using OpenHands Agent\n- [Browser Session Recording](https://docs.openhands.dev/sdk/guides/browser-session-recording.md): Record and replay your agent's browser sessions using rrweb.\n- [Browser Use](https://docs.openhands.dev/sdk/guides/agent-browser-use.md): Enable web browsing and interaction capabilities for your agent.\n- [Context Condenser](https://docs.openhands.dev/sdk/guides/context-condenser.md): Manage agent memory by condensing conversation history to save tokens.\n- [Conversation with Async](https://docs.openhands.dev/sdk/guides/convo-async.md): Use async/await for concurrent agent operations and non-blocking execution.\n- [Creating Custom Agent](https://docs.openhands.dev/sdk/guides/agent-custom.md): Learn how to design specialized agents with custom tool sets\n- [Critic (Experimental)](https://docs.openhands.dev/sdk/guides/critic.md): Real-time evaluation of agent actions using an LLM-based critic model, with built-in iterative refinement.\n- [Custom Tools](https://docs.openhands.dev/sdk/guides/custom-tools.md): Tools define what agents can do. The SDK includes built-in tools for common operations and supports creating custom tools for specialized needs.\n- [Custom Tools with Remote Agent Server](https://docs.openhands.dev/sdk/guides/agent-server/custom-tools.md): Learn how to use custom tools with a remote agent server by building a custom base image that includes your tool implementations.\n- [Custom Visualizer](https://docs.openhands.dev/sdk/guides/convo-custom-visualizer.md): Customize conversation visualization by creating custom visualizers or configuring the default visualizer.\n- [Docker Sandbox](https://docs.openhands.dev/sdk/guides/agent-server/docker-sandbox.md): Run agent server in isolated Docker containers for security and reproducibility.\n- [Exception Handling](https://docs.openhands.dev/sdk/guides/llm-error-handling.md): Provider‑agnostic exceptions raised by the SDK and recommended patterns for handling them.\n- [FAQ](https://docs.openhands.dev/sdk/faq.md): Frequently asked questions about the OpenHands SDK\n- [File-Based Agents](https://docs.openhands.dev/sdk/guides/agent-file-based.md): Define specialized sub-agents as simple Markdown files with YAML frontmatter — no Python code required.\n- [Fork a Conversation](https://docs.openhands.dev/sdk/guides/convo-fork.md): Branch off an existing conversation for follow-up exploration without contaminating the original.\n- [Getting Started](https://docs.openhands.dev/sdk/getting-started.md): Install the OpenHands SDK and build AI agents that write software.\n- [GPT-5 Preset (ApplyPatchTool)](https://docs.openhands.dev/sdk/guides/llm-gpt5-preset.md): Use the GPT-5 preset to build an agent that swaps the standard FileEditorTool for ApplyPatchTool.\n- [Hello World](https://docs.openhands.dev/sdk/guides/hello-world.md): The simplest possible OpenHands agent - configure an LLM, create an agent, and complete a task.\n- [Hooks](https://docs.openhands.dev/sdk/guides/hooks.md): Use lifecycle hooks to observe, log, and customize agent execution.\n- [Image Input](https://docs.openhands.dev/sdk/guides/llm-image-input.md): Send images to multimodal agents for vision-based tasks and analysis.\n- [Interactive Terminal](https://docs.openhands.dev/sdk/guides/agent-interactive-terminal.md): Enable agents to interact with terminal applications like ipython, python REPL, and other interactive CLI tools.\n- [Iterative Refinement](https://docs.openhands.dev/sdk/guides/iterative-refinement.md): Implement iterative refinement workflows where agents refine their work based on critique feedback until quality thresholds are met.\n- [LLM Fallback Strategy](https://docs.openhands.dev/sdk/guides/llm-fallback.md): Automatically try alternate LLMs when the primary model fails with a transient error.\n- [LLM Profile Store](https://docs.openhands.dev/sdk/guides/llm-profile-store.md): Save, load, and manage reusable LLM configurations so you never repeat setup code again.\n- [LLM Registry](https://docs.openhands.dev/sdk/guides/llm-registry.md): Dynamically select and configure language models using the LLM registry.\n- [LLM Streaming](https://docs.openhands.dev/sdk/guides/llm-streaming.md): Stream LLM responses token-by-token for real-time display and interactive user experiences.\n- [LLM Subscriptions](https://docs.openhands.dev/sdk/guides/llm-subscriptions.md): Use your ChatGPT Plus/Pro subscription to access Codex models without consuming API credits.\n- [Local Agent Server](https://docs.openhands.dev/sdk/guides/agent-server/local-server.md): Run agents through a local HTTP server with RemoteConversation for client-server architecture.\n- [Metrics Tracking](https://docs.openhands.dev/sdk/guides/metrics.md): Track token usage, costs, and latency metrics for your agents.\n- [Model Context Protocol](https://docs.openhands.dev/sdk/guides/mcp.md): Model Context Protocol (MCP) enables dynamic tool integration from external servers. Agents can discover and use MCP-provided tools automatically.\n- [Model Routing](https://docs.openhands.dev/sdk/guides/llm-routing.md): Route agent's LLM requests to different models.\n- [Observability & Tracing](https://docs.openhands.dev/sdk/guides/observability.md): Enable OpenTelemetry tracing to monitor and debug your agent's execution with tools like Laminar, MLflow, Honeycomb, or any OTLP-compatible backend.\n- [OpenHands Cloud Workspace](https://docs.openhands.dev/sdk/guides/agent-server/cloud-workspace.md): Connect to OpenHands Cloud for fully managed sandbox environments with optional SaaS credential inheritance.\n- [Overview](https://docs.openhands.dev/sdk/guides/agent-server/overview.md): Run agents on remote servers with isolated workspaces for production deployments.\n- [Parallel Tool Execution](https://docs.openhands.dev/sdk/guides/parallel-tool-execution.md): Execute multiple tools concurrently within a single LLM response to improve throughput for independent operations.\n- [Pause and Resume](https://docs.openhands.dev/sdk/guides/convo-pause-and-resume.md): Pause agent execution, perform operations, and resume without losing state.\n- [Persistence](https://docs.openhands.dev/sdk/guides/convo-persistence.md): Save and restore conversation state for multi-session workflows.\n- [Plugins](https://docs.openhands.dev/sdk/guides/plugins.md): Plugins bundle skills, hooks, MCP servers, agents, and commands into reusable packages that extend agent capabilities.\n- [PR Review](https://docs.openhands.dev/sdk/guides/github-workflows/pr-review.md): Use OpenHands Agent to generate meaningful pull request review\n- [Reasoning](https://docs.openhands.dev/sdk/guides/llm-reasoning.md): Access model reasoning traces from Anthropic extended thinking and OpenAI responses API.\n- [Secret Registry](https://docs.openhands.dev/sdk/guides/secrets.md): Provide environment variables and secrets to agent workspace securely.\n- [Security & Action Confirmation](https://docs.openhands.dev/sdk/guides/security.md): Control agent action execution through confirmation policy and security analyzer.\n- [Send Message While Running](https://docs.openhands.dev/sdk/guides/convo-send-message-while-running.md): Interrupt running agents to provide additional context or corrections.\n- [Software Agent SDK](https://docs.openhands.dev/sdk.md): Build AI agents that write software. A clean, modular SDK with production-ready tools.\n- [Stuck Detector](https://docs.openhands.dev/sdk/guides/agent-stuck-detector.md): Detect and handle stuck agents automatically with timeout mechanisms.\n- [Sub-Agent Delegation](https://docs.openhands.dev/sdk/guides/agent-delegation.md): Enable parallel task execution by delegating work to multiple sub-agents that run independently and return consolidated results.\n- [Task Tool Set](https://docs.openhands.dev/sdk/guides/task-tool-set.md): Delegate complex work to specialized sub-agents that run synchronously and return results to the parent agent.\n- [Theory of Mind (TOM) Agent](https://docs.openhands.dev/sdk/guides/agent-tom-agent.md): Enable your agent to understand user intent and preferences through Theory of Mind capabilities, providing personalized guidance based on user modeling.\n- [TODO Management](https://docs.openhands.dev/sdk/guides/github-workflows/todo-management.md): Implement TODOs using OpenHands Agent\n\n## Examples\n\nSource: [`examples/`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples)\n\n### [`01_standalone_sdk/`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/01_standalone_sdk)\n\n- [`01_hello_world.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/01_hello_world.py)\n- [`02_custom_tools.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/02_custom_tools.py)\n- [`03_activate_skill.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/03_activate_skill.py)\n- [`04_confirmation_mode_example.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/04_confirmation_mode_example.py)\n- [`05_use_llm_registry.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/05_use_llm_registry.py)\n- [`06_interactive_terminal_w_reasoning.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/06_interactive_terminal_w_reasoning.py)\n- [`07_mcp_integration.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/07_mcp_integration.py)\n- [`08_mcp_with_oauth.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/08_mcp_with_oauth.py)\n- [`09_pause_example.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/09_pause_example.py)\n- [`10_persistence.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/10_persistence.py)\n- [`11_async.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/11_async.py)\n- [`12_custom_secrets.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/12_custom_secrets.py)\n- [`13_get_llm_metrics.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/13_get_llm_metrics.py)\n- [`14_context_condenser.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/14_context_condenser.py)\n- [`15_browser_use.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/15_browser_use.py)\n- [`16_llm_security_analyzer.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/16_llm_security_analyzer.py)\n- [`17_image_input.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/17_image_input.py)\n- [`18_send_message_while_processing.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/18_send_message_while_processing.py)\n- [`19_llm_routing.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/19_llm_routing.py)\n- [`20_stuck_detector.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/20_stuck_detector.py)\n- [`21_generate_extraneous_conversation_costs.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/21_generate_extraneous_conversation_costs.py)\n- [`22_anthropic_thinking.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/22_anthropic_thinking.py)\n- [`23_responses_reasoning.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/23_responses_reasoning.py)\n- [`24_planning_agent_workflow.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/24_planning_agent_workflow.py)\n- [`25_agent_delegation.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/25_agent_delegation.py)\n- [`26_custom_visualizer.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/26_custom_visualizer.py)\n- [`27_observability_laminar.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/27_observability_laminar.py)\n- [`28_ask_agent_example.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/28_ask_agent_example.py)\n- [`29_llm_streaming.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/29_llm_streaming.py)\n- [`30_tom_agent.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/30_tom_agent.py)\n- [`31_iterative_refinement.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/31_iterative_refinement.py)\n- [`32_configurable_security_policy.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/32_configurable_security_policy.py)\n- [`33_hooks`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/01_standalone_sdk/33_hooks)\n- [`34_critic_example.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/34_critic_example.py)\n- [`35_subscription_login.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/35_subscription_login.py)\n- [`36_event_json_to_openai_messages.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/36_event_json_to_openai_messages.py)\n- [`37_llm_profile_store`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/01_standalone_sdk/37_llm_profile_store)\n- [`38_browser_session_recording.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/38_browser_session_recording.py)\n- [`39_llm_fallback.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/39_llm_fallback.py)\n- [`40_acp_agent_example.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/40_acp_agent_example.py)\n- [`41_task_tool_set.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/41_task_tool_set.py)\n- [`42_file_based_subagents.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/42_file_based_subagents.py)\n- [`43_mixed_marketplace_skills`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/01_standalone_sdk/43_mixed_marketplace_skills)\n- [`44_model_switching_in_convo.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/44_model_switching_in_convo.py)\n- [`45_parallel_tool_execution.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/45_parallel_tool_execution.py)\n- [`46_agent_settings.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/46_agent_settings.py)\n- [`47_defense_in_depth_security.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/47_defense_in_depth_security.py)\n- [`48_conversation_fork.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/48_conversation_fork.py)\n- [`49_switch_llm_tool.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/49_switch_llm_tool.py)\n- [`50_async_cancellation.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/50_async_cancellation.py)\n- [`51_agent_hooks`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/01_standalone_sdk/51_agent_hooks)\n- [`52_dynamic_workflow.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/52_dynamic_workflow.py)\n- [`53_client_defined_tools.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/53_client_defined_tools.py)\n\n### [`02_remote_agent_server/`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/02_remote_agent_server)\n\n- [`01_convo_with_local_agent_server.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/01_convo_with_local_agent_server.py)\n- [`02_convo_with_docker_sandboxed_server.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/02_convo_with_docker_sandboxed_server.py)\n- [`03_browser_use_with_docker_sandboxed_server.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/03_browser_use_with_docker_sandboxed_server.py)\n- [`04_convo_with_api_sandboxed_server.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/04_convo_with_api_sandboxed_server.py)\n- [`05_vscode_with_docker_sandboxed_server.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/05_vscode_with_docker_sandboxed_server.py)\n- [`06_custom_tool`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/02_remote_agent_server/06_custom_tool)\n- [`07_convo_with_cloud_workspace.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/07_convo_with_cloud_workspace.py)\n- [`08_convo_with_apptainer_sandboxed_server.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/08_convo_with_apptainer_sandboxed_server.py)\n- [`09_acp_agent_with_remote_runtime.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/09_acp_agent_with_remote_runtime.py)\n- [`10_cloud_workspace_share_credentials.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/10_cloud_workspace_share_credentials.py)\n- [`11_conversation_fork.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/11_conversation_fork.py)\n- [`12_settings_and_secrets_api.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/12_settings_and_secrets_api.py)\n- [`13_workspace_get_llm.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/13_workspace_get_llm.py)\n- [`14_client_defined_tools.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/14_client_defined_tools.py)\n- [`15_openai_compatible_gateway.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/15_openai_compatible_gateway.py)\n- [`hook_scripts`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/02_remote_agent_server/hook_scripts)\n- [`scripts`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/02_remote_agent_server/scripts)\n\n### [`03_github_workflows/`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/03_github_workflows)\n\n- [`01_basic_action`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/03_github_workflows/01_basic_action)\n- [`02_pr_review`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/03_github_workflows/02_pr_review)\n- [`03_todo_management`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/03_github_workflows/03_todo_management)\n- [`04_datadog_debugging`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/03_github_workflows/04_datadog_debugging)\n- [`05_posthog_debugging`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/03_github_workflows/05_posthog_debugging)\n\n### [`04_llm_specific_tools/`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/04_llm_specific_tools)\n\n- [`01_gpt5_apply_patch_preset.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/04_llm_specific_tools/01_gpt5_apply_patch_preset.py)\n- [`02_gemini_file_tools.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/04_llm_specific_tools/02_gemini_file_tools.py)\n\n### [`05_skills_and_plugins/`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/05_skills_and_plugins)\n\n- [`01_loading_agentskills`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/05_skills_and_plugins/01_loading_agentskills)\n- [`02_loading_plugins`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/05_skills_and_plugins/02_loading_plugins)\n- [`03_managing_installed_skills`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/05_skills_and_plugins/03_managing_installed_skills)"
|
|
356
|
+
"content": "# OpenHands Software Agent SDK\n\nAll SDK documentation lives at <https://docs.openhands.dev/sdk>.\n\nFor the full topic index, fetch <https://docs.openhands.dev/llms.txt> and read\nthe \"OpenHands Software Agent SDK\" section.\n\n## Quick reference\n\nInstall: `pip install openhands-sdk openhands-tools`\n\n```python\nimport os\n\nfrom openhands.sdk import LLM, Agent, Conversation, Tool\nfrom openhands.tools.file_editor import FileEditorTool\nfrom openhands.tools.task_tracker import TaskTrackerTool\nfrom openhands.tools.terminal import TerminalTool\n\n\nllm = LLM(\n model=os.getenv(\"LLM_MODEL\", \"gpt-5.5\"),\n api_key=os.getenv(\"LLM_API_KEY\"),\n base_url=os.getenv(\"LLM_BASE_URL\", None),\n)\n\nagent = Agent(\n llm=llm,\n tools=[\n Tool(name=TerminalTool.name),\n Tool(name=FileEditorTool.name),\n Tool(name=TaskTrackerTool.name),\n ],\n)\n\ncwd = os.getcwd()\nconversation = Conversation(agent=agent, workspace=cwd)\n\nconversation.send_message(\"Write 3 facts about the current project into FACTS.txt.\")\nconversation.run()\nprint(\"All done!\")\n```\n\n## Core classes (`openhands.sdk`)\n\n| Class | Purpose |\n|---|---|\n| [`Agent`](https://docs.openhands.dev/sdk/arch/agent.md) | Reasoning-action loop |\n| [`Condenser`](https://docs.openhands.dev/sdk/arch/condenser.md) | Conversation history compression system |\n| [`Conversation`](https://docs.openhands.dev/sdk/arch/conversation.md) | Conversation orchestration system |\n| [`Event`](https://docs.openhands.dev/sdk/arch/events.md) | Typed event framework |\n| [`LLM`](https://docs.openhands.dev/sdk/arch/llm.md) | Provider-agnostic language model interface |\n| [`SecurityAnalyzer`](https://docs.openhands.dev/sdk/arch/security.md) | Action security analysis and validation |\n| [`Skill`](https://docs.openhands.dev/sdk/arch/skill.md) | Reusable prompt system |\n| [`Tool / ToolDefinition`](https://docs.openhands.dev/sdk/arch/tool-system.md) | Action-observation tool framework |\n| [`Workspace`](https://docs.openhands.dev/sdk/arch/workspace.md) | Execution environment abstraction |\n\n## API reference\n\n[`openhands.sdk.agent`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.agent.md), [`openhands.sdk.conversation`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.conversation.md), [`openhands.sdk.event`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.event.md), [`openhands.sdk.llm`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.llm.md), [`openhands.sdk.security`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.security.md), [`openhands.sdk.tool`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.tool.md), [`openhands.sdk.utils`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.utils.md), [`openhands.sdk.workspace`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.workspace.md)\n\n## Guides\n\n- [ACP Agent](https://docs.openhands.dev/sdk/guides/agent-acp.md): Delegate to an ACP-compatible server (Claude Code, Gemini CLI, etc.) instead of calling an LLM directly.\n- [Agent Settings](https://docs.openhands.dev/sdk/guides/agent-settings.md): Configure, serialize, and recreate agents from structured settings.\n- [Agent Skills & Context](https://docs.openhands.dev/sdk/guides/skill.md): Skills add specialized behaviors, domain knowledge, and context-aware triggers to your agent through structured prompts.\n- [API-based Sandbox](https://docs.openhands.dev/sdk/guides/agent-server/api-sandbox.md): Connect to hosted API-based agent server for fully managed infrastructure.\n- [Apptainer Sandbox](https://docs.openhands.dev/sdk/guides/agent-server/apptainer-sandbox.md): Run agent server in rootless Apptainer containers for HPC and shared computing environments.\n- [Ask Agent Questions](https://docs.openhands.dev/sdk/guides/convo-ask-agent.md): Get sidebar replies from the agent during conversation execution without interrupting the main flow.\n- [Assign Reviews](https://docs.openhands.dev/sdk/guides/github-workflows/assign-reviews.md): Automate PR management with intelligent reviewer assignment and workflow notifications using OpenHands Agent\n- [Browser Session Recording](https://docs.openhands.dev/sdk/guides/browser-session-recording.md): Record and replay your agent's browser sessions using rrweb.\n- [Browser Use](https://docs.openhands.dev/sdk/guides/agent-browser-use.md): Enable web browsing and interaction capabilities for your agent.\n- [Context Condenser](https://docs.openhands.dev/sdk/guides/context-condenser.md): Manage agent memory by condensing conversation history to save tokens.\n- [Conversation Goals](https://docs.openhands.dev/sdk/guides/agent-server/conversation-goals.md): Add a resumable goal strategy to a normal agent-server conversation.\n- [Conversation with Async](https://docs.openhands.dev/sdk/guides/convo-async.md): Use async/await for concurrent agent operations and non-blocking execution.\n- [Creating Custom Agent](https://docs.openhands.dev/sdk/guides/agent-custom.md): Learn how to design specialized agents with custom tool sets\n- [Critic (Experimental)](https://docs.openhands.dev/sdk/guides/critic.md): Real-time evaluation of agent actions using an LLM-based critic model, with built-in iterative refinement.\n- [Custom Tools](https://docs.openhands.dev/sdk/guides/custom-tools.md): Tools define what agents can do. The SDK includes built-in tools for common operations and supports creating custom tools for specialized needs.\n- [Custom Tools with Remote Agent Server](https://docs.openhands.dev/sdk/guides/agent-server/custom-tools.md): Learn how to use custom tools with a remote agent server by building a custom base image that includes your tool implementations.\n- [Custom Visualizer](https://docs.openhands.dev/sdk/guides/convo-custom-visualizer.md): Customize conversation visualization by creating custom visualizers or configuring the default visualizer.\n- [Deferred Init (Warm-Pool)](https://docs.openhands.dev/sdk/guides/agent-server/deferred-init.md): Pre-warm agent-server pods before a user is matched, then activate them at runtime with POST /api/init.\n- [Docker Sandbox](https://docs.openhands.dev/sdk/guides/agent-server/docker-sandbox.md): Run agent server in isolated Docker containers for security and reproducibility.\n- [Exception Handling](https://docs.openhands.dev/sdk/guides/llm-error-handling.md): Provider‑agnostic exceptions raised by the SDK and recommended patterns for handling them.\n- [FAQ](https://docs.openhands.dev/sdk/faq.md): Frequently asked questions about the OpenHands SDK\n- [File-Based Agents](https://docs.openhands.dev/sdk/guides/agent-file-based.md): Define specialized sub-agents as simple Markdown files with YAML frontmatter — no Python code required.\n- [Fork a Conversation](https://docs.openhands.dev/sdk/guides/convo-fork.md): Branch off an existing conversation for follow-up exploration without contaminating the original.\n- [Getting Started](https://docs.openhands.dev/sdk/getting-started.md): Install the OpenHands SDK and build AI agents that write software.\n- [Goal Completion Loop](https://docs.openhands.dev/sdk/guides/convo-goal.md): Drive a conversation toward a verifiable objective with a judge-driven, self-continuing completion loop.\n- [GPT-5 Preset (ApplyPatchTool)](https://docs.openhands.dev/sdk/guides/llm-gpt5-preset.md): Use the GPT-5 preset to build an agent that swaps the standard FileEditorTool for ApplyPatchTool.\n- [Hello World](https://docs.openhands.dev/sdk/guides/hello-world.md): The simplest possible OpenHands agent - configure an LLM, create an agent, and complete a task.\n- [Hooks](https://docs.openhands.dev/sdk/guides/hooks.md): Use lifecycle hooks to observe, log, and customize agent execution.\n- [Image Input](https://docs.openhands.dev/sdk/guides/llm-image-input.md): Send images to multimodal agents for vision-based tasks and analysis.\n- [Interactive Terminal](https://docs.openhands.dev/sdk/guides/agent-interactive-terminal.md): Enable agents to interact with terminal applications like ipython, python REPL, and other interactive CLI tools.\n- [Iterative Refinement](https://docs.openhands.dev/sdk/guides/iterative-refinement.md): Implement iterative refinement workflows where agents refine their work based on critique feedback until quality thresholds are met.\n- [LLM Fallback Strategy](https://docs.openhands.dev/sdk/guides/llm-fallback.md): Automatically try alternate LLMs when the primary model fails with a transient error.\n- [LLM Profile Store](https://docs.openhands.dev/sdk/guides/llm-profile-store.md): Save, load, and manage reusable LLM configurations so you never repeat setup code again.\n- [LLM Registry](https://docs.openhands.dev/sdk/guides/llm-registry.md): Dynamically select and configure language models using the LLM registry.\n- [LLM Streaming](https://docs.openhands.dev/sdk/guides/llm-streaming.md): Stream LLM responses token-by-token for real-time display and interactive user experiences.\n- [LLM Subscriptions](https://docs.openhands.dev/sdk/guides/llm-subscriptions.md): Use your ChatGPT Plus/Pro subscription to access Codex models without consuming API credits.\n- [Local Agent Server](https://docs.openhands.dev/sdk/guides/agent-server/local-server.md): Install and run an OpenHands Agent Server on your machine, then connect to it from the SDK.\n- [Metrics Tracking](https://docs.openhands.dev/sdk/guides/metrics.md): Track token usage, costs, and latency metrics for your agents.\n- [Model Context Protocol](https://docs.openhands.dev/sdk/guides/mcp.md): Model Context Protocol (MCP) enables dynamic tool integration from external servers. Agents can discover and use MCP-provided tools automatically.\n- [Model Routing](https://docs.openhands.dev/sdk/guides/llm-routing.md): Route agent's LLM requests to different models.\n- [Observability & Tracing](https://docs.openhands.dev/sdk/guides/observability.md): Enable OpenTelemetry tracing to monitor and debug your agent's execution with tools like Laminar, MLflow, Honeycomb, or any OTLP-compatible backend.\n- [OpenAI-Compatible Endpoint](https://docs.openhands.dev/sdk/guides/agent-server/openai-gateway.md): Call an OpenHands agent-server through the OpenAI Chat Completions protocol.\n- [OpenHands Cloud Workspace](https://docs.openhands.dev/sdk/guides/agent-server/cloud-workspace.md): Connect to OpenHands Cloud for fully managed sandbox environments with optional SaaS credential inheritance.\n- [Overview](https://docs.openhands.dev/sdk/guides/agent-server/overview.md): Run agents on remote servers with isolated workspaces for production deployments.\n- [Parallel Tool Execution](https://docs.openhands.dev/sdk/guides/parallel-tool-execution.md): Execute multiple tools concurrently within a single LLM response to improve throughput for independent operations.\n- [Pause and Resume](https://docs.openhands.dev/sdk/guides/convo-pause-and-resume.md): Pause agent execution, perform operations, and resume without losing state.\n- [Persistence](https://docs.openhands.dev/sdk/guides/convo-persistence.md): Save and restore conversation state for multi-session workflows.\n- [Plugins](https://docs.openhands.dev/sdk/guides/plugins.md): Plugins bundle skills, hooks, MCP servers, agents, and commands into reusable packages that extend agent capabilities.\n- [PR Review](https://docs.openhands.dev/sdk/guides/github-workflows/pr-review.md): Use OpenHands Agent to generate meaningful pull request review\n- [Reasoning](https://docs.openhands.dev/sdk/guides/llm-reasoning.md): Access model reasoning traces from Anthropic extended thinking and OpenAI responses API.\n- [Secret Registry](https://docs.openhands.dev/sdk/guides/secrets.md): Provide environment variables and secrets to agent workspace securely.\n- [Security & Action Confirmation](https://docs.openhands.dev/sdk/guides/security.md): Control agent action execution through confirmation policy and security analyzer.\n- [Send Message While Running](https://docs.openhands.dev/sdk/guides/convo-send-message-while-running.md): Interrupt running agents to provide additional context or corrections.\n- [Software Agent SDK](https://docs.openhands.dev/sdk.md): Build AI agents that write software. A clean, modular SDK with production-ready tools.\n- [Stuck Detector](https://docs.openhands.dev/sdk/guides/agent-stuck-detector.md): Detect and handle stuck agents automatically with timeout mechanisms.\n- [Task Tool Set](https://docs.openhands.dev/sdk/guides/task-tool-set.md): Delegate complex work to specialized sub-agents that run synchronously and return results to the parent agent.\n- [Theory of Mind (TOM) Agent](https://docs.openhands.dev/sdk/guides/agent-tom-agent.md): Enable your agent to understand user intent and preferences through Theory of Mind capabilities, providing personalized guidance based on user modeling.\n- [TODO Management](https://docs.openhands.dev/sdk/guides/github-workflows/todo-management.md): Implement TODOs using OpenHands Agent\n\n## Examples\n\nSource: [`examples/`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples)\n\n### [`01_standalone_sdk/`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/01_standalone_sdk)\n\n- [`01_hello_world.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/01_hello_world.py)\n- [`02_custom_tools.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/02_custom_tools.py)\n- [`03_activate_skill.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/03_activate_skill.py)\n- [`04_confirmation_mode_example.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/04_confirmation_mode_example.py)\n- [`05_use_llm_registry.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/05_use_llm_registry.py)\n- [`06_interactive_terminal_w_reasoning.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/06_interactive_terminal_w_reasoning.py)\n- [`07_mcp_integration.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/07_mcp_integration.py)\n- [`08_mcp_with_oauth.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/08_mcp_with_oauth.py)\n- [`09_pause_example.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/09_pause_example.py)\n- [`10_persistence.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/10_persistence.py)\n- [`11_async.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/11_async.py)\n- [`12_custom_secrets.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/12_custom_secrets.py)\n- [`13_get_llm_metrics.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/13_get_llm_metrics.py)\n- [`14_context_condenser.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/14_context_condenser.py)\n- [`15_browser_use.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/15_browser_use.py)\n- [`16_llm_security_analyzer.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/16_llm_security_analyzer.py)\n- [`17_image_input.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/17_image_input.py)\n- [`18_send_message_while_processing.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/18_send_message_while_processing.py)\n- [`19_llm_routing.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/19_llm_routing.py)\n- [`20_stuck_detector.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/20_stuck_detector.py)\n- [`21_generate_extraneous_conversation_costs.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/21_generate_extraneous_conversation_costs.py)\n- [`22_anthropic_thinking.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/22_anthropic_thinking.py)\n- [`23_responses_reasoning.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/23_responses_reasoning.py)\n- [`24_planning_agent_workflow.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/24_planning_agent_workflow.py)\n- [`25_agent_delegation.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/25_agent_delegation.py)\n- [`26_custom_visualizer.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/26_custom_visualizer.py)\n- [`27_observability_laminar.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/27_observability_laminar.py)\n- [`28_ask_agent_example.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/28_ask_agent_example.py)\n- [`29_llm_streaming.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/29_llm_streaming.py)\n- [`30_tom_agent.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/30_tom_agent.py)\n- [`31_iterative_refinement.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/31_iterative_refinement.py)\n- [`32_configurable_security_policy.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/32_configurable_security_policy.py)\n- [`33_hooks`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/01_standalone_sdk/33_hooks)\n- [`34_critic_example.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/34_critic_example.py)\n- [`35_subscription_login.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/35_subscription_login.py)\n- [`36_event_json_to_openai_messages.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/36_event_json_to_openai_messages.py)\n- [`37_llm_profile_store`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/01_standalone_sdk/37_llm_profile_store)\n- [`38_browser_session_recording.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/38_browser_session_recording.py)\n- [`39_llm_fallback.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/39_llm_fallback.py)\n- [`40_acp_agent_example.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/40_acp_agent_example.py)\n- [`41_task_tool_set.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/41_task_tool_set.py)\n- [`42_file_based_subagents.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/42_file_based_subagents.py)\n- [`44_model_switching_in_convo.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/44_model_switching_in_convo.py)\n- [`45_parallel_tool_execution.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/45_parallel_tool_execution.py)\n- [`46_agent_settings.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/46_agent_settings.py)\n- [`47_defense_in_depth_security.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/47_defense_in_depth_security.py)\n- [`48_conversation_fork.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/48_conversation_fork.py)\n- [`49_switch_llm_tool.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/49_switch_llm_tool.py)\n- [`50_async_cancellation.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/50_async_cancellation.py)\n- [`51_agent_hooks`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/01_standalone_sdk/51_agent_hooks)\n- [`52_dynamic_workflow.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/52_dynamic_workflow.py)\n- [`53_client_defined_tools.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/53_client_defined_tools.py)\n- [`54_goal_completion_loop.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/54_goal_completion_loop.py)\n\n### [`02_remote_agent_server/`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/02_remote_agent_server)\n\n- [`01_convo_with_local_agent_server.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/01_convo_with_local_agent_server.py)\n- [`02_convo_with_docker_sandboxed_server.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/02_convo_with_docker_sandboxed_server.py)\n- [`03_browser_use_with_docker_sandboxed_server.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/03_browser_use_with_docker_sandboxed_server.py)\n- [`04_convo_with_api_sandboxed_server.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/04_convo_with_api_sandboxed_server.py)\n- [`05_vscode_with_docker_sandboxed_server.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/05_vscode_with_docker_sandboxed_server.py)\n- [`06_custom_tool`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/02_remote_agent_server/06_custom_tool)\n- [`07_convo_with_cloud_workspace.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/07_convo_with_cloud_workspace.py)\n- [`08_convo_with_apptainer_sandboxed_server.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/08_convo_with_apptainer_sandboxed_server.py)\n- [`09_acp_agent_with_remote_runtime.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/09_acp_agent_with_remote_runtime.py)\n- [`10_cloud_workspace_share_credentials.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/10_cloud_workspace_share_credentials.py)\n- [`11_conversation_fork.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/11_conversation_fork.py)\n- [`12_settings_and_secrets_api.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/12_settings_and_secrets_api.py)\n- [`13_workspace_get_llm.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/13_workspace_get_llm.py)\n- [`14_client_defined_tools.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/14_client_defined_tools.py)\n- [`15_openai_compatible_gateway.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/15_openai_compatible_gateway.py)\n- [`16_deferred_init.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/16_deferred_init.py)\n- [`hook_scripts`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/02_remote_agent_server/hook_scripts)\n- [`scripts`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/02_remote_agent_server/scripts)\n\n### [`03_github_workflows/`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/03_github_workflows)\n\n- [`01_basic_action`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/03_github_workflows/01_basic_action)\n- [`02_pr_review`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/03_github_workflows/02_pr_review)\n- [`03_todo_management`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/03_github_workflows/03_todo_management)\n- [`04_datadog_debugging`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/03_github_workflows/04_datadog_debugging)\n- [`05_posthog_debugging`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/03_github_workflows/05_posthog_debugging)\n\n### [`04_llm_specific_tools/`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/04_llm_specific_tools)\n\n- [`01_gpt5_apply_patch_preset.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/04_llm_specific_tools/01_gpt5_apply_patch_preset.py)\n- [`02_gemini_file_tools.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/04_llm_specific_tools/02_gemini_file_tools.py)\n\n### [`05_skills_and_plugins/`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/05_skills_and_plugins)\n\n- [`01_loading_agentskills`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/05_skills_and_plugins/01_loading_agentskills)\n- [`02_loading_plugins`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/05_skills_and_plugins/02_loading_plugins)\n- [`03_managing_installed_skills`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/05_skills_and_plugins/03_managing_installed_skills)\n- [`04_mixed_marketplace_skills`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/05_skills_and_plugins/04_mixed_marketplace_skills)"
|
|
331
357
|
},
|
|
332
358
|
{
|
|
333
359
|
"name": "pdflatex",
|
|
@@ -398,7 +424,7 @@ export const SKILLS_CATALOG = [
|
|
|
398
424
|
"triggers": [
|
|
399
425
|
"/slack-monitor:poll"
|
|
400
426
|
],
|
|
401
|
-
"content": "# Slack Channel Monitor\n\nCreate a cron automation that polls up to 10 Slack channels every minute.\nWhen a message containing the **trigger phrase** (default: `@openhands`) is\ndetected it:\n\n1. Adds a 👀 reaction to the triggering message.\n2. Opens an OpenHands conversation with the message and recent channel context.\n3. Posts a reply in the Slack thread with a link to the conversation.\n\nOn every subsequent run:\n- New Slack thread replies are forwarded only when they contain the trigger\n phrase, so unrelated conversation in the thread is ignored.\n- When the conversation finishes (or errors), the agent's final response is\n posted back to the Slack thread.\n- Completed conversations stay in a short follow-up watch window, allowing\n triggered Slack replies to continue the same OpenHands conversation.\n\n> **Local mode only.** This automation targets the local OpenHands setup\n> (`dev:automation` stack). A cloud/webhook-based variant is out of scope here.\n\n---\n\n## Prerequisites\n\n### Required secrets\n\nVerify that at least one of the following secrets is set in\n**OpenHands Settings → Secrets** before proceeding:\n\n| Secret name | Token type | Minimum scopes |\n|---|---|---|\n| `SLACK_BOT_TOKEN` | Bot (`xoxb-…`) | `channels:history`, `channels:read`, `reactions:write`, `chat:write` |\n| `SLACK_USER_TOKEN` | User (`xoxp-…`) | Same as bot, plus `search:read` for multi-channel efficiency |\n\nCheck with:\n```bash\n# For bot token:\ncurl -s https://slack.com/api/auth.test -H \"Authorization: Bearer $SLACK_BOT_TOKEN\" \\\n | python3 -c \"import json,sys; d=json.load(sys.stdin); print('ok' if d.get('ok') else d.get('error'))\"\n\n# For user token:\ncurl -s https://slack.com/api/auth.test -H \"Authorization: Bearer $SLACK_USER_TOKEN\" \\\n | python3 -c \"import json,sys; d=json.load(sys.stdin); print('ok' if d.get('ok') else d.get('error'))\"\n```\n\nIf neither token is present, inform the user and stop - the automation cannot\nfunction without Slack credentials.\n\n### Optional secret\n\n| Secret name | Default | Purpose |\n|---|---|---|\n| `OPENHANDS_URL` | `http://localhost:8000` | Base URL used to build conversation links posted in Slack |\n\n---\n\n## Setup Workflow\n\nFollow these steps in order.\n\n### Step 1 - Collect channels\n\nAsk the user: *\"Which Slack channels should be monitored? You can provide\nchannel names (e.g. `#general`) or IDs (e.g. `C0123456789`).\"*\n\n**If the user provides channel names**, resolve them to IDs:\n\n```bash\nSLACK_TOKEN=\"${SLACK_BOT_TOKEN:-$SLACK_USER_TOKEN}\"\ncurl -s \"https://slack.com/api/conversations.list?types=public_channel,private_channel&limit=200&exclude_archived=true\" \\\n -H \"Authorization: Bearer $SLACK_TOKEN\" \\\n | python3 -c \"\nimport json, sys\ndata = json.load(sys.stdin)\nif not data.get('ok'):\n print('ERROR:', data.get('error'))\n exit(1)\nnames = set(n.lstrip('#') for n in ['CHANNEL_NAMES_HERE'.split(',')])\nfor ch in data.get('channels', []):\n if ch['name'] in names:\n print(f\\\"{ch['name']} → {ch['id']}\\\")\n\"\n```\n\nReplace `CHANNEL_NAMES_HERE` with the comma-separated names the user provided.\n\n**If `conversations.list` returns `missing_scope` or `not_authed`:**\nInform the user: *\"The token doesn't have permission to list channels. Please\nprovide the channel IDs directly (right-click a channel in Slack → Copy link - \nthe last path segment starting with `C` is the ID).\"*\n\n**If the bot token lacks `channels:read`** for private channels, the user can\neither invite the bot first (`/invite @botname`) or switch to a user token.\n\nCollect up to 10 channel IDs. Record them as a Python list literal, e.g.:\n```python\n[\"C0123456789\", \"C9876543210\"]\n```\n\n### Step 2 - Collect trigger phrase\n\nAsk the user: *\"What trigger phrase should OpenHands respond to?\n(Press Enter to use the default: `@openhands`)\"*\n\nAccepted values: any non-empty string unlikely to appear accidentally, e.g.\n`@openhands`, `jazz hands`, `take-me-to-funky-town`.\n\n### Step 3 - Generate the automation script\n\nRead `scripts/main.py` from this skill's directory and **copy it verbatim**.\nApply exactly three constant substitutions near the top of the file:\n\n> **Do not reimplement, simplify, or hand-write a replacement script.**\n> The template already contains the correct secret-loading, state-path,\n> conversation-creation, and context-forwarding logic. Only the three\n> configuration constants below should change unless syntax validation fails.\n\n| Placeholder | Replace with |\n|---|---|\n| `TRIGGER_PHRASE = \"@openhands\"` | `TRIGGER_PHRASE = \"{user_phrase}\"` |\n| `CHANNEL_IDS: list[str] = []` | `CHANNEL_IDS: list[str] = {channel_id_list}` |\n| `DEFAULT_OPENHANDS_URL = \"http://localhost:8000\"` | `DEFAULT_OPENHANDS_URL = \"{url}\"` (keep default if user has no preference) |\n\nWrite the customised script to a temporary directory:\n```bash\nmkdir -p /tmp/slack-monitor-build\n# copy scripts/main.py to /tmp/slack-monitor-build/main.py\n# then replace only the three constants above\n```\n\nValidate syntax before packaging:\n```bash\npython3 -m py_compile /tmp/slack-monitor-build/main.py && echo \"Syntax OK\"\n```\n\nThen run a quick integrity check to confirm the template structure is still\npresent and only the configuration block was customised:\n```bash\ngrep -n 'TRIGGER_PHRASE = \"' /tmp/slack-monitor-build/main.py\ngrep -n 'CHANNEL_IDS: list\\[str\\] =' /tmp/slack-monitor-build/main.py\ngrep -n 'DEFAULT_OPENHANDS_URL = \"' /tmp/slack-monitor-build/main.py\ngrep -n 'def get_secret' /tmp/slack-monitor-build/main.py\ngrep -n 'def _state_file_path' /tmp/slack-monitor-build/main.py\ngrep -n 'def create_conversation' /tmp/slack-monitor-build/main.py\n```\n\nIf any of those checks fail, stop and re-copy the template instead of trying to\nrepair a hand-written variant.\n\n### Step 4 - Package and upload\n\nDetermine the Automation backend URL and auth from the `<RUNTIME_SERVICES>`\nblock in your system context:\n- Use the **Automation backend** `url_from_agent` as `OPENHANDS_HOST`\n- Auth: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY`\n\nIf no Automation backend is listed in `<RUNTIME_SERVICES>`, stop and tell\nthe user to start the full automation stack.\n\n```bash\ntar -czf /tmp/slack-monitor.tar.gz -C /tmp/slack-monitor-build .\n\n# OPENHANDS_HOST: read from <RUNTIME_SERVICES> Automation backend url_from_agent\nOPENHANDS_HOST=\"<automation-url-from-runtime-services>\"\n\nTARBALL_PATH=$(curl -s -X POST \\\n \"${OPENHANDS_HOST}/api/automation/v1/uploads?name=slack-channel-monitor\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/gzip\" \\\n --data-binary @/tmp/slack-monitor.tar.gz \\\n | python3 -c \"import json,sys; print(json.load(sys.stdin)['tarball_path'])\")\n\necho \"Uploaded: $TARBALL_PATH\"\n```\n\nIf the upload fails with a size error, the tarball must be under 1 MB.\n`main.py` is under 15 KB so this should never trigger.\n\n### Step 5 - Create the automation\n\n```bash\ncurl -s -X POST \"${OPENHANDS_HOST}/api/automation/v1\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\n \\\"name\\\": \\\"Slack Channel Monitor\\\",\n \\\"trigger\\\": {\\\"type\\\": \\\"cron\\\", \\\"schedule\\\": \\\"* * * * *\\\"},\n \\\"tarball_path\\\": \\\"$TARBALL_PATH\\\",\n \\\"entrypoint\\\": \\\"python3 main.py\\\",\n \\\"timeout\\\": 55\n }\" | python3 -m json.tool\n```\n\nA 55-second timeout keeps runs well within the 60-second cron window.\n\nRecord the returned `id` - share it with the user as confirmation.\n\n### Step 6 - Confirm\n\nTell the user:\n\n> ✅ **Slack Channel Monitor** is running!\n>\n> - Automation ID: `{id}`\n> - Channels: `{channel list}`\n> - Trigger phrase: `{phrase}`\n> - Polling every minute via cron `* * * * *`\n> - State file: `~/.openhands/workspaces/automation-state/slack_poller_{id}.json`\n>\n> Send a message containing `{phrase}` in any monitored channel to test it.\n> The bot will react with 👀 and reply with a link to the new conversation.\n\n---\n\n## Runtime Behaviour (per poll)\n\nEach cron run executes `main.py`, which runs **10 polling iterations** (every\n5 seconds) within the 55-second timeout window. Each iteration:\n\n1. **Loads state** from the JSON file (see `references/state-schema.md`).\n2. **Resolves the Slack token** - checks `SLACK_USER_TOKEN` then `SLACK_BOT_TOKEN`.\n3. **Fetches new messages:**\n - User token + `search:read` + > 1 channel → single `search.messages` call\n (searches for the trigger phrase across all channels).\n - Otherwise → one `conversations.history` call per channel.\n4. **Fetches due thread replies** - polls at most one tracked thread per\n iteration using per-thread exponential backoff to stay within Slack rate\n limits.\n5. **Processes messages** in chronological order:\n - Skips messages already in `processed_ts` (dedup across the overlap window).\n - Skips bot messages and any `ts` in `bot_message_ts`.\n - Reply in a tracked thread whose text contains the trigger phrase → forwards\n a follow-up request to the existing conversation and resets the follow-up\n watch window. Replies without the trigger phrase are marked processed and\n ignored.\n - Contains trigger phrase outside a tracked conversation → 👀 reaction, create\n a new conversation, post link.\n - Thread replies: agent receives full thread history for context.\n - Root messages: agent receives the trigger text only.\n6. **Checks conversation statuses** - for each active conversation where\n `time.time() - last_activity > 15 s`:\n - If status is `idle`, `finished`, `error`, or `stuck` → fetch the agent's\n final response via `/api/conversations/{id}/agent_final_response` and post\n it to the Slack thread. Mark the record `watching` for five minutes so\n triggered follow-up replies can continue the same conversation.\n7. **Advances `last_poll`** to `now - 10 s` (overlap window prevents boundary\n races). If a conversation creation failed, pins `last_poll` further back to\n retry on the next iteration.\n8. **Saves state** (including `processed_ts`) and continues to the next iteration.\n9. After all iterations, fires the completion callback.\n\nDebug output is written to both stdout and a persistent log at:\n```\n{WORKSPACE_BASE_ROOT}/automation-state/slack_poller_debug.log\n```\n\n---\n\n## Additional Resources\n\n### Reference Files\n\n- **`references/slack-api.md`** - Slack token types, required scopes, API\n endpoint reference, rate limits, and common error codes.\n- **`references/state-schema.md`** - State JSON schema, field definitions,\n example file, and conversation lifecycle diagram.\n\n### Script Template\n\n- **`scripts/main.py`** - The complete automation script. Customise the three\n constants at the top (`TRIGGER_PHRASE`, `CHANNEL_IDS`, `DEFAULT_OPENHANDS_URL`)\n before packaging.\n\n---\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---|---|---|\n| Bot doesn't react to messages | Token missing or bot not in channel | Verify token with `auth.test`; `/invite @botname` |\n| `not_in_channel` error in run logs | Bot token used but bot not a member | Invite bot or switch to user token |\n| `missing_scope` error | Token lacks required scopes | Re-install Slack app with correct scopes (see `references/slack-api.md`) |\n| No messages detected | `last_poll` timestamp is in the future | Delete the state file to reset; it will be recreated on next run |\n| Conversation link 404 | `OPENHANDS_URL` points to wrong host | Set the `OPENHANDS_URL` secret to the correct base URL |\n| Summary never posted | Conversation stuck in `running` state | Check conversation in the OpenHands UI; the agent may need intervention |\n| Duplicate conversations created | `processed_ts` state missing or corrupted | Delete the state file to reset; dedup will rebuild on next run |\n| Trigger message processed on each cron run | State file deleted between runs | Ensure `automation-state/` directory is persistent across runs |\n| Debug info needed | Need detailed per-message trace | Check `{WORKSPACE_BASE_ROOT}/automation-state/slack_poller_debug.log` |"
|
|
427
|
+
"content": "# Slack Channel Monitor\n\nCreate a cron automation that polls up to 10 Slack channels every minute.\nWhen a message containing the **trigger phrase** (default: `@openhands`) is\ndetected it:\n\n1. Adds a 👀 reaction to the triggering message.\n2. Opens an OpenHands conversation with the message and recent channel context.\n3. Posts a reply in the Slack thread with a link to the conversation.\n\nOn every subsequent run:\n- New Slack thread replies are forwarded only when they contain the trigger\n phrase, so unrelated conversation in the thread is ignored.\n- When the conversation finishes (or errors), the agent's final response is\n posted back to the Slack thread.\n- Completed conversations stay in a short follow-up watch window, allowing\n triggered Slack replies to continue the same OpenHands conversation.\n\n> **Local mode only.** This automation targets the local OpenHands setup\n> (`dev:automation` stack). A cloud/webhook-based variant is out of scope here.\n\n---\n\n## Prerequisites\n\n### Required secrets\n\nVerify that at least one of the following secrets is set in\n**OpenHands Settings → Secrets** before proceeding:\n\n| Secret name | Token type | Minimum scopes |\n|---|---|---|\n| `SLACK_BOT_TOKEN` | Bot (`xoxb-…`) | `channels:history`, `channels:read`, `reactions:write`, `chat:write` |\n| `SLACK_USER_TOKEN` | User (`xoxp-…`) | Same as bot, plus `search:read` for multi-channel efficiency |\n\nCheck with:\n```bash\n# For bot token:\ncurl -s https://slack.com/api/auth.test -H \"Authorization: Bearer $SLACK_BOT_TOKEN\" \\\n | python3 -c \"import json,sys; d=json.load(sys.stdin); print('ok' if d.get('ok') else d.get('error'))\"\n\n# For user token:\ncurl -s https://slack.com/api/auth.test -H \"Authorization: Bearer $SLACK_USER_TOKEN\" \\\n | python3 -c \"import json,sys; d=json.load(sys.stdin); print('ok' if d.get('ok') else d.get('error'))\"\n```\n\nIf neither token is present, inform the user and stop - the automation cannot\nfunction without Slack credentials.\n\n### Optional secret\n\n| Secret name | Default | Purpose |\n|---|---|---|\n| `OPENHANDS_URL` | `http://localhost:8000` | Base URL used to build conversation links posted in Slack |\n\n---\n\n## Setup Workflow\n\nFollow these steps in order.\n\n### Step 1 - Collect channels\n\nAsk the user: *\"Which Slack channels should be monitored? You can provide\nchannel names (e.g. `#general`) or IDs (e.g. `C0123456789`).\"*\n\n**If the user provides channel names**, resolve them to IDs:\n\n```bash\nSLACK_TOKEN=\"${SLACK_BOT_TOKEN:-$SLACK_USER_TOKEN}\"\ncurl -s \"https://slack.com/api/conversations.list?types=public_channel,private_channel&limit=200&exclude_archived=true\" \\\n -H \"Authorization: Bearer $SLACK_TOKEN\" \\\n | python3 -c \"\nimport json, sys\ndata = json.load(sys.stdin)\nif not data.get('ok'):\n print('ERROR:', data.get('error'))\n exit(1)\nnames = set(n.lstrip('#') for n in ['CHANNEL_NAMES_HERE'.split(',')])\nfor ch in data.get('channels', []):\n if ch['name'] in names:\n print(f\\\"{ch['name']} → {ch['id']}\\\")\n\"\n```\n\nReplace `CHANNEL_NAMES_HERE` with the comma-separated names the user provided.\n\n**If `conversations.list` returns `missing_scope` or `not_authed`:**\nInform the user: *\"The token doesn't have permission to list channels. Please\nprovide the channel IDs directly (right-click a channel in Slack → Copy link - \nthe last path segment starting with `C` is the ID).\"*\n\n**If the bot token lacks `channels:read`** for private channels, the user can\neither invite the bot first (`/invite @botname`) or switch to a user token.\n\nCollect up to 10 channel IDs. Record them as a Python list literal, e.g.:\n```python\n[\"C0123456789\", \"C9876543210\"]\n```\n\n### Step 2 - Collect trigger phrase\n\nAsk the user: *\"What trigger phrase should OpenHands respond to?\n(Press Enter to use the default: `@openhands`)\"*\n\nAccepted values: any non-empty string unlikely to appear accidentally, e.g.\n`@openhands`, `jazz hands`, `take-me-to-funky-town`.\n\n### Step 3 - Generate the automation script\n\nRead `scripts/main.py` from this skill's directory and **copy it verbatim**.\nApply exactly three constant substitutions near the top of the file:\n\n> **Do not reimplement, simplify, or hand-write a replacement script.**\n> The template already contains the correct secret-loading, state-path,\n> conversation-creation, and context-forwarding logic. Only the three\n> configuration constants below should change unless syntax validation fails.\n\n| Placeholder | Replace with |\n|---|---|\n| `TRIGGER_PHRASE = \"@openhands\"` | `TRIGGER_PHRASE = \"{user_phrase}\"` |\n| `CHANNEL_IDS: list[str] = []` | `CHANNEL_IDS: list[str] = {channel_id_list}` |\n| `DEFAULT_OPENHANDS_URL = \"http://localhost:8000\"` | `DEFAULT_OPENHANDS_URL = \"{url}\"` (keep default if user has no preference) |\n\nWrite the customised script to a temporary directory:\n```bash\nmkdir -p /tmp/slack-monitor-build\n# copy scripts/main.py to /tmp/slack-monitor-build/main.py\n# then replace only the three constants above\n```\n\nValidate syntax before packaging:\n```bash\npython3 -m py_compile /tmp/slack-monitor-build/main.py && echo \"Syntax OK\"\n```\n\nThen run a quick integrity check to confirm the template structure is still\npresent and only the configuration block was customised:\n```bash\ngrep -n 'TRIGGER_PHRASE = \"' /tmp/slack-monitor-build/main.py\ngrep -n 'CHANNEL_IDS: list\\[str\\] =' /tmp/slack-monitor-build/main.py\ngrep -n 'DEFAULT_OPENHANDS_URL = \"' /tmp/slack-monitor-build/main.py\ngrep -n 'def get_secret' /tmp/slack-monitor-build/main.py\ngrep -n 'def _state_file_path' /tmp/slack-monitor-build/main.py\ngrep -n 'def create_conversation' /tmp/slack-monitor-build/main.py\n```\n\nIf any of those checks fail, stop and re-copy the template instead of trying to\nrepair a hand-written variant.\n\n### Step 4 - Package and upload\n\nDetermine the Automation backend URL and auth from the `<RUNTIME_SERVICES>`\nblock in your system context:\n- Use the **Automation backend** `url_from_agent` as `OPENHANDS_HOST`\n- Auth: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY`\n\nIf no Automation backend is listed in `<RUNTIME_SERVICES>`, stop and tell\nthe user to start the full automation stack.\n\n```bash\ntar -czf /tmp/slack-monitor.tar.gz -C /tmp/slack-monitor-build .\n\n# OPENHANDS_HOST: read from <RUNTIME_SERVICES> Automation backend url_from_agent\nOPENHANDS_HOST=\"<automation-url-from-runtime-services>\"\n\nTARBALL_PATH=$(curl -s -X POST \\\n \"${OPENHANDS_HOST}/api/automation/v1/uploads?name=slack-channel-monitor\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/gzip\" \\\n --data-binary @/tmp/slack-monitor.tar.gz \\\n | python3 -c \"import json,sys; print(json.load(sys.stdin)['tarball_path'])\")\n\necho \"Uploaded: $TARBALL_PATH\"\n```\n\nIf the upload fails with a size error, the tarball must be under 1 MB.\n`main.py` is under 15 KB so this should never trigger.\n\n### Step 5 - Create the automation\n\n```bash\ncurl -s -X POST \"${OPENHANDS_HOST}/api/automation/v1\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\n \\\"name\\\": \\\"Slack Channel Monitor\\\",\n \\\"trigger\\\": {\\\"type\\\": \\\"cron\\\", \\\"schedule\\\": \\\"* * * * *\\\"},\n \\\"tarball_path\\\": \\\"$TARBALL_PATH\\\",\n \\\"entrypoint\\\": \\\"python3 main.py\\\",\n \\\"timeout\\\": 55\n }\" | python3 -m json.tool\n```\n\nA 55-second timeout keeps runs well within the 60-second cron window.\n\nRecord the returned `id` - share it with the user as confirmation.\n\n### Step 6 - Confirm\n\nTell the user:\n\n> ✅ **Slack Channel Monitor** is running!\n>\n> - Automation ID: `{id}`\n> - Channels: `{channel list}`\n> - Trigger phrase: `{phrase}`\n> - Polling every minute via cron `* * * * *`\n> - State file: `~/.openhands/workspaces/automation-state/slack_poller_{id}.json`\n>\n> Send a message containing `{phrase}` in any monitored channel to test it.\n> The bot will react with 👀 and reply with a link to the new conversation.\n\n---\n\n## Runtime Behaviour (per poll)\n\nEach cron run executes `main.py`, which runs **10 polling iterations** (every\n5 seconds) within the 55-second timeout window. Each iteration:\n\n1. **Loads state** from the JSON file (see `references/state-schema.md`).\n2. **Resolves the Slack token** - checks `SLACK_USER_TOKEN` then `SLACK_BOT_TOKEN`.\n3. **Fetches new messages:**\n - User token + `search:read` + > 1 channel → single `search.messages` call\n (searches for the trigger phrase across all channels).\n - Otherwise → one `conversations.history` call per channel.\n4. **Fetches due thread replies** - polls at most one tracked thread per\n iteration using per-thread exponential backoff to stay within Slack rate\n limits.\n5. **Processes messages** in chronological order:\n - Skips messages already in `processed_ts` (dedup across the overlap window).\n - Skips bot messages and any `ts` in `bot_message_ts`.\n - Reply in a tracked thread whose text contains the trigger phrase → forwards\n a follow-up request to the existing conversation and resets the follow-up\n watch window. Replies without the trigger phrase are marked processed and\n ignored.\n - Contains trigger phrase outside a tracked conversation → 👀 reaction, create\n a new conversation, post link.\n - Thread replies: agent receives full thread history for context.\n - Root messages: agent receives the trigger text only.\n6. **Checks conversation statuses** - for each active conversation where\n `time.time() - last_activity > 15 s`:\n - If status is `idle`, `finished`, `error`, or `stuck` → fetch the agent's\n final response via `/api/conversations/{id}/agent_final_response` and post\n it to the Slack thread using Slack's `markdown_text` field so Markdown\n formatting renders correctly. Mark the record `watching` for five minutes\n so triggered follow-up replies can continue the same conversation.\n7. **Advances `last_poll`** to `now - 10 s` (overlap window prevents boundary\n races). If a conversation creation failed, pins `last_poll` further back to\n retry on the next iteration.\n8. **Saves state** (including `processed_ts`) and continues to the next iteration.\n9. After all iterations, fires the completion callback.\n\nDebug output is written to both stdout and a persistent log at:\n```\n{WORKSPACE_BASE_ROOT}/automation-state/slack_poller_debug.log\n```\n\n---\n\n## Additional Resources\n\n### Reference Files\n\n- **`references/slack-api.md`** - Slack token types, required scopes, API\n endpoint reference, rate limits, and common error codes.\n- **`references/state-schema.md`** - State JSON schema, field definitions,\n example file, and conversation lifecycle diagram.\n\n### Script Template\n\n- **`scripts/main.py`** - The complete automation script. Customise the three\n constants at the top (`TRIGGER_PHRASE`, `CHANNEL_IDS`, `DEFAULT_OPENHANDS_URL`)\n before packaging.\n\n---\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---|---|---|\n| Bot doesn't react to messages | Token missing or bot not in channel | Verify token with `auth.test`; `/invite @botname` |\n| `not_in_channel` error in run logs | Bot token used but bot not a member | Invite bot or switch to user token |\n| `missing_scope` error | Token lacks required scopes | Re-install Slack app with correct scopes (see `references/slack-api.md`) |\n| No messages detected | `last_poll` timestamp is in the future | Delete the state file to reset; it will be recreated on next run |\n| Conversation link 404 | `OPENHANDS_URL` points to wrong host | Set the `OPENHANDS_URL` secret to the correct base URL |\n| Summary never posted | Conversation stuck in `running` state | Check conversation in the OpenHands UI; the agent may need intervention |\n| Duplicate conversations created | `processed_ts` state missing or corrupted | Delete the state file to reset; dedup will rebuild on next run |\n| Trigger message processed on each cron run | State file deleted between runs | Ensure `automation-state/` directory is persistent across runs |\n| Debug info needed | Need detailed per-message trace | Check `{WORKSPACE_BASE_ROOT}/automation-state/slack_poller_debug.log` |"
|
|
402
428
|
},
|
|
403
429
|
{
|
|
404
430
|
"name": "slack-standup-digest",
|