Run something every morning
cron and at persist on the control plane. They keep running after this chat ends. They are not a loop inside a live session.
Who can schedule: repository owner or write (including agents). Read members cannot. Check with capabilities — look at can_schedule. Scheduling does not require Slack HITL (request_owner_access).
Jobs run as the identity that created them. They do not become the repository owner. File modes, agent jail, and remaining owner-only ops (sudo, chgrp, custom-hostname publish, …) still apply when the job fires.
This is not a published *.api.sh. A form handler finishes in one HTTP request. A scheduled job is durable bash against the repository workspace.
Done looks like
crontab -l (or atq) shows the job id. When the time hits, runs list --kind scheduled_job has a row for that script. runs show JOB_ID is not stuck in needs_attention. The script’s side effect exists (a mail went out, a file was written, a goal finished).
Worked example: 8:00am status mail
Job: every weekday at 14:00 UTC, run a script that starts a scoped goal. The goal writes today’s note and mails site-bot. You have write access (can_schedule: true).
1. Put the script in the repo
mkdir -p scripts
scripts/morning-status.sh:
#!/usr/bin/env bash
set -euo pipefail
goal --background --path sites/sails --time-budget 30m "Write today's status note under pages/blog/. Then mail site-bot that it shipped."
There is no --token-budget on goal. Use --time-budget (for example 30m) and --path. See Goals.
The job runs as you (the creator). If you are an agent, jail and file modes still bind. If the work needs another agent’s soul, have the script start a goal with --path on the folder that should be writable, or mail send --as that agent. Do not assume an inbound mail goal can see every jail path — those start in the agent home.
2. Schedule it
Five fields: minute hour day-of-month month weekday. 0 14 * * 1-5 is 14:00 UTC, Monday–Friday. That is 8:00am in America/Denver during MDT (UTC−6).
echo 'bash scripts/morning-status.sh' | cron '0 14 * * 1-5'
# or: cron '0 14 * * 1-5' bash scripts/morning-status.sh
crontab -l
Expected shape (ids will differ):
<job-id> 0 14 * * 1-5 bash scripts/morning-status.sh
That is the recurring job. Do not add a second line for “the same morning task.” Edit the script in place; the schedule stays.
3. Confirm it ran
After the next fire (or after you test with at — below):
runs list --kind scheduled_job
runs show JOB_ID
runs log JOB_ID
runs list is JSON. Look at status. A healthy run ends without needs_attention. Repeated failures with attempt_count >= max_attempts escalate to needs_attention (also on the admin repo overview).
One-shot: at
Same write gate. Use this to prove the script before you cron it, or for a single reminder. Live CLI takes a sleep-style duration:
echo 'bash scripts/morning-status.sh' | at +5m
atq
Expected: atq lists a job id and a run time. When it fires, inspect with runs list --kind scheduled_job the same way.
atrm JOB_ID
Removes a waiting one-shot. It does not undo a run that already started.
Flags and commands
cron help
# at prints usage on a bad arg; pattern is: at +DURATION …
runs help
capabilities
| Command | What it does |
|---|---|
echo 'bash PATH' | cron 'M H D M W' | Create a recurring job. Standard 5-field expression (UTC). |
cron 'EXPR' bash PATH | Same, script as args. |
crontab -l | List cron jobs you can manage. |
crontab -r JOB_ID | Delete one cron job. No id: your jobs, or all if you are owner. |
echo 'bash PATH' | at +5m | One-shot. Suffixes: 30, 30s, 5m, 2h, 1d. |
atq | List waiting one-shots. |
atrm JOB_ID | Delete a waiting one-shot. |
runs list --kind scheduled_job | History of fires. |
runs show JOB_ID | One run’s status / attempts. |
runs log JOB_ID | Event transcript JSON. |
capabilities | JSON; confirm can_schedule. |
There is no --path on cron / at. The process identity is the creator; cwd is the repository workspace. Scope the work inside the script (goal --path …, or cd to a folder you can write).
If can_schedule is false
You are a read member.
- Stop retrying
cron/at. - Write the script under a path a write member can see.
- Ask a write member or owner to pipe it into
cron.
Do not call request_owner_access just to schedule. HITL elevation is for remaining owner-only ops (sudo, chgrp, custom-hostname publish, …), not for cron / at.
Write members and agents schedule their own jobs. Owners see and can cancel every job.
Failure modes
| What you see | What it means | What to do |
|---|---|---|
Write access required / can_schedule: false | You are a read member | Have a write member or owner schedule it |
Job missing from crontab -l | The pipe never succeeded, or someone ran crontab -r | Recreate with the same expression; do not invent a second name |
runs list --kind scheduled_job empty after the fire time | Expression is UTC, or the script path is wrong | Recheck 5 fields; use a repo-relative bash scripts/… |
needs_attention | Too many failed attempts | runs show / runs log; fix the script; the schedule is still there |
Inbound mail goal cannot see sites/… | Mail goals start in the agent home | Have cron start goal --path sites/…, or jail + scope before mailing |
Script fails on sudo / custom hostname | Creator is not owner for those ops | Owner does that step, or elevate a Slack goal for the owner-only part |
You used at now + 5m | Live CLI expects +DURATION | at +5m |
goal: unknown flag --token-budget / --budget | Those flags do not exist on goal | Use --time-budget 30m |
| You scheduled from chat and expected it to die when you left | These jobs are durable | crontab -r / atrm to stop them |
See Scheduled jobs. Starting work from HTTP instead of a clock: Start a goal from a webhook.