messages_in table with kind='task' and a process_after timestamp — the same inbox machinery, swept by the same host sweep, as any chat message. You drive it through the ncl tasks CLI: ask the agent in chat and it runs ncl tasks on your behalf, or run the commands yourself from the host. This tutorial assumes a working install with a wired agent (see your first agent).
1
Schedule a task in chat
Ask your agent, in whatever chat it’s wired to:Mechanically, the agent runs
ncl tasks create with a --prompt, a cron --recurrence (0 9 * * 1-5), and (for one-shots) a --process-after timestamp for the first run. NanoClaw provisions an isolated per-task session for the series and inserts a messages_in row there with kind='task', status pending, your process_after, the cron expression, and series_id set to the task’s own id. See the session DB schema for the columns.The agent confirms with the generated id. Passing --name "standup" yields a readable id like standup-a25c; without a name, ids are t-<hex>. For a one-shot task — “remind me tomorrow at 2pm to review the budget” — it passes --process-after and omits --recurrence.2
Know what fires it
Nothing watches the clock per-task. The host sweep visits every active session every 60 seconds, counts pending rows with
process_after <= now, and wakes the task session’s container if one isn’t already running. The agent’s poll loop then picks the task up like any inbound message and executes the prompt.Two consequences worth internalizing:- ±60 seconds is the precision floor. A task due at 09:00:00 fires whenever the next sweep tick lands, plus container cold-start if nothing is warm. Don’t schedule anything that needs second-level timing.
- Recurring tasks are clones, not loops. When a recurring occurrence completes, the next sweep tick computes the next run with cron-parser, inserts a fresh
pendingrow carrying the sameseries_idforward, and clearsrecurrenceon the completed row so it isn’t re-cloned. The series id is the stable handle; individual occurrence ids come and go. Once a series has no live occurrence left, the sweep garbage-collects its per-task session.
3
Manage it with ncl tasks
You can manage tasks by asking the agent in chat:Behind those phrases (and available directly from the host) is the
ncl tasks resource:Full flags are in the ncl tasks reference.
4
Gate frequent tasks with a script
Every firing that reaches the agent is a full model API call. For tasks that run more than a few times a day, The contract, from
ncl tasks create accepts an optional --script — a bash script that runs before the agent is invoked:container/agent-runner/src/scheduling/task-script.ts:- When the task fires, the script runs first inside the container — 30-second timeout, 1 MB output cap.
- The last line of stdout must be JSON:
{ "wakeAgent": true|false, "data": ... }. wakeAgent: false— or any script error, timeout, or invalid output — marks the occurrence completed without invoking the agent. A recurring series still continues: the sweep clones the next occurrence from the completed row.wakeAgent: true— the agent wakes withdatainjected into the task prompt asscriptOutput.
5
Set the timezone
When you say “9am”, three layers have to agree on whose 9am. The chain: the host resolves Restart the host after changing it; the value is read once at startup.
TIMEZONE once at startup — process.env.TZ, then the .env file’s TZ, then the system locale, falling back to UTC, each candidate validated as a real IANA identifier (src/config.ts). Every container is spawned with -e TZ=<that value> (src/container-runner.ts), and the agent-runner’s timezone.ts resolves the same way inside.The result: naive timestamps the agent passes (2026-06-11T09:00:00, no offset) and cron expressions — both at schedule time and when the host sweep computes the next recurrence — are all interpreted in that one instance timezone. To pin it:6
Inspect from the host
Because scheduling is a host CLI resource now, you read it directly:Under the hood each series lives as
messages_in rows with kind='task' in the agent group’s isolated per-task session (data/v2-sessions/<agent_group_id>/<task_session_id>/inbound.db), not the central DB. A healthy recurring series shows one pending row (the next run) plus one completed row per past firing, all sharing a series_id — but ncl tasks get already surfaces that history, so you rarely need to open the database yourself.What you built
A recurring task that is nothing but data: amessages_in row the 60-second host sweep wakes a container for, a clone-on-completion loop keyed by series_id, and an optional script gate between “the task fired” and “the agent got called”. No daemon, no crontab — ncl tasks cancel ends the series and the sweep reclaims its session.
Next steps
- ncl tasks reference — every verb and flag on the
tasksresource - Session DB schema — every column on
messages_in - Multi-agent swarm — schedule work that fans out to other agents