# Schedules

> Building flows · https://spoletta.com/docs/schedules

A schedule runs a flow on its own: every morning, every ten minutes, every Monday. Schedules are kept on their own page, reached from the **Schedules** link on the flow list.

## Adding a schedule

1.  **Pick the flow**
    
    Under **Flow**, choose any flow or procedure (procedures are marked “(procedure)”).
    
2.  **Pick when**
    
    **When** offers presets: Every minute, Every 5 minutes, Every 15 minutes, Every hour, Every day at 9:00, Weekdays at 9:00, Mondays at 9:00, or Custom. **Cron** holds the expression: five fields, or a descriptor such as `@hourly`.
    
3.  **Pick the time zone**
    
    **Time zone** defaults to your browser's. A preview below shows *Next: …*, or what is wrong with the expression. Press **New schedule**.
    

| Cron | Runs |
| --- | --- |
| `0 7 * * 1-5` | Weekdays at 7:00 |
| `*/10 * * * *` | Every ten minutes |
| `0 9 * * 1` | Mondays at 9:00 |
| `@hourly` | At the start of every hour |

## Managing schedules

Each schedule shows its next time (or *Disabled*) and how its last run ended. Toggle it off and on, edit it with **Save** and **Cancel**, or delete it; its runs stay. Scheduled runs show a clock in the flow's runs list.

## When times are skipped

-   A time is skipped while the flow's last run is still going, for example waiting on a person. The schedule then reads *Skipping its times until this run ends.*
-   Times missed while the server was down are skipped too; they are not caught up later.

## Designing for no one at the keyboard

A scheduled run has no one to fill anything in, so start it with a step that fetches its work: a [Sentry](https://spoletta.com/docs/tools.md#sentry) or [script](https://spoletta.com/docs/tools.md#scripts) process, or an agent that finds the work itself. Reviews later in the flow are fine: the run waits for them, but later times are skipped while it does.

> **Tip:** Combine a schedule with a [stored list with a unique attribute](https://spoletta.com/docs/lists.md) so each run handles only what is new. Run the flow by hand first and watch its steps before you schedule it.

## Fetching only what changed

A script in a run that a schedule started gets `$SINCE` (or `$LAST_RUN`, the same value): when the schedule's last run that ended done started, as a Unix timestamp. Use it to fetch only what changed since, such as the pull requests updated after it.

-   It is empty on the schedule's first run, and in runs started by hand, so have the script fall back.
-   A run that fails, is stopped or is terminated doesn't move it on, so the next run fetches that stretch again.
-   Make what the script lists stored with a unique attribute, so what was already handled in the overlap is skipped. An item whose own run fails comes round again only if it changes again.

Fetch changes: the pull requests updated since the last run that got through:

```bash
# Empty on the schedule's first run: take the last day then.
since=${SINCE:-$(( $(date +%s) - 86400 ))}
curl -fsS "https://api.github.com/repos/acme/shop/pulls?state=all&sort=updated&direction=desc" \
  -H "Authorization: Bearer $GITHUB_TOKEN" |
  jq --argjson since "$since" \
    '[.[] | select((.updated_at | fromdateiso8601) >= $since) | {number, title, url: .html_url}]'
```

### A cursor of your own

When a timestamp isn't enough, such as for the newest `updated_at` seen or an offset, keep a cursor in an object instead. Make a *Cursor* object, tick **Store what runs generate**, and have the fetching process both take and yield it. Each run then starts with the Cursor the last run that ended done left; see [Stored objects](https://spoletta.com/docs/objects.md#stored-objects).

Fetch changes: takes Cursor, yields Cursor and Changes:

```bash
# No Cursor file on the first run.
after=$(jq -r '.updated_at // empty' "$INPUTS/Cursor" 2>/dev/null)
changes=$(curl -fsS "https://tracker.example.com/api/changes?after=$after" \
  -H "Authorization: Bearer $TRACKER_TOKEN")
jq -n --argjson c "$changes" --arg after "$after" \
  '{Changes: $c, Cursor: {updated_at: ([$c[].updated_at, $after] | max)}}'
```

Leave the Cursor's attributes not unique: a run that leaves it as it was would otherwise be a duplicate. Only a whole run of the flow carries it over, not a flow process running the flow.
