Skip to content

Reminders

Reminders are autonomous agent sessions that fire on a schedule. You define what the agent should do, pick a schedule (once, interval, or cron), and optionally deliver the result to Slack, Discord, or Mattermost. The daemon handles the rest.

Nobody is in the chat when a reminder fires, so it can’t answer an approval prompt. A tool call that would prompt is denied. To prepare a reminder for that, read Run reminders, webhooks and scripts unattended. For the commands that create and manage reminders, see netclaw reminder.

The scheduling subsystem is on by default. Toggle it in ~/.netclaw/config/netclaw.json:

{
"Scheduling": {
"Enabled": true
}
}
FieldTypeDefaultDescription
EnabledbooltrueWhen false, reminders stop firing and the agent’s reminder tools (set_reminder, cancel_reminder, list_reminders, get_reminder_history, run_reminder) are unregistered.

Disabling doesn’t delete existing reminders. Re-enable and they pick up where they left off. The daemon restarts itself when netclaw.json changes.

Reminder data lives alongside other netclaw state:

~/.netclaw/schedules/reminders/
├── daily-standup.json # reminder definition
├── daily-standup.history.jsonl # execution history (append-only)
├── infra-check.json
└── infra-check.history.jsonl

Each reminder gets a JSON definition and a JSONL history log. The filename is the reminder ID. History is capped at 500 entries per reminder, and the oldest entries are trimmed automatically. Each run also gets a session, with its log at ~/.netclaw/sessions/reminder_<id>_<fire time>-<hash>/logs/session.log.

This is the stored format, and what netclaw reminder validate checks:

FieldTypeRequiredDescription
idstringYesKebab-case slug, up to 50 characters. create and set_reminder normalize whatever you pass. The prefix restart-resume- is reserved: both refuse it, and netclaw reminder import fails with HTTP 400 for a file that uses it
titlestringYesHuman-readable name
instructionsstringYesPrompt for the agent session when the reminder fires
schedule.typeenumYesOneShot, Interval, or Cron
schedule.fireAtMslongOneShotUnix timestamp in milliseconds. Must be in the future
schedule.intervalTickslongInterval.NET ticks (1 tick = 100 ns). create fills this in from 2h and similar
schedule.cronExpressionstringCron5-field cron expression, UTC unless it starts with CRON_TZ=<zone>
schedule.originalExpressionstringNoWhat you typed, for display ("6h", "0 */6 * * *")
delivery.kindenumYesCurrentSession, Channel, or None
delivery.transportstringChannelslack, discord, or mattermost
delivery.addressstringChannelTarget channel or user. See Reminder targets
delivery.sessionIdstringCurrentSessionFilled in by the daemon from the creating session
deliveryRequiredboolNoDefault true. Fails the run if a Channel delivery doesn’t happen. No effect for CurrentSession
deliveryInstructionsstringNoGuidance for what to put in the delivery message
audienceenumYesPersonal, Team, or Public. Decides which tools and files the run can use. See Audience scoping
boundarystringYesTrust boundary: boundary:personal, boundary:team, boundary:public or boundary:trusted-instance. The daemon rejects one the audience doesn’t allow, such as boundary:personal on a Team reminder
enabledboolNoDefault true
expiresAtMslongNoAuto-disable after this time. Meant for recurring reminders
createdBy, createdAtMs, updatedAtMsstring, longNoBookkeeping. create and set_reminder both write llm-tool
consecutiveFailures, terminalOutcomeint, enumNoMaintained by the daemon. See Auto-disable

A file without audience or boundary is rejected when the daemon loads it, and the reminder is never scheduled. netclaw reminder validate fails with missing required properties including: 'boundary'.

{
"id": "infra-check",
"title": "Infrastructure health check",
"instructions": "Run netclaw doctor and summarize any degraded services.",
"schedule": {
"type": "Interval",
"intervalTicks": 216000000000,
"originalExpression": "6h"
},
"delivery": {
"kind": "None"
},
"audience": "Personal",
"boundary": "boundary:personal"
}
{
"id": "daily-standup",
"title": "Daily standup summary",
"instructions": "Check yesterday's reminder history and git commits. Post a standup summary.",
"schedule": {
"type": "Cron",
"cronExpression": "CRON_TZ=America/Chicago 0 9 * * MON-FRI",
"originalExpression": "CRON_TZ=America/Chicago 0 9 * * MON-FRI"
},
"delivery": {
"kind": "Channel",
"transport": "slack",
"address": "C12345678"
},
"deliveryRequired": true,
"deliveryInstructions": "Post the summary to the standup channel. Keep it under 5 bullet points.",
"audience": "Team",
"boundary": "boundary:team"
}

Check a file with netclaw reminder validate <file>, then load it with netclaw reminder import. Or skip the file and use netclaw reminder create.

TypeFiresMinimumFormat
OneShotOnce, then stopsAny future timeRelative duration (30m, 2h) or ISO 8601 timestamp
IntervalRepeats on a fixed duration60 secondsDuration string (2m, 2h, 1d)
CronOn a cron scheduleNone5-field cron expression, UTC or CRON_TZ=<zone>

An interval reminder fires its first run one interval after you create or re-enable it, not at a fixed wall-clock time.

Cron expressions run in UTC unless they start with CRON_TZ=<IANA zone> . The prefix makes the schedule follow that zone, including daylight saving changes:

Terminal window
netclaw reminder create standup cron "CRON_TZ=Europe/Brussels 0 9 * * MON-FRI" "Post the standup summary."

Use an IANA zone ID like America/Chicago. IDs with spaces (Eastern Standard Time) and unknown zones are rejected when you create, import or validate the reminder:

[FAIL] Error: Invalid CRON_TZ prefix. Unknown time zone 'Eastern'. Use an IANA id such as America/Chicago.

Delivery decides whether a reminder talks back to a live conversation or starts a fresh one.

KindWhat happensRequirements
NoneThe agent runs silently. The result is recorded in history onlyNothing
ChannelThe agent posts the result to a channel or DM with the send_channel_message tooltransport and address, with that channel configured
CurrentSessionThe reminder re-enters the conversation it was created in and replies thereA live session at creation: Slack, Discord, Mattermost, TUI or SignalR

CurrentSession is a deferred turn. “Remind me in this thread after lunch” is a CurrentSession reminder. The session has to still exist when it fires. The run ends as soon as the session accepts the fire, and netclaw doesn’t wait for or retry the reply. If posting the reply then fails for good, the session logs an error and sends a reminder.delivery.failed warning alert. While an earlier fire of the same reminder is still queued in that session, the newer fire is dropped.

Channel needs no live session and proactively posts to a target you name. “Every weekday at 9am, post a standup summary to #ops” is a Channel reminder, and it’s the right choice for anything recurring. The agent sets delivery_kind to current_session, channel or none when it calls set_reminder, and the CLI offers none and channel.

The target format depends on the transport. set_reminder and create check it when the reminder is created.

TransportChannel targetUser or DM target
slack#channel-name or a C.../G... channel ID@username or a U... user ID
discordchannel:<channelId> or <#channelId>dm:<userId>, @<userId> or <@userId>, delivered to that user’s DM
mattermostchannel:<channelId>@<userId>, delivered to that user’s DM

Discord and Mattermost IDs look the same for channels and users, so both require the channel:, dm: or @ prefix. A bare ID is rejected with a disambiguation error. Discord DM delivery needs AllowDirectMessages: true and the user in AllowedUserIds, the same access controls that gate live Discord chat.

When deliveryRequired is true (the default) and the delivery kind is Channel, the run fails if the agent doesn’t call send_channel_message, or if the call returns an error. A failed run emits a ReminderExecutionFailed warning alert.

A reminder’s audience is the audience its session runs as, which decides its tools, file access and approval policy exactly as in a chat of that audience. A reminder made by the agent inherits the audience of the chat that created it. A reminder made with the CLI is Personal. The agent can pass a lower audience to set_reminder, and can’t pass a higher one than its own.

The reminder tools only see reminders at or below the caller’s audience:

CallerReminders it can list, run, cancel, read history for and update
Personal chatPersonal, Team, Public
Team chatTeam, Public
Public chatPublic
CLI and HTTP API (operator)All

The run’s tool access follows the deployment posture as well: under a Team posture, a reminder created for the Personal audience, for example from a Slack DM, runs with Team access. After an upgrade, run_reminder refuses such an older reminder and a narrower chat doesn’t list it, so recreate it from a chat.

A reminder outside the caller’s scope reads as not found, and set_reminder can’t overwrite it. Personal and Team sessions get the reminder tools by default, and Public sessions don’t. If you’ve stored an explicit AllowedTools list for Team, it replaces the default, so add run_reminder to it.

run_reminder carries out an existing reminder’s exact prompt in the current chat, so a person can answer its approval prompts. Use it through the /run-reminder <id> slash command or netclaw reminder run <id>. It’s the way to save the grants a scheduled run needs, and the unattended runs guide shows it end to end.

  • The chat must have the reminder’s own audience. A Personal chat can’t test a Team reminder, because the test would have wider permissions than the scheduled run. The tool says which audience to use.
  • A CLI chat is Personal, narrowed to your deployment posture. On a Personal host netclaw reminder run only tests Personal reminders, so test a Team reminder from a Team channel.
  • It refuses to run in a chat where nobody can answer prompts, such as a headless script.
  • A one-shot that has already ended can’t be tested: run_reminder answers Reminder '<id>' ended (<outcome>) at <time>. A one-shot reminder deletes itself after it ends. Set it again with set_reminder to run it. Test a one-shot before its fire time.
  • A reminder reaches only the child runs of the session where it fires, so a test in another chat doesn’t prove the scheduled run can start one.
  • The steps are real. The test doesn’t change the schedule or the history.

To watch a reminder fire inside the current conversation instead, ask the agent to call set_reminder with a short one-shot (10s) and current_session delivery.

SettingValueConfigurable?
Execution timeout1 hour per attemptNo
Stall timeout20 minutes without session outputNo
History retention500 entries per reminderNo

Each reminder runs at most one occurrence at a time. If a fire arrives while the previous run is still going, netclaw skips it and counts it. There’s no limit on how many different reminders run at once. 0.26.0 removed the old capacity gate that skipped reminders when the daemon was busy.

A run that produces no session output for 20 minutes concludes as failed with Reminder execution stalled: no session output for 00:20:00. and releases its slot. A stall counts as a failure.

A message from the operator or a trusted person during a reminder turn ends that turn after its current tool batch. The reminder records success and skips its remaining steps, though child runs the turn already started carry on. An open child approval request holds reminder fires for that session until the prompt closes. After downtime a reminder gets at most one catch-up fire.

Recurring reminders get an instruction appended: if the agent decides the reminder’s purpose is permanently fulfilled, it should call cancel_reminder.

Failures surface two ways.

On demand. netclaw reminder status <id> reports the failure streak, skipped-fire count, whether a run is in flight and the last five outcomes.

Pushed to the channel. When a Channel reminder fails, netclaw posts a plain-language notice to that reminder’s destination channel, where its results normally land:

Reminder "Daily standup summary" failed: send_channel_message: channel not found

A broken reminder posts at most five times before it disables itself. On the fifth, threshold-hitting failure the message is the auto-disable notice instead:

Reminder "Daily standup summary" was automatically disabled after 5 consecutive failures. Last error: send_channel_message: channel not found

Two things aren’t posted to the channel:

  • Skipped fires. A fire dropped because the previous run was still going isn’t a failure. It’s counted in reminder status and never posted.
  • CurrentSession and None failures. They have no destination channel, so they go out as operational alerts (ReminderExecutionFailed, ReminderAutoDisabled, and reminder.delivery.failed when a CurrentSession reply can’t be posted) to the alert destinations you set up.

A failure to schedule the next occurrence emits ReminderScheduleFailed, counts toward auto-disable and raises the same notices. Posting a notice is fire-and-forget. If the channel is unreachable, netclaw logs it and carries on.

Two mechanisms stop reminders automatically.

After 5 failed runs in a row, the reminder is disabled and a ReminderAutoDisabled critical alert fires, and terminalOutcome becomes Failed. A retry of a recurring occurrence that expires counts as one failure. denied runs don’t count. A single success resets the counter. Fix the cause, then re-enable:

Terminal window
netclaw reminder enable <id>

Enabling clears the failure count and the terminal outcome, and reschedules from now.

Set expiresAtMs (or --expires-in on the CLI) to stop a recurring reminder after a deadline. It fires normally until then and disables on the next scheduled fire. --expires-in and set_reminder reject it on one-shot reminders.

The agent manages reminders through five tools, all in the scheduling group:

ToolDescription
set_reminderCreate or update (upsert) a reminder
cancel_reminderDelete a one-shot, or disable a recurring reminder and keep its definition
list_remindersList active or all reminders
get_reminder_historyReturn execution history (default 20, max 100)
run_reminderReturn a reminder’s exact prompt so the agent can run it in this chat

These tools are registered only when Scheduling.Enabled is true, and each one is limited to the reminders its audience can see.

A one-shot reminder deletes itself when it ends, whether it was delivered, failed, was denied, was cancelled or expired. It leaves netclaw reminder list at once. netclaw keeps an end record with the outcome (completed, failed, cancelled or expired) and the run history for 12 days, then purges both. netclaw reminder status and history answer during that time. A one-shot whose run was denied ends as failed, and its end record keeps the denial text and the prompt, so you can set it again once the grant exists. A one-shot that fails is retried before it ends. A one-shot that comes due while the daemon is down is still delivered when it returns, for up to 7 days after its time. Past that, it expires.

At startup and every 12 hours, the daemon sweeps one-shots whose time has passed. It ends leftovers, finishes an end that a crash interrupted, and purges end records older than 12 days. A disabled one-shot whose time has passed ends as cancelled. Expired recurring reminders are disabled and kept. See netclaw reminder for the CLI view.

Symptom: reminder history or reminder status shows denied, and the file wasn’t written or the command didn’t run.

Cause: The agent called a tool that needs approval, and nobody could answer. The call was denied, the agent said so, and the run finished without a failure count.

Fix: Follow the unattended runs guide. The daemon log has approval_required_unattended with the session that made the call.

Symptom: Consecutive fails is climbing and Recent history shows a delivery error.

Cause: Usually a missing Slack channel or an invalid delivery target.

Fix: Run netclaw reminder status <id> for the actual error. A channel reminder also posts it to its destination channel. Fix the target, then netclaw reminder enable <id> if it already disabled itself.

Symptom: Skipped occurrences goes up on every fire.

Cause: A run takes longer than the interval, so each new fire lands while the last run is still going.

Fix: Lengthen the schedule or shrink the work. The daemon log says source=active for this. arrived too late to finish before its delivery deadline means the scheduler delivered the fire too late to finish.

Symptom: Nothing at the scheduled time and Skipped occurrences is 0.

Cause: The daemon is down, scheduling is off, the reminder is disabled, or it was rejected on load.

Fix: Run netclaw status, check Scheduling.Enabled, and netclaw reminder show <id> to confirm enabled is true. A definition missing audience or boundary is rejected when loaded, so look for it in netclaw reminder list.

Symptom: A 0 9 * * * reminder fires at 9:00 UTC.

Cause: Cron runs in UTC unless you say otherwise.

Fix: Add a prefix: CRON_TZ=America/Chicago 0 9 * * *.

Run netclaw reminder list to confirm the reminder appears with the right schedule and next fire time. After it fires, netclaw reminder history <id> shows the session ID to pass to netclaw sessions. If it needs shell or file access, test it with netclaw reminder run <id> before the schedule does.