Skip to content

Commit 511c656

Browse files
tyler6204steipete
authored andcommitted
feat(cron): introduce delivery modes for isolated jobs
- Added support for new delivery modes in cron jobs: `announce`, `deliver`, and `none`. - Updated documentation to reflect changes in delivery options and usage examples. - Enhanced the cron job schema to include delivery configuration. - Refactored related CLI commands and UI components to accommodate the new delivery settings. - Improved handling of legacy delivery fields for backward compatibility. This update allows users to choose how output from isolated jobs is delivered, enhancing flexibility in job management.
1 parent 3a03e38 commit 511c656

File tree

24 files changed

+604
-205
lines changed

24 files changed

+604
-205
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Docs: https://docs.openclaw.ai
4545

4646
- Feishu: add Feishu/Lark plugin support + docs. (#7313) Thanks @jiulingyun (openclaw-cn).
4747
- Web UI: add Agents dashboard for managing agent files, tools, skills, models, channels, and cron jobs.
48+
- Cron: add announce delivery mode for isolated jobs (CLI + Control UI) and delivery mode config.
4849
- Memory: implement the opt-in QMD backend for workspace memory. (#3160) Thanks @vignesh07.
4950
- Security: add healthcheck skill and bootstrap audit guidance. (#7641) Thanks @Takhoffman.
5051
- Config: allow setting a default subagent thinking level via `agents.defaults.subagents.thinking` (and per-agent `agents.list[].subagents.thinking`). (#7372) Thanks @tyler6204.

docs/automation/cron-jobs.md

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ cron is the mechanism.
2323
- Jobs persist under `~/.openclaw/cron/` so restarts don’t lose schedules.
2424
- Two execution styles:
2525
- **Main session**: enqueue a system event, then run on the next heartbeat.
26-
- **Isolated**: run a dedicated agent turn in `cron:<jobId>`, optionally deliver output.
26+
- **Isolated**: run a dedicated agent turn in `cron:<jobId>`, with a delivery mode (legacy summary, announce, full output, or none).
2727
- Wakeups are first-class: a job can request “wake now” vs “next heartbeat”.
2828

2929
## Quick start (actionable)
@@ -53,7 +53,7 @@ openclaw cron add \
5353
--tz "America/Los_Angeles" \
5454
--session isolated \
5555
--message "Summarize overnight updates." \
56-
--deliver \
56+
--announce \
5757
--channel slack \
5858
--to "channel:C1234567890"
5959
```
@@ -96,7 +96,7 @@ A cron job is a stored record with:
9696

9797
- a **schedule** (when it should run),
9898
- a **payload** (what it should do),
99-
- optional **delivery** (where output should be sent).
99+
- optional **delivery mode** (announce, full output, or none).
100100
- optional **agent binding** (`agentId`): run the job under a specific agent; if
101101
missing or unknown, the gateway falls back to the default agent.
102102

@@ -136,9 +136,12 @@ Key behaviors:
136136

137137
- Prompt is prefixed with `[cron:<jobId> <job name>]` for traceability.
138138
- Each run starts a **fresh session id** (no prior conversation carry-over).
139-
- A summary is posted to the main session (prefix `Cron`, configurable).
140-
- `wakeMode: "now"` triggers an immediate heartbeat after posting the summary.
141-
- If `payload.deliver: true`, output is delivered to a channel; otherwise it stays internal.
139+
- Legacy behavior (no `delivery` field): a summary is posted to the main session (prefix `Cron`, configurable).
140+
- `delivery.mode` (isolated-only) chooses what happens instead of the legacy summary:
141+
- `announce`: subagent-style summary delivered immediately to a chat.
142+
- `deliver`: full agent output delivered immediately to a chat.
143+
- `none`: internal only (no main summary, no delivery).
144+
- `wakeMode: "now"` triggers an immediate heartbeat after posting the **legacy** summary.
142145

143146
Use isolated jobs for noisy, frequent, or "background chores" that shouldn't spam
144147
your main chat history.
@@ -155,17 +158,29 @@ Common `agentTurn` fields:
155158
- `message`: required text prompt.
156159
- `model` / `thinking`: optional overrides (see below).
157160
- `timeoutSeconds`: optional timeout override.
158-
- `deliver`: `true` to send output to a channel target.
159-
- `channel`: `last` or a specific channel.
160-
- `to`: channel-specific target (phone/chat/channel id).
161-
- `bestEffortDeliver`: avoid failing the job if delivery fails.
161+
162+
Delivery config (isolated jobs only):
163+
164+
- `delivery.mode`: `none` | `announce` | `deliver`.
165+
- `delivery.channel`: `last` or a specific channel.
166+
- `delivery.to`: channel-specific target (phone/chat/channel id).
167+
- `delivery.bestEffort`: avoid failing the job if delivery fails (deliver mode).
168+
169+
Legacy delivery fields (still accepted when `delivery` is omitted):
170+
171+
- `payload.deliver`: `true` to send output to a channel target.
172+
- `payload.channel`: `last` or a specific channel.
173+
- `payload.to`: channel-specific target (phone/chat/channel id).
174+
- `payload.bestEffortDeliver`: avoid failing the job if delivery fails.
162175

163176
Isolation options (only for `session=isolated`):
164177

165178
- `postToMainPrefix` (CLI: `--post-prefix`): prefix for the system event in main.
166179
- `postToMainMode`: `summary` (default) or `full`.
167180
- `postToMainMaxChars`: max chars when `postToMainMode=full` (default 8000).
168181

182+
Note: isolation post-to-main settings apply to legacy jobs (no `delivery` field). If `delivery` is set, the legacy summary is skipped.
183+
169184
### Model and thinking overrides
170185

171186
Isolated jobs (`agentTurn`) can override the model and thinking level:
@@ -185,19 +200,24 @@ Resolution priority:
185200

186201
### Delivery (channel + target)
187202

188-
Isolated jobs can deliver output to a channel. The job payload can specify:
203+
Isolated jobs can deliver output to a channel via the top-level `delivery` config:
189204

190-
- `channel`: `whatsapp` / `telegram` / `discord` / `slack` / `mattermost` (plugin) / `signal` / `imessage` / `last`
191-
- `to`: channel-specific recipient target
205+
- `delivery.mode`: `announce` (subagent-style summary) or `deliver` (full output).
206+
- `delivery.channel`: `whatsapp` / `telegram` / `discord` / `slack` / `mattermost` (plugin) / `signal` / `imessage` / `last`.
207+
- `delivery.to`: channel-specific recipient target.
192208

193-
If `channel` or `to` is omitted, cron can fall back to the main session’s “last route”
194-
(the last place the agent replied).
209+
Delivery config is only valid for isolated jobs (`sessionTarget: "isolated"`).
195210

196-
Delivery notes:
211+
If `delivery.channel` or `delivery.to` is omitted, cron can fall back to the main session’s
212+
“last route” (the last place the agent replied).
197213

198-
- If `to` is set, cron auto-delivers the agent’s final output even if `deliver` is omitted.
199-
- Use `deliver: true` when you want last-route delivery without an explicit `to`.
200-
- Use `deliver: false` to keep output internal even if a `to` is present.
214+
Legacy behavior (no `delivery` field):
215+
216+
- If `payload.to` is set, cron auto-delivers the agent’s final output even if `payload.deliver` is omitted.
217+
- Use `payload.deliver: true` when you want last-route delivery without an explicit `to`.
218+
- Use `payload.deliver: false` to keep output internal even if a `to` is present.
219+
220+
If `delivery` is set, it overrides legacy payload delivery fields and skips the legacy main-session summary.
201221

202222
Target format reminders:
203223

@@ -248,13 +268,14 @@ Recurring, isolated job with delivery:
248268
"wakeMode": "next-heartbeat",
249269
"payload": {
250270
"kind": "agentTurn",
251-
"message": "Summarize overnight updates.",
252-
"deliver": true,
271+
"message": "Summarize overnight updates."
272+
},
273+
"delivery": {
274+
"mode": "announce",
253275
"channel": "slack",
254276
"to": "channel:C1234567890",
255-
"bestEffortDeliver": true
256-
},
257-
"isolation": { "postToMainPrefix": "Cron", "postToMainMode": "summary" }
277+
"bestEffort": true
278+
}
258279
}
259280
```
260281

@@ -263,7 +284,7 @@ Notes:
263284
- `schedule.kind`: `at` (`atMs`), `every` (`everyMs`), or `cron` (`expr`, optional `tz`).
264285
- `atMs` and `everyMs` are epoch milliseconds.
265286
- `sessionTarget` must be `"main"` or `"isolated"` and must match `payload.kind`.
266-
- Optional fields: `agentId`, `description`, `enabled`, `deleteAfterRun`, `isolation`.
287+
- Optional fields: `agentId`, `description`, `enabled`, `deleteAfterRun`, `delivery`, `isolation`.
267288
- `wakeMode` defaults to `"next-heartbeat"` when omitted.
268289

269290
### cron.update params
@@ -341,7 +362,7 @@ openclaw cron add \
341362
--wake now
342363
```
343364

344-
Recurring isolated job (deliver to WhatsApp):
365+
Recurring isolated job (announce to WhatsApp):
345366

346367
```bash
347368
openclaw cron add \
@@ -350,7 +371,7 @@ openclaw cron add \
350371
--tz "America/Los_Angeles" \
351372
--session isolated \
352373
--message "Summarize inbox + calendar for today." \
353-
--deliver \
374+
--announce \
354375
--channel whatsapp \
355376
--to "+15551234567"
356377
```

docs/automation/cron-vs-heartbeat.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ Cron jobs run at **exact times** and can run in isolated sessions without affect
9090
- **Exact timing**: 5-field cron expressions with timezone support.
9191
- **Session isolation**: Runs in `cron:<jobId>` without polluting main history.
9292
- **Model overrides**: Use a cheaper or more powerful model per job.
93-
- **Delivery control**: Can deliver directly to a channel; still posts a summary to main by default (configurable).
93+
- **Delivery control**: Choose `announce` (summary), `deliver` (full output), or `none`. Legacy jobs still post a summary to main by default.
94+
- **Immediate delivery**: Announce/deliver modes post directly without waiting for heartbeat.
9495
- **No agent context needed**: Runs even if main session is idle or compacted.
9596
- **One-shot support**: `--at` for precise future timestamps.
9697

@@ -104,12 +105,12 @@ openclaw cron add \
104105
--session isolated \
105106
--message "Generate today's briefing: weather, calendar, top emails, news summary." \
106107
--model opus \
107-
--deliver \
108+
--announce \
108109
--channel whatsapp \
109110
--to "+15551234567"
110111
```
111112

112-
This runs at exactly 7:00 AM New York time, uses Opus for quality, and delivers directly to WhatsApp.
113+
This runs at exactly 7:00 AM New York time, uses Opus for quality, and announces a summary directly to WhatsApp.
113114

114115
### Cron example: One-shot reminder
115116

@@ -173,7 +174,7 @@ The most efficient setup uses **both**:
173174

174175
```bash
175176
# Daily morning briefing at 7am
176-
openclaw cron add --name "Morning brief" --cron "0 7 * * *" --session isolated --message "..." --deliver
177+
openclaw cron add --name "Morning brief" --cron "0 7 * * *" --session isolated --message "..." --announce
177178

178179
# Weekly project review on Mondays at 9am
179180
openclaw cron add --name "Weekly review" --cron "0 9 * * 1" --session isolated --message "..." --model opus
@@ -245,7 +246,7 @@ Use `--session isolated` when you want:
245246

246247
- A clean slate without prior context
247248
- Different model or thinking settings
248-
- Output delivered directly to a channel (summary still posts to main by default)
249+
- Announce summaries or deliver full output directly to a channel
249250
- History that doesn't clutter main session
250251

251252
```bash
@@ -256,7 +257,7 @@ openclaw cron add \
256257
--message "Weekly codebase analysis..." \
257258
--model opus \
258259
--thinking high \
259-
--deliver
260+
--announce
260261
```
261262

262263
## Cost Considerations

docs/cli/cron.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,17 @@ Tip: run `openclaw cron --help` for the full command surface.
2121
Update delivery settings without changing the message:
2222

2323
```bash
24-
openclaw cron edit <job-id> --deliver --channel telegram --to "123456789"
24+
openclaw cron edit <job-id> --announce --channel telegram --to "123456789"
2525
```
2626

2727
Disable delivery for an isolated job:
2828

2929
```bash
3030
openclaw cron edit <job-id> --no-deliver
3131
```
32+
33+
Deliver full output (instead of announce):
34+
35+
```bash
36+
openclaw cron edit <job-id> --deliver --channel slack --to "channel:C1234567890"
37+
```

docs/web/control-ui.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ you revoke it with `openclaw devices revoke --device <id> --role <role>`. See
7979
- Logs: live tail of gateway file logs with filter/export (`logs.tail`)
8080
- Update: run a package/git update + restart (`update.run`) with a restart report
8181

82+
Cron jobs panel notes:
83+
84+
- For isolated jobs, choose a delivery mode: legacy main summary, announce summary, deliver full output, or none.
85+
- Channel/target fields appear when announce or deliver is selected.
86+
8287
## Chat behavior
8388

8489
- `chat.send` is **non-blocking**: it acks immediately with `{ runId, status: "started" }` and the response streams via `chat` events.

src/agents/tools/cron-tool.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ JOB SCHEMA (for add action):
174174
"name": "string (optional)",
175175
"schedule": { ... }, // Required: when to run
176176
"payload": { ... }, // Required: what to execute
177+
"delivery": { ... }, // Optional: announce/deliver output (isolated only)
177178
"sessionTarget": "main" | "isolated", // Required
178179
"enabled": true | false // Optional, default true
179180
}
@@ -190,7 +191,13 @@ PAYLOAD TYPES (payload.kind):
190191
- "systemEvent": Injects text as system event into session
191192
{ "kind": "systemEvent", "text": "<message>" }
192193
- "agentTurn": Runs agent with message (isolated sessions only)
193-
{ "kind": "agentTurn", "message": "<prompt>", "model": "<optional>", "thinking": "<optional>", "timeoutSeconds": <optional>, "deliver": <optional-bool>, "channel": "<optional>", "to": "<optional>", "bestEffortDeliver": <optional-bool> }
194+
{ "kind": "agentTurn", "message": "<prompt>", "model": "<optional>", "thinking": "<optional>", "timeoutSeconds": <optional> }
195+
196+
DELIVERY (isolated-only, top-level):
197+
{ "mode": "none|announce|deliver", "channel": "<optional>", "to": "<optional>", "bestEffort": <optional-bool> }
198+
199+
LEGACY DELIVERY (payload, only when delivery is omitted):
200+
{ "deliver": <optional-bool>, "channel": "<optional>", "to": "<optional>", "bestEffortDeliver": <optional-bool> }
194201
195202
CRITICAL CONSTRAINTS:
196203
- sessionTarget="main" REQUIRES payload.kind="systemEvent"

src/cli/cron-cli.test.ts

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -213,20 +213,15 @@ describe("cron cli", () => {
213213
const updateCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update");
214214
const patch = updateCall?.[2] as {
215215
patch?: {
216-
payload?: {
217-
kind?: string;
218-
message?: string;
219-
deliver?: boolean;
220-
channel?: string;
221-
to?: string;
222-
};
216+
payload?: { kind?: string; message?: string };
217+
delivery?: { mode?: string; channel?: string; to?: string };
223218
};
224219
};
225220

226221
expect(patch?.patch?.payload?.kind).toBe("agentTurn");
227-
expect(patch?.patch?.payload?.deliver).toBe(true);
228-
expect(patch?.patch?.payload?.channel).toBe("telegram");
229-
expect(patch?.patch?.payload?.to).toBe("19098680");
222+
expect(patch?.patch?.delivery?.mode).toBe("deliver");
223+
expect(patch?.patch?.delivery?.channel).toBe("telegram");
224+
expect(patch?.patch?.delivery?.to).toBe("19098680");
230225
expect(patch?.patch?.payload?.message).toBeUndefined();
231226
});
232227

@@ -242,11 +237,11 @@ describe("cron cli", () => {
242237

243238
const updateCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update");
244239
const patch = updateCall?.[2] as {
245-
patch?: { payload?: { kind?: string; deliver?: boolean } };
240+
patch?: { payload?: { kind?: string }; delivery?: { mode?: string } };
246241
};
247242

248243
expect(patch?.patch?.payload?.kind).toBe("agentTurn");
249-
expect(patch?.patch?.payload?.deliver).toBe(false);
244+
expect(patch?.patch?.delivery?.mode).toBe("none");
250245
});
251246

252247
it("does not include undefined delivery fields when updating message", async () => {
@@ -272,6 +267,7 @@ describe("cron cli", () => {
272267
to?: string;
273268
bestEffortDeliver?: boolean;
274269
};
270+
delivery?: unknown;
275271
};
276272
};
277273

@@ -283,6 +279,7 @@ describe("cron cli", () => {
283279
expect(patch?.patch?.payload).not.toHaveProperty("channel");
284280
expect(patch?.patch?.payload).not.toHaveProperty("to");
285281
expect(patch?.patch?.payload).not.toHaveProperty("bestEffortDeliver");
282+
expect(patch?.patch).not.toHaveProperty("delivery");
286283
});
287284

288285
it("includes delivery fields when explicitly provided with message", async () => {
@@ -313,20 +310,16 @@ describe("cron cli", () => {
313310
const updateCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.update");
314311
const patch = updateCall?.[2] as {
315312
patch?: {
316-
payload?: {
317-
message?: string;
318-
deliver?: boolean;
319-
channel?: string;
320-
to?: string;
321-
};
313+
payload?: { message?: string };
314+
delivery?: { mode?: string; channel?: string; to?: string };
322315
};
323316
};
324317

325318
// Should include everything
326319
expect(patch?.patch?.payload?.message).toBe("Updated message");
327-
expect(patch?.patch?.payload?.deliver).toBe(true);
328-
expect(patch?.patch?.payload?.channel).toBe("telegram");
329-
expect(patch?.patch?.payload?.to).toBe("19098680");
320+
expect(patch?.patch?.delivery?.mode).toBe("deliver");
321+
expect(patch?.patch?.delivery?.channel).toBe("telegram");
322+
expect(patch?.patch?.delivery?.to).toBe("19098680");
330323
});
331324

332325
it("includes best-effort delivery when provided with message", async () => {

0 commit comments

Comments
 (0)