feat(cron): add direct job lookup

Signed-off-by: samzong <samzong.lu@gmail.com>
This commit is contained in:
samzong
2026-05-11 23:19:52 +08:00
committed by Peter Steinberger
parent dc83a10733
commit 380a679313
31 changed files with 263 additions and 31 deletions

View File

@@ -27,6 +27,7 @@ Cron is the Gateway's built-in scheduler. It persists jobs, wakes the agent at t
<Step title="Check your jobs">
```bash
openclaw cron list
openclaw cron get <job-id>
openclaw cron show <job-id>
```
</Step>
@@ -359,6 +360,9 @@ When `hooks.enabled=true` and `hooks.gmail.account` is set, the Gateway starts `
# List all jobs
openclaw cron list
# Get one stored job as JSON
openclaw cron get <jobId>
# Show one job, including resolved delivery route
openclaw cron show <jobId>

View File

@@ -220,6 +220,7 @@ Manual run and inspection:
```bash
openclaw cron list
openclaw cron list --agent ops
openclaw cron get <job-id>
openclaw cron show <job-id>
openclaw cron run <job-id>
openclaw cron run <job-id> --due
@@ -228,6 +229,8 @@ openclaw cron runs --id <job-id> --limit 50
`openclaw cron list` shows all matching jobs by default. Pass `--agent <id>` to show only jobs whose effective normalized agent id matches; jobs without a stored agent id count as the configured default agent.
`openclaw cron get <job-id>` returns the stored job JSON directly. Use `cron show <job-id>` when you want the human-readable view with delivery-route preview.
`cron list --json` and `cron show <job-id> --json` include a top-level `status` field on each job, computed from `enabled`, `state.runningAtMs`, and `state.lastRunStatus`. Values: `disabled`, `running`, `ok`, `error`, `skipped`, or `idle`. This mirrors the human-readable status column so external tooling can read job state without re-deriving it.
`cron runs` entries include delivery diagnostics with the intended cron target, the resolved target, message-tool sends, fallback use, and delivered state.

View File

@@ -256,6 +256,7 @@ openclaw [--dev] [--profile <name>] <command>
cron
status
list
get
add
edit
rm

View File

@@ -467,7 +467,7 @@ enumeration of `src/gateway/server-methods/*.ts`.
</Accordion>
<Accordion title="Automation, skills, and tools">
- Automation: `wake` schedules an immediate or next-heartbeat wake text injection; `cron.list`, `cron.status`, `cron.add`, `cron.update`, `cron.remove`, `cron.run`, `cron.runs` manage scheduled work.
- Automation: `wake` schedules an immediate or next-heartbeat wake text injection; `cron.get`, `cron.list`, `cron.status`, `cron.add`, `cron.update`, `cron.remove`, `cron.run`, `cron.runs` manage scheduled work.
- Skills and tools: `commands.list`, `skills.*`, `tools.catalog`, `tools.effective`, `tools.invoke`.
</Accordion>

View File

@@ -193,7 +193,7 @@ describe("cron tool", () => {
action: "remove",
jobId: "job-other",
}),
).rejects.toThrow("Cron tool is restricted to removing the current cron job.");
).rejects.toThrow("Cron tool is restricted to the current cron job.");
expect(callGatewayMock).not.toHaveBeenCalled();
});
@@ -233,7 +233,7 @@ describe("cron tool", () => {
const tool = createTestCronTool({ selfRemoveOnlyJobId: "job-current" });
await expect(tool.execute("call-runs-denied", args)).rejects.toThrow(
"Cron tool is restricted to removing the current cron job.",
"Cron tool is restricted to the current cron job.",
);
expect(callGatewayMock).not.toHaveBeenCalled();
@@ -258,6 +258,33 @@ describe("cron tool", () => {
expect(result.details).toEqual({ enabled: true });
});
it("allows scoped isolated cron runs to get the current job", async () => {
callGatewayMock.mockResolvedValueOnce({ id: "job-current", name: "current" });
const tool = createTestCronTool({ selfRemoveOnlyJobId: "job-current" });
const result = await tool.execute("call-get", {
action: "get",
jobId: "job-current",
});
const params = expectSingleGatewayCallMethod("cron.get");
expect(params).toStrictEqual({ id: "job-current" });
expect(result.details).toEqual({ id: "job-current", name: "current" });
});
it.each([
["another job", { action: "get", jobId: "job-other" }],
["missing job id", { action: "get" }],
])("denies scoped isolated cron runs from getting %s", async (_label, args) => {
const tool = createTestCronTool({ selfRemoveOnlyJobId: "job-current" });
await expect(tool.execute("call-get-denied", args)).rejects.toThrow(
"Cron tool is restricted to the current cron job.",
);
expect(callGatewayMock).not.toHaveBeenCalled();
});
it("allows scoped isolated cron runs to list only the current job", async () => {
callGatewayMock.mockResolvedValueOnce({
jobs: [
@@ -366,7 +393,7 @@ describe("cron tool", () => {
const tool = createTestCronTool({ selfRemoveOnlyJobId: "job-current" });
await expect(tool.execute("call-denied", args)).rejects.toThrow(
"Cron tool is restricted to removing the current cron job.",
"Cron tool is restricted to the current cron job.",
);
expect(callGatewayMock).not.toHaveBeenCalled();
@@ -437,6 +464,8 @@ describe("cron tool", () => {
["remove", { action: "remove", id: "job-2" }, { id: "job-2" }],
["run", { action: "run", jobId: "job-1" }, { id: "job-1", mode: "force" }],
["run", { action: "run", id: "job-2" }, { id: "job-2", mode: "force" }],
["get", { action: "get", jobId: "job-1" }, { id: "job-1" }],
["get", { action: "get", id: "job-2" }, { id: "job-2" }],
["runs", { action: "runs", jobId: "job-1" }, { id: "job-1" }],
["runs", { action: "runs", id: "job-2" }, { id: "job-2" }],
])("%s sends id to gateway", async (action, args, expectedParams) => {

View File

@@ -29,7 +29,17 @@ import { resolveInternalSessionKey, resolveMainSessionAlias } from "./sessions-h
// We spell out job/patch properties so that LLMs know what fields to send.
// Nested unions are avoided; runtime validation happens in normalizeCronJob*.
const CRON_ACTIONS = ["status", "list", "add", "update", "remove", "run", "runs", "wake"] as const;
const CRON_ACTIONS = [
"status",
"list",
"get",
"add",
"update",
"remove",
"run",
"runs",
"wake",
] as const;
const CRON_SCHEDULE_KINDS = ["at", "every", "cron"] as const;
const CRON_WAKE_MODES = ["now", "next-heartbeat"] as const;
@@ -315,7 +325,6 @@ export const CronToolSchema = Type.Object(
type CronToolOptions = {
agentSessionKey?: string;
currentDeliveryContext?: DeliveryContext;
/** Restrict this cron tool instance to removing only this active cron job. */
selfRemoveOnlyJobId?: string;
};
@@ -350,7 +359,7 @@ function readCronJobIdParam(params: Record<string, unknown>) {
return readStringParam(params, "jobId") ?? readStringParam(params, "id");
}
const CRON_SELF_REMOVE_SCOPE_ERROR = "Cron tool is restricted to removing the current cron job.";
const CRON_SELF_REMOVE_SCOPE_ERROR = "Cron tool is restricted to the current cron job.";
function readCronSelfRemoveOnlyJobId(opts: CronToolOptions | undefined) {
return opts?.selfRemoveOnlyJobId?.trim() || undefined;
@@ -369,7 +378,7 @@ function assertCronSelfRemoveScope(
if (!selfRemoveOnlyJobId || isCronSelfIntrospectionAction(action)) {
return;
}
if (action === "remove" || action === "runs") {
if (action === "get" || action === "remove" || action === "runs") {
const id = readCronJobIdParam(params);
if (id && id === selfRemoveOnlyJobId) {
return;
@@ -623,13 +632,14 @@ export function createCronTool(opts?: CronToolOptions, deps?: CronToolDeps): Any
name: "cron",
ownerOnly: isOpenClawOwnerOnlyCoreToolName("cron"),
displaySummary: CRON_TOOL_DISPLAY_SUMMARY,
description: `Manage Gateway cron jobs (status/list/add/update/remove/run/runs) and send wake events. Use this for reminders, "check back later" requests, delayed follow-ups, and recurring tasks. Do not emulate scheduling with exec sleep or process polling.
description: `Manage Gateway cron jobs (status/list/get/add/update/remove/run/runs) and send wake events. Use this for reminders, "check back later" requests, delayed follow-ups, and recurring tasks. Do not emulate scheduling with exec sleep or process polling.
Main-session cron jobs enqueue system events for heartbeat handling. Isolated cron jobs create background task runs that appear in \`openclaw tasks\`.
ACTIONS:
- status: Check cron scheduler status
- list: List jobs (use includeDisabled:true to include disabled; agentId filters by agent, auto-filled from session)
- get: Get one job by id (requires jobId)
- add: Create job (requires job object, see schema below)
- update: Modify job (requires jobId + patch object)
- remove: Delete job (requires jobId)
@@ -692,7 +702,7 @@ CRITICAL CONSTRAINTS:
Default: prefer isolated agentTurn jobs unless the user explicitly wants current-session binding.
RESTRICTED CRON RUNS:
- Some isolated cron runs receive a narrow cron grant for self-cleanup. In that mode, read-only status and list are for self-introspection only, runs (job run history) is allowed for the current job only, and mutation actions remain limited to removing the current cron job.
- Some isolated cron runs receive a narrow cron grant for self-cleanup. In that mode, read-only status and list are for self-introspection only, get/runs are allowed for the current job only, and mutation actions remain limited to removing the current cron job.
WAKE MODES (for wake action):
- "next-heartbeat" (default): Wake on next heartbeat
@@ -756,6 +766,13 @@ Use jobId as the canonical identifier; id is accepted for compatibility. Use con
selfRemoveOnlyJobId ? filterCronListResultToJobId(result, selfRemoveOnlyJobId) : result,
);
}
case "get": {
const id = readCronJobIdParam(params);
if (!id) {
throw new Error("jobId required (id accepted for backward compatibility)");
}
return jsonResult(await callGateway("cron.get", gatewayOpts, { id }));
}
case "add": {
// Flat-params recovery: non-frontier models (e.g. Grok) sometimes flatten
// job properties to the top level alongside `action` instead of nesting

View File

@@ -513,6 +513,14 @@ describe("cron cli", () => {
expect(listCall?.[2]).toEqual({ includeDisabled: false, agentId: "ops" });
});
it("routes cron get to cron.get with the provided id", async () => {
await runCronCommand(["cron", "get", "job-1"]);
const getCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "cron.get");
expect(getCall?.[2]).toEqual({ id: "job-1" });
expect(stdoutText()).toContain('"id": "job-1"');
});
it("paginates cron show lookups", async () => {
resetGatewayMock();
callGatewayFromCli.mockImplementation(

View File

@@ -110,6 +110,21 @@ export function registerCronSimpleCommands(cron: Command) {
enabled: false,
});
addGatewayClientOptions(
cron
.command("get")
.description("Get a cron job as JSON")
.argument("<id>", "Job id")
.action(async (id, opts) => {
try {
const res = await callGatewayFromCli("cron.get", opts, { id: String(id) });
printCronJson(res);
} catch (err) {
handleCronCliError(err);
}
}),
);
addGatewayClientOptions(
cron
.command("show")

View File

@@ -29,6 +29,7 @@ export interface CronServiceContract {
run(id: string, mode?: CronRunMode): Promise<CronServiceRunResult>;
enqueueRun(id: string, mode?: CronRunMode): Promise<CronServiceRunResult>;
getJob(id: string): CronJob | undefined;
readJob(id: string): Promise<CronJob | undefined>;
getDefaultAgentId(): string | undefined;
wake(opts: { mode: CronWakeMode; text: string; sessionKey?: string }): CronWakeResult;
}

View File

@@ -10,10 +10,10 @@ const logger = createNoopLogger();
const { makeStorePath } = createCronStoreHarness({ prefix: "openclaw-cron-get-job-" });
installCronTestHooks({ logger });
function createCronService(storePath: string) {
function createCronService(storePath: string, cronEnabled = true) {
return new CronService({
storePath,
cronEnabled: true,
cronEnabled,
log: logger,
enqueueSystemEvent: vi.fn(),
requestHeartbeat: vi.fn(),
@@ -38,7 +38,8 @@ describe("CronService.getJob", () => {
});
expect(cron.getJob(added.id)?.id).toBe(added.id);
expect(cron.getJob("missing-job-id")).toBeUndefined();
await expect(cron.readJob(added.id)).resolves.toEqual(added);
await expect(cron.readJob("missing-job-id")).resolves.toBeUndefined();
} finally {
cron.stop();
}
@@ -59,6 +60,12 @@ describe("CronService.getJob", () => {
payload: { kind: "systemEvent", text: "ping" },
delivery: { mode: "webhook", to: "https://example.invalid/cron" },
});
await expect(cron.readJob(webhookJob.id)).resolves.toMatchObject({
delivery: {
mode: "webhook",
to: "https://example.invalid/cron",
},
});
expect(cron.getJob(webhookJob.id)?.delivery).toEqual({
mode: "webhook",
to: "https://example.invalid/cron",
@@ -67,4 +74,27 @@ describe("CronService.getJob", () => {
cron.stop();
}
});
it("loads persisted jobs for direct reads without starting the scheduler", async () => {
const { storePath } = await makeStorePath();
const writer = createCronService(storePath);
await writer.start();
const persisted = await writer.add({
name: "persisted-job",
enabled: true,
schedule: { kind: "every", everyMs: 60_000 },
sessionTarget: "main",
wakeMode: "next-heartbeat",
payload: { kind: "systemEvent", text: "ping" },
});
writer.stop();
const reader = createCronService(storePath, false);
await expect(reader.readJob(persisted.id)).resolves.toMatchObject({
id: persisted.id,
name: "persisted-job",
});
expect(reader.getJob(persisted.id)).toBeDefined();
});
});

View File

@@ -64,6 +64,10 @@ export class CronService implements CronServiceContract {
return this.state.store?.jobs.find((job) => job.id === id);
}
async readJob(id: string): Promise<CronJob | undefined> {
return await ops.readJob(this.state, id);
}
getDefaultAgentId(): string | undefined {
return this.state.deps.defaultAgentId;
}

View File

@@ -236,6 +236,13 @@ export async function list(state: CronServiceState, opts?: { includeDisabled?: b
});
}
export async function readJob(state: CronServiceState, id: string) {
return await locked(state, async () => {
await ensureLoadedForRead(state);
return state.store?.jobs.find((job) => job.id === id);
});
}
function resolveEnabledFilter(opts?: CronListPageOptions): CronJobsEnabledFilter {
if (opts?.enabled === "all" || opts?.enabled === "enabled" || opts?.enabled === "disabled") {
return opts.enabled;

View File

@@ -113,6 +113,7 @@ const METHOD_SCOPE_GROUPS: Record<OperatorScope, readonly string[]> = {
"sessions.usage",
"sessions.usage.timeseries",
"sessions.usage.logs",
"cron.get",
"cron.list",
"cron.status",
"cron.runs",

View File

@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import {
validateCronAddParams,
validateCronGetParams,
validateCronListParams,
validateCronRemoveParams,
validateCronRunParams,
@@ -54,6 +55,13 @@ describe("cron protocol validators", () => {
expect(validateCronUpdateParams({ jobId: "job-2", patch: { enabled: true } })).toBe(true);
});
it("accepts get params for id and jobId selectors", () => {
expect(validateCronGetParams({ id: "job-1" })).toBe(true);
expect(validateCronGetParams({ jobId: "job-2" })).toBe(true);
expect(validateCronGetParams({})).toBe(false);
expect(validateCronGetParams({ id: "" })).toBe(false);
});
it("accepts delivery threadId on add and update params", () => {
expect(
validateCronAddParams({

View File

@@ -146,6 +146,8 @@ import {
ConnectParamsSchema,
type CronAddParams,
CronAddParamsSchema,
type CronGetParams,
CronGetParamsSchema,
type CronJob,
CronJobSchema,
type CronListParams,
@@ -671,6 +673,7 @@ export const validateSkillsSearchParams = ajv.compile<SkillsSearchParams>(Skills
export const validateSkillsDetailParams = ajv.compile<SkillsDetailParams>(SkillsDetailParamsSchema);
export const validateCronListParams = ajv.compile<CronListParams>(CronListParamsSchema);
export const validateCronStatusParams = ajv.compile<CronStatusParams>(CronStatusParamsSchema);
export const validateCronGetParams = ajv.compile<CronGetParams>(CronGetParamsSchema);
export const validateCronAddParams = ajv.compile<CronAddParams>(CronAddParamsSchema);
export const validateCronUpdateParams = ajv.compile<CronUpdateParams>(CronUpdateParamsSchema);
export const validateCronRemoveParams = ajv.compile<CronRemoveParams>(CronRemoveParamsSchema);
@@ -940,6 +943,7 @@ export {
CronJobSchema,
CronListParamsSchema,
CronStatusParamsSchema,
CronGetParamsSchema,
CronAddParamsSchema,
CronUpdateParamsSchema,
CronRemoveParamsSchema,
@@ -1122,6 +1126,7 @@ export type {
CronJob,
CronListParams,
CronStatusParams,
CronGetParams,
CronAddParams,
CronUpdateParams,
CronRemoveParams,

View File

@@ -351,6 +351,8 @@ export const CronListParamsSchema = Type.Object(
export const CronStatusParamsSchema = Type.Object({}, { additionalProperties: false });
export const CronGetParamsSchema = cronIdOrJobIdParams({});
export const CronAddParamsSchema = Type.Object(
{
name: NonEmptyString,

View File

@@ -116,6 +116,7 @@ import {
} from "./config.js";
import {
CronAddParamsSchema,
CronGetParamsSchema,
CronJobSchema,
CronListParamsSchema,
CronRemoveParamsSchema,
@@ -454,6 +455,7 @@ export const ProtocolSchemas = {
CronJob: CronJobSchema,
CronListParams: CronListParamsSchema,
CronStatusParams: CronStatusParamsSchema,
CronGetParams: CronGetParamsSchema,
CronAddParams: CronAddParamsSchema,
CronUpdateParams: CronUpdateParamsSchema,
CronRemoveParams: CronRemoveParamsSchema,

View File

@@ -190,6 +190,7 @@ export type SkillsUpdateParams = SchemaType<"SkillsUpdateParams">;
export type CronJob = SchemaType<"CronJob">;
export type CronListParams = SchemaType<"CronListParams">;
export type CronStatusParams = SchemaType<"CronStatusParams">;
export type CronGetParams = SchemaType<"CronGetParams">;
export type CronAddParams = SchemaType<"CronAddParams">;
export type CronUpdateParams = SchemaType<"CronUpdateParams">;
export type CronRemoveParams = SchemaType<"CronRemoveParams">;

View File

@@ -43,6 +43,17 @@ describe("createLazyGatewayCronState", () => {
expect(cron.status).toHaveBeenCalledTimes(1);
});
it("loads the cron service for direct job reads", async () => {
const cron = createCronService();
hoisted.setState(createCronState(cron));
const lazy = createLazyGatewayCronState(createParams());
await lazy.cron.readJob("demo");
expect(hoisted.buildGatewayCronService).toHaveBeenCalledTimes(1);
expect(cron.readJob).toHaveBeenCalledWith("demo");
});
it("starts the loaded cron service once", async () => {
const cron = createCronService();
hoisted.setState(createCronState(cron));
@@ -140,6 +151,7 @@ function createCronService(): CronServiceContract {
run: vi.fn(async () => ({ ok: true, ran: false, reason: "invalid-spec" }) as never),
enqueueRun: vi.fn(async () => ({ ok: true, ran: false, reason: "invalid-spec" }) as never),
getJob: vi.fn(() => undefined),
readJob: vi.fn(async () => undefined),
getDefaultAgentId: vi.fn(() => "default"),
wake: vi.fn(() => ({ ok: true })),
};

View File

@@ -102,6 +102,9 @@ export function createLazyGatewayCronState(params: LazyGatewayCronParams): Gatew
}
return loaded.state.cron.getJob(id);
},
async readJob(id) {
return await (await load()).state.cron.readJob(id);
},
getDefaultAgentId() {
if (!loaded) {
return undefined;

View File

@@ -156,6 +156,7 @@ const BASE_METHODS = [
"node.pending.ack",
"node.invoke.result",
"node.event",
"cron.get",
"cron.list",
"cron.status",
"cron.add",

View File

@@ -23,6 +23,7 @@ import {
errorShape,
formatValidationErrors,
validateCronAddParams,
validateCronGetParams,
validateCronListParams,
validateCronRemoveParams,
validateCronRunParams,
@@ -248,6 +249,39 @@ export const cronHandlers: GatewayRequestHandlers = {
const status = await context.cron.status();
respond(true, status, undefined);
},
"cron.get": async ({ params, respond, context }) => {
if (!validateCronGetParams(params)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
`invalid cron.get params: ${formatValidationErrors(validateCronGetParams.errors)}`,
),
);
return;
}
const p = params as { id?: string; jobId?: string };
const jobId = p.id ?? p.jobId;
if (!jobId) {
respond(
false,
undefined,
errorShape(ErrorCodes.INVALID_REQUEST, "invalid cron.get params: missing id"),
);
return;
}
const job = await context.cron.readJob(jobId);
if (!job) {
respond(
false,
undefined,
errorShape(ErrorCodes.INVALID_REQUEST, `cron job not found: ${jobId}`),
);
return;
}
respond(true, job, undefined);
},
"cron.add": async ({ params, respond, context }) => {
const sessionKey =
typeof (params as { sessionKey?: unknown } | null)?.sessionKey === "string"

View File

@@ -78,6 +78,7 @@ function createCronContext(currentJob?: CronJob) {
getDefaultAgentId: vi.fn(() => "main"),
getJob: vi.fn(() => currentJob),
wake: vi.fn(() => ({ ok: true }) as const),
readJob: vi.fn(async (id: string) => (id === currentJob?.id ? currentJob : undefined)),
},
logGateway: {
info: vi.fn(),
@@ -100,6 +101,20 @@ async function invokeCronAdd(params: Record<string, unknown>) {
return { context, respond };
}
async function invokeCronGet(params: Record<string, unknown>, currentJob?: CronJob) {
const context = createCronContext(currentJob);
const respond = vi.fn();
await cronHandlers["cron.get"]({
req: {} as never,
params: params as never,
respond: respond as never,
context: context as never,
client: null,
isWebchatConnect: () => false,
});
return { context, respond };
}
async function invokeCronUpdate(params: Record<string, unknown>, currentJob: CronJob) {
const context = createCronContext(currentJob);
const respond = vi.fn();
@@ -231,6 +246,24 @@ describe("cron method validation", () => {
expect(respond).toHaveBeenCalledWith(true, { id: "cron-1" }, undefined);
});
it("returns a single cron job for cron.get", async () => {
const job = createCronJob({ id: "cron-42", name: "single job" });
const { context, respond } = await invokeCronGet({ id: "cron-42" }, job);
expect(context.cron.readJob).toHaveBeenCalledWith("cron-42");
expect(respond).toHaveBeenCalledWith(true, job, undefined);
});
it("returns INVALID_REQUEST when cron.get cannot find the job", async () => {
const { respond } = await invokeCronGet({ jobId: "missing" });
expectResponseError(respond, {
code: "INVALID_REQUEST",
messageIncludes: "cron job not found: missing",
});
});
it("accepts threadId on announce delivery update params", async () => {
getRuntimeConfig.mockReturnValue({
channels: {

View File

@@ -469,6 +469,16 @@ describe("gateway server cron", () => {
detail: "webhook",
});
const getRes = await directCronReq(cronState, "cron.get", { id: String(dailyJobId) });
expect(getRes.ok).toBe(true);
expect((getRes.payload as { id?: unknown } | null)?.id).toBe(dailyJobId);
expect((getRes.payload as { name?: unknown } | null)?.name).toBe("daily");
const missingGetRes = await directCronReq(cronState, "cron.get", { id: "missing-job-id" });
expect(missingGetRes.ok).toBe(false);
expect(missingGetRes.error?.code).toBe("INVALID_REQUEST");
expect(missingGetRes.error?.message).toContain("cron job not found: missing-job-id");
const routeAtMs = Date.now() - 1;
const routeRes = await directCronReq(cronState, "cron.add", {
name: "route test",

View File

@@ -101,6 +101,7 @@ function createMockCronService(): CronServiceContract {
run: vi.fn(async () => ({ ok: true, ran: false, reason: "not-due" })),
enqueueRun: vi.fn(async () => ({ ok: true, ran: false, reason: "not-due" })),
getJob: vi.fn(() => undefined),
readJob: vi.fn(async () => undefined),
getDefaultAgentId: vi.fn(() => undefined),
wake: vi.fn(() => ({ ok: true })),
} as CronServiceContract;

View File

@@ -137,12 +137,12 @@
},
{
"deferLoading": true,
"description": "Manage Gateway cron jobs (status/list/add/update/remove/run/runs) and send wake events. Use this for reminders, \"check back later\" requests, delayed follow-ups, and recurring tasks. Do not emulate scheduling with exec sleep or process polling.\n\nMain-session cron jobs enqueue system events for heartbeat handling. Isolated cron jobs create background task runs that appear in `openclaw tasks`.\n\nACTIONS:\n- status: Check cron scheduler status\n- list: List jobs (use includeDisabled:true to include disabled; agentId filters by agent, auto-filled from session)\n- add: Create job (requires job object, see schema below)\n- update: Modify job (requires jobId + patch object)\n- remove: Delete job (requires jobId)\n- run: Trigger job immediately (requires jobId)\n- runs: Get job run history (requires jobId)\n- wake: Send wake event (requires text, optional mode)\n\nJOB SCHEMA (for add action):\n{\n \"name\": \"string (optional)\",\n \"schedule\": { ... }, // Required: when to run\n \"payload\": { ... }, // Required: what to execute\n \"delivery\": { ... }, // Optional: announce summary (isolated/current/session:xxx only) or webhook POST\n \"sessionTarget\": \"main\" | \"isolated\" | \"current\" | \"session:<custom-id>\", // Optional, defaults based on context\n \"enabled\": true | false // Optional, default true\n}\n\nSESSION TARGET OPTIONS:\n- \"main\": Run in the main session (requires payload.kind=\"systemEvent\")\n- \"isolated\": Run in an ephemeral isolated session (requires payload.kind=\"agentTurn\")\n- \"current\": Bind to the current session where the cron is created (resolved at creation time)\n- \"session:<custom-id>\": Run in a persistent named session (e.g., \"session:project-alpha-daily\")\n\nDEFAULT BEHAVIOR (unchanged for backward compatibility):\n- payload.kind=\"systemEvent\" → defaults to \"main\"\n- payload.kind=\"agentTurn\" → defaults to \"isolated\"\nTo use current session binding, explicitly set sessionTarget=\"current\".\n\nSCHEDULE TYPES (schedule.kind):\n- \"at\": One-shot at absolute time\n { \"kind\": \"at\", \"at\": \"<ISO-8601 timestamp>\" }\n- \"every\": Recurring interval\n { \"kind\": \"every\", \"everyMs\": <interval-ms>, \"anchorMs\": <optional-start-ms> }\n- \"cron\": Cron expression evaluated in the supplied timezone, or the Gateway host local timezone when tz is omitted\n { \"kind\": \"cron\", \"expr\": \"<cron-expression>\", \"tz\": \"<optional-IANA-timezone>\" }\n Write expr in the selected timezone's local wall-clock time; do not convert the requested local time to UTC first.\n If tz is omitted, do not assume UTC; the Gateway host local timezone is used.\n Example: \"Remind me every day at 6pm Shanghai time\" -> { \"kind\": \"cron\", \"expr\": \"0 18 * * *\", \"tz\": \"Asia/Shanghai\" }\n\nFor schedule.kind=\"at\", ISO timestamps without an explicit timezone are treated as UTC.\n\nPAYLOAD TYPES (payload.kind):\n- \"systemEvent\": Injects text as system event into session\n { \"kind\": \"systemEvent\", \"text\": \"<message>\" }\n- \"agentTurn\": Runs agent with message (isolated sessions only)\n { \"kind\": \"agentTurn\", \"message\": \"<prompt>\", \"model\": \"<optional>\", \"thinking\": \"<optional>\", \"timeoutSeconds\": <optional, 0 means no timeout> }\n\nDELIVERY (top-level):\n { \"mode\": \"none|announce|webhook\", \"channel\": \"<optional>\", \"to\": \"<optional>\", \"threadId\": \"<optional>\", \"bestEffort\": <optional-bool> }\n - Default for isolated agentTurn jobs (when delivery omitted): \"announce\"\n - announce: send to chat channel (optional channel/to target)\n - threadId: chat thread/topic id for channels that support threaded delivery\n - webhook: send finished-run event as HTTP POST to delivery.to (URL required)\n - If the task needs to send to a specific chat/recipient, set announce delivery.channel/to; do not call messaging tools inside the run.\n\nCRITICAL CONSTRAINTS:\n- sessionTarget=\"main\" REQUIRES payload.kind=\"systemEvent\"\n- sessionTarget=\"isolated\" | \"current\" | \"session:xxx\" REQUIRES payload.kind=\"agentTurn\"\n- For webhook callbacks, use delivery.mode=\"webhook\" with delivery.to set to a URL.\nDefault: prefer isolated agentTurn jobs unless the user explicitly wants current-session binding.\n\nRESTRICTED CRON RUNS:\n- Some isolated cron runs receive a narrow cron grant for self-cleanup. In that mode, read-only status and list are for self-introspection only, runs (job run history) is allowed for the current job only, and mutation actions remain limited to removing the current cron job.\n\nWAKE MODES (for wake action):\n- \"next-heartbeat\" (default): Wake on next heartbeat\n- \"now\": Wake immediately\n\nUse jobId as the canonical identifier; id is accepted for compatibility. Use contextMessages (0-10) to add previous messages as context to the job text.",
"description": "Manage Gateway cron jobs (status/list/get/add/update/remove/run/runs) and send wake events. Use this for reminders, \"check back later\" requests, delayed follow-ups, and recurring tasks. Do not emulate scheduling with exec sleep or process polling.\n\nMain-session cron jobs enqueue system events for heartbeat handling. Isolated cron jobs create background task runs that appear in `openclaw tasks`.\n\nACTIONS:\n- status: Check cron scheduler status\n- list: List jobs (use includeDisabled:true to include disabled; agentId filters by agent, auto-filled from session)\n- get: Get one job by id (requires jobId)\n- add: Create job (requires job object, see schema below)\n- update: Modify job (requires jobId + patch object)\n- remove: Delete job (requires jobId)\n- run: Trigger job immediately (requires jobId)\n- runs: Get job run history (requires jobId)\n- wake: Send wake event (requires text, optional mode)\n\nJOB SCHEMA (for add action):\n{\n \"name\": \"string (optional)\",\n \"schedule\": { ... }, // Required: when to run\n \"payload\": { ... }, // Required: what to execute\n \"delivery\": { ... }, // Optional: announce summary (isolated/current/session:xxx only) or webhook POST\n \"sessionTarget\": \"main\" | \"isolated\" | \"current\" | \"session:<custom-id>\", // Optional, defaults based on context\n \"enabled\": true | false // Optional, default true\n}\n\nSESSION TARGET OPTIONS:\n- \"main\": Run in the main session (requires payload.kind=\"systemEvent\")\n- \"isolated\": Run in an ephemeral isolated session (requires payload.kind=\"agentTurn\")\n- \"current\": Bind to the current session where the cron is created (resolved at creation time)\n- \"session:<custom-id>\": Run in a persistent named session (e.g., \"session:project-alpha-daily\")\n\nDEFAULT BEHAVIOR (unchanged for backward compatibility):\n- payload.kind=\"systemEvent\" → defaults to \"main\"\n- payload.kind=\"agentTurn\" → defaults to \"isolated\"\nTo use current session binding, explicitly set sessionTarget=\"current\".\n\nSCHEDULE TYPES (schedule.kind):\n- \"at\": One-shot at absolute time\n { \"kind\": \"at\", \"at\": \"<ISO-8601 timestamp>\" }\n- \"every\": Recurring interval\n { \"kind\": \"every\", \"everyMs\": <interval-ms>, \"anchorMs\": <optional-start-ms> }\n- \"cron\": Cron expression evaluated in the supplied timezone, or the Gateway host local timezone when tz is omitted\n { \"kind\": \"cron\", \"expr\": \"<cron-expression>\", \"tz\": \"<optional-IANA-timezone>\" }\n Write expr in the selected timezone's local wall-clock time; do not convert the requested local time to UTC first.\n If tz is omitted, do not assume UTC; the Gateway host local timezone is used.\n Example: \"Remind me every day at 6pm Shanghai time\" -> { \"kind\": \"cron\", \"expr\": \"0 18 * * *\", \"tz\": \"Asia/Shanghai\" }\n\nFor schedule.kind=\"at\", ISO timestamps without an explicit timezone are treated as UTC.\n\nPAYLOAD TYPES (payload.kind):\n- \"systemEvent\": Injects text as system event into session\n { \"kind\": \"systemEvent\", \"text\": \"<message>\" }\n- \"agentTurn\": Runs agent with message (isolated sessions only)\n { \"kind\": \"agentTurn\", \"message\": \"<prompt>\", \"model\": \"<optional>\", \"thinking\": \"<optional>\", \"timeoutSeconds\": <optional, 0 means no timeout> }\n\nDELIVERY (top-level):\n { \"mode\": \"none|announce|webhook\", \"channel\": \"<optional>\", \"to\": \"<optional>\", \"threadId\": \"<optional>\", \"bestEffort\": <optional-bool> }\n - Default for isolated agentTurn jobs (when delivery omitted): \"announce\"\n - announce: send to chat channel (optional channel/to target)\n - threadId: chat thread/topic id for channels that support threaded delivery\n - webhook: send finished-run event as HTTP POST to delivery.to (URL required)\n - If the task needs to send to a specific chat/recipient, set announce delivery.channel/to; do not call messaging tools inside the run.\n\nCRITICAL CONSTRAINTS:\n- sessionTarget=\"main\" REQUIRES payload.kind=\"systemEvent\"\n- sessionTarget=\"isolated\" | \"current\" | \"session:xxx\" REQUIRES payload.kind=\"agentTurn\"\n- For webhook callbacks, use delivery.mode=\"webhook\" with delivery.to set to a URL.\nDefault: prefer isolated agentTurn jobs unless the user explicitly wants current-session binding.\n\nRESTRICTED CRON RUNS:\n- Some isolated cron runs receive a narrow cron grant for self-cleanup. In that mode, read-only status and list are for self-introspection only, get/runs are allowed for the current job only, and mutation actions remain limited to removing the current cron job.\n\nWAKE MODES (for wake action):\n- \"next-heartbeat\" (default): Wake on next heartbeat\n- \"now\": Wake immediately\n\nUse jobId as the canonical identifier; id is accepted for compatibility. Use contextMessages (0-10) to add previous messages as context to the job text.",
"inputSchema": {
"additionalProperties": true,
"properties": {
"action": {
"enum": ["status", "list", "add", "update", "remove", "run", "runs", "wake"],
"enum": ["status", "list", "get", "add", "update", "remove", "run", "runs", "wake"],
"type": "string"
},
"agentId": {

View File

@@ -137,12 +137,12 @@
},
{
"deferLoading": true,
"description": "Manage Gateway cron jobs (status/list/add/update/remove/run/runs) and send wake events. Use this for reminders, \"check back later\" requests, delayed follow-ups, and recurring tasks. Do not emulate scheduling with exec sleep or process polling.\n\nMain-session cron jobs enqueue system events for heartbeat handling. Isolated cron jobs create background task runs that appear in `openclaw tasks`.\n\nACTIONS:\n- status: Check cron scheduler status\n- list: List jobs (use includeDisabled:true to include disabled; agentId filters by agent, auto-filled from session)\n- add: Create job (requires job object, see schema below)\n- update: Modify job (requires jobId + patch object)\n- remove: Delete job (requires jobId)\n- run: Trigger job immediately (requires jobId)\n- runs: Get job run history (requires jobId)\n- wake: Send wake event (requires text, optional mode)\n\nJOB SCHEMA (for add action):\n{\n \"name\": \"string (optional)\",\n \"schedule\": { ... }, // Required: when to run\n \"payload\": { ... }, // Required: what to execute\n \"delivery\": { ... }, // Optional: announce summary (isolated/current/session:xxx only) or webhook POST\n \"sessionTarget\": \"main\" | \"isolated\" | \"current\" | \"session:<custom-id>\", // Optional, defaults based on context\n \"enabled\": true | false // Optional, default true\n}\n\nSESSION TARGET OPTIONS:\n- \"main\": Run in the main session (requires payload.kind=\"systemEvent\")\n- \"isolated\": Run in an ephemeral isolated session (requires payload.kind=\"agentTurn\")\n- \"current\": Bind to the current session where the cron is created (resolved at creation time)\n- \"session:<custom-id>\": Run in a persistent named session (e.g., \"session:project-alpha-daily\")\n\nDEFAULT BEHAVIOR (unchanged for backward compatibility):\n- payload.kind=\"systemEvent\" → defaults to \"main\"\n- payload.kind=\"agentTurn\" → defaults to \"isolated\"\nTo use current session binding, explicitly set sessionTarget=\"current\".\n\nSCHEDULE TYPES (schedule.kind):\n- \"at\": One-shot at absolute time\n { \"kind\": \"at\", \"at\": \"<ISO-8601 timestamp>\" }\n- \"every\": Recurring interval\n { \"kind\": \"every\", \"everyMs\": <interval-ms>, \"anchorMs\": <optional-start-ms> }\n- \"cron\": Cron expression evaluated in the supplied timezone, or the Gateway host local timezone when tz is omitted\n { \"kind\": \"cron\", \"expr\": \"<cron-expression>\", \"tz\": \"<optional-IANA-timezone>\" }\n Write expr in the selected timezone's local wall-clock time; do not convert the requested local time to UTC first.\n If tz is omitted, do not assume UTC; the Gateway host local timezone is used.\n Example: \"Remind me every day at 6pm Shanghai time\" -> { \"kind\": \"cron\", \"expr\": \"0 18 * * *\", \"tz\": \"Asia/Shanghai\" }\n\nFor schedule.kind=\"at\", ISO timestamps without an explicit timezone are treated as UTC.\n\nPAYLOAD TYPES (payload.kind):\n- \"systemEvent\": Injects text as system event into session\n { \"kind\": \"systemEvent\", \"text\": \"<message>\" }\n- \"agentTurn\": Runs agent with message (isolated sessions only)\n { \"kind\": \"agentTurn\", \"message\": \"<prompt>\", \"model\": \"<optional>\", \"thinking\": \"<optional>\", \"timeoutSeconds\": <optional, 0 means no timeout> }\n\nDELIVERY (top-level):\n { \"mode\": \"none|announce|webhook\", \"channel\": \"<optional>\", \"to\": \"<optional>\", \"threadId\": \"<optional>\", \"bestEffort\": <optional-bool> }\n - Default for isolated agentTurn jobs (when delivery omitted): \"announce\"\n - announce: send to chat channel (optional channel/to target)\n - threadId: chat thread/topic id for channels that support threaded delivery\n - webhook: send finished-run event as HTTP POST to delivery.to (URL required)\n - If the task needs to send to a specific chat/recipient, set announce delivery.channel/to; do not call messaging tools inside the run.\n\nCRITICAL CONSTRAINTS:\n- sessionTarget=\"main\" REQUIRES payload.kind=\"systemEvent\"\n- sessionTarget=\"isolated\" | \"current\" | \"session:xxx\" REQUIRES payload.kind=\"agentTurn\"\n- For webhook callbacks, use delivery.mode=\"webhook\" with delivery.to set to a URL.\nDefault: prefer isolated agentTurn jobs unless the user explicitly wants current-session binding.\n\nRESTRICTED CRON RUNS:\n- Some isolated cron runs receive a narrow cron grant for self-cleanup. In that mode, read-only status and list are for self-introspection only, runs (job run history) is allowed for the current job only, and mutation actions remain limited to removing the current cron job.\n\nWAKE MODES (for wake action):\n- \"next-heartbeat\" (default): Wake on next heartbeat\n- \"now\": Wake immediately\n\nUse jobId as the canonical identifier; id is accepted for compatibility. Use contextMessages (0-10) to add previous messages as context to the job text.",
"description": "Manage Gateway cron jobs (status/list/get/add/update/remove/run/runs) and send wake events. Use this for reminders, \"check back later\" requests, delayed follow-ups, and recurring tasks. Do not emulate scheduling with exec sleep or process polling.\n\nMain-session cron jobs enqueue system events for heartbeat handling. Isolated cron jobs create background task runs that appear in `openclaw tasks`.\n\nACTIONS:\n- status: Check cron scheduler status\n- list: List jobs (use includeDisabled:true to include disabled; agentId filters by agent, auto-filled from session)\n- get: Get one job by id (requires jobId)\n- add: Create job (requires job object, see schema below)\n- update: Modify job (requires jobId + patch object)\n- remove: Delete job (requires jobId)\n- run: Trigger job immediately (requires jobId)\n- runs: Get job run history (requires jobId)\n- wake: Send wake event (requires text, optional mode)\n\nJOB SCHEMA (for add action):\n{\n \"name\": \"string (optional)\",\n \"schedule\": { ... }, // Required: when to run\n \"payload\": { ... }, // Required: what to execute\n \"delivery\": { ... }, // Optional: announce summary (isolated/current/session:xxx only) or webhook POST\n \"sessionTarget\": \"main\" | \"isolated\" | \"current\" | \"session:<custom-id>\", // Optional, defaults based on context\n \"enabled\": true | false // Optional, default true\n}\n\nSESSION TARGET OPTIONS:\n- \"main\": Run in the main session (requires payload.kind=\"systemEvent\")\n- \"isolated\": Run in an ephemeral isolated session (requires payload.kind=\"agentTurn\")\n- \"current\": Bind to the current session where the cron is created (resolved at creation time)\n- \"session:<custom-id>\": Run in a persistent named session (e.g., \"session:project-alpha-daily\")\n\nDEFAULT BEHAVIOR (unchanged for backward compatibility):\n- payload.kind=\"systemEvent\" → defaults to \"main\"\n- payload.kind=\"agentTurn\" → defaults to \"isolated\"\nTo use current session binding, explicitly set sessionTarget=\"current\".\n\nSCHEDULE TYPES (schedule.kind):\n- \"at\": One-shot at absolute time\n { \"kind\": \"at\", \"at\": \"<ISO-8601 timestamp>\" }\n- \"every\": Recurring interval\n { \"kind\": \"every\", \"everyMs\": <interval-ms>, \"anchorMs\": <optional-start-ms> }\n- \"cron\": Cron expression evaluated in the supplied timezone, or the Gateway host local timezone when tz is omitted\n { \"kind\": \"cron\", \"expr\": \"<cron-expression>\", \"tz\": \"<optional-IANA-timezone>\" }\n Write expr in the selected timezone's local wall-clock time; do not convert the requested local time to UTC first.\n If tz is omitted, do not assume UTC; the Gateway host local timezone is used.\n Example: \"Remind me every day at 6pm Shanghai time\" -> { \"kind\": \"cron\", \"expr\": \"0 18 * * *\", \"tz\": \"Asia/Shanghai\" }\n\nFor schedule.kind=\"at\", ISO timestamps without an explicit timezone are treated as UTC.\n\nPAYLOAD TYPES (payload.kind):\n- \"systemEvent\": Injects text as system event into session\n { \"kind\": \"systemEvent\", \"text\": \"<message>\" }\n- \"agentTurn\": Runs agent with message (isolated sessions only)\n { \"kind\": \"agentTurn\", \"message\": \"<prompt>\", \"model\": \"<optional>\", \"thinking\": \"<optional>\", \"timeoutSeconds\": <optional, 0 means no timeout> }\n\nDELIVERY (top-level):\n { \"mode\": \"none|announce|webhook\", \"channel\": \"<optional>\", \"to\": \"<optional>\", \"threadId\": \"<optional>\", \"bestEffort\": <optional-bool> }\n - Default for isolated agentTurn jobs (when delivery omitted): \"announce\"\n - announce: send to chat channel (optional channel/to target)\n - threadId: chat thread/topic id for channels that support threaded delivery\n - webhook: send finished-run event as HTTP POST to delivery.to (URL required)\n - If the task needs to send to a specific chat/recipient, set announce delivery.channel/to; do not call messaging tools inside the run.\n\nCRITICAL CONSTRAINTS:\n- sessionTarget=\"main\" REQUIRES payload.kind=\"systemEvent\"\n- sessionTarget=\"isolated\" | \"current\" | \"session:xxx\" REQUIRES payload.kind=\"agentTurn\"\n- For webhook callbacks, use delivery.mode=\"webhook\" with delivery.to set to a URL.\nDefault: prefer isolated agentTurn jobs unless the user explicitly wants current-session binding.\n\nRESTRICTED CRON RUNS:\n- Some isolated cron runs receive a narrow cron grant for self-cleanup. In that mode, read-only status and list are for self-introspection only, get/runs are allowed for the current job only, and mutation actions remain limited to removing the current cron job.\n\nWAKE MODES (for wake action):\n- \"next-heartbeat\" (default): Wake on next heartbeat\n- \"now\": Wake immediately\n\nUse jobId as the canonical identifier; id is accepted for compatibility. Use contextMessages (0-10) to add previous messages as context to the job text.",
"inputSchema": {
"additionalProperties": true,
"properties": {
"action": {
"enum": ["status", "list", "add", "update", "remove", "run", "runs", "wake"],
"enum": ["status", "list", "get", "add", "update", "remove", "run", "runs", "wake"],
"type": "string"
},
"agentId": {

View File

@@ -137,12 +137,12 @@
},
{
"deferLoading": true,
"description": "Manage Gateway cron jobs (status/list/add/update/remove/run/runs) and send wake events. Use this for reminders, \"check back later\" requests, delayed follow-ups, and recurring tasks. Do not emulate scheduling with exec sleep or process polling.\n\nMain-session cron jobs enqueue system events for heartbeat handling. Isolated cron jobs create background task runs that appear in `openclaw tasks`.\n\nACTIONS:\n- status: Check cron scheduler status\n- list: List jobs (use includeDisabled:true to include disabled; agentId filters by agent, auto-filled from session)\n- add: Create job (requires job object, see schema below)\n- update: Modify job (requires jobId + patch object)\n- remove: Delete job (requires jobId)\n- run: Trigger job immediately (requires jobId)\n- runs: Get job run history (requires jobId)\n- wake: Send wake event (requires text, optional mode)\n\nJOB SCHEMA (for add action):\n{\n \"name\": \"string (optional)\",\n \"schedule\": { ... }, // Required: when to run\n \"payload\": { ... }, // Required: what to execute\n \"delivery\": { ... }, // Optional: announce summary (isolated/current/session:xxx only) or webhook POST\n \"sessionTarget\": \"main\" | \"isolated\" | \"current\" | \"session:<custom-id>\", // Optional, defaults based on context\n \"enabled\": true | false // Optional, default true\n}\n\nSESSION TARGET OPTIONS:\n- \"main\": Run in the main session (requires payload.kind=\"systemEvent\")\n- \"isolated\": Run in an ephemeral isolated session (requires payload.kind=\"agentTurn\")\n- \"current\": Bind to the current session where the cron is created (resolved at creation time)\n- \"session:<custom-id>\": Run in a persistent named session (e.g., \"session:project-alpha-daily\")\n\nDEFAULT BEHAVIOR (unchanged for backward compatibility):\n- payload.kind=\"systemEvent\" → defaults to \"main\"\n- payload.kind=\"agentTurn\" → defaults to \"isolated\"\nTo use current session binding, explicitly set sessionTarget=\"current\".\n\nSCHEDULE TYPES (schedule.kind):\n- \"at\": One-shot at absolute time\n { \"kind\": \"at\", \"at\": \"<ISO-8601 timestamp>\" }\n- \"every\": Recurring interval\n { \"kind\": \"every\", \"everyMs\": <interval-ms>, \"anchorMs\": <optional-start-ms> }\n- \"cron\": Cron expression evaluated in the supplied timezone, or the Gateway host local timezone when tz is omitted\n { \"kind\": \"cron\", \"expr\": \"<cron-expression>\", \"tz\": \"<optional-IANA-timezone>\" }\n Write expr in the selected timezone's local wall-clock time; do not convert the requested local time to UTC first.\n If tz is omitted, do not assume UTC; the Gateway host local timezone is used.\n Example: \"Remind me every day at 6pm Shanghai time\" -> { \"kind\": \"cron\", \"expr\": \"0 18 * * *\", \"tz\": \"Asia/Shanghai\" }\n\nFor schedule.kind=\"at\", ISO timestamps without an explicit timezone are treated as UTC.\n\nPAYLOAD TYPES (payload.kind):\n- \"systemEvent\": Injects text as system event into session\n { \"kind\": \"systemEvent\", \"text\": \"<message>\" }\n- \"agentTurn\": Runs agent with message (isolated sessions only)\n { \"kind\": \"agentTurn\", \"message\": \"<prompt>\", \"model\": \"<optional>\", \"thinking\": \"<optional>\", \"timeoutSeconds\": <optional, 0 means no timeout> }\n\nDELIVERY (top-level):\n { \"mode\": \"none|announce|webhook\", \"channel\": \"<optional>\", \"to\": \"<optional>\", \"threadId\": \"<optional>\", \"bestEffort\": <optional-bool> }\n - Default for isolated agentTurn jobs (when delivery omitted): \"announce\"\n - announce: send to chat channel (optional channel/to target)\n - threadId: chat thread/topic id for channels that support threaded delivery\n - webhook: send finished-run event as HTTP POST to delivery.to (URL required)\n - If the task needs to send to a specific chat/recipient, set announce delivery.channel/to; do not call messaging tools inside the run.\n\nCRITICAL CONSTRAINTS:\n- sessionTarget=\"main\" REQUIRES payload.kind=\"systemEvent\"\n- sessionTarget=\"isolated\" | \"current\" | \"session:xxx\" REQUIRES payload.kind=\"agentTurn\"\n- For webhook callbacks, use delivery.mode=\"webhook\" with delivery.to set to a URL.\nDefault: prefer isolated agentTurn jobs unless the user explicitly wants current-session binding.\n\nRESTRICTED CRON RUNS:\n- Some isolated cron runs receive a narrow cron grant for self-cleanup. In that mode, read-only status and list are for self-introspection only, runs (job run history) is allowed for the current job only, and mutation actions remain limited to removing the current cron job.\n\nWAKE MODES (for wake action):\n- \"next-heartbeat\" (default): Wake on next heartbeat\n- \"now\": Wake immediately\n\nUse jobId as the canonical identifier; id is accepted for compatibility. Use contextMessages (0-10) to add previous messages as context to the job text.",
"description": "Manage Gateway cron jobs (status/list/get/add/update/remove/run/runs) and send wake events. Use this for reminders, \"check back later\" requests, delayed follow-ups, and recurring tasks. Do not emulate scheduling with exec sleep or process polling.\n\nMain-session cron jobs enqueue system events for heartbeat handling. Isolated cron jobs create background task runs that appear in `openclaw tasks`.\n\nACTIONS:\n- status: Check cron scheduler status\n- list: List jobs (use includeDisabled:true to include disabled; agentId filters by agent, auto-filled from session)\n- get: Get one job by id (requires jobId)\n- add: Create job (requires job object, see schema below)\n- update: Modify job (requires jobId + patch object)\n- remove: Delete job (requires jobId)\n- run: Trigger job immediately (requires jobId)\n- runs: Get job run history (requires jobId)\n- wake: Send wake event (requires text, optional mode)\n\nJOB SCHEMA (for add action):\n{\n \"name\": \"string (optional)\",\n \"schedule\": { ... }, // Required: when to run\n \"payload\": { ... }, // Required: what to execute\n \"delivery\": { ... }, // Optional: announce summary (isolated/current/session:xxx only) or webhook POST\n \"sessionTarget\": \"main\" | \"isolated\" | \"current\" | \"session:<custom-id>\", // Optional, defaults based on context\n \"enabled\": true | false // Optional, default true\n}\n\nSESSION TARGET OPTIONS:\n- \"main\": Run in the main session (requires payload.kind=\"systemEvent\")\n- \"isolated\": Run in an ephemeral isolated session (requires payload.kind=\"agentTurn\")\n- \"current\": Bind to the current session where the cron is created (resolved at creation time)\n- \"session:<custom-id>\": Run in a persistent named session (e.g., \"session:project-alpha-daily\")\n\nDEFAULT BEHAVIOR (unchanged for backward compatibility):\n- payload.kind=\"systemEvent\" → defaults to \"main\"\n- payload.kind=\"agentTurn\" → defaults to \"isolated\"\nTo use current session binding, explicitly set sessionTarget=\"current\".\n\nSCHEDULE TYPES (schedule.kind):\n- \"at\": One-shot at absolute time\n { \"kind\": \"at\", \"at\": \"<ISO-8601 timestamp>\" }\n- \"every\": Recurring interval\n { \"kind\": \"every\", \"everyMs\": <interval-ms>, \"anchorMs\": <optional-start-ms> }\n- \"cron\": Cron expression evaluated in the supplied timezone, or the Gateway host local timezone when tz is omitted\n { \"kind\": \"cron\", \"expr\": \"<cron-expression>\", \"tz\": \"<optional-IANA-timezone>\" }\n Write expr in the selected timezone's local wall-clock time; do not convert the requested local time to UTC first.\n If tz is omitted, do not assume UTC; the Gateway host local timezone is used.\n Example: \"Remind me every day at 6pm Shanghai time\" -> { \"kind\": \"cron\", \"expr\": \"0 18 * * *\", \"tz\": \"Asia/Shanghai\" }\n\nFor schedule.kind=\"at\", ISO timestamps without an explicit timezone are treated as UTC.\n\nPAYLOAD TYPES (payload.kind):\n- \"systemEvent\": Injects text as system event into session\n { \"kind\": \"systemEvent\", \"text\": \"<message>\" }\n- \"agentTurn\": Runs agent with message (isolated sessions only)\n { \"kind\": \"agentTurn\", \"message\": \"<prompt>\", \"model\": \"<optional>\", \"thinking\": \"<optional>\", \"timeoutSeconds\": <optional, 0 means no timeout> }\n\nDELIVERY (top-level):\n { \"mode\": \"none|announce|webhook\", \"channel\": \"<optional>\", \"to\": \"<optional>\", \"threadId\": \"<optional>\", \"bestEffort\": <optional-bool> }\n - Default for isolated agentTurn jobs (when delivery omitted): \"announce\"\n - announce: send to chat channel (optional channel/to target)\n - threadId: chat thread/topic id for channels that support threaded delivery\n - webhook: send finished-run event as HTTP POST to delivery.to (URL required)\n - If the task needs to send to a specific chat/recipient, set announce delivery.channel/to; do not call messaging tools inside the run.\n\nCRITICAL CONSTRAINTS:\n- sessionTarget=\"main\" REQUIRES payload.kind=\"systemEvent\"\n- sessionTarget=\"isolated\" | \"current\" | \"session:xxx\" REQUIRES payload.kind=\"agentTurn\"\n- For webhook callbacks, use delivery.mode=\"webhook\" with delivery.to set to a URL.\nDefault: prefer isolated agentTurn jobs unless the user explicitly wants current-session binding.\n\nRESTRICTED CRON RUNS:\n- Some isolated cron runs receive a narrow cron grant for self-cleanup. In that mode, read-only status and list are for self-introspection only, get/runs are allowed for the current job only, and mutation actions remain limited to removing the current cron job.\n\nWAKE MODES (for wake action):\n- \"next-heartbeat\" (default): Wake on next heartbeat\n- \"now\": Wake immediately\n\nUse jobId as the canonical identifier; id is accepted for compatibility. Use contextMessages (0-10) to add previous messages as context to the job text.",
"inputSchema": {
"additionalProperties": true,
"properties": {
"action": {
"enum": ["status", "list", "add", "update", "remove", "run", "runs", "wake"],
"enum": ["status", "list", "get", "add", "update", "remove", "run", "runs", "wake"],
"type": "string"
},
"agentId": {

View File

@@ -217,8 +217,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
"roughTokens": 140
},
"dynamicToolsJson": {
"chars": 43000,
"roughTokens": 10750
"chars": 43053,
"roughTokens": 10764
},
"openClawDeveloperInstructions": {
"chars": 5436,
@@ -229,8 +229,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
"roughTokens": 7129
},
"totalWithDynamicToolsJson": {
"chars": 71518,
"roughTokens": 17880
"chars": 71571,
"roughTokens": 17893
},
"userInputText": {
"chars": 870,

View File

@@ -217,8 +217,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
"roughTokens": 140
},
"dynamicToolsJson": {
"chars": 42691,
"roughTokens": 10673
"chars": 42744,
"roughTokens": 10686
},
"openClawDeveloperInstructions": {
"chars": 4412,
@@ -229,8 +229,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
"roughTokens": 6748
},
"totalWithDynamicToolsJson": {
"chars": 69685,
"roughTokens": 17422
"chars": 69738,
"roughTokens": 17435
},
"userInputText": {
"chars": 370,

View File

@@ -218,8 +218,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
"roughTokens": 140
},
"dynamicToolsJson": {
"chars": 43869,
"roughTokens": 10968
"chars": 43922,
"roughTokens": 10981
},
"openClawDeveloperInstructions": {
"chars": 4412,
@@ -230,8 +230,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
"roughTokens": 7155
},
"totalWithDynamicToolsJson": {
"chars": 72490,
"roughTokens": 18123
"chars": 72543,
"roughTokens": 18136
},
"userInputText": {
"chars": 608,