- name
- openhands-automation
- description
- This skill should be used when the user asks to "create an automation", "schedule a task", "set up a cron job", "webhook integration", "event-triggered automation", or mentions automations, scheduled tasks, cron scheduling, or webhook events in OpenHands Cloud.
- triggers
- ["automation","automations","scheduled task","cron job","cron schedule","webhook","webhooks","event trigger","github event","pull request automation","issue automation","/automation:create"]
# OpenHands Automations
Create and manage automations that run inside an OpenHands agent server — triggered by cron schedules or webhook events (GitHub, custom services).
Windows PowerShell equivalents for the automation API `curl` examples and shell-variable conventions are in `references/windows.md`.
## Automation Creation Process
The agent must follow these steps when creating an automation:
* Quickly check that you can access the correct automations backend using the auth mechanism below
* Quickly check that you can access any necessary integrations (e.g. GitHub, Slack); if access fails, inform the user and stop
* Ask the user for any necessary information, e.g. if you need the name of a Slack channel or GitHub repo to proceed
* Write the code or prompt that will be sent to the automations backend _inside the current workspace_
* Show the code to the user with the `canvas_ui` tool if available, otherwise present it in a fenced code block in your reply
* Message the user with a concise summary of how the automation will behave, and ask if they are ready to deploy it
## Architecture
Two components work together to run automations:
**Automation Service** (API at `OPENHANDS_HOST/api/automation/v1`)
Manages 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.
**Agent Server** (accessible as `AGENT_SERVER_URL` inside script runs)
Manages 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.
The 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.
**Key environment variables:**
| Variable | Availability | Description |
|---|---|---|
| `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). |
| `AGENT_SERVER_URL` | Injected into scripts at run time, local Agent Canvas only | Internal URL of the agent server. Available inside script execution context; **not** an ambient environment variable outside of a running script, and not set on OpenHands Cloud or Enterprise. |
| `OPENHANDS_HOST` | Shell convention only — set manually | Base URL for the automation service API. **Not a real environment variable.** Set it from an explicit host, a detected local Agent Canvas server, or the cloud default. Used in all `curl` examples throughout this skill. |
> **⚠️ CRITICAL — Agent behavior rules:**
>
> 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.
>
> **Instant-recognition patterns — these are always deterministic, never use an LLM preset:**
> - "post a quote / message / fact every N minutes" (rotating from a list)
> - "send a scheduled reminder / standup / digest"
> - "ping a health-check URL on a schedule"
> - "post to Slack / webhook every N minutes"
> - Any task where the full output could be written as a static template right now
>
> 1. **For LLM-appropriate work, default to preset endpoints.** They handle all SDK boilerplate, tarball packaging, and upload automatically:
> - **Prompt preset** (`POST /v1/preset/prompt`) — for tasks expressed as a natural language prompt that benefit from agent reasoning
> - **Plugin preset** (`POST /v1/preset/plugin`) — when plugins with skills, MCP configs, or commands are needed
> 2. **Present the options, then build the one the user picks.** Whenever rule 0 applies, or neither preset is a clean fit (custom Python dependencies, a non-Python entrypoint, a multi-file project, direct SDK lifecycle control), lay the choices out side by side with a one-line note on cost and reliability for each, and let the user choose:
> - **Prompt preset** - natural language prompt execution (LLM-driven)
> - **Plugin preset** - load plugins with extended capabilities (skills, MCP, hooks, commands)
> - **Custom script** - full control over code, with or without an LLM; see `references/custom-automation.md`
>
> Do not silently fall back to either side. The "ready to deploy?" confirmation in the creation process applies to every path equally; a custom script needs no extra permission beyond it, and a preset gets no pass on it.
> 3. **Building a custom script:** follow `references/custom-automation.md` for tarball packaging, validation, and upload. The *Custom Script Example (No LLM)* below shows the whole flow end to end.
> 4. **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:
> - **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.
> - 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."
### No-LLM Script Helpers
When building a deterministic custom script, these two stdlib-only functions are required. Copy them verbatim - they use variables injected by the automation service: `SESSION_API_KEY`, plus `AGENT_SERVER_URL` on a local Agent Canvas or `OPENHANDS_CLOUD_API_URL` and `SANDBOX_ID` on OpenHands Cloud and Enterprise.
```python
import json, os, urllib.request
def get_secret(name):
"""Fetch a named secret: from the agent server when AGENT_SERVER_URL is set, else from the OpenHands API."""
url = os.environ.get("AGENT_SERVER_URL", "").rstrip("/")
key = os.environ.get("SESSION_API_KEY") or os.environ.get("OH_SESSION_API_KEYS_0", "")
if url: # local Agent Canvas
url = f"{url}/api/settings/secrets/{name}"
else: # OpenHands Cloud / Enterprise
api = os.environ["OPENHANDS_CLOUD_API_URL"].rstrip("/")
url = f"{api}/api/v1/sandboxes/{os.environ['SANDBOX_ID']}/settings/secrets/{name}"
with urllib.request.urlopen(urllib.request.Request(
url, headers={"X-Session-API-Key": key}
)) as r:
return r.read().decode().strip()
def fire_callback(status="COMPLETED", error=None):
"""Signal run completion. MUST be called on every exit path — success AND error."""
url = os.environ.get("AUTOMATION_CALLBACK_URL", "")
if not url: return
body = {"status": status, "run_id": os.environ.get("AUTOMATION_RUN_ID", "")}
if error: body["error"] = error
try:
urllib.request.urlopen(urllib.request.Request(url, data=json.dumps(body).encode(), headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {os.environ.get('AUTOMATION_CALLBACK_API_KEY', '')}",
}))
except Exception as e: print(f"Callback error: {e}")
```
Entrypoint 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.
**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.
---
## Authentication
All requests require authentication:
- Cloud (default `https://app.all-hands.dev`): Bearer token:
`-H "Authorization: Bearer ${OPENHANDS_API_KEY}"`
- Local Agent Canvas (`http://localhost:8001`): session API key through `X-Session-API-Key`:
`-H "X-Session-API-Key: ${OPENHANDS_AUTOMATION_API_KEY:-${SESSION_API_KEY}}"`
The curl examples below show cloud Bearer authentication. For local Agent Canvas, replace that header with the `X-Session-API-Key` header above.
## API Endpoints
### Determining the API Host
**Before making API calls, determine the correct host:**
The 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:
- Look for a `<HOST>` value in the system prompt or runtime-services block. If present, use that URL.
- If running inside a local Agent Canvas stack and no explicit host is provided, use `http://localhost:8001` for the local automation/agent-server API.
- Otherwise default to `https://app.all-hands.dev`.
For a local Agent Canvas server, validate the endpoint before making a mutating request and authenticate with the session key through `X-Session-API-Key` when that API requires it. Do not use the cloud default merely because no `<HOST>` value is present.
```bash
# Choose the host that matches the detected environment:
OPENHANDS_HOST="http://localhost:8001" # local Agent Canvas
# OPENHANDS_HOST="https://app.all-hands.dev" # OpenHands Cloud
```
### Automation Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/automation/v1/preset/prompt` | POST | **Create automation from a prompt (recommended)** |
| `/api/automation/v1/preset/plugin` | POST | **Create automation with plugins** |
| `/api/automation/v1` | GET | List automations |
| `/api/automation/v1/{id}` | GET | Get automation details |
| `/api/automation/v1/{id}` | PATCH | Update automation |
| `/api/automation/v1/{id}` | DELETE | Delete automation |
| `/api/automation/v1/{id}/dispatch` | POST | Trigger a run manually |
| `/api/automation/v1/{id}/runs` | GET | List automation runs |
### Custom Webhook Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/automation/v1/webhooks` | POST | Register a custom webhook source |
| `/api/automation/v1/webhooks` | GET | List all custom webhooks |
| `/api/automation/v1/webhooks/{id}` | GET | Get webhook details |
| `/api/automation/v1/webhooks/{id}` | PATCH | Update webhook settings |
| `/api/automation/v1/webhooks/{id}` | DELETE | Delete a webhook |
| `/api/automation/v1/webhooks/{id}/rotate-secret` | POST | Rotate signing secret |
---
## Trigger Types
Automations support two trigger types:
| Trigger Type | Use Case |
|--------------|----------|
| **Cron** | Run on a schedule (daily, weekly, hourly, etc.) |
| **Event** | Run when a webhook event occurs (GitHub PR opened, issue commented, etc.) — **requires a publicly reachable deployment** |
---
## Creating Automations
Two preset endpoints simplify automation creation by handling SDK boilerplate, tarball packaging, and upload automatically:
1. **Prompt Preset** — Execute a natural language prompt (simple tasks)
2. **Plugin Preset** — Load plugins with skills, MCP configs, and commands (extended capabilities)
---
### Prompt Preset
Use the **preset/prompt endpoint** for simple automations. Provide a natural language prompt describing the task.
#### How It Works
1. Send a prompt describing the task (e.g., "Generate a weekly status report")
2. 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
3. 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
#### Request
```bash
curl -X POST "${OPENHANDS_HOST}/api/automation/v1/preset/prompt" \
-H "Authorization: Bearer ${OPENHANDS_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "My Automation Name",
"prompt": "What the automation should do",
"trigger": {
"type": "cron",
"schedule": "0 9 * * *",
"timezone": "UTC"
}
}'
```
#### Request Fields
| Field | Required | Description |
|-------|----------|-------------|
| `name` | Yes | Name of the automation (1-500 characters) |
| `prompt` | Yes | Natural language instructions (1-50,000 characters) |
| `trigger` | Yes | Trigger configuration — either `cron` or `event` (see below) |
| `timeout` | No | Max execution time in seconds (default: system maximum) |
| `repos` | No | Repositories to clone (see [Repository Cloning](#repository-cloning)) |
**Cron Trigger Fields:**
| Field | Required | Description |
|-------|----------|-------------|
| `trigger.type` | Yes | `"cron"` |
| `trigger.schedule` | Yes | Cron expression (5 fields: min hour day month weekday) |
| `trigger.timezone` | No | IANA timezone (default: `"UTC"`) |
**Event Trigger Fields:**
| Field | Required | Description |
|-------|----------|-------------|
| `trigger.type` | Yes | `"event"` |
| `trigger.source` | Yes | Event source: `"github"` or custom webhook source name |
| `trigger.on` | Yes | Event key pattern(s) to match (see Event Keys below) |
| `trigger.filter` | No | JMESPath expression for payload filtering (see Filter Expressions below) |
#### Prompt Tips
Write 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:
- `"Generate a weekly status report summarizing the team's GitHub activity and post it to Slack"`
- `"Read yesterday's new error reports in Sentry, group them by likely root cause, and open one GitHub issue per new failure pattern with a proposed fix"`
GitHubで見る