Skip to main content
v2 has no scheduler service. A scheduled task is a row in the session’s messages_in table with kind='task' and a process_after timestamp — same inbox, same flow as any chat message. The agent is the entire scheduling interface: you create, change, and cancel tasks by asking it in chat. 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:
Scout, every weekday at 9am remind me to post my standup update
Mechanically, the agent calls the schedule_task MCP tool with a prompt, a processAfter timestamp for the first run, and a cron recurrence (0 9 * * 1-5). The container can’t write the host-owned inbound database, so the tool emits a system action through messages_out; the host applies it, inserting a messages_in row 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 (task-<timestamp>-<random>). For a one-shot task — “remind me tomorrow at 2pm to review the budget” — it simply 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 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 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.
3

Manage it in chat

All management goes through the agent too:
Scout, list my scheduled tasks
Scout, pause the standup reminder
Scout, move the standup reminder to 9:30
Scout, cancel the standup reminder
Exact tool behaviors behind those phrases:
ToolBehavior
list_tasksOne row per series — the live pending or paused occurrence, not the pile of completed firings. The id shown is the series id, which every other tool expects.
update_taskEdits the live occurrence in place (matches by id or series id). Omitted fields are untouched; an empty string clears recurrence (the task becomes one-shot) or script. If nothing matches, the host messages the agent back so it can tell you.
pause_task / resume_taskFlip status between pending and paused. Paused rows are never counted as due, so they don’t fire or wake anything.
cancel_taskMarks all live rows in the series completed and nulls recurrence — the series ends, history rows stay.
Full parameter schemas are in the MCP tools reference.
4

Gate frequent tasks with a script

Every firing that reaches the agent is a Claude API call. For tasks that run more than a few times a day, schedule_task accepts an optional script — a bash script that runs before the agent is invoked:
Scout, every hour check for open PRs needing my review. Use a script that
curls the GitHub API and only wake yourself if there are any.
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 — 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.
  4. wakeAgent: true — the agent wakes with data injected into the task prompt as scriptOutput.
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 all interpreted in that one zone. To pin it:
# in .env (or the host's environment)
TZ=Europe/Madrid
Restart the host after changing it; the value is read once at startup.
6

Inspect from the host

There is no ncl tasks resource — the agent is the interface, and the rows live in each session’s inbound database, not the central DB. To look at them directly, find the session:
ncl sessions list --agent-group-id <agent-id>
then read its inbound database (host-owned — query read-only, never write to it):
sqlite3 "file:data/v2-sessions/<agent_group_id>/<session_id>/inbound.db?mode=ro" \
  "SELECT id, status, process_after, recurrence, series_id
     FROM messages_in WHERE kind='task' ORDER BY process_after"
A healthy recurring series shows one pending row (the next run) plus one completed row per past firing, all sharing a series_id.

What you built

A recurring task that is nothing but data: a messages_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 “Claude got called”. No daemon, no crontab — delete the row series (via cancel_task) and it’s gone.

Next steps

Last modified on June 10, 2026