Skip to content

Tasks, Handlers, and Schedules

A task is anything you want to happen on a schedule — an API call, a Slack message, an AI-generated report, a health check. Define what it does, when it runs, and how it handles failure. Cronlet takes care of the rest.

A task combines:

  • human-readable metadata (name, description)
  • a handler
  • a schedule
  • execution controls (retryAttempts, retryBackoff, retryDelay, timeout)
  • lifecycle controls (active, maxRuns, expiresAt)
  • optional callbackUrl
  • optional metadata

Task create shape

Cloud task creation accepts a source field at the API boundary. In normal product usage this is set for you:

  • dashboard task creation writes source: "dashboard"
  • SDK task creation writes source: "sdk"
  • MCP/agent task creation writes source: "mcp"
await cronlet.tasks.create({
name: "API health check",
description: "Ping the production health endpoint",
handler: {
type: "webhook",
url: "https://api.example.com/health",
method: "GET",
},
schedule: {
type: "every",
interval: "5m",
},
timezone: "UTC",
retryAttempts: 2,
retryBackoff: "linear",
retryDelay: "5s",
timeout: "30s",
active: true,
callbackUrl: "https://app.example.com/api/cronlet/callback",
metadata: {
userId: "user_123",
monitorId: "monitor_456",
},
maxRuns: 100,
expiresAt: "2026-12-31T23:59:59.000Z",
});

API request shape:

{
name: string;
description?: string;
handler: HandlerConfig;
schedule: ScheduleConfig;
timezone?: string;
retryAttempts?: number;
retryBackoff?: "linear" | "exponential";
retryDelay?: string;
timeout?: string;
active?: boolean;
source?: "dashboard" | "mcp" | "sdk";
callbackUrl?: string;
metadata?: Record<string, unknown>;
maxRuns?: number;
expiresAt?: string;
}

Handlers

webhook

Use this when Cronlet should call an HTTP endpoint directly.

handler: {
type: "webhook",
url: "https://api.example.com/webhook",
method: "POST",
headers: {
"X-Source": "cronlet",
},
body: {
kind: "report",
},
auth: {
type: "bearer",
secretName: "INTERNAL_API_TOKEN",
},
}

Supported fields:

  • url: string
  • method?: "GET" | "POST"
  • headers?: Record<string, string>
  • body?: unknown
  • auth?: { type: "bearer" | "basic" | "header"; secretName: string }

tools

Use this when Cronlet should execute a hosted multi-step workflow.

handler: {
type: "tools",
steps: [
{
tool: "http.get",
args: {
url: "https://api.example.com/report",
},
outputKey: "report",
},
{
tool: "slack.post",
args: {
channel: "#ops",
text: "Weekly digest: {{report.body}}",
secretName: "SLACK_TOKEN",
},
},
],
}

Each tool step supports:

  • tool: string
  • args: Record<string, unknown>
  • outputKey?: string

Current tool names visible in the product UI include:

  • http.get
  • http.post
  • slack.post
  • email.send
  • log
  • sleep

code

The shared schemas include a code handler type, but the hosted worker currently rejects it. Do not build production Cloud integrations around hosted code handlers until that runtime is actually shipped.

Schedules

The SDK accepts schedule objects and a constrained schedule string grammar. The API still receives structured ScheduleConfig objects.

Every

schedule: {
type: "every",
interval: "15m",
}

Supported duration format:

  • 100ms
  • 1s
  • 5m
  • 1h
  • 1d

String form:

schedule: "every 15 minutes"

Daily

schedule: {
type: "daily",
times: ["09:00", "17:00"],
}

String form:

schedule: "daily at 9am"

Weekly

schedule: {
type: "weekly",
days: ["mon", "wed", "fri"],
time: "09:00",
}

String forms:

schedule: "weekdays at 5pm"
schedule: "every friday at 9am"

Monthly

schedule: {
type: "monthly",
day: "last-fri",
time: "17:00",
}

String forms:

schedule: "monthly on the 1st at 9am"
schedule: "monthly on the last friday at 9am"

Supported day values:

  • integers 1..31
  • "last"
  • "last-mon"
  • "last-tue"
  • "last-wed"
  • "last-thu"
  • "last-fri"
  • "last-sat"
  • "last-sun"

Once

schedule: {
type: "once",
at: "2026-03-15T09:00:00.000Z",
}

String form:

schedule: "once at 2026-03-15 09:00"

Cron

schedule: {
type: "cron",
expression: "0 9 * * 1-5",
}

Timezone

Use an IANA timezone string:

timezone: "America/New_York"

If omitted, the SDK defaults to UTC.

Execution controls

retryAttempts: 3,
retryBackoff: "exponential",
retryDelay: "30s",
timeout: "5m",

Constraints from the shared schema:

  • retryAttempts: 1..10
  • retryBackoff: "linear" or "exponential"
  • retryDelay: duration string
  • timeout: duration string

Lifecycle controls

Pause on create

active: false

Limit total runs

maxRuns: 20

Expire at a specific time

expiresAt: "2026-06-30T23:59:59.000Z"

Metadata

Treat metadata as the bridge between Cronlet and your product data model.

Recommended metadata shape:

metadata: {
userId: "user_123",
orgId: "org_456",
feature: "scheduled-report",
entityId: "report_789",
createdBy: "agent",
}

Use stable IDs, not display names.

Patching tasks

await cronlet.tasks.patch(taskId, {
schedule: {
type: "every",
interval: "1m",
},
timezone: "UTC",
callbackUrl: null,
metadata: {
userId: "user_123",
monitorId: "monitor_456",
mode: "degraded",
},
maxRuns: null,
expiresAt: null,
});

patch() supports updates for:

  • name
  • description
  • handler
  • schedule
  • timezone
  • retryAttempts
  • retryBackoff
  • retryDelay
  • timeout
  • active
  • callbackUrl
  • metadata
  • maxRuns
  • expiresAt

Runs

Task execution creates runs with:

  • status
  • attempt
  • durationMs
  • output
  • logs
  • errorMessage

Read runs via:

await cronlet.runs.list(taskId, 20);
await cronlet.runs.get(runId);

Best practices

  • make task names readable in the dashboard
  • use metadata for ownership and reconciliation
  • store task.id in your own database
  • prefer webhook when your product already owns the business logic
  • prefer tools when you want Cronlet-hosted step orchestration
  • use maxRuns and expiresAt for bounded workflows

Next steps