Skip to main content
v2 has no scheduler service. A scheduled task is a row in an isolated per-task session’s 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 pending row carrying the same series_id forward, and clears recurrence on 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, ncl tasks create accepts an optional --script — a bash script that runs before the agent is invoked:
The contract, from container/agent-runner/src/scheduling/task-script.ts:
  1. When the task fires, the script runs first inside the container — 30-second timeout, 1 MB output cap.
  2. The last line of stdout must be JSON: { "wakeAgent": true|false, "data": ... }.
  3. wakeAgent: false marks 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, then ncl tasks resume <id>.
  4. wakeAgent: true — the agent wakes with data injected into the task prompt as scriptOutput. The prompt’s <task> element carries both the scheduled time (time, from process_after) and a fresh current_time stamped when the run reaches the agent — so relative wording like “today” stays correct even when a run fires late.
Be honest about what this saves: the container still wakes on every firing (the sweep can’t know the script’s verdict in advance). The script gates the expensive part — the agent invocation — not the container spawn. Skip scripts entirely for tasks that need the agent’s judgment every time, like daily briefings.
5

Set the timezone

When you say “9am”, three layers have to agree on whose 9am. The chain: the host resolves 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:
Restart the host after changing it; the install default is read once at startup.A group can override the default without touching .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: a messages_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

Last modified on September 4, 2026