| name | openclaw-migrate |
| description | Migrate OpenClaw cron jobs into Claude Code routines (claude.ai Code → Routines). Use when the user wants to convert OpenClaw cron configurations into persistent server-side routines. |
| allowed-tools | Read Glob Grep Bash(ls *) Bash(cat *) RemoteTrigger |
OpenClaw → Routines Migration
Migrate OpenClaw cron jobs into Claude Code routines on claude.ai (managed under Code → Routines, created via the RemoteTrigger tool).
Scope: This skill only handles cron-job → routine migration. Jobs that can't be expressed as routines are flagged and left for the user to keep on OpenClaw (see Step 2, Category B).
Usage
The user invokes this skill with one of:
/openclaw-migrate — auto-detect and migrate cron jobs found at ~/.openclaw/cron/jobs.json
/openclaw-migrate <path> — migrate a specific jobs file
Arguments are available via $ARGUMENTS.
Step 1: Locate the jobs file
Look for:
~/.openclaw/cron/jobs.json
- Alternative:
~/.openclaw/config.json may contain a jobs array
If the user provided a specific path in $ARGUMENTS, use that.
If no jobs file is found, tell the user and ask them to provide the path.
Step 2: Detect secrets and environment variables
Before migrating anything, inventory the environment variables that the cron jobs reference, then classify them by whether a routine can actually consume them.
Critical context
Routines run server-side on claude.ai. They cannot read the user's local shell environment (~/.zshrc, ~/.bashrc), ~/.claude/settings.local.json, or locally-configured MCP server env fields. The RemoteTrigger body has no env-var field — there is no way to inject arbitrary secrets into a routine at creation time. If a job needs a credentialed service, there are only three paths:
- Wrap the service as an OAuth 2.0 MCP connector on claude.ai (the standard path — Slack, Gmail, Linear, Discord, and any custom API that supports OAuth).
- Inline a low-sensitivity value directly into the prompt (the value will be stored in plaintext on claude.ai — only acceptable for non-secret config like a public base URL).
- Don't migrate this job as a routine. Leave the job running on OpenClaw.
Where to look
Check these locations for .env files:
~/.openclaw/.env
~/.openclaw/cron/.env
~/.openclaw/env
Also scan the job prompts for references to environment variables ($VAR, ${VAR}, process.env.VAR).
What to report
List every environment variable key you find. NEVER print, echo, or display the values. Only show the key names.
Group them into the three categories below and give the user the matching instruction:
Category A — MCP connector credentials (tokens for Slack, Discord, Gmail, Linear, or any service you plan to wrap as an OAuth MCP connector):
These are configured on claude.ai, not in a local env var. Go to claude.ai → Settings → Connectors, and either:
- Use the existing connector for a well-known service (Slack, Gmail, Linear, GitHub, etc.) — sign in via OAuth.
- Add a custom connector for your own service — this requires your service to speak MCP and support OAuth 2.0 with refresh tokens.
Once the connector exists, routines reference it via mcp_connections.connector_uuid in the routine body. The routine never sees the raw token.
Category B — Service credentials that aren't OAuth MCP-wrappable (e.g. DATABASE_URL, bare-bearer-token API keys, AWS access keys):
There is no routine-compatible path for these secrets. Options:
- Wrap the service as an OAuth MCP connector (requires OAuth 2.0 + refresh tokens on the server side; bearer-only won't work).
- Keep this job on OpenClaw — the migration skips it.
The skill flags these jobs during the preview and asks you to decide per-job.
Category C — Low-sensitivity config (e.g. API_BASE_URL, NODE_ENV, LOG_LEVEL):
Inline these values directly into the rewritten prompt. Example: instead of curl $API_BASE_URL/health, write curl https://api.example.com/health. The value is stored in plaintext on claude.ai as part of the routine prompt — only do this for values you're comfortable exposing there.
Unclear keys — if you can't classify an env var from the context:
The following env vars were found but I can't tell how they're used. Review each and tell me which category it falls into (A/B/C) before I build the routine prompts:
(list the keys)
Blocking check
After presenting the report, ask the user:
"Before I create routines:
- For Category A keys, have you configured the corresponding MCP connector(s) on claude.ai?
- For Category B keys, which handling do you want per job — OAuth-wrap the service, or keep on OpenClaw?
- For Category C keys, confirm it's OK to inline the values in the routine prompt.
Jobs depending on unresolved Category B secrets will be skipped or flagged, not silently created."
Wait for explicit answers before continuing to Step 3.
Step 3: Migrate cron jobs into routines
Important platform context
-
Terminology. The server-side feature is called "Routines" in the claude.ai UI (Code → Routines). The Claude Code tool is still named RemoteTrigger. This doc uses "routine" for the feature and RemoteTrigger for the tool.
-
Routines run behind an egress proxy. Only allowlisted domains (package registries, GitHub, Google, Anthropic) can be reached via curl/fetch. To call external APIs from a routine, the API must be registered as an MCP connector with OAuth 2.0 on claude.ai.
-
MCP connectors with OAuth work. If the user has an external API that supports OAuth 2.0 and is registered as a custom connector on claude.ai, routines can call it natively via MCP tools — bypasses the egress proxy entirely.
-
MCP tools require subagent delegation. Due to a known bug (GitHub #43397), MCP tools don't load in the main routine session. All routine prompts that use MCP tools MUST be prefixed with: DO NOT DO ANY WORK YOURSELF. ASSIGN THIS ENTIRE TASK TO AN AGENT.
-
Billing is on Max subscription. Routines bill to the user's Max subscription, not API credits. Verified via API billing CSV exports.
-
Refresh tokens are required for OAuth MCP. Access tokens expire. Without refresh tokens, routines fail after the first token expiry with no human present to re-authorize.
-
Gmail MCP connector is available. claude.ai has a native Gmail connector that can read and send email. This replaces legacy email CLIs for email operations.
Reading the jobs file
The jobs file can be:
- A JSON array of job objects
- An object with a
jobs, cron, tasks, or schedules key containing an array
- A single job object
Job field aliases
OpenClaw jobs use inconsistent field names. Normalize by checking these aliases in order:
- Name:
name, id, jobId
- Cron expression:
cron, cronExpression, schedule_cron
- One-shot time:
at, fireAt, schedule_at
- Interval (ms):
every, interval, schedule_every
- Prompt:
message, prompt, description, task
- Session:
session, sessionType
- Timezone:
tz, timezone
- Enabled:
enabled (default true)
- Delivery:
delivery, output, channel, notify
Schedule mapping rules
Cron expressions — pass through directly. 5-field cron expressions are the same format. Cron is in UTC on routines; convert from the user's timezone if the job has a tz.
One-shot timestamps (at field) — convert to a pinned cron expression:
- Parse the ISO 8601 timestamp
- Extract minute, hour, day-of-month, month
- Build cron:
{minute} {hour} {dom} {month} *
- Warn that one-shot routines are not auto-deleted after firing
Millisecond intervals (every field) — convert to cron if possible:
every is in milliseconds. Convert to minutes first.
- If minutes < 60 and 60 is evenly divisible by minutes: use
*/{minutes} * * * *
- If it's a clean number of hours and 24 is evenly divisible: use
0 */{hours} * * *
- If it's exactly 24 hours: use
0 0 * * *
- If none of these work, warn the user that this interval can't be represented as cron and skip it
Delivery channel handling
OpenClaw delivery config tells the job where to send output. Inject delivery instructions into the task prompt, and when possible, attach the corresponding MCP connection to the routine.
The delivery field can be a string (just the channel type) or an object with:
- Type:
type, channel, or platform
- Target:
target, channel_id, to, or id
Native channels (no delivery action needed — the routine runs to completion; there is no documented automatic notification, users check results via claude.ai → Code → Routines):
cowork, app, notification, desktop
Known MCP channels (append instructions to the prompt AND add mcp_connections if the user has the connector configured):
slack → prompt: "After completing the task, send the results to the Slack channel '{target}' using the Slack MCP connector."
discord → prompt: "After completing the task, send the results to the Discord channel '{target}' using the Discord MCP connector."
email → prompt: "After completing the task, email the results to '{target}'."
linear → prompt: "After completing the task, create or update a Linear issue in project '{target}' with the results."
Channels with suggested replacements (offer alternatives):
telegram → suggest replacing with Slack or Discord MCP connector (if configured). There is no documented claude.ai equivalent for a Telegram-style push channel.
push, apns, fcm → no documented Claude Code equivalent for routine completions. Suggest Slack/Discord MCP as the replacement.
Unknown channels (warn the user):
- For any other channel type (pagerduty, webhook, etc.), warn that there's no automatic mapping and include the original delivery config in the warning.
Timezone warnings
If a job has a tz or timezone field, warn that routine cron runs in UTC and the expression may need adjustment.
Disabled jobs
Create with enabled: false and note in the summary.
Preview and confirmation
Before creating anything, show the user a summary of ALL jobs:
| # | Job Name | Schedule | Cron Expression | Delivery | Enabled | Warnings |
|---|
Wait for explicit user confirmation before proceeding.
Fetching existing routines
Before creating, call RemoteTrigger with action: "list" to check for existing routines. If a routine with a matching name already exists, warn the user and ask whether to skip, update, or create a duplicate.
Creating the routine
For each job, call RemoteTrigger with action: "create" and a body like:
{
"name": "{job name}",
"cron_expression": "{converted cron}",
"enabled": true,
"job_config": {
"ccr": {
"environment_id": "{environment_id from existing triggers}",
"events": [
{
"data": {
"uuid": "{generate a fresh lowercase v4 uuid}",
"session_id": "",
"type": "user",
"parent_tool_use_id": null,
"message": {
"content": "{prompt — see below for MCP prefix requirement}",
Notes on the routine body:
name: use the original OpenClaw job name, cleaned up for readability
cron_expression: the converted 5-field cron expression. Cron is in UTC.
environment_id: required. Get this from an existing routine via RemoteTrigger action: "list". Do NOT create environments via the Anthropic API — those are a different system.
events[].data.uuid: generate a fresh lowercase v4 UUID for each routine
enabled: true unless the original job was disabled
session_context.allowed_tools: default to ["Bash", "Read", "Write", "Edit", "Glob", "Grep"].
session_context.model: prefer the model from an existing routine (check the list call response). If no existing routine exists, fall back to "claude-sonnet-4-6". Either way, ask the user if they want a different model — the hard-coded fallback may not be current as new models ship.
session_context.sources: if the job references a specific git repository, include it as [{"git_repository": {"url": "..."}}]. Otherwise omit.
persist_session: default to false unless the job explicitly needs session continuity
MCP connections
If the job needs to call external APIs, attach the corresponding MCP connector:
"mcp_connections": [
{"connector_uuid": "...", "name": "...", "url": "https://..."}
]
To find connector UUIDs: check the user's existing routines (from the list call) for mcp_connections entries. If the user has already configured a connector on claude.ai, its UUID will appear there.
Important: The external API must be registered as a custom MCP connector on claude.ai with OAuth 2.0 authentication. Bearer-token-only MCP servers cannot be registered as connectors.
Subagent delegation for MCP tools
If the routine uses any MCP connections, the prompt MUST be prefixed with:
DO NOT DO ANY WORK YOURSELF. ASSIGN THIS ENTIRE TASK TO AN AGENT.
This is required due to a known bug (GitHub #43397) where MCP tools don't load in the main routine session. The subagent receives the MCP tools correctly. Without this prefix, the agent will report that MCP tools are not available.
Rewriting prompts for remote routines
OpenClaw job prompts often contain references to OpenClaw-specific infrastructure that won't work in a routine:
- legacy email CLI commands → replace with Gmail MCP connector instructions ("use the Gmail tools to read/send email")
message action=send channel=telegram → replace with Slack or Discord MCP connector instructions (no documented Telegram equivalent in Claude Code)
- Container paths (
/home/node/.openclaw/...) → remove, the agent runs in a clean environment
cd /app && node dist/index.js nodes push → remove, use MCP connectors for delivery instead
- Local model references (
ollama/<your-local-model>) → remove, the routine uses the model specified in session_context.model
- Environment variable setup (
export PATH=... YOUR_LEGACY_CLI_PASSWORD=...) → remove, these are OpenClaw container-specific
- MCP wrapper scripts (
./scripts/mcp-call.sh) → replace with direct MCP tool calls since the connector is attached
The rewritten prompt should reference MCP tools by name (e.g., "call the Gmail send_message tool") rather than curl/script invocations.
After each successful create, note the returned trigger_id (the tool's underlying ID for the routine) for the summary.
Output
After migration is complete, print a summary:
- Number of routines created (list their
trigger_ids)
- Environment variables that still need to be configured (keys only, never values)
- Any warnings or items that need manual attention
- Remind the user that routine cron runs in UTC
- If secrets were detected but the user skipped setup, remind them which jobs may fail
Managing the routines afterwards:
- claude.ai → Code → Routines (the UI, including delete — there is no delete action on the
RemoteTrigger tool)
RemoteTrigger with action: "list" to see all routines
RemoteTrigger with action: "update" and trigger_id to modify one
RemoteTrigger with action: "run" and trigger_id to test immediately