import { Type, type TSchema } from "typebox"; import { loadConfig } from "../../config/config.js"; import { normalizeCronJobCreate, normalizeCronJobPatch } from "../../cron/normalize.js"; import type { CronDelivery, CronMessageChannel } from "../../cron/types.js"; import { normalizeHttpWebhookUrl } from "../../cron/webhook-url.js"; import { parseAgentSessionKey } from "../../sessions/session-key-utils.js"; import { extractTextFromChatContent } from "../../shared/chat-content.js"; import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
} from "../../shared/string-coerce.js"; import { isRecord, truncateUtf16Safe } from "../../utils.js"; import { resolveSessionAgentId } from "../agent-scope.js"; import { optionalStringEnum, stringEnum } from "../schema/typebox.js"; import { CRON_TOOL_DISPLAY_SUMMARY } from "../tool-description-presets.js"; import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js"; import { callGatewayTool, readGatewayCallOptions, type GatewayCallOptions } from "./gateway.js"; import { isOpenClawOwnerOnlyCoreToolName } from "./owner-only-tools.js"; import { resolveInternalSessionKey, resolveMainSessionAlias } from "./sessions-helpers.js";
// We spell out job/patch properties so that LLMs know what fields to send. // Nested unions are avoided; runtime validation happens in normalizeCronJob*.
// Omitting `failureAlert` means "leave defaults/unchanged"; `false` explicitly disables alerts. // Runtime handles `failureAlert === false` in cron/service/timer.ts. // The schema declares `type: "object"` to stay compatible with providers that // enforce an OpenAPI 3.0 subset (e.g. Gemini via GitHub Copilot). The // description tells the LLM that `false` is also accepted. const CronFailureAlertSchema = Type.Optional(
Type.Unsafe<Record<string, unknown> | false>({
type: "object",
properties: {
after: Type.Optional(Type.Number({ description: "Failures before alerting" })),
channel: Type.Optional(Type.String({ description: "Alert channel" })),
to: Type.Optional(Type.String({ description: "Alert target" })),
cooldownMs: Type.Optional(Type.Number({ description: "Cooldown between alerts in ms" })),
mode: optionalStringEnum(["announce", "webhook"] as const),
accountId: Type.Optional(Type.String()),
},
additionalProperties: true,
description: "Failure alert config object, or the boolean value false to disable alerts for this job",
}),
);
const CronJobObjectSchema = Type.Optional(
Type.Object(
{
name: Type.Optional(Type.String({ description: "Job name" })),
schedule: CronScheduleSchema,
sessionTarget: Type.Optional(
Type.String({
description: 'Session target: "main", "isolated", "current", or "session:<id>"',
}),
),
wakeMode: optionalStringEnum(CRON_WAKE_MODES, { description: "When to wake the session" }),
payload: CronPayloadSchema,
delivery: CronDeliverySchema,
agentId: nullableStringSchema("Agent id, or null to keep it unset"),
description: Type.Optional(Type.String({ description: "Human-readable description" })),
enabled: Type.Optional(Type.Boolean()),
deleteAfterRun: Type.Optional(Type.Boolean({ description: "Delete after first execution" })),
sessionKey: nullableStringSchema("Explicit session key, or null to clear it"),
failureAlert: CronFailureAlertSchema,
},
{ additionalProperties: true },
),
);
function inferDeliveryFromSessionKey(agentSessionKey?: string): CronDelivery | null { const rawSessionKey = agentSessionKey?.trim(); if (!rawSessionKey) { returnnull;
} const parsed = parseAgentSessionKey(stripThreadSuffixFromSessionKey(rawSessionKey)); if (!parsed || !parsed.rest) { returnnull;
} const parts = parsed.rest.split(":").filter(Boolean); if (parts.length === 0) { returnnull;
} const head = normalizeOptionalLowercaseString(parts[0]); if (!head || head === "main" || head === "subagent" || head === "acp") { returnnull;
}
// buildAgentPeerSessionKey encodes peers as: // - direct:<peerId> // - <channel>:direct:<peerId> // - <channel>:<accountId>:direct:<peerId> // - <channel>:group:<peerId> // - <channel>:channel:<peerId> // Note: legacy keys may use "dm" instead of "direct". // Threaded sessions append :thread:<id>, which we strip so delivery targets the parent peer. // NOTE: Telegram forum topics encode as <chatId>:topic:<topicId> and should be preserved. const markerIndex = parts.findIndex(
(part) => part === "direct" || part === "dm" || part === "group" || part === "channel",
); if (markerIndex === -1) { returnnull;
} const peerId = parts
.slice(markerIndex + 1)
.join(":")
.trim(); if (!peerId) { returnnull;
}
let channel: CronMessageChannel | undefined; if (markerIndex >= 1) {
channel = normalizeOptionalLowercaseString(parts[0]) as CronMessageChannel | undefined;
}
const delivery: CronDelivery = { mode: "announce", to: peerId }; if (channel) {
delivery.channel = channel;
} return delivery;
}
export function createCronTool(opts?: CronToolOptions, deps?: CronToolDeps): AnyAgentTool { const callGateway = deps?.callGatewayTool ?? callGatewayTool; return {
label: "Cron",
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 thisfor 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)
- add: Create job (requires job object, see schema below)
- update: Modify job (requires jobId + patch object)
- remove: Delete job (requires jobId)
- run: Trigger job immediately (requires jobId)
- runs: Get job run history (requires jobId)
- wake: Send wake event (requires text, optional mode)
JOB SCHEMA (for add action):
{ "name": "string (optional)", "schedule": { ... }, // Required: when to run "payload": { ... }, // Required: what to execute "delivery": { ... }, // Optional: announce summary (isolated/current/session:xxx only) or webhook POST "sessionTarget": "main" | "isolated" | "current" | "session:<custom-id>", // Optional, defaults based on context "enabled": true | false// Optional, default true
}
SESSION TARGET OPTIONS:
- "main": Run in the main session (requires payload.kind="systemEvent")
- "isolated": Run in an ephemeral isolated session (requires payload.kind="agentTurn")
- "current": Bind to the current session where the cron is created (resolved at creation time)
- "session:<custom-id>": Run in a persistent named session (e.g., "session:project-alpha-daily")
DEFAULT BEHAVIOR (unchanged for backward compatibility):
- payload.kind="systemEvent" → defaults to "main"
- payload.kind="agentTurn" → defaults to "isolated"
To use current session binding, explicitly set sessionTarget="current".
ISO timestamps without an explicit timezone are treated as UTC.
PAYLOAD TYPES (payload.kind):
- "systemEvent": Injects text as system event into session
{ "kind": "systemEvent", "text": "<message>" }
- "agentTurn": Runs agent with message (isolated sessions only)
{ "kind": "agentTurn", "message": "<prompt>", "model": "<optional>", "thinking": "<optional>", "timeoutSeconds": <optional, 0 means no timeout> }
DELIVERY (top-level):
{ "mode": "none|announce|webhook", "channel": "<optional>", "to": "<optional>", "bestEffort": <optional-bool> }
- Defaultfor isolated agentTurn jobs (when delivery omitted): "announce"
- announce: send to chat channel (optional channel/to target)
- webhook: send finished-run event as HTTP POST to delivery.to (URL required)
- If the task needs to send to a specific chat/recipient, set announce delivery.channel/to; donot call messaging tools inside the run.
CRITICAL CONSTRAINTS:
- sessionTarget="main" REQUIRES payload.kind="systemEvent"
- sessionTarget="isolated" | "current" | "session:xxx" REQUIRES payload.kind="agentTurn"
- For webhook callbacks, use delivery.mode="webhook" with delivery.to set to a URL. Default: prefer isolated agentTurn jobs unless the user explicitly wants current-session binding.
WAKE MODES (for wake action):
- "next-heartbeat" (default): Wake on next heartbeat
- "now": Wake immediately
Use jobId as the canonical identifier; id is accepted for compatibility. Use contextMessages (0-10) to add previous messages as context to the job text.`,
parameters: CronToolSchema,
execute: async (_toolCallId, args) => { const params = args as Record<string, unknown>; const action = readStringParam(params, "action", { required: true }); const gatewayOpts: GatewayCallOptions = {
...readGatewayCallOptions(params),
timeoutMs: typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs)
? params.timeoutMs
: 60_000,
};
switch (action) { case"status": return jsonResult(await callGateway("cron.status", gatewayOpts, {})); case"list": return jsonResult(
await callGateway("cron.list", gatewayOpts, {
includeDisabled: Boolean(params.includeDisabled),
}),
); case"add": { // Flat-params recovery: non-frontier models (e.g. Grok) sometimes flatten // job properties to the top level alongside `action` instead of nesting // them inside `job`. When `params.job` is missing or empty, reconstruct // a synthetic job object from any recognised top-level job fields. // See: https://github.com/openclaw/openclaw/issues/11310 if (isMissingOrEmptyObject(params.job)) { const synthetic = recoverCronObjectFromFlatParams(params); // Only use the synthetic job if at least one meaningful field is present // (schedule, payload, message, or text are the minimum signals that the // LLM intended to create a job). if (synthetic.found && hasCronCreateSignal(synthetic.value)) {
params.job = synthetic.value;
}
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.