|
| 1 | +--- |
| 2 | +title: Plugin With Script |
| 3 | +description: Bundle a script in a plugin and have its command and skill tell the agent to run it. |
| 4 | +icon: file-code |
| 5 | +--- |
| 6 | + |
| 7 | +{/* GENERATED from OpenHands/enterprise-cookbook@main (plugin-with-script/README.md). Edit the source, not this file. */} |
| 8 | + |
| 9 | +<Card title="View source on GitHub" icon="github" href="https://github.com/OpenHands/enterprise-cookbook/tree/main/plugin-with-script" horizontal /> |
| 10 | + |
| 11 | +A plugin can ship code as well as instructions. This example's |
| 12 | +[`city-weather/`](https://github.com/OpenHands/enterprise-cookbook/tree/main/plugin-with-script/city-weather) plugin bundles a small Python script, and its |
| 13 | +command and skill tell the agent to run that script and show what it prints. |
| 14 | +The agent doesn't work out the API calls itself; it runs one command. |
| 15 | + |
| 16 | +Put the work in a script when it should be the same every time: calling an API, |
| 17 | +parsing a file, or formatting a report. The result is repeatable, uses fewer |
| 18 | +tokens than having the agent improvise, and you can test the script on its own. |
| 19 | + |
| 20 | +## How It Works |
| 21 | + |
| 22 | +```mermaid |
| 23 | +sequenceDiagram |
| 24 | + participant U as User |
| 25 | + participant O as OpenHands |
| 26 | + participant A as Agent |
| 27 | + participant S as weather.py |
| 28 | + participant M as Open-Meteo |
| 29 | + U->>O: /city-weather:now Tokyo |
| 30 | + O->>O: fetch the plugin into the sandbox |
| 31 | + O->>A: message + command text + "Skill location: …/commands/now.md" |
| 32 | + A->>S: python3 …/skills/city-weather/scripts/weather.py "Tokyo" |
| 33 | + S->>M: geocode the city, fetch the forecast |
| 34 | + M-->>S: JSON |
| 35 | + S-->>A: formatted report |
| 36 | + A-->>U: the report, as printed |
| 37 | +``` |
| 38 | + |
| 39 | +1. OpenHands fetches the plugin into the sandbox, under |
| 40 | + `~/.openhands/cache/plugins/`. |
| 41 | +2. The message `/city-weather:now Tokyo` triggers the plugin's command. The |
| 42 | + agent receives the command's instructions along with the absolute path of the |
| 43 | + file they came from. |
| 44 | +3. The instructions say where the script is relative to that file, so the agent |
| 45 | + runs it by its absolute path and shows the output. |
| 46 | + |
| 47 | +## Run It |
| 48 | + |
| 49 | +<Tabs> |
| 50 | + <Tab title="Load via API"> |
| 51 | + Use the companion [`load-plugin`](/cookbook/load-plugin) example: |
| 52 | + |
| 53 | + ```bash |
| 54 | + cd ../load-plugin |
| 55 | + pip install requests |
| 56 | + export OH_API_KEY="sk-oh-..." |
| 57 | + python load_plugin.py \ |
| 58 | + --repo-path plugin-with-script/city-weather \ |
| 59 | + --message "/city-weather:now Tokyo" |
| 60 | + ``` |
| 61 | + </Tab> |
| 62 | + |
| 63 | + <Tab title="Launch via Badge"> |
| 64 | + Click to run the command on launch: |
| 65 | + |
| 66 | + [](https://app.all-hands.dev/launch?plugins=W3sic291cmNlIjogImdpdGh1YjpPcGVuSGFuZHMvZW50ZXJwcmlzZS1jb29rYm9vayIsICJyZWYiOiAibWFpbiIsICJyZXBvX3BhdGgiOiAicGx1Z2luLXdpdGgtc2NyaXB0L2NpdHktd2VhdGhlciIsICJwYXJhbWV0ZXJzIjogeyJjaXR5IjogIlRva3lvIn19XQ%3D%3D\&message=%2Fcity-weather%3Anow%20Tokyo) |
| 67 | + |
| 68 | + Or load the plugin and ask in your own words, such as "What's the weather in |
| 69 | + Paris?": |
| 70 | + |
| 71 | + [](https://app.all-hands.dev/launch?plugins=W3sic291cmNlIjogImdpdGh1YjpPcGVuSGFuZHMvZW50ZXJwcmlzZS1jb29rYm9vayIsICJyZWYiOiAibWFpbiIsICJyZXBvX3BhdGgiOiAicGx1Z2luLXdpdGgtc2NyaXB0L2NpdHktd2VhdGhlciJ9XQ%3D%3D) |
| 72 | + </Tab> |
| 73 | +</Tabs> |
| 74 | + |
| 75 | +The conversation runs the script once and finishes with its report: |
| 76 | + |
| 77 | +```text |
| 78 | +City Weather Report for Tokyo, Japan |
| 79 | +
|
| 80 | +- Current Time: 2026-10-10 01:30 GMT+9 |
| 81 | +- Temperature: 62°F / 16°C |
| 82 | +- Current Precipitation: 0.0 mm |
| 83 | +
|
| 84 | +Precipitation Forecast (Next 4 Hours): |
| 85 | +| Time | Probability | |
| 86 | +|-------|-------------| |
| 87 | +| 01:00 | 0% | |
| 88 | +| 02:00 | 0% | |
| 89 | +| 03:00 | 0% | |
| 90 | +| 04:00 | 0% | |
| 91 | +``` |
| 92 | + |
| 93 | +To use the skill instead of the command, send a message such as |
| 94 | +`--message "What's the weather in Paris?"`. The skill is triggered by "weather |
| 95 | +in", "weather for", and "forecast for", and runs the same script. |
| 96 | + |
| 97 | +<Tip> |
| 98 | + To test changes from a branch before they're merged, pass `--ref <branch>` to |
| 99 | + `load_plugin.py`. For OpenHands Enterprise, pass `--base-url` with your |
| 100 | + install's URL, or rebuild the badges with |
| 101 | + [`build_launch_url.py --base-url`](/cookbook/launch-plugin-badge). |
| 102 | +</Tip> |
| 103 | + |
| 104 | +## Run the Script Locally |
| 105 | + |
| 106 | +The script uses only the Python standard library and the free |
| 107 | +[Open-Meteo](https://open-meteo.com/) API, which needs no key. Run it directly |
| 108 | +to check it before the agent does: |
| 109 | + |
| 110 | +```bash |
| 111 | +python3 city-weather/skills/city-weather/scripts/weather.py "New York" |
| 112 | +``` |
| 113 | + |
| 114 | +```text |
| 115 | +City Weather Report for New York, United States |
| 116 | +
|
| 117 | +- Current Time: 2026-10-09 12:30 GMT-4 |
| 118 | +- Temperature: 70°F / 21°C |
| 119 | +- Current Precipitation: 0.0 mm |
| 120 | +... |
| 121 | +``` |
| 122 | + |
| 123 | +If the city isn't found or Open-Meteo can't be reached, it prints an error and |
| 124 | +exits with status 1, and the instructions tell the agent to show that error. |
| 125 | + |
| 126 | +## How the Agent Finds the Script |
| 127 | + |
| 128 | +The plugin is fetched to a path in the sandbox that the plugin author can't |
| 129 | +know in advance. When a command or skill fires, OpenHands adds the location of |
| 130 | +its file to what the agent sees: |
| 131 | + |
| 132 | +```text |
| 133 | +Skill location: /home/openhands/.openhands/cache/plugins/enterprise-cookbook-3c5c88fa6e51c50a/plugin-with-script/city-weather/commands/now.md |
| 134 | +(Use this path to resolve relative file references in the skill content below) |
| 135 | +``` |
| 136 | + |
| 137 | +So write the script's path relative to the file that refers to it: |
| 138 | + |
| 139 | +| File | Location given to the agent | Script path in the instructions | |
| 140 | +| ------------------------------ | --------------------------------------- | ------------------------------------------------------- | |
| 141 | +| `commands/now.md` | `<plugin>/commands/now.md` | `<plugin>/skills/city-weather/scripts/weather.py` | |
| 142 | +| `skills/city-weather/SKILL.md` | `<plugin>/skills/city-weather/SKILL.md` | `scripts/weather.py`, relative to the skill's directory | |
| 143 | + |
| 144 | +The command spells out the relationship so the agent doesn't have to guess: |
| 145 | + |
| 146 | +````markdown city-weather/commands/now.md expandable |
| 147 | +--- |
| 148 | +argument-hint: <city> |
| 149 | +description: Report the current weather and precipitation forecast for a city |
| 150 | +--- |
| 151 | + |
| 152 | +# City Weather Report |
| 153 | + |
| 154 | +Report the weather for the city named in **$ARGUMENTS** by running the script |
| 155 | +bundled with this plugin. Don't call the weather APIs yourself. |
| 156 | + |
| 157 | +## Instructions |
| 158 | + |
| 159 | +1. Read the city from the arguments: **$ARGUMENTS**. If it is empty, ask the |
| 160 | + user which city they want and wait for their answer. |
| 161 | +2. Find the script. The skill location given above is this file, |
| 162 | + `<plugin>/commands/now.md`. The script is in the same plugin at |
| 163 | + `<plugin>/skills/city-weather/scripts/weather.py`. |
| 164 | +3. Run it with the city as its argument, quoted: |
| 165 | + |
| 166 | + ```bash |
| 167 | + python3 <plugin>/skills/city-weather/scripts/weather.py "<city>" |
| 168 | + ``` |
| 169 | + |
| 170 | +4. Show the script's output to the user exactly as printed. If it exits with an |
| 171 | + error, show the error and stop. |
| 172 | + |
| 173 | +## Examples |
| 174 | + |
| 175 | +`/city-weather:now Tokyo` |
| 176 | +`/city-weather:now New York` |
| 177 | +```` |
| 178 | + |
| 179 | +<Info> |
| 180 | + This works for commands and skills, which the agent reads. Hooks are different: |
| 181 | + OpenHands runs them from the agent's workspace with no plugin-root path, so a |
| 182 | + hook can't call a script bundled in its plugin. That's why the hook examples, |
| 183 | + such as [`command-blacklist`](/cookbook/command-blacklist), put their script inline in |
| 184 | + `hooks.json`. |
| 185 | +</Info> |
| 186 | + |
| 187 | +## Where Scripts Go |
| 188 | + |
| 189 | +This plugin follows the |
| 190 | +[Claude Code plugin layout](https://code.claude.com/docs/en/plugins-reference), |
| 191 | +which OpenHands loads: |
| 192 | + |
| 193 | +- **A script used by one skill** goes in that skill's directory, under |
| 194 | + `skills/<skill>/scripts/`. That's where `weather.py` is. |
| 195 | +- **A script shared by several skills, or called by hooks,** goes in `scripts/` |
| 196 | + at the plugin root. |
| 197 | + |
| 198 | +The command is there so that `/city-weather:now` and the plugin's |
| 199 | +`entry_command` can start it on launch. In OpenHands, `entry_command` and |
| 200 | +`/<plugin>:<name>` refer to files in `commands/`, while a skill is triggered by |
| 201 | +keywords in the message. |
| 202 | + |
| 203 | +<Accordion title="Why not use ${CLAUDE_SKILL_DIR}?"> |
| 204 | + Claude Code replaces `${CLAUDE_SKILL_DIR}` and `${CLAUDE_PLUGIN_ROOT}` with |
| 205 | + absolute paths when it loads a skill, and its docs show bundled scripts referred |
| 206 | + to that way. OpenHands doesn't replace these variables, so the agent would see |
| 207 | + them as literal text. It gives the agent the skill's location instead, which is |
| 208 | + why this plugin's instructions use paths relative to the file. |
| 209 | + |
| 210 | + Claude Code also puts a plugin's `bin/` directory on the `PATH`. OpenHands |
| 211 | + doesn't, so call scripts by their path. |
| 212 | +</Accordion> |
| 213 | + |
| 214 | +## Plugin Structure |
| 215 | + |
| 216 | +```text |
| 217 | +city-weather/ |
| 218 | +├── .claude-plugin/ |
| 219 | +│ └── plugin.json # manifest: name, entry_command, parameters |
| 220 | +├── commands/ |
| 221 | +│ └── now.md # /city-weather:now <city> |
| 222 | +└── skills/ |
| 223 | + └── city-weather/ |
| 224 | + ├── SKILL.md # triggered by "weather in", "weather for", "forecast for" |
| 225 | + └── scripts/ |
| 226 | + └── weather.py # the script both of them run |
| 227 | +``` |
| 228 | + |
| 229 | +## Related |
| 230 | + |
| 231 | +<CardGroup cols={2}> |
| 232 | + <Card title="load-plugin" href="/cookbook/load-plugin" icon="plug"> |
| 233 | + Start a conversation with a plugin loaded through the REST API |
| 234 | + </Card> |
| 235 | + |
| 236 | + <Card title="launch-plugin-badge" href="/cookbook/launch-plugin-badge" icon="rocket"> |
| 237 | + Build launch links and badges like the ones above |
| 238 | + </Card> |
| 239 | + |
| 240 | + <Card title="command-blacklist" href="/cookbook/command-blacklist" icon="shield-halved"> |
| 241 | + A plugin whose script runs as a hook, inlined in hooks.json |
| 242 | + </Card> |
| 243 | + |
| 244 | + <Card title="Plugins overview" href="/overview/plugins" icon="book-open"> |
| 245 | + What plugins are and the format they follow |
| 246 | + </Card> |
| 247 | + |
| 248 | + <Card title="Plugin Launcher" href="/openhands/usage/cloud/plugin-launcher" icon="book-open"> |
| 249 | + The /launch route the badges use |
| 250 | + </Card> |
| 251 | + |
| 252 | + <Card title="Claude Code skills" href="https://code.claude.com/docs/en/skills" icon="arrow-up-right-from-square"> |
| 253 | + Supporting files in a skill directory, including scripts/ |
| 254 | + </Card> |
| 255 | +</CardGroup> |
0 commit comments