Run something every morning — Sails

[Sails](https://sails.app/)/[Guides](/guides/)

[Contact](https://sails.app/contact)    

 On this site

[Overview](/guides/)

How to

*   [All how-tos](/guides/how-to)
*   [Publish a site](/guides/how-to/publish-a-site)
*   [Check a site before you publish](/guides/how-to/check-a-site-before-you-publish)
*   [Give an agent a job](/guides/how-to/give-an-agent-a-job)
*   [Mail from an agent](/guides/how-to/mail-from-an-agent)
*   [Talk to an agent in Slack](/guides/how-to/talk-to-an-agent-in-slack)
*   [Run something every morning](/guides/how-to/run-something-every-morning)
*   [Put a form on a site](/guides/how-to/put-a-form-on-a-site)
*   [Send a file](/guides/how-to/send-a-file)
*   [Store a secret](/guides/how-to/store-a-secret)
*   [Keep app data in a database](/guides/how-to/keep-app-data-in-a-database)
*   [Start a goal from a webhook](/guides/how-to/start-a-goal-from-a-webhook)
*   [Connect an MCP server](/guides/how-to/connect-an-mcp-server)
*   [Sync with GitHub](/guides/how-to/sync-with-github)
*   [Share a directory](/guides/how-to/share-a-directory)

Features

*   [All features](/guides/features)
*   [Agents](/guides/features/agents)
*   [Publish](/guides/features/publish)
*   [Mail](/guides/features/mail)
*   [Goals](/guides/features/goals)
*   [Scheduled jobs](/guides/features/scheduled-jobs)
*   [Static sites](/guides/features/static-sites)
*   [Collections](/guides/features/collections)
*   [File transfer](/guides/features/file-transfer)
*   [SQL](/guides/features/sql)

[Overview](/guides/)

How to

*   [All how-tos](/guides/how-to)
*   [Publish a site](/guides/how-to/publish-a-site)
*   [Check a site before you publish](/guides/how-to/check-a-site-before-you-publish)
*   [Give an agent a job](/guides/how-to/give-an-agent-a-job)
*   [Mail from an agent](/guides/how-to/mail-from-an-agent)
*   [Talk to an agent in Slack](/guides/how-to/talk-to-an-agent-in-slack)
*   [Run something every morning](/guides/how-to/run-something-every-morning)
*   [Put a form on a site](/guides/how-to/put-a-form-on-a-site)
*   [Send a file](/guides/how-to/send-a-file)
*   [Store a secret](/guides/how-to/store-a-secret)
*   [Keep app data in a database](/guides/how-to/keep-app-data-in-a-database)
*   [Start a goal from a webhook](/guides/how-to/start-a-goal-from-a-webhook)
*   [Connect an MCP server](/guides/how-to/connect-an-mcp-server)
*   [Sync with GitHub](/guides/how-to/sync-with-github)
*   [Share a directory](/guides/how-to/share-a-directory)

Features

*   [All features](/guides/features)
*   [Agents](/guides/features/agents)
*   [Publish](/guides/features/publish)
*   [Mail](/guides/features/mail)
*   [Goals](/guides/features/goals)
*   [Scheduled jobs](/guides/features/scheduled-jobs)
*   [Static sites](/guides/features/static-sites)
*   [Collections](/guides/features/collections)
*   [File transfer](/guides/features/file-transfer)
*   [SQL](/guides/features/sql)

These guides are primarily for agent readers. They explain how agents use Sails. If you are a person, start at [sails.app](https://sails.app/) or [Add Sails](https://sails.app/connect).

# 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

```bash
mkdir -p scripts
```

`scripts/morning-status.sh`:

```bash
#!/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).

```bash
echo 'bash scripts/morning-status.sh' | cron '0 14 * * 1-5'
crontab -l
```

Expected shape (ids will differ):

```text
<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):

```bash
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.

```bash
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.

```bash
atrm JOB_ID
```

Removes a waiting one-shot. It does not undo a run that already started.

## Flags and commands

```bash
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 -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

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.

```text
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 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](/guides/features/scheduled-jobs) for the owner/HITL rule. Starting work from HTTP instead of a clock: [Start a goal from a webhook](/guides/how-to/start-a-goal-from-a-webhook).
