Run something every morning
cron and at are owner-only. They persist on the control plane. They keep running after this chat ends. They are not a loop inside a live session.
Write members and agents cannot schedule jobs unless the current goal was elevated through Slack HITL (request_owner_access, then Approve). Elevation lasts for that goal only. It does not make the agent a repository owner.
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, as the creating owner.
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 are a repository owner.
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 --token-budget 100000 "Write today's status note under pages/blog/. Then mail site-bot that it shipped."
The job already runs as the owner, so it can see the whole repo. If the work needs an 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'
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 owner gate. Use this to prove the script before you cron it, or for a single reminder.
echo 'bash scripts/morning-status.sh' | at now + 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 help
runs help
| Command | What it does |
|---|---|
echo 'bash PATH' | cron 'M H D M W' | Create a recurring job. Standard 5-field expression. |
crontab -l | List cron jobs (ids + schedules). |
crontab -r JOB_ID | Delete one cron job. |
echo 'bash PATH' | at now + 5m | One-shot. Also accepts a timestamp the at parser understands. |
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. |
There is no --path on cron / at. The process identity is the creating owner; cwd is the repository workspace. Scope the work inside the script (goal --path …, or cd to a folder you own).
Do not busy-retry cron / at / crontab if you see repository owner access required. Alternate syntax will not help.
If you are not the owner
- Stop retrying.
- Write the script under
~(or the folder you can write). - Tell a repository owner to pipe it into
cron, or — in a Slack-triggered goal — callrequest_owner_accessonce with a clear reason, wait for Approve, then schedule in that same goal. - On Deny: do not request elevation again. Leave the script for the owner.
request_owner_access reason:
"Need owner cron to run scripts/morning-status.sh weekdays at 14:00 UTC."
After Approve, retry cron / at once. Elevation ends when the goal terminates.
Failure modes
| What you see | What it means | What to do |
|---|---|---|
repository owner access required | You are an agent or write member, and this goal is not elevated | Owner schedules it, or one request_owner_access in a Slack goal |
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 |
| You scheduled from chat and expected it to die when you left | These jobs are durable | crontab -r / atrm to stop them |
Agent assumed cron after agent unjail | Full disk is not owner | Still need HITL or an owner |
See Scheduled jobs for the owner/HITL rule. Starting work from HTTP instead of a clock: Start a goal from a webhook.