feat: cron ISO at + delete-after-run

This commit is contained in:
Peter Steinberger
2026-01-13 04:55:39 +00:00
parent 8f105288d2
commit 75a7855223
17 changed files with 221 additions and 17 deletions

View File

@@ -1,5 +1,10 @@
# Changelog # Changelog
## 2026.1.13-1
### Changes
- Cron: accept ISO timestamps for one-shot schedules (UTC) and allow optional delete-after-run; wired into CLI + macOS editor.
## 2026.1.12-4 ## 2026.1.12-4
### Changes ### Changes

View File

@@ -15,6 +15,7 @@ extension CronJobEditor {
self.description = job.description ?? "" self.description = job.description ?? ""
self.agentId = job.agentId ?? "" self.agentId = job.agentId ?? ""
self.enabled = job.enabled self.enabled = job.enabled
self.deleteAfterRun = job.deleteAfterRun ?? false
self.sessionTarget = job.sessionTarget self.sessionTarget = job.sessionTarget
self.wakeMode = job.wakeMode self.wakeMode = job.wakeMode
@@ -149,6 +150,11 @@ extension CronJobEditor {
"wakeMode": self.wakeMode.rawValue, "wakeMode": self.wakeMode.rawValue,
"payload": payload, "payload": payload,
] ]
if self.scheduleKind == .at {
root["deleteAfterRun"] = self.deleteAfterRun
} else if self.job?.deleteAfterRun != nil {
root["deleteAfterRun"] = false
}
if !description.isEmpty { root["description"] = description } if !description.isEmpty { root["description"] = description }
if !agentId.isEmpty { if !agentId.isEmpty {
root["agentId"] = agentId root["agentId"] = agentId

View File

@@ -31,6 +31,7 @@ struct CronJobEditor: View {
@State var enabled: Bool = true @State var enabled: Bool = true
@State var sessionTarget: CronSessionTarget = .main @State var sessionTarget: CronSessionTarget = .main
@State var wakeMode: CronWakeMode = .nextHeartbeat @State var wakeMode: CronWakeMode = .nextHeartbeat
@State var deleteAfterRun: Bool = false
enum ScheduleKind: String, CaseIterable, Identifiable { case at, every, cron; var id: String { rawValue } } enum ScheduleKind: String, CaseIterable, Identifiable { case at, every, cron; var id: String { rawValue } }
@State var scheduleKind: ScheduleKind = .every @State var scheduleKind: ScheduleKind = .every
@@ -156,6 +157,11 @@ struct CronJobEditor: View {
.labelsHidden() .labelsHidden()
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
} }
GridRow {
self.gridLabel("Auto-delete")
Toggle("Delete after successful run", isOn: self.$deleteAfterRun)
.toggleStyle(.switch)
}
case .every: case .every:
GridRow { GridRow {
self.gridLabel("Every") self.gridLabel("Every")

View File

@@ -149,6 +149,7 @@ struct CronJob: Identifiable, Codable, Equatable {
var name: String var name: String
var description: String? var description: String?
var enabled: Bool var enabled: Bool
var deleteAfterRun: Bool?
let createdAtMs: Int let createdAtMs: Int
let updatedAtMs: Int let updatedAtMs: Int
let schedule: CronSchedule let schedule: CronSchedule

View File

@@ -94,6 +94,9 @@ extension CronSettings {
func detailCard(_ job: CronJob) -> some View { func detailCard(_ job: CronJob) -> some View {
VStack(alignment: .leading, spacing: 10) { VStack(alignment: .leading, spacing: 10) {
LabeledContent("Schedule") { Text(self.scheduleSummary(job.schedule)).font(.callout) } LabeledContent("Schedule") { Text(self.scheduleSummary(job.schedule)).font(.callout) }
if case .at = job.schedule, job.deleteAfterRun == true {
LabeledContent("Auto-delete") { Text("after success") }
}
if let desc = job.description, !desc.isEmpty { if let desc = job.description, !desc.isEmpty {
LabeledContent("Description") { Text(desc).font(.callout) } LabeledContent("Description") { Text(desc).font(.callout) }
} }

View File

@@ -11,6 +11,7 @@ struct CronSettings_Previews: PreviewProvider {
name: "Daily summary", name: "Daily summary",
description: nil, description: nil,
enabled: true, enabled: true,
deleteAfterRun: nil,
createdAtMs: 0, createdAtMs: 0,
updatedAtMs: 0, updatedAtMs: 0,
schedule: .every(everyMs: 86_400_000, anchorMs: nil), schedule: .every(everyMs: 86_400_000, anchorMs: nil),
@@ -64,6 +65,7 @@ extension CronSettings {
name: "Daily summary", name: "Daily summary",
description: "Summary job", description: "Summary job",
enabled: true, enabled: true,
deleteAfterRun: nil,
createdAtMs: 1_700_000_000_000, createdAtMs: 1_700_000_000_000,
updatedAtMs: 1_700_000_100_000, updatedAtMs: 1_700_000_100_000,
schedule: .cron(expr: "0 8 * * *", tz: "UTC"), schedule: .cron(expr: "0 8 * * *", tz: "UTC"),

View File

@@ -20,6 +20,24 @@ cron is the mechanism.
- **Isolated**: run a dedicated agent turn in `cron:<jobId>`, optionally deliver output. - **Isolated**: run a dedicated agent turn in `cron:<jobId>`, optionally deliver output.
- Wakeups are first-class: a job can request “wake now” vs “next heartbeat”. - Wakeups are first-class: a job can request “wake now” vs “next heartbeat”.
## Beginner-friendly overview
Think of a cron job as: **when** to run + **what** to do.
1) **Choose a schedule**
- One-shot reminder → `schedule.kind = "at"` (CLI: `--at`)
- Repeating job → `schedule.kind = "every"` or `schedule.kind = "cron"`
- If your ISO timestamp omits a timezone, it is treated as **UTC**.
2) **Choose where it runs**
- `sessionTarget: "main"` → run during the next heartbeat with main context.
- `sessionTarget: "isolated"` → run a dedicated agent turn in `cron:<jobId>`.
3) **Choose the payload**
- Main session → `payload.kind = "systemEvent"`
- Isolated session → `payload.kind = "agentTurn"`
Optional: `deleteAfterRun: true` removes successful one-shot jobs from the store.
## Concepts ## Concepts
### Jobs ### Jobs
@@ -32,10 +50,11 @@ A cron job is a stored record with:
Jobs are identified by a stable `jobId` (used by CLI/Gateway APIs). Jobs are identified by a stable `jobId` (used by CLI/Gateway APIs).
In agent tool calls, `jobId` is canonical; legacy `id` is accepted for compatibility. In agent tool calls, `jobId` is canonical; legacy `id` is accepted for compatibility.
Jobs can optionally auto-delete after a successful one-shot run via `deleteAfterRun: true`.
### Schedules ### Schedules
Cron supports three schedule kinds: Cron supports three schedule kinds:
- `at`: one-shot timestamp (ms since epoch). - `at`: one-shot timestamp (ms since epoch). Gateway accepts ISO 8601 and coerces to UTC.
- `every`: fixed interval (ms). - `every`: fixed interval (ms).
- `cron`: 5-field cron expression with optional IANA timezone. - `cron`: 5-field cron expression with optional IANA timezone.
@@ -143,6 +162,17 @@ Disable cron entirely:
## CLI quickstart ## CLI quickstart
One-shot reminder (UTC ISO, auto-delete after success):
```bash
clawdbot cron add \
--name "Send reminder" \
--at "2026-01-12T18:00:00Z" \
--session main \
--system-event "Reminder: submit expense report." \
--wake now \
--delete-after-run
```
One-shot reminder (main session, wake immediately): One-shot reminder (main session, wake immediately):
```bash ```bash
clawdbot cron add \ clawdbot cron add \

View File

@@ -60,7 +60,7 @@ export function buildAgentSystemPrompt(params: {
browser: "Control web browser", browser: "Control web browser",
canvas: "Present/eval/snapshot the Canvas", canvas: "Present/eval/snapshot the Canvas",
nodes: "List/describe/notify/camera/screen on paired nodes", nodes: "List/describe/notify/camera/screen on paired nodes",
cron: "Manage cron jobs and wake events", cron: "Manage cron jobs and wake events (use for reminders)",
message: "Send messages and provider actions", message: "Send messages and provider actions",
gateway: gateway:
"Restart, apply config, or run updates on the running Clawdbot process", "Restart, apply config, or run updates on the running Clawdbot process",
@@ -211,7 +211,7 @@ export function buildAgentSystemPrompt(params: {
"- browser: control clawd's dedicated browser", "- browser: control clawd's dedicated browser",
"- canvas: present/eval/snapshot the Canvas", "- canvas: present/eval/snapshot the Canvas",
"- nodes: list/describe/notify/camera/screen on paired nodes", "- nodes: list/describe/notify/camera/screen on paired nodes",
"- cron: manage cron jobs and wake events", "- cron: manage cron jobs and wake events (use for reminders)",
"- sessions_list: list sessions", "- sessions_list: list sessions",
"- sessions_history: fetch session history", "- sessions_history: fetch session history",
"- sessions_send: send to another session", "- sessions_send: send to another session",

View File

@@ -1,4 +1,5 @@
import type { Command } from "commander"; import type { Command } from "commander";
import { parseAbsoluteTimeMs } from "../cron/parse.js";
import type { CronJob, CronSchedule } from "../cron/types.js"; import type { CronJob, CronSchedule } from "../cron/types.js";
import { danger } from "../globals.js"; import { danger } from "../globals.js";
import { PROVIDER_IDS } from "../providers/registry.js"; import { PROVIDER_IDS } from "../providers/registry.js";
@@ -57,10 +58,8 @@ function parseDurationMs(input: string): number | null {
function parseAtMs(input: string): number | null { function parseAtMs(input: string): number | null {
const raw = input.trim(); const raw = input.trim();
if (!raw) return null; if (!raw) return null;
const asNum = Number(raw); const absolute = parseAbsoluteTimeMs(raw);
if (Number.isFinite(asNum) && asNum > 0) return Math.floor(asNum); if (absolute) return absolute;
const parsed = Date.parse(raw);
if (Number.isFinite(parsed)) return parsed;
const dur = parseDurationMs(raw); const dur = parseDurationMs(raw);
if (dur) return Date.now() + dur; if (dur) return Date.now() + dur;
return null; return null;
@@ -294,6 +293,7 @@ export function registerCronCli(program: Command) {
.requiredOption("--name <name>", "Job name") .requiredOption("--name <name>", "Job name")
.option("--description <text>", "Optional description") .option("--description <text>", "Optional description")
.option("--disabled", "Create job disabled", false) .option("--disabled", "Create job disabled", false)
.option("--delete-after-run", "Delete one-shot job after it succeeds", false)
.option("--agent <id>", "Agent id for this job") .option("--agent <id>", "Agent id for this job")
.option("--session <target>", "Session target (main|isolated)", "main") .option("--session <target>", "Session target (main|isolated)", "main")
.option( .option(
@@ -468,6 +468,7 @@ export function registerCronCli(program: Command) {
name, name,
description, description,
enabled: !opts.disabled, enabled: !opts.disabled,
deleteAfterRun: Boolean(opts.deleteAfterRun),
agentId, agentId,
schedule, schedule,
sessionTarget, sessionTarget,
@@ -578,6 +579,8 @@ export function registerCronCli(program: Command) {
.option("--description <text>", "Set description") .option("--description <text>", "Set description")
.option("--enable", "Enable job", false) .option("--enable", "Enable job", false)
.option("--disable", "Disable job", false) .option("--disable", "Disable job", false)
.option("--delete-after-run", "Delete one-shot job after it succeeds", false)
.option("--keep-after-run", "Keep one-shot job after it succeeds", false)
.option("--session <target>", "Session target (main|isolated)") .option("--session <target>", "Session target (main|isolated)")
.option("--agent <id>", "Set agent id") .option("--agent <id>", "Set agent id")
.option("--clear-agent", "Unset agent and use default", false) .option("--clear-agent", "Unset agent and use default", false)
@@ -630,6 +633,13 @@ export function registerCronCli(program: Command) {
throw new Error("Choose --enable or --disable, not both"); throw new Error("Choose --enable or --disable, not both");
if (opts.enable) patch.enabled = true; if (opts.enable) patch.enabled = true;
if (opts.disable) patch.enabled = false; if (opts.disable) patch.enabled = false;
if (opts.deleteAfterRun && opts.keepAfterRun) {
throw new Error(
"Choose --delete-after-run or --keep-after-run, not both",
);
}
if (opts.deleteAfterRun) patch.deleteAfterRun = true;
if (opts.keepAfterRun) patch.deleteAfterRun = false;
if (typeof opts.session === "string") if (typeof opts.session === "string")
patch.sessionTarget = opts.session; patch.sessionTarget = opts.session;
if (typeof opts.wake === "string") patch.wakeMode = opts.wake; if (typeof opts.wake === "string") patch.wakeMode = opts.wake;

View File

@@ -75,4 +75,40 @@ describe("normalizeCronJobCreate", () => {
const payload = normalized.payload as Record<string, unknown>; const payload = normalized.payload as Record<string, unknown>;
expect(payload.provider).toBe("telegram"); expect(payload.provider).toBe("telegram");
}); });
it("coerces ISO schedule.at to atMs (UTC)", () => {
const normalized = normalizeCronJobCreate({
name: "iso at",
enabled: true,
schedule: { at: "2026-01-12T18:00:00" },
sessionTarget: "main",
wakeMode: "next-heartbeat",
payload: {
kind: "systemEvent",
text: "hi",
},
}) as unknown as Record<string, unknown>;
const schedule = normalized.schedule as Record<string, unknown>;
expect(schedule.kind).toBe("at");
expect(schedule.atMs).toBe(Date.parse("2026-01-12T18:00:00Z"));
});
it("coerces ISO schedule.atMs string to atMs (UTC)", () => {
const normalized = normalizeCronJobCreate({
name: "iso atMs",
enabled: true,
schedule: { kind: "at", atMs: "2026-01-12T18:00:00" },
sessionTarget: "main",
wakeMode: "next-heartbeat",
payload: {
kind: "systemEvent",
text: "hi",
},
}) as unknown as Record<string, unknown>;
const schedule = normalized.schedule as Record<string, unknown>;
expect(schedule.kind).toBe("at");
expect(schedule.atMs).toBe(Date.parse("2026-01-12T18:00:00Z"));
});
}); });

View File

@@ -1,4 +1,5 @@
import { normalizeAgentId } from "../routing/session-key.js"; import { normalizeAgentId } from "../routing/session-key.js";
import { parseAbsoluteTimeMs } from "./parse.js";
import { migrateLegacyCronPayload } from "./payload-migration.js"; import { migrateLegacyCronPayload } from "./payload-migration.js";
import type { CronJobCreate, CronJobPatch } from "./types.js"; import type { CronJobCreate, CronJobPatch } from "./types.js";
@@ -19,11 +20,32 @@ function isRecord(value: unknown): value is UnknownRecord {
function coerceSchedule(schedule: UnknownRecord) { function coerceSchedule(schedule: UnknownRecord) {
const next: UnknownRecord = { ...schedule }; const next: UnknownRecord = { ...schedule };
const kind = typeof schedule.kind === "string" ? schedule.kind : undefined; const kind = typeof schedule.kind === "string" ? schedule.kind : undefined;
const atMsRaw = schedule.atMs;
const atRaw = schedule.at;
const parsedAtMs =
typeof atMsRaw === "string"
? parseAbsoluteTimeMs(atMsRaw)
: typeof atRaw === "string"
? parseAbsoluteTimeMs(atRaw)
: null;
if (!kind) { if (!kind) {
if (typeof schedule.atMs === "number") next.kind = "at"; if (
typeof schedule.atMs === "number" ||
typeof schedule.at === "string" ||
typeof schedule.atMs === "string"
)
next.kind = "at";
else if (typeof schedule.everyMs === "number") next.kind = "every"; else if (typeof schedule.everyMs === "number") next.kind = "every";
else if (typeof schedule.expr === "string") next.kind = "cron"; else if (typeof schedule.expr === "string") next.kind = "cron";
} }
if (typeof schedule.atMs !== "number" && parsedAtMs !== null) {
next.atMs = parsedAtMs;
}
if ("at" in next) delete next.at;
return next; return next;
} }

21
src/cron/parse.ts Normal file
View File

@@ -0,0 +1,21 @@
const ISO_TZ_RE = /(Z|[+-]\d{2}:?\d{2})$/i;
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
const ISO_DATE_TIME_RE = /^\d{4}-\d{2}-\d{2}T/;
function normalizeUtcIso(raw: string) {
if (ISO_TZ_RE.test(raw)) return raw;
if (ISO_DATE_RE.test(raw)) return `${raw}T00:00:00Z`;
if (ISO_DATE_TIME_RE.test(raw)) return `${raw}Z`;
return raw;
}
export function parseAbsoluteTimeMs(input: string): number | null {
const raw = input.trim();
if (!raw) return null;
if (/^\d+$/.test(raw)) {
const n = Number(raw);
if (Number.isFinite(n) && n > 0) return Math.floor(n);
}
const parsed = Date.parse(normalizeUtcIso(raw));
return Number.isFinite(parsed) ? parsed : null;
}

View File

@@ -81,6 +81,46 @@ describe("CronService", () => {
await store.cleanup(); await store.cleanup();
}); });
it("runs a one-shot job and deletes it after success when requested", async () => {
const store = await makeStorePath();
const enqueueSystemEvent = vi.fn();
const requestHeartbeatNow = vi.fn();
const cron = new CronService({
storePath: store.storePath,
cronEnabled: true,
log: noopLogger,
enqueueSystemEvent,
requestHeartbeatNow,
runIsolatedAgentJob: vi.fn(async () => ({ status: "ok" })),
});
await cron.start();
const atMs = Date.parse("2025-12-13T00:00:02.000Z");
const job = await cron.add({
name: "one-shot delete",
enabled: true,
deleteAfterRun: true,
schedule: { kind: "at", atMs },
sessionTarget: "main",
wakeMode: "now",
payload: { kind: "systemEvent", text: "hello" },
});
vi.setSystemTime(new Date("2025-12-13T00:00:02.000Z"));
await vi.runOnlyPendingTimersAsync();
const jobs = await cron.list({ includeDisabled: true });
expect(jobs.find((j) => j.id === job.id)).toBeUndefined();
expect(enqueueSystemEvent).toHaveBeenCalledWith("hello", {
agentId: undefined,
});
expect(requestHeartbeatNow).toHaveBeenCalled();
cron.stop();
await store.cleanup();
});
it("wakeMode now waits for heartbeat completion when available", async () => { it("wakeMode now waits for heartbeat completion when available", async () => {
const store = await makeStorePath(); const store = await makeStorePath();
const enqueueSystemEvent = vi.fn(); const enqueueSystemEvent = vi.fn();

View File

@@ -193,6 +193,7 @@ export class CronService {
name: normalizeRequiredName(input.name), name: normalizeRequiredName(input.name),
description: normalizeOptionalText(input.description), description: normalizeOptionalText(input.description),
enabled: input.enabled !== false, enabled: input.enabled !== false,
deleteAfterRun: input.deleteAfterRun,
createdAtMs: now, createdAtMs: now,
updatedAtMs: now, updatedAtMs: now,
schedule: input.schedule, schedule: input.schedule,
@@ -229,6 +230,8 @@ export class CronService {
if ("description" in patch) if ("description" in patch)
job.description = normalizeOptionalText(patch.description); job.description = normalizeOptionalText(patch.description);
if (typeof patch.enabled === "boolean") job.enabled = patch.enabled; if (typeof patch.enabled === "boolean") job.enabled = patch.enabled;
if (typeof patch.deleteAfterRun === "boolean")
job.deleteAfterRun = patch.deleteAfterRun;
if (patch.schedule) job.schedule = patch.schedule; if (patch.schedule) job.schedule = patch.schedule;
if (patch.sessionTarget) job.sessionTarget = patch.sessionTarget; if (patch.sessionTarget) job.sessionTarget = patch.sessionTarget;
if (patch.wakeMode) job.wakeMode = patch.wakeMode; if (patch.wakeMode) job.wakeMode = patch.wakeMode;
@@ -472,6 +475,8 @@ export class CronService {
job.state.lastError = undefined; job.state.lastError = undefined;
this.emit({ jobId: job.id, action: "started", runAtMs: startedAt }); this.emit({ jobId: job.id, action: "started", runAtMs: startedAt });
let deleted = false;
const finish = async ( const finish = async (
status: "ok" | "error" | "skipped", status: "ok" | "error" | "skipped",
err?: string, err?: string,
@@ -484,14 +489,21 @@ export class CronService {
job.state.lastDurationMs = Math.max(0, endedAt - startedAt); job.state.lastDurationMs = Math.max(0, endedAt - startedAt);
job.state.lastError = err; job.state.lastError = err;
if (job.schedule.kind === "at" && status === "ok") { const shouldDelete =
// One-shot job completed successfully; disable it. job.schedule.kind === "at" &&
job.enabled = false; status === "ok" &&
job.state.nextRunAtMs = undefined; job.deleteAfterRun === true;
} else if (job.enabled) {
job.state.nextRunAtMs = this.computeJobNextRunAtMs(job, endedAt); if (!shouldDelete) {
} else { if (job.schedule.kind === "at" && status === "ok") {
job.state.nextRunAtMs = undefined; // One-shot job completed successfully; disable it.
job.enabled = false;
job.state.nextRunAtMs = undefined;
} else if (job.enabled) {
job.state.nextRunAtMs = this.computeJobNextRunAtMs(job, endedAt);
} else {
job.state.nextRunAtMs = undefined;
}
} }
this.emit({ this.emit({
@@ -505,6 +517,12 @@ export class CronService {
nextRunAtMs: job.state.nextRunAtMs, nextRunAtMs: job.state.nextRunAtMs,
}); });
if (shouldDelete && this.store) {
this.store.jobs = this.store.jobs.filter((j) => j.id !== job.id);
deleted = true;
this.emit({ jobId: job.id, action: "removed" });
}
if (job.sessionTarget === "isolated") { if (job.sessionTarget === "isolated") {
const prefix = job.isolation?.postToMainPrefix?.trim() || "Cron"; const prefix = job.isolation?.postToMainPrefix?.trim() || "Cron";
const body = (summary ?? err ?? status).trim(); const body = (summary ?? err ?? status).trim();
@@ -592,7 +610,7 @@ export class CronService {
await finish("error", String(err)); await finish("error", String(err));
} finally { } finally {
job.updatedAtMs = nowMs; job.updatedAtMs = nowMs;
if (!opts.forced && job.enabled) { if (!opts.forced && job.enabled && !deleted) {
// Keep nextRunAtMs in sync in case the schedule advanced during a long run. // Keep nextRunAtMs in sync in case the schedule advanced during a long run.
job.state.nextRunAtMs = this.computeJobNextRunAtMs( job.state.nextRunAtMs = this.computeJobNextRunAtMs(
job, job,

View File

@@ -44,6 +44,7 @@ export type CronJob = {
name: string; name: string;
description?: string; description?: string;
enabled: boolean; enabled: boolean;
deleteAfterRun?: boolean;
createdAtMs: number; createdAtMs: number;
updatedAtMs: number; updatedAtMs: number;
schedule: CronSchedule; schedule: CronSchedule;

View File

@@ -830,6 +830,7 @@ export const CronJobSchema = Type.Object(
name: NonEmptyString, name: NonEmptyString,
description: Type.Optional(Type.String()), description: Type.Optional(Type.String()),
enabled: Type.Boolean(), enabled: Type.Boolean(),
deleteAfterRun: Type.Optional(Type.Boolean()),
createdAtMs: Type.Integer({ minimum: 0 }), createdAtMs: Type.Integer({ minimum: 0 }),
updatedAtMs: Type.Integer({ minimum: 0 }), updatedAtMs: Type.Integer({ minimum: 0 }),
schedule: CronScheduleSchema, schedule: CronScheduleSchema,
@@ -860,6 +861,7 @@ export const CronAddParamsSchema = Type.Object(
agentId: Type.Optional(Type.Union([NonEmptyString, Type.Null()])), agentId: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
description: Type.Optional(Type.String()), description: Type.Optional(Type.String()),
enabled: Type.Optional(Type.Boolean()), enabled: Type.Optional(Type.Boolean()),
deleteAfterRun: Type.Optional(Type.Boolean()),
schedule: CronScheduleSchema, schedule: CronScheduleSchema,
sessionTarget: Type.Union([Type.Literal("main"), Type.Literal("isolated")]), sessionTarget: Type.Union([Type.Literal("main"), Type.Literal("isolated")]),
wakeMode: Type.Union([Type.Literal("next-heartbeat"), Type.Literal("now")]), wakeMode: Type.Union([Type.Literal("next-heartbeat"), Type.Literal("now")]),

View File

@@ -354,6 +354,7 @@ export type CronJob = {
name: string; name: string;
description?: string; description?: string;
enabled: boolean; enabled: boolean;
deleteAfterRun?: boolean;
createdAtMs: number; createdAtMs: number;
updatedAtMs: number; updatedAtMs: number;
schedule: CronSchedule; schedule: CronSchedule;