@openhands/extensions 0.20.0 → 0.22.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.
Files changed (305) hide show
  1. package/.claude-plugin/marketplace.json +881 -0
  2. package/.codex-plugin/marketplace.json +881 -0
  3. package/.github/workflows/check-extensions.yml +4 -0
  4. package/.github/workflows/npm-publish.yml +9 -3
  5. package/.github/workflows/pr-artifacts.yml +294 -0
  6. package/.github/workflows/pr-description-check.yml +4 -2
  7. package/.plugin/marketplace.json +881 -0
  8. package/.release-please-manifest.json +1 -1
  9. package/AGENTS.md +27 -3
  10. package/README.md +6 -2
  11. package/automations/bundle-index.js +22 -3
  12. package/automations/catalog/github-agents-md-maintainer/manifest.json +2 -1
  13. package/automations/catalog/github-delivery-watchdog/manifest.json +79 -0
  14. package/automations/catalog/github-issue-to-pr/manifest.json +24 -15
  15. package/automations/catalog/github-issue-triage/manifest.json +73 -0
  16. package/automations/catalog/github-pr-reviewer/manifest.json +23 -15
  17. package/automations/catalog/gitlab-issue-to-mr/manifest.json +124 -0
  18. package/automations/catalog-index.js +17 -11
  19. package/integrations/catalog/reportportal.json +76 -0
  20. package/integrations/catalog-index.js +6 -4
  21. package/marketplaces/openhands-extensions.json +42 -0
  22. package/package.json +1 -1
  23. package/plugins/city-weather/.claude-plugin/plugin.json +13 -0
  24. package/plugins/city-weather/.codex-plugin/plugin.json +13 -0
  25. package/plugins/cobol-modernization/.claude-plugin/plugin.json +19 -0
  26. package/plugins/cobol-modernization/.codex-plugin/plugin.json +19 -0
  27. package/plugins/issue-duplicate-checker/.claude-plugin/plugin.json +13 -0
  28. package/plugins/issue-duplicate-checker/.codex-plugin/plugin.json +13 -0
  29. package/plugins/magic-test/.claude-plugin/plugin.json +13 -0
  30. package/plugins/magic-test/.codex-plugin/plugin.json +13 -0
  31. package/plugins/migration-scoring/.claude-plugin/plugin.json +19 -0
  32. package/plugins/migration-scoring/.codex-plugin/plugin.json +19 -0
  33. package/plugins/onboarding/.claude-plugin/plugin.json +20 -0
  34. package/plugins/onboarding/.codex-plugin/plugin.json +20 -0
  35. package/plugins/onboarding/skills/agent-readiness-report/references/criteria.md +144 -0
  36. package/plugins/onboarding/skills/improve-agent-readiness/references/criteria.md +144 -0
  37. package/plugins/openhands/.claude-plugin/plugin.json +13 -0
  38. package/plugins/openhands/.codex-plugin/plugin.json +13 -0
  39. package/plugins/openhands/skills/openhands-api/.claude-plugin/plugin.json +22 -0
  40. package/plugins/openhands/skills/openhands-api/.codex-plugin/plugin.json +22 -0
  41. package/plugins/openhands/skills/openhands-api/.plugin/plugin.json +22 -0
  42. package/plugins/openhands/skills/openhands-api/README.md +52 -0
  43. package/plugins/openhands/skills/openhands-api/SKILL.md +469 -0
  44. package/plugins/openhands/skills/openhands-api/references/README.md +33 -0
  45. package/plugins/openhands/skills/openhands-api/references/TROUBLESHOOTING.md +81 -0
  46. package/plugins/openhands/skills/openhands-api/references/example_prompt.md +12 -0
  47. package/plugins/openhands/skills/openhands-api/references/windows.md +44 -0
  48. package/plugins/openhands/skills/openhands-api/scripts/openhands_api.py +606 -0
  49. package/plugins/openhands/skills/openhands-api/scripts/openhands_api.ts +252 -0
  50. package/plugins/openhands/skills/openhands-automation/.claude-plugin/plugin.json +19 -0
  51. package/plugins/openhands/skills/openhands-automation/.codex-plugin/plugin.json +19 -0
  52. package/plugins/openhands/skills/openhands-automation/.plugin/plugin.json +19 -0
  53. package/plugins/openhands/skills/openhands-automation/README.md +89 -0
  54. package/plugins/openhands/skills/openhands-automation/SKILL.md +899 -0
  55. package/plugins/openhands/skills/openhands-automation/commands/automation-create.md +8 -0
  56. package/plugins/openhands/skills/openhands-automation/references/ab-testing.md +185 -0
  57. package/plugins/openhands/skills/openhands-automation/references/custom-automation.md +832 -0
  58. package/plugins/openhands/skills/openhands-automation/references/security.md +84 -0
  59. package/plugins/openhands/skills/openhands-automation/references/windows.md +57 -0
  60. package/plugins/openhands/skills/openhands-sdk/.claude-plugin/plugin.json +20 -0
  61. package/plugins/openhands/skills/openhands-sdk/.codex-plugin/plugin.json +20 -0
  62. package/plugins/openhands/skills/openhands-sdk/.plugin/plugin.json +20 -0
  63. package/plugins/openhands/skills/openhands-sdk/README.md +22 -0
  64. package/plugins/openhands/skills/openhands-sdk/SKILL.md +245 -0
  65. package/plugins/openhands/skills/openhands-sdk/commands/sdk.md +8 -0
  66. package/plugins/pr-review/.claude-plugin/plugin.json +13 -0
  67. package/plugins/pr-review/.codex-plugin/plugin.json +13 -0
  68. package/plugins/pr-review/skills/code-review/.claude-plugin/plugin.json +19 -0
  69. package/plugins/pr-review/skills/code-review/.codex-plugin/plugin.json +19 -0
  70. package/plugins/pr-review/skills/code-review/.plugin/plugin.json +19 -0
  71. package/plugins/pr-review/skills/code-review/README.md +18 -0
  72. package/plugins/pr-review/skills/code-review/SKILL.md +209 -0
  73. package/plugins/pr-review/skills/code-review/commands/codereview-roasted.md +8 -0
  74. package/plugins/pr-review/skills/code-review/commands/codereview.md +8 -0
  75. package/plugins/pr-review/skills/code-review/references/risk-evaluation.md +41 -0
  76. package/plugins/pr-review/skills/code-review/references/supply-chain-security.md +31 -0
  77. package/plugins/pr-review/skills/github-pr-review/.claude-plugin/plugin.json +18 -0
  78. package/plugins/pr-review/skills/github-pr-review/.codex-plugin/plugin.json +18 -0
  79. package/plugins/pr-review/skills/github-pr-review/.plugin/plugin.json +18 -0
  80. package/plugins/pr-review/skills/github-pr-review/README.md +145 -0
  81. package/plugins/pr-review/skills/github-pr-review/SKILL.md +187 -0
  82. package/plugins/pr-review/skills/github-pr-review/commands/github-pr-review.md +8 -0
  83. package/plugins/pr-review/skills/github-pr-review/references/windows.md +47 -0
  84. package/plugins/qa-changes/.claude-plugin/plugin.json +11 -0
  85. package/plugins/qa-changes/.codex-plugin/plugin.json +11 -0
  86. package/plugins/qa-changes/skills/github-pr-review/.claude-plugin/plugin.json +18 -0
  87. package/plugins/qa-changes/skills/github-pr-review/.codex-plugin/plugin.json +18 -0
  88. package/plugins/qa-changes/skills/github-pr-review/.plugin/plugin.json +18 -0
  89. package/plugins/qa-changes/skills/github-pr-review/README.md +145 -0
  90. package/plugins/qa-changes/skills/github-pr-review/SKILL.md +187 -0
  91. package/plugins/qa-changes/skills/github-pr-review/commands/github-pr-review.md +8 -0
  92. package/plugins/qa-changes/skills/github-pr-review/references/windows.md +47 -0
  93. package/plugins/qa-changes/skills/qa-changes/README.md +18 -0
  94. package/plugins/qa-changes/skills/qa-changes/SKILL.md +229 -0
  95. package/plugins/qa-changes/skills/qa-changes/commands/qa-changes.md +8 -0
  96. package/plugins/release-notes/.claude-plugin/plugin.json +19 -0
  97. package/plugins/release-notes/.codex-plugin/plugin.json +19 -0
  98. package/plugins/release-notes/skills/release-notes/README.md +24 -0
  99. package/plugins/release-notes/skills/release-notes/SKILL.md +19 -0
  100. package/plugins/release-notes/skills/release-notes/commands/release-notes.md +8 -0
  101. package/plugins/vulnerability-remediation/.claude-plugin/plugin.json +19 -0
  102. package/plugins/vulnerability-remediation/.codex-plugin/plugin.json +19 -0
  103. package/plugins/vulnerability-remediation/skills/github/.claude-plugin/plugin.json +19 -0
  104. package/plugins/vulnerability-remediation/skills/github/.codex-plugin/plugin.json +19 -0
  105. package/plugins/vulnerability-remediation/skills/github/.plugin/plugin.json +19 -0
  106. package/plugins/vulnerability-remediation/skills/github/README.md +42 -0
  107. package/plugins/vulnerability-remediation/skills/github/SKILL.md +107 -0
  108. package/plugins/vulnerability-remediation/skills/github/references/windows.md +48 -0
  109. package/plugins/vulnerability-remediation/skills/github/scripts/agent_conversation.py +143 -0
  110. package/plugins/vulnerability-remediation/skills/github/scripts/github_client.py +230 -0
  111. package/plugins/vulnerability-remediation/skills/security/.claude-plugin/plugin.json +18 -0
  112. package/plugins/vulnerability-remediation/skills/security/.codex-plugin/plugin.json +18 -0
  113. package/plugins/vulnerability-remediation/skills/security/.plugin/plugin.json +18 -0
  114. package/plugins/vulnerability-remediation/skills/security/README.md +38 -0
  115. package/plugins/vulnerability-remediation/skills/security/SKILL.md +33 -0
  116. package/pyproject.toml +2 -2
  117. package/python/openhands_extensions/_version.py +1 -1
  118. package/skills/add-javadoc/.claude-plugin/plugin.json +18 -0
  119. package/skills/add-javadoc/.codex-plugin/plugin.json +18 -0
  120. package/skills/add-skill/.claude-plugin/plugin.json +18 -0
  121. package/skills/add-skill/.codex-plugin/plugin.json +18 -0
  122. package/skills/agent-canvas-environment/.claude-plugin/plugin.json +19 -0
  123. package/skills/agent-canvas-environment/.codex-plugin/plugin.json +19 -0
  124. package/skills/agent-creator/.claude-plugin/plugin.json +20 -0
  125. package/skills/agent-creator/.codex-plugin/plugin.json +20 -0
  126. package/skills/agent-memory/.claude-plugin/plugin.json +18 -0
  127. package/skills/agent-memory/.codex-plugin/plugin.json +18 -0
  128. package/skills/agent-sdk-builder/.claude-plugin/plugin.json +18 -0
  129. package/skills/agent-sdk-builder/.codex-plugin/plugin.json +18 -0
  130. package/skills/azure-devops/.claude-plugin/plugin.json +18 -0
  131. package/skills/azure-devops/.codex-plugin/plugin.json +18 -0
  132. package/skills/bitbucket/.claude-plugin/plugin.json +17 -0
  133. package/skills/bitbucket/.codex-plugin/plugin.json +17 -0
  134. package/skills/bitbucket-cloud/.claude-plugin/plugin.json +18 -0
  135. package/skills/bitbucket-cloud/.codex-plugin/plugin.json +18 -0
  136. package/skills/bitbucket-data-center/.claude-plugin/plugin.json +19 -0
  137. package/skills/bitbucket-data-center/.codex-plugin/plugin.json +19 -0
  138. package/skills/code-review/.claude-plugin/plugin.json +19 -0
  139. package/skills/code-review/.codex-plugin/plugin.json +19 -0
  140. package/skills/code-simplifier/.claude-plugin/plugin.json +21 -0
  141. package/skills/code-simplifier/.codex-plugin/plugin.json +21 -0
  142. package/skills/datadog/.claude-plugin/plugin.json +19 -0
  143. package/skills/datadog/.codex-plugin/plugin.json +19 -0
  144. package/skills/deno/.claude-plugin/plugin.json +18 -0
  145. package/skills/deno/.codex-plugin/plugin.json +18 -0
  146. package/skills/discord/.claude-plugin/plugin.json +18 -0
  147. package/skills/discord/.codex-plugin/plugin.json +18 -0
  148. package/skills/docker/.claude-plugin/plugin.json +17 -0
  149. package/skills/docker/.codex-plugin/plugin.json +17 -0
  150. package/skills/evidence-based-citations/.claude-plugin/plugin.json +20 -0
  151. package/skills/evidence-based-citations/.codex-plugin/plugin.json +20 -0
  152. package/skills/flarglebargle/.claude-plugin/plugin.json +16 -0
  153. package/skills/flarglebargle/.codex-plugin/plugin.json +16 -0
  154. package/skills/frontend-design/.claude-plugin/plugin.json +21 -0
  155. package/skills/frontend-design/.codex-plugin/plugin.json +21 -0
  156. package/skills/github/.claude-plugin/plugin.json +19 -0
  157. package/skills/github/.codex-plugin/plugin.json +19 -0
  158. package/skills/github/scripts/agent_conversation.py +143 -0
  159. package/skills/github/scripts/github_client.py +230 -0
  160. package/skills/github-actions/.claude-plugin/plugin.json +19 -0
  161. package/skills/github-actions/.codex-plugin/plugin.json +19 -0
  162. package/skills/github-agents-md-maintainer/.claude-plugin/plugin.json +19 -0
  163. package/skills/github-agents-md-maintainer/.codex-plugin/plugin.json +19 -0
  164. package/skills/github-agents-md-maintainer/SKILL.md +7 -1
  165. package/skills/github-agents-md-maintainer/scripts/github_client.py +230 -0
  166. package/skills/github-agents-md-maintainer/scripts/main.py +4 -40
  167. package/skills/github-delivery-watchdog/.claude-plugin/plugin.json +9 -0
  168. package/skills/github-delivery-watchdog/.codex-plugin/plugin.json +9 -0
  169. package/skills/github-delivery-watchdog/.plugin/plugin.json +9 -0
  170. package/skills/github-delivery-watchdog/README.md +10 -0
  171. package/skills/github-delivery-watchdog/SKILL.md +35 -0
  172. package/skills/github-delivery-watchdog/commands/github-delivery-watchdog.md +8 -0
  173. package/skills/github-delivery-watchdog/scripts/github_client.py +230 -0
  174. package/skills/github-delivery-watchdog/scripts/worker.py +96 -0
  175. package/skills/github-issue-to-pr/.claude-plugin/plugin.json +19 -0
  176. package/skills/github-issue-to-pr/.codex-plugin/plugin.json +19 -0
  177. package/skills/github-issue-to-pr/SKILL.md +15 -1
  178. package/skills/github-issue-to-pr/scripts/agent_conversation.py +143 -0
  179. package/skills/github-issue-to-pr/scripts/github_client.py +230 -0
  180. package/skills/github-issue-to-pr/scripts/main.py +19 -50
  181. package/skills/github-issue-to-pr/scripts/worker.py +172 -0
  182. package/skills/github-issue-triage/.claude-plugin/plugin.json +9 -0
  183. package/skills/github-issue-triage/.codex-plugin/plugin.json +9 -0
  184. package/skills/github-issue-triage/.plugin/plugin.json +9 -0
  185. package/skills/github-issue-triage/README.md +9 -0
  186. package/skills/github-issue-triage/SKILL.md +34 -0
  187. package/skills/github-issue-triage/commands/github-issue-triage.md +8 -0
  188. package/skills/github-issue-triage/scripts/agent_conversation.py +143 -0
  189. package/skills/github-issue-triage/scripts/github_client.py +230 -0
  190. package/skills/github-issue-triage/scripts/worker.py +106 -0
  191. package/skills/github-pr-review/.claude-plugin/plugin.json +18 -0
  192. package/skills/github-pr-review/.codex-plugin/plugin.json +18 -0
  193. package/skills/github-pr-reviewer/.claude-plugin/plugin.json +20 -0
  194. package/skills/github-pr-reviewer/.codex-plugin/plugin.json +20 -0
  195. package/skills/github-pr-reviewer/SKILL.md +15 -1
  196. package/skills/github-pr-reviewer/scripts/agent_conversation.py +143 -0
  197. package/skills/github-pr-reviewer/scripts/github_client.py +230 -0
  198. package/skills/github-pr-reviewer/scripts/main.py +23 -49
  199. package/skills/github-pr-reviewer/scripts/worker.py +94 -0
  200. package/skills/github-repo-monitor/.claude-plugin/plugin.json +22 -0
  201. package/skills/github-repo-monitor/.codex-plugin/plugin.json +22 -0
  202. package/skills/github-repo-monitor/SKILL.md +7 -1
  203. package/skills/github-repo-monitor/scripts/github_client.py +230 -0
  204. package/skills/github-repo-monitor/scripts/main.py +4 -46
  205. package/skills/gitlab/.claude-plugin/plugin.json +17 -0
  206. package/skills/gitlab/.codex-plugin/plugin.json +17 -0
  207. package/skills/gitlab-issue-to-mr/.claude-plugin/plugin.json +19 -0
  208. package/skills/gitlab-issue-to-mr/.codex-plugin/plugin.json +19 -0
  209. package/skills/gitlab-issue-to-mr/.plugin/plugin.json +19 -0
  210. package/skills/gitlab-issue-to-mr/README.md +92 -0
  211. package/skills/gitlab-issue-to-mr/SKILL.md +408 -0
  212. package/skills/gitlab-issue-to-mr/commands/issue-to-mr-setup.md +8 -0
  213. package/skills/gitlab-issue-to-mr/references/state-schema.md +140 -0
  214. package/skills/gitlab-issue-to-mr/scripts/main.py +1448 -0
  215. package/skills/incident-retrospective/.claude-plugin/plugin.json +21 -0
  216. package/skills/incident-retrospective/.codex-plugin/plugin.json +21 -0
  217. package/skills/index.js +52 -6
  218. package/skills/iterate/.claude-plugin/plugin.json +13 -0
  219. package/skills/iterate/.codex-plugin/plugin.json +13 -0
  220. package/skills/jira-issue-to-pr/.claude-plugin/plugin.json +19 -0
  221. package/skills/jira-issue-to-pr/.codex-plugin/plugin.json +19 -0
  222. package/skills/jupyter/.claude-plugin/plugin.json +18 -0
  223. package/skills/jupyter/.codex-plugin/plugin.json +18 -0
  224. package/skills/kubernetes/.claude-plugin/plugin.json +18 -0
  225. package/skills/kubernetes/.codex-plugin/plugin.json +18 -0
  226. package/skills/learn-from-code-review/.claude-plugin/plugin.json +19 -0
  227. package/skills/learn-from-code-review/.codex-plugin/plugin.json +19 -0
  228. package/skills/linear/.claude-plugin/plugin.json +19 -0
  229. package/skills/linear/.codex-plugin/plugin.json +19 -0
  230. package/skills/linear-triage/.claude-plugin/plugin.json +21 -0
  231. package/skills/linear-triage/.codex-plugin/plugin.json +21 -0
  232. package/skills/news-digest/.claude-plugin/plugin.json +19 -0
  233. package/skills/news-digest/.codex-plugin/plugin.json +19 -0
  234. package/skills/notion/.claude-plugin/plugin.json +17 -0
  235. package/skills/notion/.codex-plugin/plugin.json +17 -0
  236. package/skills/npm/.claude-plugin/plugin.json +17 -0
  237. package/skills/npm/.codex-plugin/plugin.json +17 -0
  238. package/skills/openhands-api/.claude-plugin/plugin.json +22 -0
  239. package/skills/openhands-api/.codex-plugin/plugin.json +22 -0
  240. package/skills/openhands-automation/.claude-plugin/plugin.json +19 -0
  241. package/skills/openhands-automation/.codex-plugin/plugin.json +19 -0
  242. package/skills/openhands-automation/README.md +1 -1
  243. package/skills/openhands-automation/SKILL.md +18 -8
  244. package/skills/openhands-enterprise-troubleshooting/.claude-plugin/plugin.json +20 -0
  245. package/skills/openhands-enterprise-troubleshooting/.codex-plugin/plugin.json +20 -0
  246. package/skills/openhands-enterprise-troubleshooting/.plugin/plugin.json +20 -0
  247. package/skills/openhands-enterprise-troubleshooting/README.md +79 -0
  248. package/skills/openhands-enterprise-troubleshooting/SKILL.md +339 -0
  249. package/skills/openhands-enterprise-troubleshooting/references/diagnostics.md +698 -0
  250. package/skills/openhands-enterprise-troubleshooting/references/support-bundle-analysis.md +570 -0
  251. package/skills/openhands-enterprise-troubleshooting/scripts/bundle_triage.py +532 -0
  252. package/skills/openhands-sdk/.claude-plugin/plugin.json +20 -0
  253. package/skills/openhands-sdk/.codex-plugin/plugin.json +20 -0
  254. package/skills/openhands-sdk/SKILL.md +2 -0
  255. package/skills/pdflatex/.claude-plugin/plugin.json +18 -0
  256. package/skills/pdflatex/.codex-plugin/plugin.json +18 -0
  257. package/skills/plain-english-content/.claude-plugin/plugin.json +15 -0
  258. package/skills/plain-english-content/.codex-plugin/plugin.json +15 -0
  259. package/skills/prd/.claude-plugin/plugin.json +19 -0
  260. package/skills/prd/.codex-plugin/plugin.json +19 -0
  261. package/skills/research-brief/.claude-plugin/plugin.json +20 -0
  262. package/skills/research-brief/.codex-plugin/plugin.json +20 -0
  263. package/skills/security/.claude-plugin/plugin.json +18 -0
  264. package/skills/security/.codex-plugin/plugin.json +18 -0
  265. package/skills/skill-creator/.claude-plugin/plugin.json +17 -0
  266. package/skills/skill-creator/.codex-plugin/plugin.json +17 -0
  267. package/skills/slack-channel-monitor/.claude-plugin/plugin.json +21 -0
  268. package/skills/slack-channel-monitor/.codex-plugin/plugin.json +21 -0
  269. package/skills/slack-standup-digest/.claude-plugin/plugin.json +21 -0
  270. package/skills/slack-standup-digest/.codex-plugin/plugin.json +21 -0
  271. package/skills/spark-version-upgrade/.claude-plugin/plugin.json +20 -0
  272. package/skills/spark-version-upgrade/.codex-plugin/plugin.json +20 -0
  273. package/skills/ssh/.claude-plugin/plugin.json +18 -0
  274. package/skills/ssh/.codex-plugin/plugin.json +18 -0
  275. package/skills/swift-linux/.claude-plugin/plugin.json +17 -0
  276. package/skills/swift-linux/.codex-plugin/plugin.json +17 -0
  277. package/skills/technical-writing/.claude-plugin/plugin.json +15 -0
  278. package/skills/technical-writing/.codex-plugin/plugin.json +15 -0
  279. package/skills/theme-factory/.claude-plugin/plugin.json +19 -0
  280. package/skills/theme-factory/.codex-plugin/plugin.json +19 -0
  281. package/skills/ticket-to-code-change/.claude-plugin/plugin.json +21 -0
  282. package/skills/ticket-to-code-change/.codex-plugin/plugin.json +21 -0
  283. package/skills/upstream-fork-sync/.claude-plugin/plugin.json +21 -0
  284. package/skills/upstream-fork-sync/.codex-plugin/plugin.json +21 -0
  285. package/skills/uv/.claude-plugin/plugin.json +18 -0
  286. package/skills/uv/.codex-plugin/plugin.json +18 -0
  287. package/skills/vercel/.claude-plugin/plugin.json +18 -0
  288. package/skills/vercel/.codex-plugin/plugin.json +18 -0
  289. package/tests/fixtures/automations/capabilities.json +6 -3
  290. package/tests/fixtures/automations/github-pr-reviewer.json +64 -43
  291. package/tests/github_automation_helpers.py +31 -0
  292. package/tests/test_agent_conversation_dispatch.py +180 -0
  293. package/tests/test_automation_setup.py +3 -3
  294. package/tests/test_github_automation_foundation.py +73 -0
  295. package/tests/test_github_developer_delivery.py +150 -0
  296. package/tests/test_github_reviewer_delivery.py +110 -0
  297. package/tests/test_github_skill_installation.py +63 -0
  298. package/tests/test_github_triage_delivery.py +90 -0
  299. package/tests/test_github_watchdog_delivery.py +233 -0
  300. package/tests/test_gitlab_issue_to_mr.py +823 -0
  301. package/.pr/custom-automation-event-trigger.png +0 -0
  302. package/.pr/custom-automation-plugin-fields.png +0 -0
  303. package/.pr/custom-automation-prompt-review.png +0 -0
  304. package/.pr/custom-automation-prompt-scheduled.png +0 -0
  305. package/.pr/custom-automation-upload-fields.png +0 -0
@@ -0,0 +1,832 @@
1
+ # Custom Automation Reference
2
+
3
+ > **⚠️ Do NOT use this reference unless the user has explicitly requested a custom automation.** Always use the preset/prompt endpoint from the main SKILL.md first. If the preset approach cannot satisfy the requirement, explain the options to the user and let them decide.
4
+
5
+ This file contains detailed documentation for creating custom automations with user-provided code, uploads, and entrypoints.
6
+
7
+ **When to use custom automation (only if the user explicitly chooses this):**
8
+ - Full control over the automation code structure is needed
9
+ - Custom dependencies or a specific runtime are required
10
+ - The user has confirmed that the prompt preset does not meet their requirements
11
+
12
+ ## Table of Contents
13
+
14
+ 1. [Tarball Uploads](#uploading-a-tarball)
15
+ 2. [Creating Custom Automations](#creating-an-automation)
16
+ 3. [Managing Automations](#managing-automations)
17
+ 4. [Writing Automation Code](#writing-automation-code)
18
+ - [SDK-based Scripts](#sdk-based-scripts) — AI agent conversations
19
+ - [Deterministic Script (No LLM)](#deterministic-script-no-llm) — pure Python stdlib
20
+ 5. [Environment Variables](#environment-variables)
21
+ 6. [Validation Rules](#validation-rules)
22
+ 7. [Complete Examples](#complete-examples)
23
+
24
+ ---
25
+
26
+ ## Uploading a Tarball
27
+
28
+ Before creating a custom automation, you need to upload your code as a tarball. The upload endpoint streams directly to cloud storage with a **1MB size limit**.
29
+
30
+ ### Create a Tarball
31
+
32
+ ```bash
33
+ tar -czf automation.tar.gz -C /path/to/your/code .
34
+ ```
35
+
36
+ ### Tarball Structure
37
+
38
+ ```
39
+ automation.tar.gz
40
+ ├── main.py # Your entrypoint script
41
+ ├── setup.sh # Optional: install dependencies before entrypoint runs
42
+ └── requirements.txt # Optional: additional dependencies
43
+ ```
44
+
45
+ ### Validate Before Packaging
46
+
47
+ **Always validate syntax before creating the tarball.** This catches errors immediately and avoids uploading broken code that fails silently at runtime.
48
+
49
+ ```bash
50
+ python3 -m py_compile main.py # fails with a clear error on any syntax problem
51
+ bash -n setup.sh # validates shell syntax without executing
52
+ ```
53
+
54
+ Syntax checks alone don't catch a config value that's valid-but-wrong Python — e.g. `json.dumps` emitting `true`/`false` where Python expects `True`/`False`, which compiles fine and only fails at runtime. Run the script once against a synthetic event before deploying; see `references/security.md#verify-before-deploying-not-just-compile`.
55
+
56
+ Fix any errors reported before proceeding to the next step.
57
+
58
+
59
+ ### Upload the Tarball
60
+
61
+ First, determine the API host. Look for a `<HOST>` value in the system prompt. If present, use that URL. Otherwise, default to `https://app.all-hands.dev`.
62
+
63
+ Then upload:
64
+
65
+ ```bash
66
+ curl -X POST "${OPENHANDS_HOST}/api/automation/v1/uploads?name=my-automation&description=Weekly%20report%20generator" \
67
+ -H "Authorization: Bearer ${OPENHANDS_API_KEY}" \
68
+ -H "Content-Type: application/gzip" \
69
+ --data-binary @automation.tar.gz
70
+ ```
71
+
72
+ ### Upload Response
73
+
74
+ ```json
75
+ {
76
+ "id": "550e8400-e29b-41d4-a716-446655440000",
77
+ "tarball_path": "oh-internal://uploads/550e8400-e29b-41d4-a716-446655440000",
78
+ "status": "COMPLETED",
79
+ "size_bytes": 12345
80
+ }
81
+ ```
82
+
83
+ **Important:** Save the `tarball_path` value - you'll need it when creating the automation.
84
+
85
+ ### Upload Status Values
86
+
87
+ | Status | Description |
88
+ |--------|-------------|
89
+ | `UPLOADING` | Upload in progress |
90
+ | `COMPLETED` | Upload successful, `tarball_path` is available |
91
+ | `FAILED` | Upload failed, check `error_message` |
92
+
93
+ ---
94
+
95
+ ## Creating an Automation
96
+
97
+ Once you have a tarball uploaded (or an external URL), create the automation:
98
+
99
+ ```bash
100
+ curl -X POST "${OPENHANDS_HOST}/api/automation/v1" \
101
+ -H "Authorization: Bearer ${OPENHANDS_API_KEY}" \
102
+ -H "Content-Type: application/json" \
103
+ -d '{
104
+ "name": "Weekly Report Generator",
105
+ "trigger": {
106
+ "type": "cron",
107
+ "schedule": "0 9 * * 1",
108
+ "timezone": "UTC"
109
+ },
110
+ "tarball_path": "oh-internal://uploads/550e8400-e29b-41d4-a716-446655440000",
111
+ "entrypoint": "python main.py",
112
+ "timeout": 300
113
+ }'
114
+ ```
115
+
116
+ ### Request Fields
117
+
118
+ | Field | Required | Description |
119
+ |-------|----------|-------------|
120
+ | `name` | Yes | Name of the automation (1-500 characters) |
121
+ | `trigger.type` | Yes | Must be `"cron"` |
122
+ | `trigger.schedule` | Yes | Cron expression (5 fields: min hour day month weekday) |
123
+ | `trigger.timezone` | No | IANA timezone (default: `"UTC"`) |
124
+ | `tarball_path` | Yes | Path to code tarball (see Tarball Path Formats below) |
125
+ | `entrypoint` | Yes | Command to execute (e.g., `"python main.py"`, `"uv run script.py"`) |
126
+ | `setup_script_path` | No | Relative path to setup script inside tarball |
127
+ | `timeout` | No | Max execution time in seconds (1-600, default: 600) |
128
+
129
+ ### Tarball Path Formats
130
+
131
+ | Format | Example | Description |
132
+ |--------|---------|-------------|
133
+ | Internal upload | `oh-internal://uploads/{uuid}` | Uploaded via `/api/v1/uploads` |
134
+ | S3 | `s3://bucket/path/file.tar.gz` | AWS S3 bucket |
135
+ | GCS | `gs://bucket/path/file.tar.gz` | Google Cloud Storage |
136
+ | HTTPS | `https://example.com/file.tar.gz` | Public HTTPS URL |
137
+
138
+ ### Response (HTTP 201)
139
+
140
+ ```json
141
+ {
142
+ "id": "123e4567-e89b-12d3-a456-426614174000",
143
+ "name": "Weekly Report Generator",
144
+ "trigger": {
145
+ "type": "cron",
146
+ "schedule": "0 9 * * 1",
147
+ "timezone": "UTC"
148
+ },
149
+ "tarball_path": "oh-internal://uploads/550e8400-e29b-41d4-a716-446655440000",
150
+ "entrypoint": "python main.py",
151
+ "enabled": true,
152
+ "created_at": "2025-03-25T10:00:00Z"
153
+ }
154
+ ```
155
+
156
+ ---
157
+
158
+ ## Managing Automations
159
+
160
+ ### List Automations
161
+
162
+ ```bash
163
+ curl "${OPENHANDS_HOST}/api/automation/v1?limit=20" \
164
+ -H "Authorization: Bearer ${OPENHANDS_API_KEY}"
165
+ ```
166
+
167
+ ### Get Automation Details
168
+
169
+ ```bash
170
+ curl "${OPENHANDS_HOST}/api/automation/v1/{automation_id}" \
171
+ -H "Authorization: Bearer ${OPENHANDS_API_KEY}"
172
+ ```
173
+
174
+ ### Update Automation
175
+
176
+ ```bash
177
+ curl -X PATCH "${OPENHANDS_HOST}/api/automation/v1/{automation_id}" \
178
+ -H "Authorization: Bearer ${OPENHANDS_API_KEY}" \
179
+ -H "Content-Type: application/json" \
180
+ -d '{"enabled": false}'
181
+ ```
182
+
183
+ ### Delete Automation
184
+
185
+ ```bash
186
+ curl -X DELETE "${OPENHANDS_HOST}/api/automation/v1/{automation_id}" \
187
+ -H "Authorization: Bearer ${OPENHANDS_API_KEY}"
188
+ ```
189
+
190
+ ### Manually Trigger a Run
191
+
192
+ ```bash
193
+ curl -X POST "${OPENHANDS_HOST}/api/automation/v1/{automation_id}/dispatch" \
194
+ -H "Authorization: Bearer ${OPENHANDS_API_KEY}"
195
+ ```
196
+
197
+ ### List Automation Runs
198
+
199
+ ```bash
200
+ curl "${OPENHANDS_HOST}/api/automation/v1/{automation_id}/runs?limit=20" \
201
+ -H "Authorization: Bearer ${OPENHANDS_API_KEY}"
202
+ ```
203
+
204
+ **Run Status Values:**
205
+ | Status | Description |
206
+ |--------|-------------|
207
+ | `PENDING` | Run scheduled, waiting for dispatch |
208
+ | `RUNNING` | Execution in progress |
209
+ | `COMPLETED` | Run finished successfully |
210
+ | `FAILED` | Run failed, check `error_detail` |
211
+
212
+ ---
213
+
214
+ ## Writing Automation Code
215
+
216
+ ### How Execution Works
217
+
218
+ When a run is triggered, the automation service uploads your tarball to the agent server, which unpacks it, runs `setup.sh` to install dependencies, then executes your entrypoint. Your script therefore runs **inside the agent server** — not in a separate process.
219
+
220
+ The agent server exposes an HTTP API (at `AGENT_SERVER_URL`) for managing conversations. A **conversation** is an AI agent interaction that can use tools: bash commands, file editing, web browsing, and so on. Your script uses the SDK's `OpenHandsCloudWorkspace` (pointing to `AGENT_SERVER_URL`) to start, monitor, and stop conversations running in that same agent server.
221
+
222
+ Key points:
223
+ - **Your script and its conversations share the same agent server.** There is no network hop to a remote service.
224
+ - **Conversations are asynchronous.** You can fire one and continue, fire several concurrently, or start none at all (e.g. if your script fetches external data and decides no action is needed).
225
+ - **The completion callback** is sent by `OpenHandsCloudWorkspace.__exit__` when the `with` block exits, telling the automation service the run is done. For async patterns, defer exiting until the conversation is in the desired state.
226
+ - **Secrets** stored in the agent server are accessed via its REST API: `GET {AGENT_SERVER_URL}/api/settings/secrets/{name}` with `X-Session-API-Key: {SESSION_API_KEY}`. SDK scripts can also call `workspace.get_llm()` to get the configured LLM.
227
+
228
+ **SDK Documentation:** https://docs.openhands.dev/sdk
229
+
230
+ ### SDK-based Scripts
231
+
232
+ For scripts that start AI agent conversations, install the SDK packages in `setup.sh`:
233
+
234
+ ```bash
235
+ #!/bin/bash
236
+ set -e
237
+
238
+ # Install uv for fast dependency management (recommended)
239
+ curl -LsSf https://astral.sh/uv/install.sh | sh
240
+ export PATH="$HOME/.local/bin:$PATH"
241
+
242
+ # Install the OpenHands SDK packages from PyPI using uv
243
+ uv pip install -q openhands-sdk openhands-workspace openhands-tools
244
+ ```
245
+
246
+ ### Basic Automation Structure
247
+
248
+ ```python
249
+ """Example automation using the OpenHands SDK."""
250
+ import os
251
+
252
+ from openhands.sdk import Conversation
253
+ from openhands.tools.preset.default import get_default_agent
254
+ from openhands.workspace import OpenHandsCloudWorkspace
255
+
256
+ # AGENT_SERVER_URL is set by the automation service for the agent server URL.
257
+ # SESSION_API_KEY / OH_SESSION_API_KEYS_0 authenticate against that server.
258
+ api_key = os.environ.get("SESSION_API_KEY") or os.environ.get("OH_SESSION_API_KEYS_0", "")
259
+ api_url = os.environ.get("AGENT_SERVER_URL", "")
260
+
261
+ # OpenHandsCloudWorkspace connects back to the agent server to manage conversations.
262
+ # __exit__ sends the completion callback to the automation service.
263
+ with OpenHandsCloudWorkspace(
264
+ local_agent_server_mode=True,
265
+ cloud_api_url=api_url,
266
+ cloud_api_key=api_key,
267
+ ) as workspace:
268
+ llm = workspace.get_llm()
269
+ agent = get_default_agent(llm=llm, cli_mode=True)
270
+ conversation = Conversation(agent=agent, workspace=workspace)
271
+ conversation.send_message("Your automation prompt here")
272
+ conversation.run()
273
+ conversation.close()
274
+ # OpenHandsCloudWorkspace.__exit__ fires the completion callback here.
275
+ ```
276
+
277
+ ### Selecting an LLM profile
278
+
279
+ The automation service injects an `AUTOMATION_MODEL` environment variable holding the **LLM profile name** the automation should run with. This is the profile a user selects when editing the automation, or — when none is selected — a snapshot of the user's active profile taken when the automation was created. To honor it on every run, read `AUTOMATION_MODEL` and pass it to `workspace.get_llm(profile_name=...)`:
280
+
281
+ ```python
282
+ # AUTOMATION_MODEL is the selected profile name (absent when none was chosen).
283
+ model_profile = os.environ.get("AUTOMATION_MODEL") or None
284
+ try:
285
+ llm = workspace.get_llm(profile_name=model_profile)
286
+ except FileNotFoundError:
287
+ # The profile was renamed or deleted after the automation was created;
288
+ # fall back to the user's default LLM rather than failing the run.
289
+ if not model_profile:
290
+ raise
291
+ print(f"profile {model_profile!r} not found; falling back to default profile")
292
+ llm = workspace.get_llm()
293
+ ```
294
+
295
+ Calling `workspace.get_llm()` with no `profile_name` always uses the user's default LLM. The built-in prompt and plugin presets already follow the pattern above; custom scripts should too so the selected profile is honored regardless of trigger type or execution backend.
296
+
297
+ ### Conversation Persistence
298
+
299
+ Conversations started during a run remain accessible in the OpenHands UI after the run completes — users can view the history and continue interacting. By default, `Conversation` does not delete the conversation on close:
300
+
301
+ ```python
302
+ # Default: conversation persists after close (users can view/continue it)
303
+ conversation = Conversation(agent=agent, workspace=workspace)
304
+
305
+ # Explicitly persist (same as default)
306
+ conversation = Conversation(agent=agent, workspace=workspace, delete_on_close=False)
307
+
308
+ # Delete conversation resources on close
309
+ conversation = Conversation(agent=agent, workspace=workspace, delete_on_close=True)
310
+ ```
311
+
312
+ The agent server itself persists until it times out or is manually deleted; this is managed by the automation service, not by the workspace.
313
+
314
+ ### Conversation Patterns
315
+
316
+ #### Pattern 1: Synchronous (run and wait)
317
+
318
+ The simplest pattern — start a conversation, block until it finishes, then exit (firing the callback).
319
+
320
+ ```python
321
+ conversation.send_message("Analyze the latest deployment logs and summarise any errors")
322
+ conversation.run() # blocks until the agent finishes or times out
323
+ conversation.close()
324
+ ```
325
+
326
+ #### Pattern 2: Conditional (fetch data first, then decide)
327
+
328
+ A common pattern where the script queries an external source and only starts a conversation if needed.
329
+
330
+ ```python
331
+ import httpx
332
+
333
+ response = httpx.get("https://api.example.com/alerts", headers={"Authorization": f"Bearer {token}"})
334
+ alerts = response.json().get("alerts", [])
335
+
336
+ if not alerts:
337
+ print("No alerts — nothing to do.")
338
+ else:
339
+ # Only now do we spin up an agent conversation
340
+ with OpenHandsCloudWorkspace(local_agent_server_mode=True, cloud_api_url=api_url, cloud_api_key=api_key) as workspace:
341
+ llm = workspace.get_llm()
342
+ agent = get_default_agent(llm=llm, cli_mode=True)
343
+ conversation = Conversation(agent=agent, workspace=workspace)
344
+ conversation.send_message(f"Investigate these alerts and open GitHub issues: {alerts}")
345
+ conversation.run()
346
+ conversation.close()
347
+ ```
348
+
349
+ #### Pattern 3: Wait for conversation completion (polling)
350
+
351
+ Start a conversation without blocking, do other work, then poll until the conversation reaches a terminal state before exiting. The callback fires only after the conversation is done.
352
+
353
+ `ConversationExecutionStatus.is_terminal()` returns `True` for `FINISHED`, `ERROR`, and `STUCK`. Call `refresh_from_server()` before checking status — `execution_status` uses a cached value and won't update automatically.
354
+
355
+ ```python
356
+ import time
357
+ from openhands.sdk.conversation.state import ConversationExecutionStatus
358
+
359
+ with OpenHandsCloudWorkspace(local_agent_server_mode=True, cloud_api_url=api_url, cloud_api_key=api_key) as workspace:
360
+ llm = workspace.get_llm()
361
+ agent = get_default_agent(llm=llm, cli_mode=True)
362
+ conversation = Conversation(agent=agent, workspace=workspace)
363
+ conversation.send_message("Run a long analysis task")
364
+ # Conversation is now running asynchronously in the agent server.
365
+
366
+ # Do other work here while conversation runs...
367
+
368
+ # Wait until the conversation reaches a terminal state.
369
+ while True:
370
+ time.sleep(5)
371
+ conversation.refresh_from_server()
372
+ if conversation.execution_status.is_terminal():
373
+ break
374
+
375
+ conversation.close()
376
+ # Callback fires here — after the conversation has finished.
377
+ ```
378
+
379
+ #### Pattern 4: Deferred callback via stop hook
380
+
381
+ For cases where the automation script needs to exit while a conversation is still running, use a `stop` hook to fire the completion callback from within the agent server when the conversation finishes.
382
+
383
+ The `stop` hook runs a shell command when the agent stops. The agent server's environment includes `AUTOMATION_CALLBACK_URL`, `AUTOMATION_CALLBACK_API_KEY`, and `AUTOMATION_RUN_ID`, so the hook can call the automation service directly.
384
+
385
+ ```python
386
+ from openhands.sdk.hooks import HookConfig, HookDefinition, HookMatcher
387
+
388
+ # Shell command that fires the completion callback when the agent stops.
389
+ # Runs inside the agent server — env vars are available at hook execution time.
390
+ stop_hook = HookConfig(
391
+ stop=[
392
+ HookMatcher(hooks=[
393
+ HookDefinition(
394
+ command=(
395
+ 'curl -sf -X POST "$AUTOMATION_CALLBACK_URL" '
396
+ '-H "Authorization: Bearer $AUTOMATION_CALLBACK_API_KEY" '
397
+ '-H "Content-Type: application/json" '
398
+ '-d \'{"status":"COMPLETED","run_id":"$AUTOMATION_RUN_ID"}\' || true'
399
+ )
400
+ )
401
+ ])
402
+ ]
403
+ )
404
+
405
+ with OpenHandsCloudWorkspace(local_agent_server_mode=True, cloud_api_url=api_url, cloud_api_key=api_key) as workspace:
406
+ llm = workspace.get_llm()
407
+ agent = get_default_agent(llm=llm, cli_mode=True)
408
+ conversation = Conversation(agent=agent, workspace=workspace, hook_config=stop_hook)
409
+ conversation.send_message("Do some long-running work")
410
+ # Don't call run() — the conversation runs asynchronously.
411
+ # When the agent stops, the stop hook will fire the callback.
412
+ # OpenHandsCloudWorkspace.__exit__ also fires a callback here (on script exit).
413
+ # The automation service should handle receiving two callbacks for the same run.
414
+ ```
415
+
416
+ > **Note:** When using the stop hook pattern, the automation service receives two completion callbacks — one from `OpenHandsCloudWorkspace.__exit__` when the script exits, and one from the stop hook when the conversation finishes. Ensure your automation service handles duplicate callbacks gracefully.
417
+
418
+ ---
419
+
420
+ ### Deterministic Script (No LLM)
421
+
422
+ For tasks that don't need AI reasoning — sending a Slack message, calling an API, rotating from a fixed list — skip the SDK entirely. Use pure Python stdlib with `python3 main.py` as the entrypoint and no `setup.sh`.
423
+
424
+ **Accessing secrets** — custom secrets are not injected into the subprocess environment automatically. Fetch them via the agent server's REST API:
425
+
426
+ ```python
427
+ import os, urllib.request
428
+
429
+ def get_secret(name: str) -> str:
430
+ url = os.environ.get("AGENT_SERVER_URL", "").rstrip("/")
431
+ key = os.environ.get("SESSION_API_KEY") or os.environ.get("OH_SESSION_API_KEYS_0", "")
432
+ req = urllib.request.Request(
433
+ f"{url}/api/settings/secrets/{name}",
434
+ headers={"X-Session-API-Key": key},
435
+ )
436
+ with urllib.request.urlopen(req) as r:
437
+ return r.read().decode().strip()
438
+ ```
439
+
440
+ **Firing the callback** — without the SDK, POST to `AUTOMATION_CALLBACK_URL` before exiting. If you never fire the callback the run stays `RUNNING` until the watchdog marks it `FAILED`.
441
+
442
+ ```python
443
+ import json, os, urllib.request
444
+
445
+ def fire_callback(status: str = "COMPLETED", error: str | None = None) -> None:
446
+ url = os.environ.get("AUTOMATION_CALLBACK_URL", "")
447
+ if not url:
448
+ return
449
+ body = {"status": status, "run_id": os.environ.get("AUTOMATION_RUN_ID", "")}
450
+ if error:
451
+ body["error"] = error
452
+ req = urllib.request.Request(url, data=json.dumps(body).encode(), headers={
453
+ "Content-Type": "application/json",
454
+ "Authorization": f"Bearer {os.environ.get('AUTOMATION_CALLBACK_API_KEY', '')}",
455
+ })
456
+ try:
457
+ urllib.request.urlopen(req)
458
+ except Exception as e:
459
+ print(f"Callback error (non-fatal): {e}")
460
+ ```
461
+
462
+ ---
463
+
464
+ ## State Persistence (KV Store)
465
+
466
+ Polling automations that run on a schedule need to remember state between runs — for example, the timestamp of the last processed event, or which conversation IDs are currently active. Storing this in a local file does not work on cloud deployments where each run may land on a fresh pod.
467
+
468
+ The automation service provides a built-in key-value store scoped per-automation. It is available in every run when the service is configured with `AUTOMATION_KV_SECRET`. Detect availability by checking for `AUTOMATION_KV_TOKEN` in the environment.
469
+
470
+ ### KV Store API
471
+
472
+ | Endpoint | Method | Description |
473
+ |----------|--------|-------------|
474
+ | `/v1/kv/{key}` | GET | Get value (404 if not found) |
475
+ | `/v1/kv/{key}` | PUT | Set value (201 on create, 200 on update) |
476
+ | `/v1/kv/{key}` | DELETE | Delete key |
477
+ | `/v1/kv/{key}/incr` | POST | Atomic integer increment |
478
+ | `/v1/kv/{key}/decr` | POST | Atomic integer decrement |
479
+ | `/v1/kv/{key}/rpush` | POST | Append item to a list |
480
+ | `/v1/kv/{key}/lpop` | POST | Pop item from front of a list |
481
+
482
+ **Authentication:** `Authorization: Bearer $AUTOMATION_KV_TOKEN`
483
+
484
+ **Base URL:** `$AUTOMATION_API_URL` (e.g., `https://app.all-hands.dev/api/automation`)
485
+
486
+ Values are arbitrary JSON (dict, list, number, string). All keys are isolated per-automation — different automations cannot access each other's data.
487
+
488
+ ### KV Store Helpers
489
+
490
+ Copy these helpers into any deterministic script that needs state persistence:
491
+
492
+ ```python
493
+ import json, os, urllib.error, urllib.request
494
+
495
+ _KV_TOKEN = os.environ.get("AUTOMATION_KV_TOKEN", "")
496
+ _KV_BASE = os.environ.get("AUTOMATION_API_URL", "").rstrip("/")
497
+
498
+
499
+ def kv_available() -> bool:
500
+ """Return True when the KV store is reachable in this run."""
501
+ return bool(_KV_TOKEN and _KV_BASE)
502
+
503
+
504
+ def kv_get(key: str):
505
+ """Fetch a value from the KV store. Returns None if the key does not exist."""
506
+ req = urllib.request.Request(
507
+ f"{_KV_BASE}/v1/kv/{key}",
508
+ headers={"Authorization": f"Bearer {_KV_TOKEN}"},
509
+ )
510
+ try:
511
+ with urllib.request.urlopen(req) as r:
512
+ return json.loads(r.read())["value"]
513
+ except urllib.error.HTTPError as exc:
514
+ if exc.code == 404:
515
+ return None
516
+ raise
517
+
518
+
519
+ def kv_set(key: str, value) -> None:
520
+ """Write a value to the KV store."""
521
+ req = urllib.request.Request(
522
+ f"{_KV_BASE}/v1/kv/{key}",
523
+ data=json.dumps(value).encode(),
524
+ headers={
525
+ "Authorization": f"Bearer {_KV_TOKEN}",
526
+ "Content-Type": "application/json",
527
+ },
528
+ method="PUT",
529
+ )
530
+ with urllib.request.urlopen(req) as r:
531
+ r.read()
532
+ ```
533
+
534
+ ### Load / Save Pattern
535
+
536
+ For polling scripts that maintain a single state document, use a KV-first pattern with a local-file fallback so the script also works in local/dev environments where the KV store is not configured:
537
+
538
+ ```python
539
+ import json, os, urllib.error, urllib.request
540
+ from pathlib import Path
541
+
542
+ _KV_TOKEN = os.environ.get("AUTOMATION_KV_TOKEN", "")
543
+ _KV_BASE = os.environ.get("AUTOMATION_API_URL", "").rstrip("/")
544
+ _STATE_KEY = "state"
545
+
546
+
547
+ def kv_available() -> bool:
548
+ return bool(_KV_TOKEN and _KV_BASE)
549
+
550
+
551
+ def kv_get(key: str):
552
+ req = urllib.request.Request(
553
+ f"{_KV_BASE}/v1/kv/{key}",
554
+ headers={"Authorization": f"Bearer {_KV_TOKEN}"},
555
+ )
556
+ try:
557
+ with urllib.request.urlopen(req) as r:
558
+ return json.loads(r.read())["value"]
559
+ except urllib.error.HTTPError as exc:
560
+ if exc.code == 404:
561
+ return None
562
+ raise
563
+
564
+
565
+ def kv_set(key: str, value) -> None:
566
+ req = urllib.request.Request(
567
+ f"{_KV_BASE}/v1/kv/{key}",
568
+ data=json.dumps(value).encode(),
569
+ headers={
570
+ "Authorization": f"Bearer {_KV_TOKEN}",
571
+ "Content-Type": "application/json",
572
+ },
573
+ method="PUT",
574
+ )
575
+ with urllib.request.urlopen(req) as r:
576
+ r.read()
577
+
578
+
579
+ def _state_file_path() -> Path:
580
+ workspace = os.environ.get("WORKSPACE_BASE", "")
581
+ if workspace:
582
+ root = Path(workspace).resolve().parent.parent
583
+ else:
584
+ root = Path.home() / ".openhands" / "workspaces"
585
+ state_dir = root / "automation-state"
586
+ state_dir.mkdir(parents=True, exist_ok=True)
587
+ payload = json.loads(os.environ.get("AUTOMATION_EVENT_PAYLOAD", "{}"))
588
+ automation_id = payload.get("automation_id", "default")
589
+ return state_dir / f"my_poller_{automation_id}.json"
590
+
591
+
592
+ def _default_state() -> dict:
593
+ return {"version": 1, "last_poll": None}
594
+
595
+
596
+ def load_state() -> dict:
597
+ if kv_available():
598
+ data = kv_get(_STATE_KEY)
599
+ if data is not None:
600
+ print("State loaded from KV store")
601
+ return data
602
+ return _default_state()
603
+ path = _state_file_path()
604
+ if path.exists():
605
+ try:
606
+ return json.loads(path.read_text())
607
+ except Exception as exc:
608
+ print(f"Warning: state file unreadable ({exc}); starting fresh")
609
+ return _default_state()
610
+
611
+
612
+ def save_state(state: dict) -> None:
613
+ if kv_available():
614
+ kv_set(_STATE_KEY, state)
615
+ print("State saved to KV store")
616
+ return
617
+ path = _state_file_path()
618
+ tmp = path.with_suffix(".tmp")
619
+ tmp.write_text(json.dumps(state, indent=2))
620
+ tmp.replace(path)
621
+ print(f"State saved to {path}")
622
+ ```
623
+
624
+ > **Why a single document?** The KV store uses a single-document model under the hood (all keys for an automation share one encrypted row). Storing your entire state under a single key like `"state"` is the most efficient pattern — it avoids multiple round-trips and ensures atomic reads and writes.
625
+
626
+ ---
627
+
628
+ ## Environment Variables
629
+
630
+ The automation service injects these environment variables into every run:
631
+
632
+ | Variable | Alt name | Description |
633
+ |----------|----------|-------------|
634
+ | `AGENT_SERVER_URL` | — | Agent server URL. Used as `cloud_api_url` for `OpenHandsCloudWorkspace`, and as the base URL for secret lookups |
635
+ | `OH_SESSION_API_KEYS_0` | `SESSION_API_KEY` | Session API key. Used as `cloud_api_key` for `OpenHandsCloudWorkspace`, and as `X-Session-API-Key` for REST API calls |
636
+ | `AUTOMATION_CALLBACK_URL` | — | POST here to mark the run complete (done automatically by `OpenHandsCloudWorkspace.__exit__`, or manually in no-LLM scripts) |
637
+ | `AUTOMATION_CALLBACK_API_KEY` | — | Bearer token for the completion callback POST |
638
+ | `AUTOMATION_RUN_ID` | — | Run ID to include in the completion callback payload |
639
+ | `AUTOMATION_EVENT_PAYLOAD` | — | JSON with trigger context: `automation_id`, `automation_name`, `trigger` type, and (for webhook runs) the raw event payload |
640
+ | `AUTOMATION_API_URL` | — | Base URL of the automation service (e.g., `https://app.all-hands.dev/api/automation`). Used to reach the KV store API |
641
+ | `AUTOMATION_KV_TOKEN` | — | Bearer token for the KV store API. Present whenever the service has `AUTOMATION_KV_SECRET` configured. Check for this variable to detect KV availability |
642
+
643
+ > **Note:** The session API key has two names: `SESSION_API_KEY` (cloud) and `OH_SESSION_API_KEYS_0` (local/dev). Always read both — see the code examples above.
644
+
645
+ ---
646
+
647
+ ## Validation Rules
648
+
649
+ - **Name**: 1-500 characters
650
+ - **Cron schedule**: Valid 5-field cron expression
651
+ - **Entrypoint**: Relative path, no shell metacharacters (`;`, `&`, `|`, etc.)
652
+ - **Setup script path**: Relative path, no path traversal (`..`)
653
+ - **Timeout**: 1-600 seconds (10 minutes max)
654
+ - **Tarball size**: 1MB max for uploads
655
+
656
+ ---
657
+
658
+ ## Complete Examples
659
+
660
+ ### No LLM (deterministic)
661
+
662
+ ```bash
663
+ OPENHANDS_HOST="https://app.all-hands.dev"
664
+ mkdir my-automation && cd my-automation
665
+
666
+ cat > main.py << 'PYEOF'
667
+ """Post a random quote to Slack — no LLM, no SDK."""
668
+ import json, os, random, sys, urllib.request
669
+
670
+ QUOTES = ["Stay hungry, stay foolish.", "Done is better than perfect."]
671
+ CHANNEL = "C12345678"
672
+
673
+ def get_secret(name):
674
+ url = os.environ.get("AGENT_SERVER_URL", "").rstrip("/")
675
+ key = os.environ.get("SESSION_API_KEY") or os.environ.get("OH_SESSION_API_KEYS_0", "")
676
+ req = urllib.request.Request(f"{url}/api/settings/secrets/{name}",
677
+ headers={"X-Session-API-Key": key})
678
+ with urllib.request.urlopen(req) as r:
679
+ return r.read().decode().strip()
680
+
681
+ def fire_callback(status="COMPLETED", error=None):
682
+ url = os.environ.get("AUTOMATION_CALLBACK_URL", "")
683
+ if not url: return
684
+ body = {"status": status, "run_id": os.environ.get("AUTOMATION_RUN_ID", "")}
685
+ if error: body["error"] = error
686
+ req = urllib.request.Request(url, data=json.dumps(body).encode(), headers={
687
+ "Content-Type": "application/json",
688
+ "Authorization": f"Bearer {os.environ.get('AUTOMATION_CALLBACK_API_KEY', '')}",
689
+ })
690
+ try: urllib.request.urlopen(req)
691
+ except Exception as e: print(f"Callback error: {e}")
692
+
693
+ try:
694
+ token = get_secret("SLACK_BOT_TOKEN")
695
+ msg = random.choice(QUOTES)
696
+ req = urllib.request.Request("https://slack.com/api/chat.postMessage",
697
+ data=json.dumps({"channel": CHANNEL, "text": msg}).encode(),
698
+ headers={"Content-Type": "application/json", "Authorization": f"Bearer {token}"})
699
+ result = json.loads(urllib.request.urlopen(req).read())
700
+ if not result.get("ok"): raise RuntimeError(result.get("error"))
701
+ print(f"Posted: {msg}")
702
+ fire_callback("COMPLETED")
703
+ except Exception as e:
704
+ print(f"ERROR: {e}", file=sys.stderr)
705
+ fire_callback("FAILED", str(e))
706
+ sys.exit(1)
707
+ PYEOF
708
+
709
+ tar -czf ../my-automation.tar.gz .
710
+
711
+ TARBALL_PATH=$(curl -s -X POST "${OPENHANDS_HOST}/api/automation/v1/uploads?name=my-automation" \
712
+ -H "Authorization: Bearer ${OPENHANDS_API_KEY}" \
713
+ -H "Content-Type: application/gzip" \
714
+ --data-binary @../my-automation.tar.gz | jq -r '.tarball_path')
715
+
716
+ curl -X POST "${OPENHANDS_HOST}/api/automation/v1" \
717
+ -H "Authorization: Bearer ${OPENHANDS_API_KEY}" \
718
+ -H "Content-Type: application/json" \
719
+ -d "{
720
+ \"name\": \"Daily Quote\",
721
+ \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 9 * * *\", \"timezone\": \"UTC\"},
722
+ \"tarball_path\": \"$TARBALL_PATH\",
723
+ \"entrypoint\": \"python3 main.py\"
724
+ }"
725
+ ```
726
+
727
+ ### SDK Script (AI conversations)
728
+
729
+ ```bash
730
+ # 0. Set the API host (use value from <HOST> in system prompt, or default)
731
+ OPENHANDS_HOST="https://app.all-hands.dev"
732
+
733
+ # 1. Create your automation code
734
+ mkdir my-automation && cd my-automation
735
+
736
+ # Create setup.sh
737
+ cat > setup.sh << 'EOF'
738
+ #!/bin/bash
739
+ set -e
740
+ uv venv .venv --quiet
741
+ uv pip install --quiet \
742
+ openhands-sdk \
743
+ openhands-workspace \
744
+ openhands-tools
745
+ EOF
746
+ chmod +x setup.sh
747
+
748
+ # Create main.py using the SDK
749
+ cat > main.py << 'EOF'
750
+ """Weekly report automation using OpenHands SDK."""
751
+ import os
752
+ import json
753
+
754
+ from openhands.sdk import Conversation
755
+ from openhands.tools.preset.default import get_default_agent
756
+ from openhands.workspace import OpenHandsCloudWorkspace
757
+
758
+ payload = json.loads(os.environ.get('AUTOMATION_EVENT_PAYLOAD', '{}'))
759
+ print(f"Running: {payload.get('automation_name')}")
760
+
761
+ api_key = os.environ.get("SESSION_API_KEY") or os.environ.get("OH_SESSION_API_KEYS_0", "")
762
+ api_url = os.environ.get("AGENT_SERVER_URL", "")
763
+
764
+ with OpenHandsCloudWorkspace(
765
+ local_agent_server_mode=True,
766
+ cloud_api_url=api_url,
767
+ cloud_api_key=api_key,
768
+ ) as workspace:
769
+ llm = workspace.get_llm()
770
+ agent = get_default_agent(llm=llm, cli_mode=True)
771
+ conversation = Conversation(agent=agent, workspace=workspace)
772
+ conversation.send_message("Generate a weekly status report")
773
+ conversation.run()
774
+ conversation.close()
775
+
776
+ print("Automation completed!")
777
+ EOF
778
+
779
+ # 2. Validate syntax before packaging
780
+ python3 -m py_compile main.py
781
+ bash -n setup.sh
782
+
783
+ # 3. Create the tarball
784
+ tar -czf ../my-automation.tar.gz .
785
+
786
+ # 4. Upload the tarball
787
+ UPLOAD_RESPONSE=$(curl -s -X POST \
788
+ "${OPENHANDS_HOST}/api/automation/v1/uploads?name=my-automation" \
789
+ -H "Authorization: Bearer ${OPENHANDS_API_KEY}" \
790
+ -H "Content-Type: application/gzip" \
791
+ --data-binary @my-automation.tar.gz)
792
+
793
+ TARBALL_PATH=$(echo "$UPLOAD_RESPONSE" | jq -r '.tarball_path')
794
+
795
+ # 5. Create the automation
796
+ curl -X POST "${OPENHANDS_HOST}/api/automation/v1" \
797
+ -H "Authorization: Bearer ${OPENHANDS_API_KEY}" \
798
+ -H "Content-Type: application/json" \
799
+ -d "{
800
+ \"name\": \"Weekly Report Generator\",
801
+ \"trigger\": {\"type\": \"cron\", \"schedule\": \"0 9 * * 1\", \"timezone\": \"UTC\"},
802
+ \"tarball_path\": \"$TARBALL_PATH\",
803
+ \"entrypoint\": \".venv/bin/python main.py\",
804
+ \"setup_script_path\": \"setup.sh\",
805
+ \"timeout\": 300
806
+ }"
807
+ ```
808
+
809
+ ---
810
+
811
+ ## Troubleshooting
812
+
813
+ ### Upload Failed: File too large
814
+ The upload limit is 1MB. Reduce your tarball size by:
815
+ - Excluding unnecessary files
816
+ - Not including `node_modules`, `.venv`, or other dependency directories
817
+
818
+ ### Automation Not Running
819
+ 1. Check if the automation is enabled (`enabled: true`)
820
+ 2. Verify the cron schedule is correct
821
+ 3. Check for validation errors in the response
822
+
823
+ ### Run fails instantly with `error_detail: null`
824
+ The script sent `fire_callback("FAILED")` immediately — before doing meaningful work. Common causes:
825
+ - A required secret was empty: the `get_secret()` call failed or returned nothing
826
+ - A missing/wrong `AGENT_SERVER_URL` or `SESSION_API_KEY`
827
+ - An import error in the entrypoint
828
+
829
+ Add `"error": str(exc)` to your `fire_callback("FAILED", ...)` call so `error_detail` is populated.
830
+
831
+ ### Run stays `RUNNING` indefinitely, then fails
832
+ The completion callback was never fired. Every code path in your script must call `fire_callback()` — including exception handlers.