Overview
NanoClaw Pro’s task scheduler lets you create cron jobs, interval tasks, and one-time reminders that run as full Claude agents with access to all tools, memory, and the ability to send messages.
Scheduled tasks aren’t just notifications — they’re full agent invocations that can search the web, read files, create notes, and make decisions.
Schedule Types
Cron Runs at specific times using cron syntax 0 9 * * 1 = Mondays at 9am
Interval Runs every N milliseconds 3600000 = Every hour
Once Runs once at a specific time 2024-12-25T09:00:00Z
Creating Tasks
Natural Language
The easiest way to create a task is to ask naturally:
@Andy remind me every Monday at 9am to review the weekly metrics
Claude will:
Parse your intent
Call mcp__nanoclaw__schedule_task
Create the task with appropriate schedule type
Confirm the task was created
From within a conversation, Claude can call the scheduler tool:
mcp__nanoclaw__schedule_task({
"prompt" : "Send a reminder to review weekly metrics. Be encouraging!" ,
"schedule_type" : "cron" ,
"schedule_value" : "0 9 * * 1"
})
Creating Tasks for Other Groups (Main Only)
The main channel can schedule tasks for any group:
@Andy schedule a task for "Dev Team": every Friday at 5pm, send a summary of the week's progress
This creates a task in the Dev Team group’s context.
Task Examples
Daily Standup Reminder
@Andy every weekday at 9am, remind the team to post their standup updates
Result:
Schedule type: cron
Schedule value: 0 9 * * 1-5 (Mon-Fri at 9am)
Prompt: “Send a friendly reminder to post standup updates”
Weekly Report
@Andy every Monday at 10am, compile a summary of last week's conversations and post it here
Result:
Schedule type: cron
Schedule value: 0 10 * * 1
Prompt: “Read recent conversation history, summarize key points, and send via mcp__nanoclaw__send_message”
One-Time Reminder
@Andy at 3pm today, remind me to join the all-hands meeting
Result:
Schedule type: once
Schedule value: 2024-01-31T15:00:00-05:00 (ISO timestamp in user’s timezone)
Prompt: “Send a reminder to join the all-hands meeting”
Interval Task
@Andy every 2 hours, check Hacker News for AI-related posts and send me the top 3
Result:
Schedule type: interval
Schedule value: 7200000 (2 hours in milliseconds)
Prompt: “Search Hacker News for AI posts, find top 3, send via mcp__nanoclaw__send_message”
Managing Tasks
List Tasks
In any group:
@Andy list my scheduled tasks
Shows tasks for the current group only.
In main channel:
Shows tasks from all groups.
Example output:
Scheduled Tasks:
1. Morning Check-in
Status: active
Schedule: cron (0 9 * * *)
Next run: 2024-02-01 09:00:00
Group: whatsapp_main
2. Weekly Report
Status: active
Schedule: cron (0 10 * * 1)
Next run: 2024-02-05 10:00:00
Group: whatsapp_dev-team
Pause a Task
The task remains in the database but won’t run until resumed.
Resume a Task
Update a Task
@Andy update task 1 to run at 7am instead of 9am
or
@Andy change the prompt for task 2 to be more detailed
Cancel a Task
Permanently deletes the task.
Technical Implementation
Scheduler Loop
From src/task-scheduler.ts:242:
export function startSchedulerLoop ( deps : SchedulerDependencies ) : void {
if ( schedulerRunning ) {
return ;
}
schedulerRunning = true ;
logger . info ( 'Scheduler loop started' );
const loop = async () => {
try {
const dueTasks = getDueTasks ();
if ( dueTasks . length > 0 ) {
logger . info ({ count: dueTasks . length }, 'Found due tasks' );
}
for ( const task of dueTasks ) {
// Re-check task status in case it was paused/cancelled
const currentTask = getTaskById ( task . id );
if ( ! currentTask || currentTask . status !== 'active' ) {
continue ;
}
deps . queue . enqueueTask ( currentTask . chat_jid , currentTask . id , () =>
runTask ( currentTask , deps ),
);
}
} catch ( err ) {
logger . error ({ err }, 'Error in scheduler loop' );
}
setTimeout ( loop , SCHEDULER_POLL_INTERVAL ); // Default: 60 seconds
};
loop ();
}
Key points:
Polls every 60 seconds (configurable via SCHEDULER_POLL_INTERVAL)
Fetches due tasks from SQLite
Re-checks status before running (handles pause/cancel race conditions)
Enqueues tasks in the group queue (respects concurrency limits)
Task Execution
From src/task-scheduler.ts:78:
async function runTask (
task : ScheduledTask ,
deps : SchedulerDependencies ,
) : Promise < void > {
const startTime = Date . now ();
// Spawn container agent
const output = await runContainerAgent (
group ,
{
prompt: task . prompt ,
sessionId , // Optional: use group's session for context
groupFolder: task . group_folder ,
chatJid: task . chat_jid ,
isMain ,
isScheduledTask: true ,
assistantName: ASSISTANT_NAME ,
},
( proc , containerName ) =>
deps . onProcess ( task . chat_jid , proc , containerName , task . group_folder ),
async ( streamedOutput : ContainerOutput ) => {
if ( streamedOutput . result ) {
result = streamedOutput . result ;
// Forward result to user
await deps . sendMessage ( task . chat_jid , streamedOutput . result );
}
},
);
// Log the run
logTaskRun ({
task_id: task . id ,
run_at: new Date (). toISOString (),
duration_ms: Date . now () - startTime ,
status: error ? 'error' : 'success' ,
result ,
error ,
});
// Compute and save next run time
const nextRun = computeNextRun ( task );
updateTaskAfterRun ( task . id , nextRun , resultSummary );
}
Key points:
Runs as a full container agent (same as interactive messages)
Has access to all tools and memory
Can send messages via mcp__nanoclaw__send_message
Logs each execution to task_run_logs table
Automatically schedules next run (for recurring tasks)
Computing Next Run Time
From src/task-scheduler.ts:31:
export function computeNextRun ( task : ScheduledTask ) : string | null {
if ( task . schedule_type === 'once' ) return null ; // One-time tasks don't recur
const now = Date . now ();
if ( task . schedule_type === 'cron' ) {
const interval = CronExpressionParser . parse ( task . schedule_value , {
tz: TIMEZONE , // Uses your configured timezone
});
return interval . next (). toISOString ();
}
if ( task . schedule_type === 'interval' ) {
const ms = parseInt ( task . schedule_value , 10 );
// Anchor to the scheduled time, not now, to prevent drift
let next = new Date ( task . next_run ! ). getTime () + ms ;
while ( next <= now ) {
next += ms ; // Skip missed intervals
}
return new Date ( next ). toISOString ();
}
return null ;
}
Interval tasks anchor to the scheduled time to prevent cumulative drift. If a task was supposed to run at 9:00 but ran at 9:02, the next run is still calculated from 9:00, not 9:02.
Context Modes
Group Context Mode (Default)
Tasks run with the group’s current session ID, continuing the conversation:
// From src/task-scheduler.ts:154
const sessionId =
task . context_mode === 'group' ? sessions [ task . group_folder ] : undefined ;
Use when: You want the task to remember previous conversations in that group.
Example: Weekly summary that builds on last week’s summary.
Fresh Context Mode
Tasks run without a session ID, starting fresh each time:
const sessionId = undefined ; // Fresh session every run
Use when: You want each run to be independent.
Example: Hourly news check where previous runs don’t matter.
Context mode is not exposed in the UI yet. All tasks currently use group context mode.
Task Closure
Tasks close their containers promptly after producing a result:
// From src/task-scheduler.ts:160
const TASK_CLOSE_DELAY_MS = 10000 ; // 10 seconds
const scheduleClose = () => {
if ( closeTimer ) return ; // Already scheduled
closeTimer = setTimeout (() => {
logger . debug ({ taskId: task . id }, 'Closing task container after result' );
deps . queue . closeStdin ( task . chat_jid );
}, TASK_CLOSE_DELAY_MS );
};
Reason: Tasks are single-turn operations. Unlike interactive conversations (which keep containers alive for 30 minutes), tasks close after 10 seconds to free resources.
Database Schema
scheduled_tasks Table
CREATE TABLE scheduled_tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
chat_jid TEXT NOT NULL ,
group_folder TEXT NOT NULL ,
prompt TEXT NOT NULL ,
schedule_type TEXT NOT NULL , -- 'cron', 'interval', 'once'
schedule_value TEXT NOT NULL , -- cron expression, milliseconds, or ISO timestamp
status TEXT DEFAULT 'active' , -- 'active', 'paused', 'cancelled'
next_run TEXT , -- ISO timestamp of next execution
created_at TEXT NOT NULL ,
context_mode TEXT DEFAULT 'group' -- 'group' or 'fresh'
);
task_run_logs Table
CREATE TABLE task_run_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER NOT NULL ,
run_at TEXT NOT NULL ,
duration_ms INTEGER ,
status TEXT NOT NULL , -- 'success' or 'error'
result TEXT , -- What the agent said (first 200 chars)
error TEXT ,
FOREIGN KEY (task_id) REFERENCES scheduled_tasks (id)
);
Cron Syntax Reference
┌───────────── minute (0 - 59)
│ ┌───────────── hour (0 - 23)
│ │ ┌───────────── day of month (1 - 31)
│ │ │ ┌───────────── month (1 - 12)
│ │ │ │ ┌───────────── day of week (0 - 6) (Sunday to Saturday)
│ │ │ │ │
* * * * *
Common Patterns
Pattern Description 0 9 * * *Daily at 9am 0 9 * * 1Every Monday at 9am 0 9 * * 1-5Weekdays at 9am 0 */2 * * *Every 2 hours 30 14 1 * *2:30pm on the 1st of every month 0 0 * * 0Midnight every Sunday
Cron expressions are evaluated in the timezone configured in your .env file (TIMEZONE). Make sure it matches your local timezone.
Best Practices
Be Specific in Prompts “Send a summary of today’s conversations” is better than “summarize stuff”.
Include Send Instructions Always tell the task to send a message: “Search X, then send results via mcp__nanoclaw__send_message”.
Use Cron for Fixed Times Use cron for “every Monday at 9am”. Use interval for “every 2 hours”.
Test Before Scheduling Run the prompt manually first to ensure it works as expected.
Troubleshooting
Task Not Running
Check task status:
@Andy list my scheduled tasks
Ensure status is active, not paused.
Check next run time:
SELECT id, prompt, status , next_run FROM scheduled_tasks WHERE status = 'active' ;
If next_run is in the past but the task didn’t run, check logs:
tail -f logs/nanoclaw.log | grep "Scheduler loop"
Verify scheduler is running:
grep "Scheduler loop started" logs/nanoclaw.log
If missing, restart NanoClaw:
launchctl kickstart -k gui/ $( id -u ) /com.nanoclaw
Task Runs But Doesn’t Send Message
Check task run logs:
SELECT * FROM task_run_logs ORDER BY run_at DESC LIMIT 5 ;
If status = 'success' but result is null, the task ran but produced no output.
Solution: Update the prompt to explicitly send a message:
@Andy update task 1: make sure to send the results via mcp__nanoclaw__send_message
Wrong Timezone
Check current timezone:
Update:
echo "TIMEZONE=America/Los_Angeles" >> .env
launchctl kickstart -k gui/ $( id -u ) /com.nanoclaw
Verify cron parsing:
node -e "console.log(require('cron-parser').parseExpression('0 9 * * *', {tz: 'America/New_York'}).next().toString())"