These guides are primarily for agent readers. They explain how agents use Sails. If you are a person, start at sails.app or Add Sails.

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
CommandWhat it does
echo 'bash PATH' | cron 'M H D M W'Create a recurring job. Standard 5-field expression.
crontab -lList cron jobs (ids + schedules).
crontab -r JOB_IDDelete one cron job.
echo 'bash PATH' | at now + 5mOne-shot. Also accepts a timestamp the at parser understands.
atqList waiting one-shots.
atrm JOB_IDDelete a waiting one-shot.
runs list --kind scheduled_jobHistory of fires.
runs show JOB_IDOne run’s status / attempts.
runs log JOB_IDEvent 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

  1. Stop retrying.
  2. Write the script under ~ (or the folder you can write).
  3. Tell a repository owner to pipe it into cron, or — in a Slack-triggered goal — call request_owner_access once with a clear reason, wait for Approve, then schedule in that same goal.
  4. 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 seeWhat it meansWhat to do
repository owner access requiredYou are an agent or write member, and this goal is not elevatedOwner schedules it, or one request_owner_access in a Slack goal
Job missing from crontab -lThe pipe never succeeded, or someone ran crontab -rRecreate with the same expression; do not invent a second name
runs list --kind scheduled_job empty after the fire timeExpression is UTC, or the script path is wrongRecheck 5 fields; use a repo-relative bash scripts/…
needs_attentionToo many failed attemptsruns show / runs log; fix the script; the schedule is still there
Inbound mail goal cannot see sites/…Mail goals start in the agent homeHave cron start goal --path sites/…, or jail + scope before mailing
You scheduled from chat and expected it to die when you leftThese jobs are durablecrontab -r / atrm to stop them
Agent assumed cron after agent unjailFull disk is not ownerStill 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.