> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cycls.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Schedule a nightly report

> Run a function on a cron, write the output to a volume, and let an agent hand it to users.

This guide is for developers automating recurring work: a nightly export, a daily
digest, a weekly reconciliation.

**Prerequisites:** `CYCLS_API_KEY`.

## 1. Write the job

A scheduled function is called with no arguments, so it computes its own
parameters.

```python reports.py theme={null}
import datetime

import cycls

reports = cycls.Volume("reports")
image = cycls.Image().pip("duckdb", "openpyxl")


@cycls.function(
    image=image,
    volumes={"/reports": reports},
    schedule=cycls.Cron("0 6 * * *", timezone="Asia/Riyadh"),
    memory="2Gi",
    timeout=1200,
)
def daily_sales():
    import pathlib
    import duckdb

    day = (datetime.date.today() - datetime.timedelta(days=1)).isoformat()
    out = pathlib.Path(f"/reports/sales/{day}.xlsx")
    out.parent.mkdir(parents=True, exist_ok=True)

    con = duckdb.connect()
    con.execute("INSTALL excel; LOAD excel;")
    con.execute(
        f"""
        COPY (
            SELECT country, sum(amount) AS revenue, count(*) AS orders
            FROM read_parquet('/reports/events/*.parquet')
            WHERE day = '{day}'
            GROUP BY 1 ORDER BY revenue DESC
        ) TO '{out}' (FORMAT xlsx, HEADER true)
        """
    )
    return {"day": day, "path": str(out)}
```

## 2. Test before scheduling

```bash theme={null}
cycls run reports.py::daily_sales
```

The schedule does not fire during `cycls run`. The local run executes the
function once so you can check the output.

## 3. Deploy

```bash theme={null}
cycls deploy reports.py
#   [DEPLOYING] Scheduled: 0 6 * * * (Asia/Riyadh)
#   [DONE] Deployment complete!
```

The deploy output confirms the schedule. From then on the platform calls the
function, with no worker of yours to keep running.

## 4. Confirm it runs

```bash theme={null}
cycls logs daily-sales -s 24h
cycls volume ls reports sales/
```

Every fire is an ordinary request, so it appears in logs like any other call.

## 5. Change or remove the schedule

Source is the truth. Edit the cron expression and redeploy to change it. Delete
the `schedule=` argument and redeploy to remove it, which the deploy output
confirms with `Schedule removed`. `cycls rm` removes both the deployment and its
schedule.

## 6. Hand the report to users

Point an agent at the same volume and let it read the files.

```python assistant.py theme={null}
import cycls

reports = cycls.Volume("reports")

llm = (
    cycls.LLM()
    .model("anthropic/claude-sonnet-4-6")
    .system(
        "Daily sales reports are at /reports/sales/<date>.xlsx. "
        "Read the requested day and summarize it. Open the file on the canvas."
    )
    .allowed_tools(["Bash", "Editor", "Canvas"])
)


@cycls.agent(
    image=cycls.Image().copy(".providers.env", ".env"),
    web=cycls.Web().auth(cycls.Clerk()).title("Sales assistant"),
    volumes={
        "/workspace": cycls.Volume("assistant-chats"),
        "/reports": reports.read_only(),
    },
)
async def assistant(context):
    async for ev in llm.run(context=context):
        yield ev
```

The read-only mount means the agent can read every report and change none of
them.

## Writing safe scheduled jobs

| Property              | What it means for your code                                                                    |
| --------------------- | ---------------------------------------------------------------------------------------------- |
| At least once         | A run can fire twice. Key output by date so a rerun overwrites rather than appends             |
| Runs can overlap      | A slow run does not block the next. Take a lock file on the volume if two runs must never race |
| Thirty minute ceiling | Split long work, or have the scheduled function call `map()` and return quickly                |
| No pause button       | Remove `schedule=` and redeploy, which is the only off switch                                  |

A safe job looks like this:

```python theme={null}
out = pathlib.Path(f"/reports/sales/{day}.xlsx")   # date-keyed, so reruns overwrite
if out.exists() and not force:
    return {"day": day, "status": "already done"}
```

## What cannot be scheduled

* Apps and agents. They serve HTTP. Schedule a function that calls them.
* Functions that take a `port`. The port contract deploys a server, which a
  schedule cannot fire.

Both fail at import with the fix in the message.

## Next

<Card title="Python reference" icon="code" href="/reference/python">
  Every exported name in one table.
</Card>
