My Claw: Build Your AI System with Claude Code
Scheduling
The claw acts without being asked. Script gates decide whether work is worth doing before spending tokens. Default: silent. Notifications: opt-in.
The claw waits. It has email access, calendar access, memory, a soul, logs, and a persistent process. But it only acts when you message it on Telegram. You have to remember to ask "what's on my calendar today?" every morning. You have to remember to check for important emails. You have to remember to ask for a weekly summary.
The claw could do all of this on its own. It just needs a schedule and permission.
This lesson makes the claw proactive. Scheduled tasks fire on a timer. A bash script checks whether the work is worth doing before spending tokens. If there's nothing to do, the script exits and no one pays for an Opus call. If there is work, the claw acts on it.
The default is silent. The claw does the work, updates its files, and waits. No messages. No notifications. If you want to be notified, you opt in. This is a tool that works for you in the background. Not a chatbot that messages you because a timer went off.
Script gates
Bash is free. Claude costs money.
Every scheduled task starts with a script gate. A short bash check that decides whether the claw should wake up. If the answer is no, the process exits with code 0 and nothing happens. If the answer is yes, the script invokes the claw with a focused prompt.
This is NanoClaw's core scheduling insight: a JSON object with wakeAgent: boolean. The gate runs every cycle. The agent runs only when warranted.
#!/bin/bash
# Gate: check if any VIP emails arrived since last check
LAST_CHECK_FILE=".claw/gates/vip-email-last-check"
DIGEST=".claw/cache/digest.md"
# No digest yet? Nothing to check.
[ -f "$DIGEST" ] || exit 0
# Get timestamp of last check
if [ -f "$LAST_CHECK_FILE" ]; then
LAST_CHECK=$(cat "$LAST_CHECK_FILE")
else
LAST_CHECK=0
fi
# Check if digest is newer than last check
DIGEST_MTIME=$(stat -f %m "$DIGEST" 2>/dev/null || stat -c %Y "$DIGEST" 2>/dev/null)
[ "$DIGEST_MTIME" -gt "$LAST_CHECK" ] || exit 0
# Check if digest mentions any VIP names
VIPS=$(grep -i "VIP\|priority\|urgent" "$DIGEST" 2>/dev/null)
if [ -z "$VIPS" ]; then
date +%s > "$LAST_CHECK_FILE"
exit 0
fi
# Gate passed: VIP email detected
echo '{"wakeAgent": true, "reason": "VIP email detected"}'
date +%s > "$LAST_CHECK_FILE"The gate checks file timestamps, greps for patterns, compares against last-run markers. All free. All instant. The claw only wakes when the gate says so.
The schedule runner
One script that runs all your scheduled tasks. Each task is a gate script paired with a prompt.
import { execFileSync, execSync } from "node:child_process";
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { log } from "./log.mjs";
const __dirname = dirname(fileURLToPath(import.meta.url));
const GATES_DIR = join(__dirname, "gates");
const SCHEDULE_FILE = join(__dirname, "schedule.json");
mkdirSync(join(__dirname, ".claw", "gates"), { recursive: true });
const tasks = JSON.parse(readFileSync(SCHEDULE_FILE, "utf-8"));
for (const task of tasks) {
try {
// Run the gate script
const gateResult = execFileSync(
join(GATES_DIR, task.gate),
{ cwd: __dirname, encoding: "utf-8", timeout: 10000 }
).trim();
// No output or empty = gate closed, skip
if (!gateResult) {
log("schedule", { task: task.name, gateResult: "closed" });
continue;
}
// Parse gate output
let gate;
try {
gate = JSON.parse(gateResult);
} catch {
gate = { wakeAgent: true, reason: gateResult };
}
if (!gate.wakeAgent) {
log("schedule", { task: task.name, gateResult: "closed" });
continue;
}
// Gate open. Run the claw with the task prompt.
log("schedule", {
task: task.name,
gateResult: "open",
reason: gate.reason,
});
const prompt = task.prompt.replace("{{reason}}", gate.reason || "");
// Scheduled tasks call claude -p directly, not through claw.mjs.
// They don't need the soul, memory, or session infrastructure.
// They're fire-and-forget: run a focused prompt, write a file, exit.
const result = execFileSync("claude", ["-p", prompt, "--no-input"], {
cwd: __dirname,
encoding: "utf-8",
timeout: 120000,
});
log("schedule-run", {
task: task.name,
reason: gate.reason,
outputLength: result.length,
});
} catch (err) {
log("error", {
component: "schedule",
task: task.name,
message: err.message,
});
}
}The schedule runner doesn't decide what to do. It runs gates and invokes the claw when gates open. The intelligence lives in the gate scripts and in the prompts.
The schedule file
[
{
"name": "vip-email-check",
"gate": "new-vip-email.sh",
"prompt": "A VIP email was detected: {{reason}}. Check the digest, draft a summary of what needs attention, and save it to .claw/alerts/vip-email.md"
},
{
"name": "daily-digest",
"gate": "morning-window.sh",
"prompt": "Compile a morning briefing from the latest digest and recent activity. Save to .claw/briefings/today.md"
},
{
"name": "calendar-conflicts",
"gate": "calendar-changed.sh",
"prompt": "Check the digest for calendar conflicts or double-bookings in the next 48 hours. If any, save to .claw/alerts/calendar-conflicts.md"
}
]Each task has a name, a gate script, and a prompt. The prompt can use {{reason}} to include context from the gate. The claw writes results to .claw/ where they persist.
Notice what the prompts don't say: "Send me a message." The claw writes files. It updates state. It does work. It doesn't interrupt you.
Time-window gates
Some tasks should only run at certain times. A morning briefing at 3 AM is useless.
#!/bin/bash
# Gate: only pass during morning hours (6-9 AM local time)
HOUR=$(date +%H)
[ "$HOUR" -ge 6 ] && [ "$HOUR" -lt 9 ] || exit 0
# Only run once per day
TODAY=$(date +%Y-%m-%d)
LAST_RUN_FILE=".claw/gates/morning-last-run"
if [ -f "$LAST_RUN_FILE" ]; then
LAST_RUN=$(cat "$LAST_RUN_FILE")
[ "$LAST_RUN" = "$TODAY" ] && exit 0
fi
echo '{"wakeAgent": true, "reason": "Morning briefing window"}'
echo "$TODAY" > "$LAST_RUN_FILE"Cheap. No API calls. No token spend. The gate fires once during the morning window and then closes for the rest of the day.
One detail: stat -f %m is macOS syntax. Inside Docker (Linux), use stat -c %Y. The VIP email gate earlier handles both with a fallback (stat -f %m "$DIGEST" 2>/dev/null || stat -c %Y "$DIGEST" 2>/dev/null). Do the same in any gate that checks file timestamps.
The schedule plist
A second launchd service, separate from the channel. The channel runs continuously for Telegram. The scheduler runs periodically for background work.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.my-claw.schedule</string>
<key>ProgramArguments</key>
<array>
<string>__CLAW_DIR__/run.sh</string>
<string>schedule</string>
</array>
<key>WorkingDirectory</key>
<string>__CLAW_DIR__</string>
<key>StartInterval</key>
<integer>900</integer>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
<key>HOME</key>
<string>__HOME__</string>
</dict>
<key>StandardOutPath</key>
<string>__CLAW_DIR__/.claw/logs/schedule.out.log</string>
<key>StandardErrorPath</key>
<string>__CLAW_DIR__/.claw/logs/schedule.err.log</string>
</dict>
</plist>StartInterval: 900 runs the scheduler every 15 minutes. No KeepAlive because this isn't a long-running process. It runs, checks gates, maybe invokes the claw, and exits. launchd fires it again in 15 minutes.
The install script from the durability lesson gets a third plist. Same pattern, different label. You now have three services: channel (continuous), gather (every 15 min), and schedule (every 15 min).
Cost architecture
This is where scheduling gets dangerous. An Opus call every 15 minutes costs real money. 96 calls per day at $0.04 each is $3.84. If nothing happened, you spent $3.84 on nothing.
Script gates fix this. If 90% of your gate checks find nothing to do, you run 10 Opus calls instead of 96. That's $0.40 instead of $3.84.
But you can go further. Not every scheduled task needs Opus. The VIP email check just needs to summarize an email. That's a Haiku job at $0.01. The calendar conflict check is pattern matching. Also Haiku. Save Opus for the operator conversation.
Model routing emerges from cost pressure, not from abstraction. When you see the bill, you'll move background tasks to cheaper models because the expensive model doesn't produce noticeably better summaries for background work.
Opt-in notifications
The claw writes files by default. If you want it to also message you, add a notify field to the task:
{
"name": "vip-email-check",
"gate": "new-vip-email.sh",
"prompt": "...",
"notify": true
}The schedule runner checks this flag after a successful run:
if (task.notify && existsSync(join(__dirname, ".claw", "owner-chat-id"))) {
const chatId = readFileSync(
join(__dirname, ".claw", "owner-chat-id"), "utf-8"
).trim();
const alertFile = result.match(/\.claw\/alerts\/[\w-]+\.md/)?.[0];
if (alertFile && existsSync(join(__dirname, alertFile))) {
const alert = readFileSync(join(__dirname, alertFile), "utf-8");
// Send via Telegram using the bot from channel.mjs
// or shell out to a simple notification script
}
}This is intentionally sketched, not production code. The notification path depends on your channel setup. The point is: the operator decides which tasks notify. The default is silence.
Writing your own gates
A gate is any executable that exits with:
Some ideas:
| Gate | What it checks | Cost |
|---|---|---|
new-vip-email.sh | Digest has VIP/urgent markers | Free |
morning-window.sh | Time of day + once-per-day lock | Free |
git-behind.sh | Local repo is behind remote | Free |
disk-space.sh | Disk usage above threshold | Free |
stale-digest.sh | Digest file older than 6 hours | Free |
weekly-summary.sh | It's Monday + hasn't run this week | Free |
Every gate is free. Every gate runs in milliseconds. The claw only wakes for real work.
The schedule skill
Last skill. By now you know the pattern cold. The claw should know about its scheduled tasks:
# Scheduling
The claw has scheduled tasks that run every 15 minutes via launchd.
## How it works
Each task has a gate script (bash) that decides whether to wake the claw.
Gates are free. The claw only runs when a gate opens.
## Files
- `schedule.json` — task definitions (name, gate, prompt, notify)
- `gates/*.sh` — gate scripts that output JSON with `wakeAgent: true` or nothing
- `service/my-claw-schedule.plist.template` — launchd periodic service
- `.claw/gates/` — per-gate state files (last-run timestamps, etc.)
## Commands
- `./service/install.sh` — installs both the channel and schedule services
- `cat schedule.json` — view configured tasks
- `node schedule.mjs` — run all gates and execute open ones manually
## When to suggest
- If the operator asks "what does the claw do automatically?"
- If the operator wants to add a new scheduled check
- If the operator asks about background work or automation costsWhat you have
A claw that acts without being asked. Scheduled tasks fire every 15 minutes. Script gates decide whether work is worth doing before spending a single token. The claw writes results to files. Notifications are opt-in.
The full system:
| Layer | What it does | Lesson |
|---|---|---|
| Substrate | Talk to Claude programmatically | 0 |
| Skills | Read email, calendar, real data | 1 |
| Background awareness | Gather context cheaply with Haiku | 2 |
| Channels | Telegram conversations with per-chat sessions | 3 |
| Memory | Self-curating operator preferences and patterns | 4 |
| Soul | Persistent identity and personality | 5 |
| Durability | Survives reboots and crashes | 6 |
| Observability | Structured logs, self-reporting, cost tracking | 7 |
| Scheduling | Proactive background work with script gates | 8 |
Nine layers. Each one builds on the last. The substrate lets you talk to Claude. Skills give it real data. Awareness keeps that data fresh. Channels give it a face. Memory gives it continuity. Soul gives it character. Durability keeps it alive. Observability lets it see itself. Scheduling lets it act.
That's a personal AI familiar. It runs on your machine, uses your data, costs a few dollars a day, and gets better as you use it. No cloud platform. No subscription. No one else's model of how an AI assistant should work.
Your claw. Your rules.