Short answer to Rajiv's question: you can carry the code, but you re-register the automation. Here's why, and the way that avoids the problem entirely.
TL;DR. An automation is two things: a definition row in the service's database (schedule, entrypoint, model, trigger) and a code tarball in a file store. The tarball is portable; the definition and its storage/secrets are per-deployment. So there is no clean "copy the tarball and it runs on OHE" — you re-create the definition on the target. The robust fix: keep the code in a git-backed plugin and there's nothing to move.
What an automation actually is
Local and cloud/OHE run the same service (OpenHands/automation), so the API surface is the same. What differs is the state behind it. One automation =
A definition row (DB): name, trigger (cron), entrypoint, setup_script_path, model, timeout, enabled, and a tarball_path pointing at the code. It also carries org_id / user_id.
A code tarball in a file store — local filesystem when you run it yourself, or gcs/s3 in cloud (chosen by the FILE_STORE env var).
The tarball travels; the row and its store/secrets don't.
Can you directly move tarballs between local and OHE?
You can move the tarball — download it (GET /automations/{id}/tarball) and upload it to the target. But that does not move the automation, because the target still has none of:
the definition row (schedule, entrypoint, model) — it lives in the target's own database;
a valid tarball_path — local points at a filesystem path; cloud points at gcs/s3, so the path is different;
the secrets / API keys / org identity — those are per-deployment.
So: it isn't an API-surface problem (the service is the same code) — it's that a definition + its storage + its secrets are deployment-scoped state, not a portable artifact. Re-registering on the target is the step you can't skip. The good news: it's mechanical — an agent or a small script can do it.
The recommended way: don't move anything
John-Mason's point on the thread is the real answer. Put the smarts outside the automation, in a plugin loaded from a git ref. Plugin-triggered automations load the plugin fresh from the given ref (branch or tag) on every run — so the code is source-controlled, and the automation config is just a schedule plus a pointer.
Then "moving to cloud" stops being a transfer:
Create the same thin automation on OHE (schedule + the same plugin ref).
It pulls the same code from git on run. Nothing to copy, nothing to keep in sync.
This also answers Rajiv's first question (source control / backup): with a git-backed plugin, the code is already in git. The only per-deployment thing left is the schedule, which a tiny API script can dump to a file and re-create anywhere.
If you must move a tarball-based (custom-script) automation
GET /automations/{id}/tarball on the source; keep the setup_script_path and entrypoint from its definition.
On the target (OHE), create the automation via the same create endpoint, uploading that tarball and re-supplying schedule / entrypoint / model.
Re-add secrets and API keys on the target — they don't travel with the tarball.
Dispatch once (POST /automations/{id}/dispatch) to confirm it runs before trusting the schedule.
Watch: don't expect the source tarball_path to be meaningful on the target — the file store is different. And confirm the target's runtime/secrets match what the script assumes, or the first real run fails quietly.
Grounded in OpenHands/automation (models.py: the definition row; storage/factory.py: FILE_STORE local/gcs/s3; automation_router.py: the tarball + dispatch endpoints). Written by smolpaws 🐾 for the #proj-automations thread.