Runpod Migrate
>-
- Skill ID
- runpod/runpod-plugins-official/runpod-migrate
- Publisher
- runpod
- Repository
- runpod-plugins-official
- Installs
- 172
- Files
- 11
- License
- Apache-2.0
- Requires
- Linux, macOS, Windows
- Synced
- Sep 16, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
runpod/runpod-plugins-official/runpod-migrateInstalls these files- SKILL.md
- evals/availability-and-fallback.eval.md
- evals/inventory-before-editing.eval.md
- evals/rest-only-scope.eval.md
- reference/breaking-changes.md
- reference/graphql-to-v2.md
- reference/rest-v1-to-v2.md
- reference/rollback-flag.md
- reference/unlocks.md
- reference/worked-example.md
- scripts/rp_api_inventory.py
What this skill tells the agent
Migrate to Runpod REST v2
Moves a codebase off the GraphQL API (api.runpod.io/graphql) and REST v1 (rest.runpod.io/v1) onto REST v2 (api.runpod.io/v2).
The payoff, in one line each — you deliver these to the user at step 6, matched to their actual code. Don't recite them now:
- See stock before you rent —
GET /v2/catalog/gpus?include=AVAILABILITY&product=POD. v1 had no catalog at all, so every capacity retry loop was blind. - Endpoints return their own job URLs —
requestUrls.run, no more string-building. - Real lifecycle states —
PROVISIONING/STARTING/ERRORand anactionslist, so wait-loops fail fast instead of timing out. - Mistakes fail loudly — unknown request fields are rejected by name, with structured errors and honest status codes.
The full set, organized as if their code does X → v2 offers Y: [reference/unlocks.md](reference/unlocks.md) — open it at step 6.
Before you touch any code
Infer the scope, state it, and move — do not open with a questionnaire:
| The user says | Scope |
|---|---|
| "migrate to v2" / nothing specific | all — REST v1 and GraphQL |
| "just the REST stuff", "leave GraphQL alone" | rest — REST v1 only |
| "get us off GraphQL" | graphql — GraphQL only |
The table resolves every phrasing, so scope is not the thing to interrupt for. Say which row you matched and carry on. The question that does need asking comes later — at step 3, when the inventory shows the code depends on a capability v2 removed. That one is a real fork and you cannot answer it for them.
Some things have no v2 equivalent and must stay on GraphQL regardless of scope: account/billing identity (myself), secrets, spot/interruptible pods, cluster create/delete. A "full" migration still leaves those calls in place — say so up front rather than letting the user discover it at the end.
Never rewrite the serverless job API. https://api.runpod.ai/v2/<endpointId>/run, /runsync, /status, /stream, /cancel is a different API that happens to have v2 in its path. It is unchanged and out of scope. The inventory reports it separately so you do not touch it.
The workflow
1. Inventory — never migrate what you have not counted
The scanner ships beside this file, in the installed skill directory — not in the user's repo. Resolve its path first; your working directory is their project:
# 1. Claude Code plugin installs expose the plugin root:
SCAN="$CLAUDE_PLUGIN_ROOT/skills/runpod-migrate/scripts/rp_api_inventory.py"
# 2. Otherwise substitute the directory you loaded this SKILL.md from — you know it:
[ -f "$SCAN" ] || SCAN="<directory containing this SKILL.md>/scripts/rp_api_inventory.py"
# 3. Last resort, search the usual install roots:
[ -f "$SCAN" ] || SCAN=$(find ~/.claude ~/.agents ~/.codex ~/.config -name rp_api_inventory.py 2>/dev/null | head -1)
python3 "$SCAN" --help >/dev/null || echo "scanner not found — resolve it before continuing"Then, from the root of the user's repo:
python3 "$SCAN" . > runpod-api-inventory.md
python3 "$SCAN" . --json > runpod-api-inventory.json # if you want to drive edits from it
python3 "$SCAN" . --scope rest # REST-only migrationsrunpod-api-inventory.md lands in the user's repo — mention it, and remove it or gitignore it before you hand the migration back.
Stdlib-only Python, no install. It reports every call site bucketed by generation — GraphQL, REST v1, v1/GraphQL field names, REST v2 already, serverless job API, SDK/CLI wrappers — plus a suggested file-by-file order.
Show the user the inventory table before editing anything. Users routinely do not know what they are on: an agent picked a version for them months ago and wrote it down nowhere. "3 files on v1, 2 on GraphQL, 1 already on v2, 2 on the job API — leave those alone" is often the single most useful output of this whole skill.
What it detects, and what it cannot
It is regex line-scanning, but the classification is what makes it usable — plain grep -r runpod gets two things actively wrong:
- `api.runpod.ai/v2` vs `api.runpod.io/v2`. One letter apart.
.aiis the serverless job API and must not be touched;.iois the control plane you are migrating to. Grepping forv2tells you the codebase is "already migrated" when it is not. - Names legal in both versions.
/podsis a v1 path and a v2 path;["pods"]is v2 envelope-unwrapping;idleTimeoutis top-level in v1 and nested underworkersin v2. The scanner suppresses a hit when the same line carries v2 context, so it reports work that remains rather than every occurrence of a word.
It also looks for field names, not just URLs, which is what catches the files that never spell "runpod": a module reading p["costPerHr"] off a wrapper's return value has no URL, no import, no operation name — and is exactly what a v2 rename breaks silently.
Four things it genuinely cannot resolve. Check them by hand, every time:
| Blind spot | How to close it |
|---|---|
Base URL lives in config, not code (settings.yaml, .env, a ConfigMap, Terraform) | The scanner does read those files, so the URL surfaces — but the call sites using it are elsewhere. Grep for whoever reads that config key. |
Paths assembled by a helper — _url("pods", pod_id, "stop") | Reported under possible indirect call sites. Advisory, because resp.json()["pods"] looks identical. Open each one. |
SDK wrappers (import runpod) | The API generation is a property of the installed version, not the code. Check requirements.txt / lockfile and the SDK's own release notes. |
| Generated clients | The OpenAPI/GraphQL document is the real source. Regenerate from the v2 spec instead of editing generated files. |
Then read the code the scanner flagged. It finds call sites; it does not understand your wrappers. Trace who calls them — a renamed response field like costPerHr → cost breaks every caller, not just the request builder. This is the one step where a code-graph or LSP index earns its keep, if one is already available.
2. Brief the breaking changes — before the diff, not after
Read [reference/breaking-changes.md](reference/breaking-changes.md) and tell the user which ones actually apply to their code. Two classes, and the second is the one they are afraid of:
- Renames and moves — loud. v2 rejects unknown request fields with
422listing them by name, so a missed rename cannot slip into production silently. - Same name, different meaning — quiet, and the reason a green test suite is not proof. The reference enumerates every one of them; the two that bite hardest:
flashbootwent from boolean to a three-value enum, and v1's/billing/endpoints(serverless spend) is v2's/billing/serverless— v2's/billing/endpointsbills a different product (public endpoints) and answers200with a correct total for that product, which is not the one the caller asked for.
3. Plan, split into required vs cleanup
Write the plan down before editing, and keep these buckets separate all the way through to the final summary:
- Required — it does not work on v2 without this.
- Cleanup — it works either way, but v2 lets you delete code (hand-built job URLs, hand-rolled availability retry, polling loops that can now be SSE).
- Decisions the user must make — the code depends on something v2 removed outright: spot/interruptible pods, savings plans,
dockerEntrypoint, placement constraints (countryCodes,minRAMPerGPU, …), podreset, per-pod GPU fallback. See breaking-changes.md Class 3 — and check it rather than working from memory, because things leave this bucket as v2 grows. CUDA pinning,templateIdand CPU endpoint writes all used to be here and are not any more.
