[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


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.