Skip to main content

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 syntax0 9 * * 1 = Mondays at 9am

Interval

Runs every N milliseconds3600000 = Every hour

Once

Runs once at a specific time2024-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:
  1. Parse your intent
  2. Call mcp__nanoclaw__schedule_task
  3. Create the task with appropriate schedule type
  4. Confirm the task was created

Using the MCP Tool Directly

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:
@Andy list all tasks
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

@Andy pause task 1
The task remains in the database but won’t run until resumed.

Resume a Task

@Andy resume task 1

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

@Andy cancel task 1
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

PatternDescription
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:
grep TIMEZONE .env
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())"

Build docs developers (and LLMs) love