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.

Scheduled jobs

at and cron persist on the control plane. They keep running after this chat ends. They are not a loop inside a live MCP session.

Who can schedule: repository owner or write (including agents). Read members cannot. Run capabilities and check can_schedule instead of guessing.

Scheduling does not need Slack HITL. Jobs run as the identity that created them (created_by). They do not become the repository owner. File modes, agent jail, and remaining owner-only ops (sudo, chgrp, custom-hostname publish, …) still apply at fire time.

Done looks like

crontab -l (or atq for a one-shot) shows a job id. When the time hits, runs list --kind scheduled_job has a row. runs show JOB_ID is not stuck in needs_attention. The script’s side effect exists (mail sent, file written, goal finished).

Worked example: weekday morning script

You have write access (can_schedule: true). Put a script in the repo, then cron it.

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."

Five-field cron (UTC): 0 14 * * 1-5 is 14:00 UTC, Monday–Friday (8:00am America/Denver during MDT).

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 differ):

<job-id>  0 14 * * 1-5  bash scripts/morning-status.sh

Edit the script in place; keep one schedule line.

One-shot: at

Live CLI takes a sleep-style duration (+5m, +2h, +1d), not wall-clock “now + …”:

echo 'bash scripts/morning-status.sh' | at +5m
atq
atrm JOB_ID

Use at to prove the script before you cron it, or for a single reminder.

Who sees which jobs

RoleCreateList / cancel
Write (human or agent)Own jobsOwn jobs
OwnerAnyEvery job; crontab -r with no id cancels all cron jobs
ReadNoNo

Write members cancel with crontab -r JOB_ID or atrm JOB_ID on jobs they created. Owners (or HITL-elevated goals) can cancel anyone’s.

Commands

cron 'M H D M W' bash scripts/job.sh
echo 'bash scripts/job.sh' | cron 'M H D M W'
crontab -l
crontab -r [JOB_ID]

at +5m bash scripts/job.sh
echo 'bash scripts/job.sh' | at +5m
atq
atrm JOB_ID

runs list --kind scheduled_job
runs show JOB_ID
runs log JOB_ID
capabilities
Command / flagWhat it does
cron 'EXPR' …Recurring job. Standard 5-field expression (UTC).
crontab -lList cron jobs you can manage.
crontab -r JOB_IDCancel one cron job. No id: your cron jobs, or all if you are owner.
at +DURATIONOne-shot. Suffixes: 30, 30s, 5m, 2h, 1d.
atq / atrmList / cancel waiting one-shots.
runs list --kind scheduled_jobFire history.
runs show / runs logStatus / attempts / transcript.
capabilitiesJSON; look at can_schedule.

There is no --path on cron / at. Cwd is the repository workspace. Scope the work inside the script (goal --path …, or cd to a folder you can write).

Failure modes

What you seeWhat it meansWhat to do
Hint that write access is required / can_schedule: falseYou are a read memberAsk a write member or owner to schedule it
Job missing from crontab -lPipe failed, or someone ran crontab -rRecreate; do not invent a second name
Empty runs list --kind scheduled_job after fire timeExpression is UTC, or script path is wrongRecheck 5 fields; use repo-relative bash scripts/…
needs_attentionattempt_count >= max_attemptsruns show / runs log; fix the script; schedule stays
Script cannot sudo / custom-hostname publishJob runs as creator, not as ownerOwner runs that part, or elevate a Slack goal for owner-only ops — not for cron itself
Agent jail blocks the folder the script editsJail still applies at fire timeagent jail the path, or start goal --path on a writable folder
You used at now + 5mLive CLI expects +DURATIONat +5m
goal: unknown flag --token-budget / --budgetThose flags do not exist on goalUse --time-budget 30m

How to: Run something every morning. Starting work from HTTP instead of a clock: Start a goal from a webhook.