Some prompts I give my coding agent aren’t really conversations. They’re chores. “Summarize what changed yesterday.” “Check if any dependency got a security release.” “Look at last night’s CI failures.” Same prompt, same folder, every day. I don’t need to sit there for them, I need them done before I open the laptop.
pi-tick is a small scheduler for Pi that I wrote to scratch
exactly this itch. The interesting bit is who uses it: mostly the agent
itself. You tell Pi what you want and when, and Pi creates the job. The OS
(launchd on macOS, your user crontab on Linux) owns the timer, and when it
fires, pi-tick starts pi in a fresh session and records everything that
happened.
No daemon of its own, no server. If you reboot, the jobs are still there.
Just ask Pi
pi-tick gives the agent a tick_create tool, so scheduling is a sentence:
$ pi -p "Create a pi-tick job called daily-mail-recap for this repo: every \
weekday at 9:00, recap yesterday's git commits in 3 bullets and email it to \
me at me@example.com. Leave it disabled for now, I want to test it first."
Created disabled job `daily-mail-recap`: weekdays at 09:00 for this repo.
It will email a 3-bullet recap of yesterday's commits to `me@example.com`
when enabled.
About 11 seconds later the job is there:
$ pi-tick list
id enabled kind schedule
daily-mail-recap no weekly weekly monday,tuesday,wednesday,thursday,friday @ 09:00
“Weekdays” became a weekly schedule for Monday to Friday. The part I liked more is the prompt Pi wrote for the job. Remember, that prompt runs later with nobody watching:
{
"id": "daily-mail-recap",
"prompt": "In ~/Dev/home-management/blog, recap the git commits made yesterday in exactly 3 concise bullets (say \"No commits yesterday\" if applicable) and email the recap to me@example.com. Use the environment's available email-sending mechanism; if sending is not configured or fails, report the failure clearly and do not expose credentials.",
"schedule": { "kind": "weekly", "value": { "days": ["monday", "tuesday", "wednesday", "thursday", "friday"], "time": "09:00" } },
"enabled": false
}
It handled the empty day and the “mail isn’t set up” case without being asked. Note that pi-tick itself doesn’t send email. The agent does, with whatever it already has: a mail CLI, a Resend API key, an MCP server.
This works mid-task too. You’re in the middle of a session, fixing a flaky test, and you say “check this test every night for a week and tell me if it fails again”. Pi schedules it and you move on.
One rule is built in on purpose. The agent can create a job and enable it in the same call, but it can’t enable a job that already exists. A prompt that goes off the rails can’t quietly switch on something you left disabled.
What a run looks like
Before trusting a job with a schedule, run it once by hand. Here’s a simpler daily recap on the repo of this blog:
$ pi-tick run morning-recap --manual
$ pi-tick show morning-recap
── morning-recap · 2026-11-15 18:45:30 · run 18109034 · 17s · 1595↑ 146↓ ──
> Read-only task, do not modify any files. Look at git log for the last
24 hours in this repo and write a 3-bullet recap of what changed and why.
→ bash: {"command":"git log --since='24 hours ago' --format=... --stat"}
23bba28a387875b60b31370fb4b7b562360c51ca
fix: fail fast in zone-harden apply; case-insensitive probe rule
…(57 more lines)
→ - Scaffolded the Hugo/PaperMod blog as a Cloudflare static-assets-only
Worker, including CI/CD, CSP/security headers, and initial content.
→ - Added declarative Cloudflare zone hardening: TLS/HSTS settings, WAF
rules, rate limiting, and a zone-harden.sh check/apply workflow.
→ - Hardened that workflow to fail immediately on Cloudflare/API errors
and made the probe-path WAF match case-insensitively.
17 seconds, about 1,700 tokens, and a fair summary of my afternoon. Happy with
it? pi-tick enable morning-recap and it runs every day at 08:30.
The log is the point
A cron job that fails quietly is worse than no cron job. Every run, scheduled
or manual, gets a record in runs.jsonl and a full transcript.
Funny enough, the first run I did for this post failed. My machine still had
pi-tick 0.1.3, Pi had since turned its pi launcher into a shell script, and
the old runner tried to start it with node:
$ pi-tick log morning-recap
jobId runId startedAt duration exitCode tokens
morning-recap 331af98c 2026-11-15 18:44:40 0s 1
morning-recap 18109034 2026-11-15 18:45:30 17s 0 1595/146
Updating to 0.1.6 fixed it, and the second row is the run above. I didn’t
have to guess. pi-tick log --failed shows only the broken runs, and the log
is plain JSONL, so jq works on it:
$ jq -r '[.startedAt[0:19], .exitCode, (.error // "-")] | @tsv' \
~/.pi/agent/tick/runs.jsonl
2026-11-15T17:44:40 1 SyntaxError: Unexpected token 'case'
2026-11-15T17:45:30 0 -
Token counts are in there too. Watch them: a job that used to cost 500 tokens and now costs 12,000 means your prompt is pulling in more context than you think.
Without the agent
Everything Pi does goes through the same CLI, so you can skip the model entirely:
$ pi-tick add morning-recap \
--prompt "Read-only task, do not modify any files. Look at git log for the last 24 hours in this repo and write a 3-bullet recap of what changed and why." \
--cwd ~/Dev/home-management/blog \
--kind daily --time 08:30
Added job 'morning-recap' (disabled; use 'pi-tick enable morning-recap' to schedule)
Inside a Pi session there’s /tick list, /tick run <id>, /tick show <id>
and /tick kill <id>, with tab completion. A job prompt that starts with /
runs as a real Pi slash command, not as text the model interprets. And
--kind once --at 2027-01-22T15:30:00Z gives you a one-shot reminder. If the
machine wakes up more than a day late for it, the job is marked once_missed
and dropped, so it won’t fire a year from now by surprise.
Things to know before you rely on it
- A sleeping Mac doesn’t fire jobs. launchd won’t wake the machine, and missed runs aren’t replayed. Same as cron. Pick times when the laptop is usually open.
- macOS permissions. Jobs started by launchd can’t show “node wants to
access Documents” dialogs. Run each job once with
/tick runso you can answer them. - Linux is minute-granular. Cron can’t go below 60 seconds; launchd goes down to 5.
- Transcripts are pruned after 7 days or 500 MB per job, so they don’t slowly fill your disk.
- One run at a time. If yesterday’s job is still running, today’s start is refused rather than piling up.
Try it
pi install npm:pi-tick
ln -s ~/.pi/agent/tick/pi-tick.mjs ~/.local/bin/pi-tick
Then open Pi and ask it to take the most boring prompt you typed this week and run it for you tomorrow morning.
Source and issues: github.com/gscode1/pi-tick.