How OpenClaw Enables Scheduled AI Agents with Nanobot CronService and CronTool

[AI Readability Summary] Nanobot adds “run on schedule” capabilities to AI Agents through CronTool and CronService. It supports reminders, recurring jobs, and one-time tasks, solving the classic limitation of traditional agents that only react to incoming messages and cannot keep running autonomously in the cloud. Keywords: AI Agent, task scheduling, CronService.

The technical specification snapshot provides a quick overview

Parameter Details
Project Nanobot / OpenClaw lightweight scheduling capability
Language Python
Scheduling protocols every / cron / at
Persistence JSON file
Core dependencies asyncio, croniter, zoneinfo, dataclasses
Codebase context OpenClaw is roughly 400,000 lines; Nanobot is an ultra-lightweight implementation
Typical capabilities Reminders, recurring execution, one-time tasks, timezone-aware scheduling
Article focus CronTool, CronService, AgentLoop, Dream

Nanobot’s recurring scheduling capability is a key part of proactive AI Agents

The main limitation of traditional agents is not answer quality, but execution timing. If the user does not send a message, the system does nothing. That breaks important workflows such as reminders, health checks, and daily report aggregation.

Nanobot closes this gap with a very lightweight design: CronTool receives LLM tool calls, while CronService computes the next execution time, persists jobs, and calls back into AgentLoop when the time arrives.

Three scheduling modes cover most real-world needs

  • Reminder: Send only a reminder message.
  • Task: Trigger a task on schedule and return the result.
  • One-time: Run once at a specified time and delete the job automatically after completion.
from dataclasses import dataclass
from typing import Literal

@dataclass
class CronSchedule:
    kind: Literal["at", "every", "cron"]  # Three scheduling types
    at_ms: int | None = None                  # One-time execution timestamp
    every_ms: int | None = None               # Fixed interval execution
    expr: str | None = None                   # Cron expression
    tz: str | None = None                     # IANA timezone

This code defines the unified time model used by the scheduling system. It is the foundational abstraction behind the entire recurring execution capability.

CronTool converts natural language intent into structured job requests

CronTool is essentially the tool layer that the Agent can call. It does not perform the actual scheduling. Instead, it converts parameters such as action, message, every_seconds, cron_expr, and at into a standard job object.

Its design focuses on three things: parameter validation, context binding, and natural-language result formatting. In particular, binding channel and chat_id ensures that messages are delivered precisely to the original conversation when the task is triggered.

The CronTool interface is small, but its boundaries are clearly defined

async def execute(action: str, message: str = "", every_seconds: int | None = None,
                  cron_expr: str | None = None, tz: str | None = None,
                  at: str | None = None, job_id: str | None = None) -> str:
    if action == "add":
        return self._add_job(message, every_seconds, cron_expr, tz, at)  # Create a job
    elif action == "list":
        return self._list_jobs()  # Query jobs
    elif action == "remove":
        return self._remove_job(job_id)  # Delete a job
    return f"Unknown action: {action}"

This code shows that CronTool works more like a command router, mapping LLM output to the scheduling service API.

CronService creates a lightweight scheduling loop by computing the next run time

CronService is the core component. It does not depend on the system crontab. Instead, it maintains its own job storage, timers, and execution callbacks, which makes it a better fit for embedded Agent architectures.

Its loop is straightforward: load jobs → compute next_run → set an asyncio timer → execute when due → update state → compute next_run again. This loop is simple enough to remain lightweight and stable.

The calculation logic for three schedule types reflects practical engineering tradeoffs

def _compute_next(job, now):
    cfg = job.schedule_config
    if job.schedule_kind == "at":
        ts = datetime.fromisoformat(cfg["at"]).timestamp()
        return ts if ts > now else 0.0  # Do not schedule expired jobs
    if job.schedule_kind == "every":
        every = cfg.get("every_seconds", 3600)
        return now + every               # Advance by a fixed interval
    if job.schedule_kind == "cron":
        return croniter(cfg["expr"], datetime.fromtimestamp(now)).get_next(datetime).timestamp()

This code demonstrates a unified evaluation model for three different time semantics. That model is also central to the scheduler’s extensibility.

Persistence and state management make the scheduling layer recoverable and observable

Nanobot does not introduce a database. Instead, it serializes jobs into JSON. For a lightweight Agent, this matches the cost model better than deploying additional middleware.

In addition to schedule and payload, each job also includes state. The state tracks next_run_at_ms, last_run_at_ms, last_status, and last_error. That makes troubleshooting and restart recovery practical.

Job execution and state updates remain strictly separated

async def _execute_job(self, job):
    start_ms = _now_ms()
    try:
        if self.on_job:
            await self.on_job(job)            # Call back into AgentLoop to execute the task
        job.state.last_status = "ok"         # Record success status
        job.state.last_error = None
    except Exception as e:
        job.state.last_status = "error"      # Record failure information
        job.state.last_error = str(e)
    job.state.last_run_at_ms = start_ms       # Update the last execution time

This code highlights the decoupling between scheduling and business execution: CronService only decides when to run, not what to run.

The combination of AgentLoop and CronService brings scheduled jobs into a unified runtime

When a job becomes due, CronService invokes the on_job callback, which eventually enters AgentLoop.process_direct(). That means regular cron jobs and user messages go through the same Agent execution pipeline, with only the session key differing.

This design has clear advantages: the system can reuse the toolset, memory, message routing, and result delivery mechanisms instead of building a separate execution framework just for scheduled jobs.

The Dream mechanism is a special internal job within the scheduling system

Dream is also triggered by CronService, but it does not go through the full AgentLoop. Instead, it performs asynchronous memory consolidation directly. It behaves more like an internal maintenance task than a user-facing scheduled conversation.

This split-path design shows that Nanobot’s scheduling system serves not only reminder-style tasks, but also the Agent’s own background cognitive maintenance.

The images show a book cover and a sharing prompt, not a system architecture diagram

TransFormer Cover

WeChat Sharing Prompt

AI Visual Insight: The second image is an animated WeChat sharing prompt from a blogging platform. It guides users through the content-sharing flow and does not contain any Agent scheduling workflow, module relationships, or code-level technical details.

The practical value of this lightweight scheduler lies in low-friction automation

For users, it turns “remember to do this” into “the system does it automatically.” For Agents, it extends the capability boundary from real-time Q&A to future execution. For system designers, it offers an embedded scheduling pattern that is much lighter than Airflow or XXL-Job.

If your goal is to build a personal assistant, a group chatbot, or a lightweight workflow Agent, the CronTool + CronService combination is well worth reusing. It is not heavy, but it is complete enough.

FAQ structured answers

1. What is the difference between Nanobot’s CronService and the system crontab?

CronService is an in-process scheduler embedded directly into the Agent runtime. It supports job state, callbacks, session delivery, and JSON persistence. By contrast, crontab is better suited for system-level script scheduling and does not understand Agent context.

2. Why separate CronTool and CronService?

With this layered design, CronTool focuses on the LLM tool interface and parameter validation, while CronService handles the job lifecycle and scheduling execution. This reduces coupling and makes it easier to replace either the front-end tool layer or the back-end execution layer.

3. Why doesn’t Dream reuse the standard cron job execution path directly?

Because Dream is an internal memory-consolidation task, it does not need conversation context or full tool permissions. Bypassing AgentLoop reduces unnecessary inference and message delivery while preserving minimal privilege and higher execution determinism.

Core summary: This article analyzes OpenClaw’s recurring execution capability through the Nanobot source code. It focuses on how CronTool, CronService, CronStore, and AgentLoop work together, and explains the design value of three scheduling modes—reminders, recurring jobs, and one-time tasks—as well as persistence, callback decoupling, timezone support, and the Dream mechanism.