Skip to content

Scheduled Tasks

Scheduled Tasks (Cron)

Cron is the Gateway’s built-in scheduler. It persists jobs, wakes the agent at the right time, and can deliver output back to a chat channel or webhook endpoint.

Quick start

Terminal window
# Add a one-shot reminder
remoteclaw cron add \
--name "Reminder" \
--at "2026-02-01T16:00:00Z" \
--session main \
--system-event "Reminder: check the cron docs draft" \
--wake now \
--delete-after-run
# Check your jobs
remoteclaw cron list
# See run history
remoteclaw cron runs --id <job-id>

How cron works

  • Cron runs inside the Gateway process (not inside the model).
  • Jobs persist at ~/.remoteclaw/cron/jobs.json so restarts do not lose schedules.
  • All cron executions create background task records.
  • One-shot jobs (--at) auto-delete after success by default.

Adding jobs

Schedule types

KindCLI flagDescription
at--atOne-shot timestamp (ISO 8601 or relative like 20m)
every--everyFixed interval
cron--cron5-field or 6-field cron expression with optional --tz

Timestamps without a timezone are treated as UTC. Add --tz America/New_York for local wall-clock scheduling.

Recurring top-of-hour expressions are automatically staggered by up to 5 minutes to reduce load spikes. Use --exact to force precise timing or --stagger 30s for an explicit window.

CLI examples

One-shot reminder (main session):

Terminal window
remoteclaw cron add \
--name "Calendar check" \
--at "20m" \
--session main \
--system-event "Next heartbeat: check calendar." \
--wake now

Recurring isolated job with delivery:

Terminal window
remoteclaw cron add \
--name "Morning brief" \
--cron "0 7 * * *" \
--tz "America/Los_Angeles" \
--session isolated \
--message "Summarize overnight updates." \
--announce \
--channel slack \
--to "channel:C1234567890"

Isolated job with model and thinking override:

Terminal window
remoteclaw cron add \
--name "Deep analysis" \
--cron "0 6 * * 1" \
--tz "America/Los_Angeles" \
--session isolated \
--message "Weekly deep analysis of project progress." \
--model "opus" \
--thinking high \
--announce

Execution styles

Style--session valueRuns inBest for
Main sessionmainNext heartbeat turnReminders, system events
IsolatedisolatedDedicated cron:<jobId>Reports, background chores
Current sessioncurrentBound at creation timeContext-aware recurring work
Custom sessionsession:custom-idPersistent named sessionWorkflows that build on history

Main session jobs enqueue a system event and optionally wake the heartbeat (--wake now or --wake next-heartbeat). They use payload.kind = "systemEvent".

Isolated jobs run a dedicated agent turn. Each run starts a fresh session (no carry-over) unless using a custom session. Default delivery is announce (summary to chat).

Custom sessions (session:xxx) persist context across runs, enabling workflows like daily standups that build on previous summaries.

Payload options for isolated jobs

  • --message: prompt text (required for isolated)
  • --model / --thinking: model and thinking level overrides
  • --light-context: skip workspace bootstrap file injection
  • --tools exec,read: restrict which tools the job can use

Delivery and output

ModeWhat happens
announceDeliver summary to target channel (default for isolated)
webhookPOST finished event payload to a URL
noneInternal only, no delivery

Use --announce --channel telegram --to "-1001234567890" for channel delivery.

For Telegram forum topics, use -1001234567890:topic:123. Slack/Discord/Mattermost targets should use explicit prefixes (channel:<id>, user:<id>).

Webhooks

Gateway can expose HTTP webhook endpoints for external triggers. Enable in config:

{
hooks: {
enabled: true,
token: "shared-secret",
path: "/hooks",
},
}

Authentication

Every request must include the hook token via header:

  • Authorization: Bearer <token> (recommended)
  • x-remoteclaw-token: <token>

Query-string tokens are rejected.

POST /hooks/wake

Enqueue a system event for the main session:

Terminal window
curl -X POST http://127.0.0.1:18789/hooks/wake \
-H 'Authorization: Bearer SECRET' \
-H 'Content-Type: application/json' \
-d '{"text":"New email received","mode":"now"}'
  • text (required): event description
  • mode (optional): now (default) or next-heartbeat

POST /hooks/agent

Run an isolated agent turn:

Terminal window
curl -X POST http://127.0.0.1:18789/hooks/agent \
-H 'Authorization: Bearer SECRET' \
-H 'Content-Type: application/json' \
-d '{"message":"Summarize inbox","name":"Email","model":"openai/gpt-5.2-mini"}'

Fields: message (required), name, agentId, wakeMode, deliver, channel, to, model, thinking, timeoutSeconds.

Mapped hooks (POST /hooks/<name>)

Custom hook names are resolved via hooks.mappings in config. Mappings can transform arbitrary payloads into wake or agent actions with templates or code transforms.

Security

  • Keep hook endpoints behind loopback, tailnet, or trusted reverse proxy.
  • Use a dedicated hook token; do not reuse gateway auth tokens.
  • Set hooks.allowedAgentIds to limit explicit agentId routing.
  • Keep hooks.allowRequestSessionKey=false unless you require caller-selected sessions.
  • Hook payloads are wrapped with safety boundaries by default.

Gmail PubSub integration

Wire Gmail inbox triggers to RemoteClaw via Google PubSub.

Prerequisites: gcloud CLI, gog (gogcli), RemoteClaw hooks enabled, Tailscale for the public HTTPS endpoint.

Terminal window
remoteclaw webhooks gmail setup --account remoteclaw@gmail.com

This writes hooks.gmail config, enables the Gmail preset, and uses Tailscale Funnel for the push endpoint.

Gateway auto-start

When hooks.enabled=true and hooks.gmail.account is set, the Gateway starts gog gmail watch serve on boot and auto-renews the watch. Set REMOTECLAW_SKIP_GMAIL_WATCHER=1 to opt out.

Manual one-time setup

  1. Select the GCP project that owns the OAuth client used by gog:
Terminal window
gcloud auth login
gcloud config set project <project-id>
gcloud services enable gmail.googleapis.com pubsub.googleapis.com
  1. Create topic and grant Gmail push access:
Terminal window
gcloud pubsub topics create gog-gmail-watch
gcloud pubsub topics add-iam-policy-binding gog-gmail-watch \
--member=serviceAccount:gmail-api-push@system.gserviceaccount.com \
--role=roles/pubsub.publisher
  1. Start the watch:
Terminal window
gog gmail watch start \
--account remoteclaw@gmail.com \
--label INBOX \
--topic projects/<project-id>/topics/gog-gmail-watch

Gmail model override

{
hooks: {
gmail: {
model: "openrouter/meta-llama/llama-3.3-70b-instruct:free",
thinking: "off",
},
},
}

Test

Terminal window
gog gmail send --account remoteclaw@gmail.com --to remoteclaw@gmail.com --subject "watch test" --body "ping"
gog gmail watch status --account remoteclaw@gmail.com

Managing jobs

Terminal window
# List all jobs
remoteclaw cron list
# Edit a job
remoteclaw cron edit <jobId> --message "Updated prompt" --model "opus"
# Force run a job now
remoteclaw cron run <jobId>
# Run only if due
remoteclaw cron run <jobId> --due
# View run history
remoteclaw cron runs --id <jobId> --limit 50
# Delete a job
remoteclaw cron remove <jobId>
# Agent selection (multi-agent setups)
remoteclaw cron add --name "Ops sweep" --cron "0 6 * * *" --session isolated --message "Check ops queue" --agent ops
remoteclaw cron edit <jobId> --clear-agent

JSON schema for tool calls

One-shot main session job:

{
"name": "Reminder",
"schedule": { "kind": "at", "at": "2026-02-01T16:00:00Z" },
"sessionTarget": "main",
"wakeMode": "now",
"payload": { "kind": "systemEvent", "text": "Reminder text" },
"deleteAfterRun": true
}

Recurring isolated job with delivery:

{
"name": "Morning brief",
"schedule": { "kind": "cron", "expr": "0 7 * * *", "tz": "America/Los_Angeles" },
"sessionTarget": "isolated",
"payload": { "kind": "agentTurn", "message": "Summarize overnight updates." },
"delivery": { "mode": "announce", "channel": "slack", "to": "channel:C1234567890" }
}

Configuration

{
cron: {
enabled: true,
store: "~/.remoteclaw/cron/jobs.json",
maxConcurrentRuns: 1,
retry: {
maxAttempts: 3,
backoffMs: [60000, 120000, 300000],
retryOn: ["rate_limit", "overloaded", "network", "server_error"],
},
webhookToken: "replace-with-dedicated-webhook-token",
sessionRetention: "24h",
runLog: { maxBytes: "2mb", keepLines: 2000 },
},
}

Disable cron: cron.enabled: false or REMOTECLAW_SKIP_CRON=1.

Retry policy

One-shot jobs: retry transient errors (rate limit, overload, network, server error) up to 3 times with exponential backoff. Permanent errors disable immediately.

Recurring jobs: exponential backoff (30s to 60m) between retries. Backoff resets after the next successful run.

Maintenance

  • cron.sessionRetention (default 24h): prune isolated run-session entries.
  • cron.runLog.maxBytes / cron.runLog.keepLines: auto-prune run-log files.

Troubleshooting

Command ladder

Terminal window
remoteclaw status
remoteclaw gateway status
remoteclaw cron status
remoteclaw cron list
remoteclaw cron runs --id <jobId> --limit 20
remoteclaw system heartbeat last
remoteclaw logs --follow
remoteclaw doctor

Cron not firing

  • Check cron.enabled and REMOTECLAW_SKIP_CRON env var.
  • Confirm the Gateway is running continuously.
  • For cron schedules, verify timezone (--tz) vs the host timezone.
  • reason: not-due in run output means manual run called without --force.

Cron fired but no delivery

  • Run succeeded but delivery mode is none means no external message is expected.
  • Delivery target missing/invalid (channel/to) means outbound was skipped.
  • Channel auth errors (unauthorized, Forbidden) mean delivery was blocked by credentials.

Heartbeat suppressed or skipped

  • reason=quiet-hours: outside activeHours.
  • requests-in-flight: main lane busy, heartbeat deferred.
  • empty-heartbeat-file: HEARTBEAT.md has no actionable content and no cron event is queued.

Timezone gotchas

  • Cron without --tz uses the gateway host timezone.
  • at schedules without timezone are treated as UTC.
  • Heartbeat activeHours uses configured timezone resolution.