# Installing OpenClaw Scheduler on an Additional Host

This guide is for setting up the scheduler on a **second or additional OpenClaw instance**. Each host runs its own independent SQLite database and its own service (launchd on macOS or systemd/PM2 on Linux). Windows hosts must run the Linux service inside WSL2; native Windows and WSL1 are not supported. The hosts do not share state. This is not a replication setup; each host schedules and dispatches jobs independently.

> **Starting fresh:** Unlike migrating from OC cron, on an additional host you'll typically create jobs from scratch. Use the job examples in README.md.
> **Need copy-paste examples?** See [Starter Recipes in the README](README.md#starter-recipes) and [Common Migrations](README.md#common-migrations).

---

## Prerequisites

| Requirement | Notes |
|-------------|-------|
| macOS, Linux, or Windows WSL2 | Tested on macOS arm64; Windows runs inside WSL2 only |
| Node.js 22 LTS, 24 LTS, or 26 Current | `node --version` (use full path if needed: `/opt/homebrew/bin/node --version`) |
| OpenClaw gateway running | With auth token |
| Git or SCP access | To clone/copy the repo |

> **macOS PATH note:** If installed via Homebrew, put the minimal PATH bootstrap in `~/.zshenv`, not only `~/.zprofile`, so non-interactive commands like `ssh host 'node cli.js status'` can find `node` too.

---

## Step 1: Install Scheduler Files

```bash
cd ~/.openclaw
git clone https://github.com/amittell/openclaw-scheduler.git scheduler
cd scheduler
```

Or copy from an existing host:
```bash
scp -r user@source-host:~/.openclaw/scheduler ~/.openclaw/scheduler
```

Or npm-first install (no git clone):
```bash
mkdir -p ~/.openclaw/scheduler
npm install --ignore-scripts=false --prefix ~/.openclaw/scheduler openclaw-scheduler@latest
npm exec --prefix ~/.openclaw/scheduler openclaw-scheduler -- help
```

Runtime state for npm installs defaults to `~/.openclaw/scheduler/`, not the package directory under `node_modules/`.

---

## Step 2: Install Dependencies

If you used the npm-first install path in Step 1, dependencies are already installed; skip to Step 3.

```bash
cd ~/.openclaw/scheduler
npm install
```

Installs `better-sqlite3` (native, compiles for your arch) and `croner`.

If `better-sqlite3` fails: `xcode-select --install` (macOS).

If Node changes later on this host, rebuild the native binding before restarting the scheduler:

```bash
cd ~/.openclaw/scheduler
npm rebuild better-sqlite3 --ignore-scripts=false
```

This is especially common after `brew upgrade node` on macOS or any major Node version switch.

---

## Step 2.5: Fix macOS shell PATH and completions

If this additional host uses `zsh`, configure shell startup so both interactive terminals and non-interactive remote commands can find Homebrew Node.

Recommended `~/.zshenv`:

```zsh
# ~/.zshenv — sourced by all zsh instances, including non-interactive SSH commands
if [ -x /opt/homebrew/bin/brew ]; then
  eval "$(/opt/homebrew/bin/brew shellenv)"
fi

export PATH="$HOME/.local/bin:$HOME/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
```

If you load OpenClaw completions in `~/.zshrc`, initialize completions first:

```zsh
autoload -Uz compinit
compinit

if [ -f "$HOME/.openclaw/completions/openclaw.zsh" ]; then
  source "$HOME/.openclaw/completions/openclaw.zsh"
fi
```

Avoid version-pinned Node paths like `/opt/homebrew/opt/node@22/bin`. Prefer `/opt/homebrew/bin/node`.

Quick verification:

```bash
ssh "$HOST" 'command -v node && node -v'
```

---

## Step 3: Run Tests

```bash
npm run verify:local
```

**All tests must pass before proceeding.**

---

## Step 4: Enable Chat Completions on Gateway

```bash
openclaw config set gateway.http.endpoints.chatCompletions.enabled true
openclaw gateway restart
```

Verify:
```bash
curl -s -o /dev/null -w "%{http_code}" \
  -X POST \
  -H "Authorization: Bearer YOUR_GATEWAY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"model":"openclaw:main","messages":[{"role":"user","content":"reply OK"}]}' \
  http://127.0.0.1:18789/v1/chat/completions
```

Expected: `200`

---

## Step 5: Migrate and Disable Only Duplicate Native Jobs

If this host has native jobs that need sidecar semantics, inspect a migration
before changing either runtime:

```bash
openclaw-scheduler migrate --dry-run --json > migration-report.json
openclaw-scheduler migrate --json
openclaw-scheduler doctor --json
openclaw cron edit <job-id> --disable
```

The importer uses `openclaw cron list/get --json`. Use `--legacy-json` only for
an old export. Disable a native job only after its imported copy passes a test
run. Keep unrelated native jobs enabled.

---

## Step 6: Optionally Move a Heartbeat

Leave native heartbeat enabled unless a verified scheduler job replaces it.
To disable it after that verification:

```bash
openclaw config set agents.defaults.heartbeat.every "0m"
# If you have per-agent heartbeat overrides, set/remove those too:
# agents.list[].heartbeat.every = "0m"
openclaw gateway restart
```

---

## Step 7: Choose a macOS launchd mode

> **Linux hosts:** For Linux additional hosts, follow the systemd setup in [INSTALL-LINUX.md](INSTALL-LINUX.md) instead of this step.

On an additional host, the same choice applies:

- **LaunchAgent**: best for a personal Mac with auto-login
- **LaunchDaemon**: best for a headless host or startup before login

Use the setup wizard to install the mode you want:

```bash
cd ~/.openclaw/scheduler
node setup.mjs --service-mode agent
# or:
node setup.mjs --service-mode daemon
```

If you installed from npm:

```bash
npm exec --prefix ~/.openclaw/scheduler openclaw-scheduler -- setup --service-mode agent
# or:
npm exec --prefix ~/.openclaw/scheduler openclaw-scheduler -- setup --service-mode daemon
```

Verify the mode you chose:

```bash
# LaunchAgent
launchctl print gui/$UID/ai.openclaw.scheduler

# LaunchDaemon
sudo launchctl print system/ai.openclaw.scheduler

# Either mode
sleep 5 && tail -5 /tmp/openclaw-scheduler.log
```

---

## Step 8: Smoke Tests

> **Note:** These smoke test commands use direct file imports and are for the git-clone install path. For npm installs, use `openclaw-scheduler` CLI commands instead.

### Isolated dispatch
```bash
cd ~/.openclaw/scheduler
node --input-type=module -e "
import { initDb, getDb } from './db.js';
import { createJob } from './jobs.js';
initDb();
const job = createJob({
  name: 'Smoke Test',
  schedule_cron: '0 0 31 2 *',
  payload_message: 'Reply with exactly: SCHEDULER_OK',
  delivery_mode: 'none',
  delete_after_run: true,
  origin: 'system',
  run_timeout_ms: 300000,
});
getDb().prepare(\"UPDATE jobs SET next_run_at = datetime('now', '-1 second') WHERE id = ?\").run(job.id);
console.log('Created smoke test:', job.id);
"
sleep 20 && tail -10 /tmp/openclaw-scheduler.log
```

Look for: `Dispatching: Smoke Test` → `Completed: Smoke Test`

### Telegram delivery
```bash
node --input-type=module -e "
import { initDb, getDb } from './db.js';
import { createJob } from './jobs.js';
initDb();
const job = createJob({
  name: 'Telegram Test',
  schedule_cron: '0 0 31 2 *',
  payload_message: 'Confirm scheduler is working. Send a brief greeting.',
  delivery_mode: 'announce',
  delivery_channel: 'telegram',
  delivery_to: 'YOUR_CHAT_ID',
  delete_after_run: true,
  origin: 'system',
  run_timeout_ms: 300000,
});
getDb().prepare(\"UPDATE jobs SET next_run_at = datetime('now', '-1 second') WHERE id = ?\").run(job.id);
console.log('Created Telegram test:', job.id);
"
```

You should receive a Telegram message within 30 seconds.

---

## Step 9: Create Your Jobs

Since this is a fresh host, create jobs from scratch using the CLI:

```bash
node cli.js jobs add '{
  "name": "My First Job",
  "schedule_cron": "0 * * * *",
  "payload_message": "Run your task here",
  "delivery_mode": "announce",
  "delivery_channel": "telegram",
  "delivery_to": "YOUR_CHAT_ID"
}'
node cli.js jobs list
node cli.js status
```

See README.md for full job examples including shell jobs, workflow chains, approval gates, and more.

---

## Step 10: Verify First Real Job

Wait for the next scheduled job and confirm:
```bash
tail -f /tmp/openclaw-scheduler.log
# Or after it fires:
node cli.js runs list <job-id>
```

---

## Rollback

If anything goes wrong:

```bash
# 1. Stop scheduler
launchctl bootout gui/$UID/ai.openclaw.scheduler     # if using LaunchAgent
sudo launchctl bootout system/ai.openclaw.scheduler  # if using LaunchDaemon

# 2. Re-enable OC cron (if you disabled it)
openclaw cron edit <job-id> --enable  # for each job
openclaw config set cron.enabled true
# remove OPENCLAW_SKIP_CRON=1 from gateway service env

# 3. Re-enable heartbeat (if you disabled it)
openclaw config set agents.defaults.heartbeat.every "5m"
openclaw gateway restart
```

---

## Validation Checklist

- [ ] `npm run verify:local` -- all checks passing
- [ ] `node cli.js status` → shows jobs, 0 stale
- [ ] `launchctl print gui/$UID/ai.openclaw.scheduler` or `sudo launchctl print system/ai.openclaw.scheduler` → running
- [ ] Log file has startup lines, no errors
- [ ] OC cron → all disabled (if applicable)
- [ ] OC heartbeat → `0m` (if applicable)
- [ ] Chat completions → 200
- [ ] Smoke test → dispatched + completed in log
- [ ] Telegram test → message received
- [ ] First real job → fires on schedule

---

## Upgrading

Already have the scheduler installed and need to update to a newer version? See [UPGRADING.md](UPGRADING.md).
