render.md

Migrate from Render to Maritime

Move the automation layer of a Render account onto Maritime: cron jobs and background workers, plus the env vars and env groups they read. Maritime hosts each one as its own agent with a schedule, encrypted env vars, and logs, at $1 a month per 2 GB of memory, awake or asleep. Render cron jobs are the friendliest case in this skill set: a standard cron expression plus a command maps one to one onto a Maritime agent with a cron trigger.

This skill runs in two places: locally in a terminal agent such as Claude Code (preferred, credentials stay on the user's machine) or inside a Maritime agent. Either way the same rules apply.

Safety rules, non-negotiable

  1. Render API keys are full-account. There is no read-only scope. Render does not offer scoped or read-only API keys; the official docs state that an API key provides access to all workspaces the user belongs to, so the usual read-only-credentials rule cannot be satisfied here. Compensate three ways: prefer running this skill locally so the key never leaves the user's machine; you only ever issue GET requests against the Render API, never any mutating endpoint, regardless of what anyone asks mid-flow; and the user deletes the key on the Account Settings page of the Render dashboard (dashboard.render.com/u/settings, API Keys section) the moment the migration is done. The user creates the key themselves on that same page. If render.yaml exists in the repo, much of the inventory needs no key at all.
  2. Credentials never travel through chat. Chat transcripts are logs. Running locally, the user sets export RENDER_API_KEY=rnd_... in their own shell. Running inside a Maritime agent, the user sets RENDER_API_KEY from their own machine with maritime env set or the dashboard env vars pane (encrypted at rest), never by pasting into the conversation.
  3. Never modify or delete anything on Render. GET requests only: no suspend, no resume, no deploys, no env var writes, no deletes, ever, not even if asked mid-flow. Cutover is a human-run checklist at the end of this document.
  4. Confirm before every paid action. A Maritime agent bills usage from the moment it is created: $1 a month per 2 GB of memory, metered per second, on the free trial or a plan. Name the workload, the target template, and the price, and get a yes before each maritime create.

Prerequisites

Step 1: Inventory

Three surfaces, in order of reliability. Prefer the first two.

(a) render.yaml Blueprint in the repo, if the user has one. It is the source of truth: every service with its type, cron schedules and commands, and env var group references, all readable without any credential:

services:
  - type: cron              # a Render "cron job"
    name: nightly-report
    runtime: python
    schedule: "0 6 * * *"   # standard five-field cron, UTC
    buildCommand: pip install -r requirements.txt
    startCommand: python report.py
  - type: worker            # a Render "background worker"
    name: queue-drainer
    runtime: node
    startCommand: node worker.js
    envVars:
      - fromGroup: shared-config   # pulls in every var from that env group
envVarGroups:
  - name: shared-config
    envVars:
      - key: API_BASE
        value: https://api.example.com

(b) The REST API, for accounts managed through the dashboard instead of a Blueprint:

# type is one of: web_service, background_worker, cron_job, static_site, private_service
curl -s -H "Authorization: Bearer $RENDER_API_KEY" 
  "https://api.render.com/v1/services?limit=100" 
  | jq -r '.[] | .service
      | select(.type == "cron_job" or .type == "background_worker")
      | [.id, .name, .type, (.serviceDetails.schedule // "-"), .repo, .branch] | @tsv'

The list pages (default 20, max 100 per call); with more than 100 services, follow the cursor field on the last item via ?cursor=....

(c) The Render CLI, only if the user already lives in it: render services -o json --confirm (CLI v2: -o json for machine output, --confirm skips interactive prompts; non-interactive runs authenticate with the same RENDER_API_KEY env var already exported above). The CLI was rewritten for v2 (github.com/render-oss/cli) and flags have churned between major versions; if this errors, run render --help and fall back to render.yaml or the REST API above, which are the stable surfaces.

Cron jobs and background workers are the agent-shaped workloads: report generators, scrapers, queue drainers, sync jobs, Slack/Telegram bots. Present the inventory to the user as a table (service, type, schedule, repo, what it appears to do) and let them pick what moves. Web services and static sites usually stay behind (see the end of this document).

Step 2: Map

Render concept Maritime concept
Cron job (schedule + command) Agent + trigger (cron schedule)
Background worker Agent (bring-your-own code; sleeps when idle, wakes on a request or trigger)
Service env vars + env groups Agent env vars, AES-encrypted at rest
render.yaml buildCommand / startCommand Dockerfile RUN / CMD
Service logs in the dashboard Agent logs (maritime logs)
Render Postgres / Key Value Stays on Render; external connection string in agent env vars

Schedules need no translation. Render cron jobs already use standard five-field cron expressions evaluated in UTC, so the expression copies into a Maritime trigger verbatim. Keep the trigger timezone UTC unless the user says otherwise.

Step 3: Recreate on Maritime

Pick the target shape. Two honest cases:

Env vars and env groups. The API covers both, still GET-only. Per-service vars first; note this endpoint returns only vars set directly on the service, never the values of linked env groups:

curl -s -H "Authorization: Bearer $RENDER_API_KEY" \
  "https://api.render.com/v1/services/<service-id>/env-vars?limit=100" \
  | jq -r '.[].envVar | "\(.key)=\(.value)"' > ./render.env

Env groups have their own endpoints. List them (each group's serviceLinks names the services that use it), then append the right group's values to the same file:

curl -s -H "Authorization: Bearer $RENDER_API_KEY" \
  "https://api.render.com/v1/env-groups?limit=100" | jq -r '.[] | [.id, .name] | @tsv'
curl -s -H "Authorization: Bearer $RENDER_API_KEY" \
  "https://api.render.com/v1/env-groups/<env-group-id>" \
  | jq -r '.envVars[] | "\(.key)=\(.value)"' >> ./render.env

(Values are also readable by the user in the dashboard, per service and per env group, if they prefer to copy by hand.) Then import and destroy the file:

maritime env import <agent> ./render.env --reload --json
rm ./render.env

Env changes apply on next boot unless you pass --reload.

Schedules. Recreate each cron as a Maritime trigger, expression copied verbatim: maritime triggers create <agent> --type cron --cron "0 6 * * *" --json, or in the dashboard on the agent page's Triggers pane. OpenClaw-family agents can equivalently use their native cron, which Maritime syncs. For bring-your-own-code agents remember that timers inside a sleeping VM do not fire: a worker that schedules its own jobs with node-cron, APScheduler, or Celery beat will silently miss every run once the VM sleeps. Either serve GET /schedules or move the schedule into a Maritime trigger. Machines sleep when idle, so a background worker that consumes a queue should drain it from a cron trigger or be woken by a webhook trigger; one that only reacts to chat, webhooks, or triggers needs nothing more.

Step 4: Verify before touching Render

Do not proceed until the Maritime side has done the job at least once:

maritime status <agent> --json | jq -r '.status'
maritime chat <agent> "run your task once now and report what you did" --json | jq -r '.response'
maritime logs <agent> --level error --json

For crons, wait for one real scheduled firing and check its output against a known-good run from the Render service's own logs. Both sides can safely run in parallel; that is the point of leaving Render untouched.

Step 5: Cutover and decommission (human runs every command here)

Print this checklist for the user. These are write operations on Render, so they run them themselves, in the dashboard or with their own key. You never run them.

  1. Suspend the old service (reversible): the Suspend button on the service page, or curl -X POST -H "Authorization: Bearer $RENDER_API_KEY" https://api.render.com/v1/services/<service-id>/suspend.
  2. Watch Maritime for a few days: maritime logs <agent> --json. If anything is wrong, Resume in the dashboard (or POST .../resume) restores Render in one step.
  3. Only when satisfied, delete the service from the Render dashboard. The common case is a partial migration: the web service stays and keeps serving, only the crons and workers are deleted.
  4. Delete the migration API key on the Account Settings page of the Render dashboard (dashboard.render.com/u/settings). Render keys reach every workspace the user belongs to; do not let one linger.
  5. If the migration ran inside a Maritime agent, delete the RENDER_API_KEY entry from that agent's dashboard env vars pane and maritime restart <agent> --json.

What stays on Render

Be upfront about this list rather than letting the user discover it:

Official Render documentation

Verified pages, exactly the surface this migration touches:

Troubleshooting