# Scheduled Execution

> Run a LevelFour check unattended with cron or a GitHub Actions workflow, and when to take webhooks instead of a schedule.

Source: https://docs.levelfour.ai/guides/scheduled-execution


A scheduled check tells you what moved since the last one: what is new in the backlog, what is executing, and what finished. Every call it makes reads, so a `read` key covers the whole job and nothing in your cloud changes.

## Run it on a schedule

### Write the check

```python title="monitor.py"
from levelfour import LevelFour

client = LevelFour()

overview = client.recommendations.get_overview()
print(overview)

processing = client.recommendations.list_in_progress()
print(processing)

for rec in client.recommendations.list(
    page_size=50,
    sort_by="monthly_savings",
    sort_order="desc",
):
    print(f"{rec.service}: ${rec.monthly_savings}/mo")
```

Three calls: [Get overview](/resources/recommendations#get-overview) for the headline numbers, [List in-progress](/resources/recommendations#list-in-progress) for what is executing right now, and [List recommendations](/resources/recommendations#list-recommendations) for the backlog, largest saving first. The TypeScript and Go form of each one sits on that same page.

### Give it a key

The SDK reads `LEVELFOUR_API_KEY` from the environment, so the client takes no arguments and the scheduler supplies the value. The crontab entry sets it inline. The workflow reads it from a repository secret of the same name.

> **Warning**
>
> A key written inline in a crontab sits in plain text on the host, and a `read` key reaches every cost figure, recommendation and savings number in your organization. Keep it in a secrets manager or a file only the job's user can read. Give the schedule a `read` key, since it applies nothing. [Key scopes](/authentication#key-scopes) has the split.

### Point a scheduler at it

**cron**

Add the line with `crontab -e`. It runs at 06:00 in the host's timezone, Monday to Friday.

```bash
0 6 * * 1-5 LEVELFOUR_API_KEY=l4_live_... python monitor.py >> /var/log/levelfour.log 2>&1
```

**GitHub Actions**

Commit `monitor.py` next to the workflow, and store the key as a repository secret named `LEVELFOUR_API_KEY`.

```yaml title=".github/workflows/monitor-savings.yml"
name: Monitor Savings
on:
  schedule:
    - cron: '0 6 * * 1-5'

jobs:
  monitor:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      - run: pip install levelfour
      - name: Check recommendations
        env:
          LEVELFOUR_API_KEY: ${{ secrets.LEVELFOUR_API_KEY }}
        run: python monitor.py
```

GitHub schedules on UTC, so the same expression fires at 06:00 UTC rather than in your own zone.

[GitHub Actions](/guides/github-actions) has three more workflows: a [scheduled cost check](/guides/github-actions#scheduled-cost-check) that runs the SDK inline rather than from a committed script, a cost summary commented on a pull request, and a daily report uploaded as a build artifact.

### Confirm the first run

> **Success**
>
> The overview numbers in `/var/log/levelfour.log` after the scheduled minute are the proof: the entry fired and the key was accepted. On GitHub Actions the same output lands in the job log.

The crontab entry redirects stderr to that file too, so a traceback there means the job ran and a call failed. An empty or missing file means the entry never fired, or the job cannot write to that path. Run `python monitor.py` by hand once before you schedule it, so a silent morning afterwards has only one place to hide.

## When to stop polling

> **Warning**
>
> A poll sees only the state at the minute it runs. An optimization that starts and finishes between two runs never appears in `list_in_progress`, and the log reads like a quiet day.

[Webhooks](/webhooks) deliver each change as it happens, covering acceptances, rejections and every stage of an optimization. [Events](/webhooks#events) has what each one means, and [Register an endpoint](/webhooks#register-an-endpoint) is the call that subscribes you. Once an endpoint is live the schedule has nothing left to do.

[Slack Integration](/guides/slack-integration) and [Google Chat Integration](/guides/google-chat-integration) are complete receivers, signature verification included.

## Next

* [Recommendations](/resources/recommendations) is every call the script makes, in Python, TypeScript and Go
* [Webhooks](/webhooks) is the push alternative, with the payloads, the signing algorithm and the retry behavior
* [GitHub Actions](/guides/github-actions) has the pull request comment, a scheduled check that runs the SDK inline, and the daily report artifact
* [Authentication](/authentication#key-scopes) is which scope a scheduled key needs
