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
# Add a one-shot reminderremoteclaw 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 jobsremoteclaw cron list
# See run historyremoteclaw cron runs --id <job-id>How cron works
- Cron runs inside the Gateway process (not inside the model).
- Jobs persist at
~/.remoteclaw/cron/jobs.jsonso 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
| Kind | CLI flag | Description |
|---|---|---|
at | --at | One-shot timestamp (ISO 8601 or relative like 20m) |
every | --every | Fixed interval |
cron | --cron | 5-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):
remoteclaw cron add \ --name "Calendar check" \ --at "20m" \ --session main \ --system-event "Next heartbeat: check calendar." \ --wake nowRecurring isolated job with delivery:
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:
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 \ --announceExecution styles
| Style | --session value | Runs in | Best for |
|---|---|---|---|
| Main session | main | Next heartbeat turn | Reminders, system events |
| Isolated | isolated | Dedicated cron:<jobId> | Reports, background chores |
| Current session | current | Bound at creation time | Context-aware recurring work |
| Custom session | session:custom-id | Persistent named session | Workflows 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
| Mode | What happens |
|---|---|
announce | Deliver summary to target channel (default for isolated) |
webhook | POST finished event payload to a URL |
none | Internal 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:
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 descriptionmode(optional):now(default) ornext-heartbeat
POST /hooks/agent
Run an isolated agent turn:
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.allowedAgentIdsto limit explicitagentIdrouting. - Keep
hooks.allowRequestSessionKey=falseunless 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.
Wizard setup (recommended)
remoteclaw webhooks gmail setup --account remoteclaw@gmail.comThis 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
- Select the GCP project that owns the OAuth client used by
gog:
gcloud auth logingcloud config set project <project-id>gcloud services enable gmail.googleapis.com pubsub.googleapis.com- Create topic and grant Gmail push access:
gcloud pubsub topics create gog-gmail-watchgcloud pubsub topics add-iam-policy-binding gog-gmail-watch \ --member=serviceAccount:gmail-api-push@system.gserviceaccount.com \ --role=roles/pubsub.publisher- Start the watch:
gog gmail watch start \ --account remoteclaw@gmail.com \ --label INBOX \ --topic projects/<project-id>/topics/gog-gmail-watchGmail model override
{ hooks: { gmail: { model: "openrouter/meta-llama/llama-3.3-70b-instruct:free", thinking: "off", }, },}Test
gog gmail send --account remoteclaw@gmail.com --to remoteclaw@gmail.com --subject "watch test" --body "ping"gog gmail watch status --account remoteclaw@gmail.comManaging jobs
# List all jobsremoteclaw cron list
# Edit a jobremoteclaw cron edit <jobId> --message "Updated prompt" --model "opus"
# Force run a job nowremoteclaw cron run <jobId>
# Run only if dueremoteclaw cron run <jobId> --due
# View run historyremoteclaw cron runs --id <jobId> --limit 50
# Delete a jobremoteclaw cron remove <jobId>
# Agent selection (multi-agent setups)remoteclaw cron add --name "Ops sweep" --cron "0 6 * * *" --session isolated --message "Check ops queue" --agent opsremoteclaw cron edit <jobId> --clear-agentJSON 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(default24h): prune isolated run-session entries.cron.runLog.maxBytes/cron.runLog.keepLines: auto-prune run-log files.
Troubleshooting
Command ladder
remoteclaw statusremoteclaw gateway statusremoteclaw cron statusremoteclaw cron listremoteclaw cron runs --id <jobId> --limit 20remoteclaw system heartbeat lastremoteclaw logs --followremoteclaw doctorCron not firing
- Check
cron.enabledandREMOTECLAW_SKIP_CRONenv var. - Confirm the Gateway is running continuously.
- For
cronschedules, verify timezone (--tz) vs the host timezone. reason: not-duein run output means manual run called without--force.
Cron fired but no delivery
- Run succeeded but delivery mode is
nonemeans 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: outsideactiveHours.requests-in-flight: main lane busy, heartbeat deferred.empty-heartbeat-file:HEARTBEAT.mdhas no actionable content and no cron event is queued.
Timezone gotchas
- Cron without
--tzuses the gateway host timezone. atschedules without timezone are treated as UTC.- Heartbeat
activeHoursuses configured timezone resolution.
Related
- Automation & Tasks — all automation mechanisms at a glance
- Background Tasks — task ledger for cron executions
- Heartbeat — periodic main-session turns
- Timezone — timezone configuration