Skill Tracer
Skills › Scientific Agent Skills › modal › scheduled-jobs.md

scheduled-jobs.md

157 lines · skills/modal/references/scheduled-jobs.md at 5f2bc03

Open on GitHub
1# Modal Scheduled Jobs
2
3Reviewed against Modal 1.6.0 and official [scheduling documentation](https://modal.com/docs/guide/cron).
4Schedules were constructed locally only; these application-specific job bodies are
5illustrative and were not deployed or triggered.
6

Modal Scheduled Jobs

Reviewed against Modal 1.6.0 and official scheduling documentation. Schedules were constructed locally only; these application-specific job bodies are illustrative and were not deployed or triggered.

Raw lines 1 to 6
7## Overview
8
9Modal supports running functions automatically on a schedule, either using cron syntax or fixed intervals. Deploy scheduled functions with `modal deploy` and they run unattended in the cloud.

Overview

Modal supports running functions automatically on a schedule, either using cron syntax or fixed intervals. Deploy scheduled functions with modal deploy and they run unattended in the cloud.

Raw lines 7 to 10
11## Schedule Types

Schedule Types

Raw lines 11 to 12
13### modal.Cron
15Standard cron syntax — stable across deploys:
17```python
18import modal
20app = modal.App("scheduled-tasks")
22# Daily at 9 AM UTC
23@app.function(schedule=modal.Cron("0 9 * * *"))
24def daily_report():
25 generate_and_send_report()
27# Every Monday at midnight
28@app.function(schedule=modal.Cron("0 0 * * 1"))
29def weekly_cleanup():
30 cleanup_old_data()
32# Every 15 minutes
33@app.function(schedule=modal.Cron("*/15 * * * *"))
34def frequent_check():
35 check_system_health()
36```
38#### Cron Syntax Reference
40```
41┌───────────── minute (0-59)
42│ ┌───────────── hour (0-23)
43│ │ ┌───────────── day of month (1-31)
44│ │ │ ┌───────────── month (1-12)
45│ │ │ │ ┌───────────── day of week (0-6, Sun=0)
46│ │ │ │ │
47* * * * *
48```
50| Pattern | Meaning |
51|---------|---------|
52| `0 9 * * *` | Daily at 9:00 AM UTC |
53| `0 */6 * * *` | Every 6 hours |
54| `*/30 * * * *` | Every 30 minutes |
55| `0 0 * * 1` | Every Monday at midnight |
56| `0 0 1 * *` | First day of every month |
57| `0 9 * * 1-5` | Weekdays at 9 AM |

modal.Cron

Standard cron syntax — stable across deploys:

import modal

app = modal.App("scheduled-tasks")

# Daily at 9 AM UTC
@app.function(schedule=modal.Cron("0 9 * * *"))
def daily_report():
    generate_and_send_report()

# Every Monday at midnight
@app.function(schedule=modal.Cron("0 0 * * 1"))
def weekly_cleanup():
    cleanup_old_data()

# Every 15 minutes
@app.function(schedule=modal.Cron("*/15 * * * *"))
def frequent_check():
    check_system_health()

Cron Syntax Reference

┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12)
│ │ │ │ ┌───────────── day of week (0-6, Sun=0)
│ │ │ │ │
* * * * *
PatternMeaning
0 9 * * *Daily at 9:00 AM UTC
0 */6 * * *Every 6 hours
*/30 * * * *Every 30 minutes
0 0 * * 1Every Monday at midnight
0 0 1 * *First day of every month
0 9 * * 1-5Weekdays at 9 AM
Raw lines 13 to 58
59### modal.Period
61Fixed interval — resets on each deploy:
63```python
64# Every 5 hours
65@app.function(schedule=modal.Period(hours=5))
66def periodic_sync():
67 sync_data()
69# Every 30 minutes
70@app.function(schedule=modal.Period(minutes=30))
71def poll_updates():
72 check_for_updates()
74# Every day
75@app.function(schedule=modal.Period(days=1))
76def daily_task():
77 ...
78```
80`modal.Period` resets its timer on each deployment. If you need a schedule that doesn't shift with deploys, use `modal.Cron`.
81Cron defaults to UTC; use `modal.Cron("0 9 * * *", timezone="America/New_York")`
82for a named timezone. Scheduled Functions must be callable with no required arguments.
83Make repeated scheduled writes idempotent; redeployment and retries are not exactly-once guarantees.

modal.Period

Fixed interval — resets on each deploy:

# Every 5 hours
@app.function(schedule=modal.Period(hours=5))
def periodic_sync():
    sync_data()

# Every 30 minutes
@app.function(schedule=modal.Period(minutes=30))
def poll_updates():
    check_for_updates()

# Every day
@app.function(schedule=modal.Period(days=1))
def daily_task():
    ...

modal.Period resets its timer on each deployment. If you need a schedule that doesn't shift with deploys, use modal.Cron. Cron defaults to UTC; use modal.Cron("0 9 * * *", timezone="America/New_York") for a named timezone. Scheduled Functions must be callable with no required arguments. Make repeated scheduled writes idempotent; redeployment and retries are not exactly-once guarantees.

Raw lines 59 to 84
85## Deploying Scheduled Functions
87Schedules only activate when deployed:
89```bash
90modal deploy script.py
91```
93`modal run` and `modal serve` do not activate schedules.

Deploying Scheduled Functions

Schedules only activate when deployed:

modal deploy script.py

modal run and modal serve do not activate schedules.

Raw lines 85 to 94
95## Monitoring
97- View scheduled runs in the **Apps** section of the Modal dashboard
98- Each run appears with its status, duration, and logs
99- Use the **"Run Now"** button on the dashboard to trigger manually

Monitoring

  • View scheduled runs in the Apps section of the Modal dashboard
  • Each run appears with its status, duration, and logs
  • Use the "Run Now" button on the dashboard to trigger manually
Raw lines 95 to 100
101## Management
103- Schedules cannot be paused — remove the schedule and redeploy to stop
104- To change a schedule, update the `schedule` parameter and redeploy
105- To stop entirely, either remove the `schedule` parameter or run `modal app stop <name>`

Management

  • Schedules cannot be paused — remove the schedule and redeploy to stop
  • To change a schedule, update the schedule parameter and redeploy
  • To stop entirely, either remove the schedule parameter or run modal app stop <name>
Raw lines 101 to 106
107## Common Patterns

Common Patterns

Raw lines 107 to 108
109### ETL Pipeline
111```python
112@app.function(
113 schedule=modal.Cron("0 2 * * *"), # 2 AM UTC daily
114 secrets=[modal.Secret.from_name("db-creds")],
115 timeout=7200,
117def etl_pipeline():
118 import os
119 data = extract(os.environ["SOURCE_DB_URL"])
120 transformed = transform(data)
121 load(transformed, os.environ["DEST_DB_URL"])
122```

ETL Pipeline

@app.function(
    schedule=modal.Cron("0 2 * * *"),  # 2 AM UTC daily
    secrets=[modal.Secret.from_name("db-creds")],
    timeout=7200,
)
def etl_pipeline():
    import os
    data = extract(os.environ["SOURCE_DB_URL"])
    transformed = transform(data)
    load(transformed, os.environ["DEST_DB_URL"])
Raw lines 109 to 123
124### Model Retraining
126```python
127@app.function(
128 schedule=modal.Cron("0 0 * * 0"), # Weekly on Sunday
129 gpu="H100",
130 volumes={"/data": data_vol, "/models": model_vol},
131 timeout=86400,
133def retrain():
134 model = train_on_latest_data("/data/training/")
135 torch.save(model.state_dict(), "/models/latest.pt")
136 model_vol.commit()
137```

Model Retraining

@app.function(
    schedule=modal.Cron("0 0 * * 0"),  # Weekly on Sunday
    gpu="H100",
    volumes={"/data": data_vol, "/models": model_vol},
    timeout=86400,
)
def retrain():
    model = train_on_latest_data("/data/training/")
    torch.save(model.state_dict(), "/models/latest.pt")
    model_vol.commit()
Raw lines 124 to 138
139### Health Checks
141```python
142@app.function(
143 image=modal.Image.debian_slim().uv_pip_install("requests"),
144 schedule=modal.Period(minutes=5),
145 secrets=[modal.Secret.from_name("slack-webhook")],
147def health_check():
148 import os, requests
149 status = check_all_services()
150 if not status["healthy"]:
151 response = requests.post(os.environ["SLACK_URL"], json={"text": f"Alert: {status}"}, timeout=30)
152 response.raise_for_status()
153```
155> The webhook URL is read from a Modal Secret (`SLACK_URL`), not hardcoded or taken
156> from untrusted input. Keep notification endpoints in Secrets and avoid POSTing to
157> URLs constructed from user-supplied data.

Health Checks

@app.function(
    image=modal.Image.debian_slim().uv_pip_install("requests"),
    schedule=modal.Period(minutes=5),
    secrets=[modal.Secret.from_name("slack-webhook")],
)
def health_check():
    import os, requests
    status = check_all_services()
    if not status["healthy"]:
        response = requests.post(os.environ["SLACK_URL"], json={"text": f"Alert: {status}"}, timeout=30)
        response.raise_for_status()
The webhook URL is read from a Modal Secret (SLACK_URL), not hardcoded or taken from untrusted input. Keep notification endpoints in Secrets and avoid POSTing to URLs constructed from user-supplied data.
Raw lines 139 to 157