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).
Agents stamped from a template may arrive with recurring tasks already defined. They are created paused — list them with
ncl tasks list --group <agent-group-id> --status paused and enable the ones you want with ncl tasks resume <task-id>.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>. Recurrences over 4 fires per day are refused unless the task carries a --script gate (or the explicit --dangerously-override-recurrence-limit flag) — a high-frequency wake with no gate is almost always a mistake. 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
There is no separate per-task scheduler. The host sweep periodically queues active sessions for reconciliation, waiting 60 seconds after the previous tick’s work drains. Message writes and container-exit events can trigger earlier checks. Reconciliation 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:- Timing is approximate. A task due at 09:00:00 runs when reconciliation finds it due, plus queue work and any container cold start. The roughly one-minute resync is not a strict latency bound; do not schedule anything that needs second-level timing.
- Recurring tasks are clones, not loops. When a recurring occurrence completes, reconciliation 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 and its container has stopped, reconciliation closes its per-task session. History remains until explicitly deleted.
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: falsemarks the occurrence completed without invoking the agent — a normal, quiet run; the sweep clones the next occurrence as usual. A script error, timeout, or invalid output is different: the occurrence is marked failed, consecutive failures back the series off exponentially (next run = the later of the cron slot and the backoff), and after 8 consecutive script failures the series auto-pauses with a run-log note — fix the script, thenncl tasks resume <id>.wakeAgent: true— the agent wakes withdatainjected into the task prompt asscriptOutput. The prompt’s<task>element carries both the scheduled time (time, fromprocess_after) and a freshcurrent_timestamped when the run reaches the agent — so relative wording like “today” stays correct even when a run fires late.
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 install default is read once at startup.A group can override the default without touching
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 interpreted in the task’s timezone: the owning group’s override when one is set, otherwise that instance value (src/modules/scheduling/create.ts). To pin the install default:.env: ncl groups config update <group> --timezone Europe/Lisbon (or pass --timezone on ncl groups create). Scheduled-task times follow the override immediately — no restart — and the group’s containers spawn with it as TZ (src/cli/resources/groups.ts, src/container-runner.ts:485). Pass "" to clear back to the install default.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 that session reconciliation 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 reconciliation closes its session once its container stops; delete removes the retained task data.
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