openclaw-scheduler 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/AGENTS.md +302 -0
  2. package/BEST-PRACTICES.md +506 -0
  3. package/CHANGELOG.md +82 -0
  4. package/CODE_OF_CONDUCT.md +22 -0
  5. package/CONTEXT.md +26 -0
  6. package/CONTRIBUTING.md +73 -0
  7. package/IMPLEMENTATION_SPEC.md +170 -0
  8. package/INSTALL-ADDITIONAL-HOST.md +333 -0
  9. package/INSTALL-LINUX.md +419 -0
  10. package/INSTALL-WINDOWS.md +305 -0
  11. package/INSTALL.md +364 -0
  12. package/JOB-QUICK-REF.md +222 -0
  13. package/LICENSE +21 -0
  14. package/QUICK-START.md +256 -0
  15. package/README.md +2170 -0
  16. package/SECURITY.md +34 -0
  17. package/UNINSTALL.md +129 -0
  18. package/UPGRADING.md +436 -0
  19. package/agents.js +67 -0
  20. package/approval.js +107 -0
  21. package/backup.js +390 -0
  22. package/bin/openclaw-scheduler.js +138 -0
  23. package/cli.js +1083 -0
  24. package/db.js +122 -0
  25. package/dispatch/529-recovery.mjs +204 -0
  26. package/dispatch/README.md +372 -0
  27. package/dispatch/config.example.json +24 -0
  28. package/dispatch/deliver-watcher.sh +57 -0
  29. package/dispatch/hooks.mjs +171 -0
  30. package/dispatch/index.mjs +1836 -0
  31. package/dispatch/watcher.mjs +1396 -0
  32. package/dispatch-queue.js +112 -0
  33. package/dispatcher-approvals.js +96 -0
  34. package/dispatcher-delivery.js +43 -0
  35. package/dispatcher-maintenance.js +242 -0
  36. package/dispatcher-shell.js +29 -0
  37. package/dispatcher-strategies.js +1280 -0
  38. package/dispatcher-utils.js +81 -0
  39. package/dispatcher.js +855 -0
  40. package/docs/adr-schedule-ownership.md +73 -0
  41. package/docs/gateway-contract.md +904 -0
  42. package/docs/plans/2026-03-09-fix-typescript-types.md +91 -0
  43. package/docs/plans/2026-03-09-test-coverage-gaps.md +83 -0
  44. package/docs/plans/2026-03-10-dispatcher-refactor.md +801 -0
  45. package/docs/trust-architecture.md +266 -0
  46. package/gateway.js +473 -0
  47. package/idempotency.js +119 -0
  48. package/index.d.ts +864 -0
  49. package/index.js +17 -0
  50. package/jobs.js +1224 -0
  51. package/messages.js +357 -0
  52. package/migrate-consolidate.js +694 -0
  53. package/migrate.js +125 -0
  54. package/package.json +130 -0
  55. package/paths.js +79 -0
  56. package/prompt-context.js +94 -0
  57. package/retrieval.js +176 -0
  58. package/runs.js +270 -0
  59. package/scheduler-schema.js +101 -0
  60. package/schema.sql +480 -0
  61. package/scripts/dispatch-cli-utils.mjs +65 -0
  62. package/scripts/inbox-consumer.mjs +288 -0
  63. package/scripts/stuck-detector.sh +18 -0
  64. package/scripts/stuck-run-detector.mjs +333 -0
  65. package/scripts/telegram-webhook-check.mjs +238 -0
  66. package/setup.mjs +724 -0
  67. package/shell-result.js +214 -0
  68. package/task-tracker.js +300 -0
  69. package/team-adapter.js +335 -0
  70. package/v02-runtime.js +599 -0
@@ -0,0 +1,73 @@
1
+ # Contributing
2
+
3
+ ## Scope
4
+
5
+ Contributions should improve one of these areas:
6
+
7
+ - runtime reliability
8
+ - workflow and queue semantics
9
+ - installation and service management
10
+ - package/install ergonomics
11
+ - documentation and tests
12
+
13
+ ## Ground Rules
14
+
15
+ - preserve durable runtime behavior
16
+ - do not remove backward-compatible CLI or schema behavior casually
17
+ - update docs when installation or runtime behavior changes
18
+ - update tests when changing scheduler semantics, payload validation, or delivery behavior
19
+
20
+ ## Development
21
+
22
+ ```bash
23
+ npm install
24
+ npm test
25
+ npm run lint
26
+ ```
27
+
28
+ ### Local Verification Gate
29
+
30
+ Before pushing or opening a PR, run the full local gate:
31
+
32
+ ```bash
33
+ npm run verify:local
34
+ ```
35
+
36
+ This runs, in order:
37
+
38
+ 1. Lint (`eslint`)
39
+ 2. TypeScript declaration smoke tests
40
+ 3. Full test suite (in-memory SQLite) -- must end with **0 failed**
41
+ 4. Coverage floor checks (statement, branch, function, line)
42
+
43
+ The same gate runs automatically via `prepublishOnly` before any `npm publish`.
44
+
45
+ If you add new features or fix bugs, add tests. The test count should only go up. Coverage expectations are enforced by the verify script -- if you drop below the floor, the gate fails.
46
+
47
+ ## Branch Model
48
+
49
+ All PRs target `main`. There are no long-lived feature branches.
50
+
51
+ ## Release Process
52
+
53
+ 1. `npm run verify:local` -- must pass completely
54
+ 2. `npm version <patch|minor|major>`
55
+ 3. `npm publish` -- `prepublishOnly` re-runs the verification gate
56
+ 4. Push the version commit and tag: `git push && git push --tags`
57
+
58
+ ### Agent-Facing Documentation
59
+
60
+ The following files ship in the npm package for agent adoption:
61
+
62
+ - `AGENTS.md` -- discovery flow, working rules, CLI commands
63
+ - `CONTEXT.md` -- repo positioning, design bias
64
+ - `JOB-QUICK-REF.md` -- copy-paste job patterns, field reference
65
+ - `docs/` -- gateway contract, trust architecture, ADRs
66
+
67
+ Update these when adding new features or changing the CLI API.
68
+
69
+ ## Pull Requests
70
+
71
+ - explain whether the change affects runtime behavior, package/install behavior, or both
72
+ - call out migration or compatibility risk explicitly
73
+ - include verification steps
@@ -0,0 +1,170 @@
1
+ # Implementation Spec — Scheduler v5 Features
2
+
3
+ ## New Schema (consolidated into schema baseline)
4
+
5
+ ### Jobs table — new columns:
6
+ ```sql
7
+ ALTER TABLE jobs ADD COLUMN delivery_guarantee TEXT DEFAULT 'at-most-once'; -- 'at-most-once'|'at-least-once'
8
+ ALTER TABLE jobs ADD COLUMN job_class TEXT DEFAULT 'standard'; -- 'standard'|'pre_compaction_flush'
9
+ ALTER TABLE jobs ADD COLUMN approval_required INTEGER DEFAULT 0; -- HITL gate
10
+ ALTER TABLE jobs ADD COLUMN approval_timeout_s INTEGER DEFAULT 3600;
11
+ ALTER TABLE jobs ADD COLUMN approval_auto TEXT DEFAULT 'reject'; -- 'approve'|'reject'
12
+ ALTER TABLE jobs ADD COLUMN context_retrieval TEXT DEFAULT 'none'; -- 'none'|'recent'|'hybrid'
13
+ ALTER TABLE jobs ADD COLUMN context_retrieval_limit INTEGER DEFAULT 5;
14
+ ```
15
+
16
+ ### Runs table — new columns:
17
+ ```sql
18
+ ALTER TABLE runs ADD COLUMN context_summary TEXT; -- JSON: {messages_injected,scope,aliases_resolved,...}
19
+ ALTER TABLE runs ADD COLUMN replay_of TEXT; -- run id if this is a crash replay
20
+ ```
21
+
22
+ ### Messages table — new column:
23
+ ```sql
24
+ ALTER TABLE messages ADD COLUMN owner TEXT; -- originator of typed message
25
+ ```
26
+ (kind enum extends to include: 'decision','constraint','fact','preference' alongside existing)
27
+
28
+ ### New table: approvals
29
+ ```sql
30
+ CREATE TABLE IF NOT EXISTS approvals (
31
+ id TEXT PRIMARY KEY,
32
+ job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
33
+ run_id TEXT REFERENCES runs(id) ON DELETE SET NULL,
34
+ status TEXT NOT NULL DEFAULT 'pending', -- pending|approved|rejected|timed_out
35
+ requested_at TEXT NOT NULL DEFAULT (datetime('now')),
36
+ resolved_at TEXT,
37
+ resolved_by TEXT, -- 'operator'|'timeout'|'api'
38
+ notes TEXT
39
+ );
40
+ CREATE INDEX IF NOT EXISTS idx_approvals_status ON approvals(status) WHERE status = 'pending';
41
+ CREATE INDEX IF NOT EXISTS idx_approvals_job ON approvals(job_id);
42
+ ```
43
+
44
+ ### schema_migrations: baseline includes these fields/tables
45
+
46
+ ---
47
+
48
+ ## Function Signatures (contracts between modules)
49
+
50
+ ### approval.js (NEW FILE) exports:
51
+ ```js
52
+ export function createApproval(jobId, runId) // returns approval record
53
+ export function getApproval(id) // by approval id
54
+ export function getPendingApproval(jobId) // latest pending for a job
55
+ export function listPendingApprovals() // all pending
56
+ export function resolveApproval(id, status, resolvedBy, notes) // approve/reject/timed_out
57
+ export function getTimedOutApprovals() // pending approvals past timeout
58
+ export function pruneApprovals(retentionDays) // clean old resolved approvals
59
+ ```
60
+
61
+ ### retrieval.js (NEW FILE) exports:
62
+ ```js
63
+ export function getRecentRunSummaries(jobId, limit) // last N run summaries (non-null)
64
+ export function searchRunSummaries(jobId, query, limit) // hybrid: substring + TF-IDF scoring
65
+ export function buildRetrievalContext(job) // returns string to inject into prompt (or '')
66
+ ```
67
+
68
+ ### jobs.js — add to createJob/updateJob allowed fields:
69
+ delivery_guarantee, job_class, approval_required, approval_timeout_s, approval_auto, context_retrieval, context_retrieval_limit
70
+
71
+ ### runs.js — new export:
72
+ ```js
73
+ export function updateContextSummary(runId, summaryObj) // store JSON context_summary
74
+ ```
75
+
76
+ ### messages.js — update:
77
+ - sendMessage: accept `owner` field
78
+ - getInbox: sort by typed priority (constraint > decision > fact > task > preference > text/other)
79
+ - New kinds accepted: 'decision','constraint','fact','preference'
80
+
81
+ ### dispatcher.js — new functions:
82
+ ```js
83
+ async function replayOrphanedRuns() // called from main() after initDb, before tick loop
84
+ async function checkApprovals() // called from tick(), checks timeouts + approved gates
85
+ // Note: context metadata is built inline within buildJobPrompt() and returned as contextMeta
86
+ ```
87
+
88
+ ### cli.js — new commands:
89
+ - `jobs approve <job-id>` — resolve pending approval as approved
90
+ - `jobs reject <job-id> [reason]` — resolve as rejected
91
+ - `approvals list` — list pending
92
+ - `approvals pending` — alias
93
+
94
+ ---
95
+
96
+ ## Feature Details
97
+
98
+ ### F1: Delivery Semantics Contract
99
+ - New field `delivery_guarantee` on jobs ('at-most-once' default | 'at-least-once')
100
+ - at-most-once: current behavior. On crash, orphaned runs marked 'crashed'.
101
+ - at-least-once: on startup, orphaned runs are replayed (new run with replay_of set).
102
+ - Document in job creation. Expose in CLI `jobs list` table.
103
+
104
+ ### F2: Flush-Before-Compaction Hook
105
+ - New field `job_class` on jobs ('standard' default | 'pre_compaction_flush')
106
+ - In buildJobPrompt: if job_class === 'pre_compaction_flush', prepend:
107
+ ```
108
+ [SYSTEM: Pre-compaction flush required]
109
+ Write a structured summary of: active decisions, constraints, task owners, open questions.
110
+ Format as labeled sections. If nothing needs flushing, respond with exactly: NO_FLUSH
111
+ [END SYSTEM]
112
+ ```
113
+ - In dispatch result handling: if content.trim() === 'NO_FLUSH', skip delivery and log 'Flush: nothing to flush'
114
+
115
+ ### F3: Context Summary / Memory Observability
116
+ - New field `context_summary` on runs (TEXT, stores JSON)
117
+ - In buildJobPrompt: collect metadata into an object: { messages_injected: N, scope: 'own'|'global', aliases_resolved: [...], job_class, delivery_guarantee, context_retrieval, retrieval_results: N }
118
+ - After creating the run, store the summary via updateContextSummary()
119
+ - Expose in `runs list` and `runs get` CLI output (note: CLI exposure is deferred)
120
+
121
+ ### F4: Typed Message Contract
122
+ - New message kinds: 'decision', 'constraint', 'fact', 'preference'
123
+ - New field `owner` on messages
124
+ - In sendMessage: validate kind against full enum, accept owner
125
+ - In getInbox: sort results by typed priority order:
126
+ 1. constraint (highest)
127
+ 2. decision
128
+ 3. fact
129
+ 4. task
130
+ 5. preference
131
+ 6. text, result, status, system, spawn (lowest)
132
+ - In buildJobPrompt: display kind and owner for typed messages:
133
+ ```
134
+ [constraint] (owner: ops-agent) Never deploy during business hours
135
+ ```
136
+
137
+ ### F5: HITL Approval Gates
138
+ - New fields on jobs: approval_required (int 0/1), approval_timeout_s (int), approval_auto (text)
139
+ - New run status: 'awaiting_approval'
140
+ - New table: approvals
141
+ - Flow:
142
+ 1. In dispatchJob: if job.approval_required AND job is chain-triggered (has parent_id):
143
+ - Create run with status 'awaiting_approval'
144
+ - Create approval record
145
+ - Send notification: "⚠️ Job '{name}' requires approval. Approve: `node cli.js jobs approve {job_id}`"
146
+ - Return (don't dispatch yet)
147
+ 2. In tick: call checkApprovals():
148
+ - For each pending approval: check if resolved or timed out
149
+ - If approved: change run status to 'pending', dispatch the job
150
+ - If timed_out: apply approval_auto policy
151
+ - If rejected: mark run as 'cancelled'
152
+ - CLI: approve/reject commands resolve the approval record
153
+
154
+ ### F6: Run Replay on Startup
155
+ - New field `replay_of` on runs
156
+ - In main(), after initDb(), before starting tick loop:
157
+ - Query: SELECT r.*, j.delivery_guarantee, j.name FROM runs r JOIN jobs j ON r.job_id = j.id WHERE r.status = 'running'
158
+ - For each orphaned run:
159
+ - If delivery_guarantee = 'at-least-once': create new run with replay_of = old run id, set old run status = 'crashed', queue for dispatch
160
+ - If delivery_guarantee = 'at-most-once': set old run status = 'crashed', advance job schedule
161
+ - Log all actions
162
+
163
+ ### F7: Hybrid Retrieval for Job Context
164
+ - New fields on jobs: context_retrieval ('none'|'recent'|'hybrid'), context_retrieval_limit (int)
165
+ - In buildJobPrompt: if context_retrieval !== 'none', call buildRetrievalContext(job) and append to prompt
166
+ - retrieval.js implements:
167
+ - getRecentRunSummaries: SELECT summary FROM runs WHERE job_id=? AND summary IS NOT NULL ORDER BY started_at DESC LIMIT ?
168
+ - searchRunSummaries: combine substring matching + simple TF-IDF scoring
169
+ - TF-IDF: tokenize query and summaries, compute term frequency * inverse document frequency, rank by score
170
+ - buildRetrievalContext: format results as "--- Prior Run Context ---\n[date] summary\n..."
@@ -0,0 +1,333 @@
1
+ # Installing OpenClaw Scheduler on an Additional Host
2
+
3
+ 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, systemd on Linux, PM2 on Windows) -- they don't share state. This is not a replication setup; each host schedules and dispatches jobs independently.
4
+
5
+ > **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.
6
+ > **Need copy-paste examples?** See [Starter Recipes in the README](README.md#starter-recipes) and [Common Migrations](README.md#common-migrations).
7
+
8
+ ---
9
+
10
+ ## Prerequisites
11
+
12
+ | Requirement | Notes |
13
+ |-------------|-------|
14
+ | macOS or Linux | Tested on macOS arm64 |
15
+ | Node.js >= 20 | `node --version` (use full path if needed: `/opt/homebrew/bin/node --version`) |
16
+ | OpenClaw gateway running | With auth token |
17
+ | Git or SCP access | To clone/copy the repo |
18
+
19
+ > **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.
20
+
21
+ ---
22
+
23
+ ## Step 1: Install Scheduler Files
24
+
25
+ ```bash
26
+ cd ~/.openclaw
27
+ git clone https://github.com/amittell/openclaw-scheduler.git scheduler
28
+ cd scheduler
29
+ ```
30
+
31
+ Or copy from an existing host:
32
+ ```bash
33
+ scp -r user@source-host:~/.openclaw/scheduler ~/.openclaw/scheduler
34
+ ```
35
+
36
+ Or npm-first install (no git clone):
37
+ ```bash
38
+ mkdir -p ~/.openclaw/scheduler
39
+ npm install --prefix ~/.openclaw/scheduler openclaw-scheduler@latest
40
+ npm exec --prefix ~/.openclaw/scheduler openclaw-scheduler -- help
41
+ ```
42
+
43
+ Runtime state for npm installs defaults to `~/.openclaw/scheduler/`, not the package directory under `node_modules/`.
44
+
45
+ ---
46
+
47
+ ## Step 2: Install Dependencies
48
+
49
+ If you used the npm-first install path in Step 1, dependencies are already installed; skip to Step 3.
50
+
51
+ ```bash
52
+ cd ~/.openclaw/scheduler
53
+ npm install
54
+ ```
55
+
56
+ Installs `better-sqlite3` (native, compiles for your arch) and `croner`.
57
+
58
+ If `better-sqlite3` fails: `xcode-select --install` (macOS).
59
+
60
+ If Node changes later on this host, rebuild the native binding before restarting the scheduler:
61
+
62
+ ```bash
63
+ cd ~/.openclaw/scheduler
64
+ npm rebuild better-sqlite3
65
+ ```
66
+
67
+ This is especially common after `brew upgrade node` on macOS or any major Node version switch.
68
+
69
+ ---
70
+
71
+ ## Step 2.5: Fix macOS shell PATH and completions
72
+
73
+ If this additional host uses `zsh`, configure shell startup so both interactive terminals and non-interactive remote commands can find Homebrew Node.
74
+
75
+ Recommended `~/.zshenv`:
76
+
77
+ ```zsh
78
+ # ~/.zshenv — sourced by all zsh instances, including non-interactive SSH commands
79
+ if [ -x /opt/homebrew/bin/brew ]; then
80
+ eval "$(/opt/homebrew/bin/brew shellenv)"
81
+ fi
82
+
83
+ export PATH="$HOME/.local/bin:$HOME/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
84
+ ```
85
+
86
+ If you load OpenClaw completions in `~/.zshrc`, initialize completions first:
87
+
88
+ ```zsh
89
+ autoload -Uz compinit
90
+ compinit
91
+
92
+ if [ -f "$HOME/.openclaw/completions/openclaw.zsh" ]; then
93
+ source "$HOME/.openclaw/completions/openclaw.zsh"
94
+ fi
95
+ ```
96
+
97
+ Avoid version-pinned Node paths like `/opt/homebrew/opt/node@22/bin`. Prefer `/opt/homebrew/bin/node`.
98
+
99
+ Quick verification:
100
+
101
+ ```bash
102
+ ssh "$HOST" 'command -v node && node -v'
103
+ ```
104
+
105
+ ---
106
+
107
+ ## Step 3: Run Tests
108
+
109
+ ```bash
110
+ SCHEDULER_DB=:memory: node test.js
111
+ ```
112
+
113
+ **All tests must pass before proceeding.**
114
+
115
+ ---
116
+
117
+ ## Step 4: Enable Chat Completions on Gateway
118
+
119
+ ```bash
120
+ openclaw config set gateway.http.endpoints.chatCompletions.enabled true
121
+ openclaw gateway restart
122
+ ```
123
+
124
+ Verify:
125
+ ```bash
126
+ curl -s -o /dev/null -w "%{http_code}" \
127
+ -X POST \
128
+ -H "Authorization: Bearer YOUR_GATEWAY_TOKEN" \
129
+ -H "Content-Type: application/json" \
130
+ -d '{"model":"openclaw:main","messages":[{"role":"user","content":"reply OK"}]}' \
131
+ http://127.0.0.1:18789/v1/chat/completions
132
+ ```
133
+
134
+ Expected: `200`
135
+
136
+ ---
137
+
138
+ ## Step 5: Disable OC Built-in Cron
139
+
140
+ If this host had OC built-in cron jobs enabled, disable them so they don't conflict with the scheduler.
141
+
142
+ ```bash
143
+ openclaw cron list
144
+ # For each enabled job:
145
+ openclaw cron edit <job-id> --disable
146
+ openclaw config set cron.enabled false
147
+ ```
148
+
149
+ Also set `OPENCLAW_SKIP_CRON=1` in your OpenClaw gateway service environment (launchctl/systemd/pm2), then restart the gateway.
150
+
151
+ Verify: `openclaw cron list` shows no enabled jobs (or "No cron jobs").
152
+
153
+ ---
154
+
155
+ ## Step 6: Disable OC Heartbeat
156
+
157
+ If this host had OC heartbeat enabled, disable it:
158
+
159
+ ```bash
160
+ openclaw config set agents.defaults.heartbeat.every "0m"
161
+ # If you have per-agent heartbeat overrides, set/remove those too:
162
+ # agents.list[].heartbeat.every = "0m"
163
+ openclaw gateway restart
164
+ ```
165
+
166
+ ---
167
+
168
+ ## Step 7: Choose a macOS launchd mode
169
+
170
+ > **Linux hosts:** For Linux additional hosts, follow the systemd setup in [INSTALL-LINUX.md](INSTALL-LINUX.md) instead of this step.
171
+
172
+ On an additional host, the same choice applies:
173
+
174
+ - **LaunchAgent**: best for a personal Mac with auto-login
175
+ - **LaunchDaemon**: best for a headless host or startup before login
176
+
177
+ Use the setup wizard to install the mode you want:
178
+
179
+ ```bash
180
+ cd ~/.openclaw/scheduler
181
+ node setup.mjs --service-mode agent
182
+ # or:
183
+ node setup.mjs --service-mode daemon
184
+ ```
185
+
186
+ If you installed from npm:
187
+
188
+ ```bash
189
+ npm exec --prefix ~/.openclaw/scheduler openclaw-scheduler -- setup --service-mode agent
190
+ # or:
191
+ npm exec --prefix ~/.openclaw/scheduler openclaw-scheduler -- setup --service-mode daemon
192
+ ```
193
+
194
+ Verify the mode you chose:
195
+
196
+ ```bash
197
+ # LaunchAgent
198
+ launchctl print gui/$UID/ai.openclaw.scheduler
199
+
200
+ # LaunchDaemon
201
+ sudo launchctl print system/ai.openclaw.scheduler
202
+
203
+ # Either mode
204
+ sleep 5 && tail -5 /tmp/openclaw-scheduler.log
205
+ ```
206
+
207
+ ---
208
+
209
+ ## Step 8: Smoke Tests
210
+
211
+ > **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.
212
+
213
+ ### Isolated dispatch
214
+ ```bash
215
+ cd ~/.openclaw/scheduler
216
+ node --input-type=module -e "
217
+ import { initDb, getDb } from './db.js';
218
+ import { createJob } from './jobs.js';
219
+ initDb();
220
+ const job = createJob({
221
+ name: 'Smoke Test',
222
+ schedule_cron: '0 0 31 2 *',
223
+ payload_message: 'Reply with exactly: SCHEDULER_OK',
224
+ delivery_mode: 'none',
225
+ delete_after_run: true,
226
+ origin: 'system',
227
+ run_timeout_ms: 300000,
228
+ });
229
+ getDb().prepare(\"UPDATE jobs SET next_run_at = datetime('now', '-1 second') WHERE id = ?\").run(job.id);
230
+ console.log('Created smoke test:', job.id);
231
+ "
232
+ sleep 20 && tail -10 /tmp/openclaw-scheduler.log
233
+ ```
234
+
235
+ Look for: `Dispatching: Smoke Test` → `Completed: Smoke Test`
236
+
237
+ ### Telegram delivery
238
+ ```bash
239
+ node --input-type=module -e "
240
+ import { initDb, getDb } from './db.js';
241
+ import { createJob } from './jobs.js';
242
+ initDb();
243
+ const job = createJob({
244
+ name: 'Telegram Test',
245
+ schedule_cron: '0 0 31 2 *',
246
+ payload_message: 'Confirm scheduler is working. Send a brief greeting.',
247
+ delivery_mode: 'announce',
248
+ delivery_channel: 'telegram',
249
+ delivery_to: 'YOUR_CHAT_ID',
250
+ delete_after_run: true,
251
+ origin: 'system',
252
+ run_timeout_ms: 300000,
253
+ });
254
+ getDb().prepare(\"UPDATE jobs SET next_run_at = datetime('now', '-1 second') WHERE id = ?\").run(job.id);
255
+ console.log('Created Telegram test:', job.id);
256
+ "
257
+ ```
258
+
259
+ You should receive a Telegram message within 30 seconds.
260
+
261
+ ---
262
+
263
+ ## Step 9: Create Your Jobs
264
+
265
+ Since this is a fresh host, create jobs from scratch using the CLI:
266
+
267
+ ```bash
268
+ node cli.js jobs add '{
269
+ "name": "My First Job",
270
+ "schedule_cron": "0 * * * *",
271
+ "payload_message": "Run your task here",
272
+ "delivery_mode": "announce",
273
+ "delivery_channel": "telegram",
274
+ "delivery_to": "YOUR_CHAT_ID"
275
+ }'
276
+ node cli.js jobs list
277
+ node cli.js status
278
+ ```
279
+
280
+ See README.md for full job examples including shell jobs, workflow chains, approval gates, and more.
281
+
282
+ ---
283
+
284
+ ## Step 10: Verify First Real Job
285
+
286
+ Wait for the next scheduled job and confirm:
287
+ ```bash
288
+ tail -f /tmp/openclaw-scheduler.log
289
+ # Or after it fires:
290
+ node cli.js runs list <job-id>
291
+ ```
292
+
293
+ ---
294
+
295
+ ## Rollback
296
+
297
+ If anything goes wrong:
298
+
299
+ ```bash
300
+ # 1. Stop scheduler
301
+ launchctl bootout gui/$UID/ai.openclaw.scheduler # if using LaunchAgent
302
+ sudo launchctl bootout system/ai.openclaw.scheduler # if using LaunchDaemon
303
+
304
+ # 2. Re-enable OC cron (if you disabled it)
305
+ openclaw cron edit <job-id> --enable # for each job
306
+ openclaw config set cron.enabled true
307
+ # remove OPENCLAW_SKIP_CRON=1 from gateway service env
308
+
309
+ # 3. Re-enable heartbeat (if you disabled it)
310
+ openclaw config set agents.defaults.heartbeat.every "5m"
311
+ openclaw gateway restart
312
+ ```
313
+
314
+ ---
315
+
316
+ ## Validation Checklist
317
+
318
+ - [ ] `SCHEDULER_DB=:memory: node test.js` -- all passing, 0 failed
319
+ - [ ] `node cli.js status` → shows jobs, 0 stale
320
+ - [ ] `launchctl print gui/$UID/ai.openclaw.scheduler` or `sudo launchctl print system/ai.openclaw.scheduler` → running
321
+ - [ ] Log file has startup lines, no errors
322
+ - [ ] OC cron → all disabled (if applicable)
323
+ - [ ] OC heartbeat → `0m` (if applicable)
324
+ - [ ] Chat completions → 200
325
+ - [ ] Smoke test → dispatched + completed in log
326
+ - [ ] Telegram test → message received
327
+ - [ ] First real job → fires on schedule
328
+
329
+ ---
330
+
331
+ ## Upgrading
332
+
333
+ Already have the scheduler installed and need to update to a newer version? See [UPGRADING.md](UPGRADING.md).