How to run a Python script on a schedule
The classic way to run a Python script on a schedule is a cron job: 0 9 * * 1 python report.py on a server or your own laptop. It works until it doesn't. The laptop is asleep at 9am, the server gets rebuilt and the crontab goes with it, an API key sits in plain text next to the script, and when a run fails nobody finds out until someone asks where Monday's report went.
With Callables, a schedule is a few clicks on a function you've already written, and every run is saved with its logs and result. Here's how.
1. Write the script as a function
Put the work in a function and give every input a default, so it can run without anyone filling in a form:
import os
from typing import Literal
import requests
from callables import deploy
@deploy(title="Weekly sales summary", secrets=["CRM_TOKEN"])
def weekly_summary(
region: Literal["NA", "EMEA", "APAC"] = "NA",
days: int = 7,
) -> str:
"""Closed deals for the last few days, as a short summary."""
resp = requests.get(
"https://crm.example.com/api/deals",
params={"region": region, "days": days, "status": "won"},
headers={"Authorization": f"Bearer {os.environ['CRM_TOKEN']}"},
timeout=30,
)
resp.raise_for_status()
deals = resp.json()["deals"]
total = sum(d["amount"] for d in deals)
heading = f"## {region}: {len(deals)} deals closed"
return f"{heading}\n\nTotal: **${total:,.0f}**"
The CRM_TOKEN secret is set once in the web app, not in the code. Push the repo to GitHub and Callables builds it. The function is now something anyone on your team can run from a form.
2. Add a schedule
Open the function and fill in its form the way you want it to run, then click Schedule… next to the Run button. Pick when it should run:
- Every hour
- Every day
- Every weekday (Monday to Friday)
- Every week, on the days you choose
- Every month
- Or any cron expression, for anything more specific
Choose a time zone, and 9am stays 9am when the clocks change. As you edit, you see the next few run times, so there's no guessing whether your cron expression means what you think.
Each schedule keeps the inputs it was made with. You can run the same function every Monday for NA and every Tuesday for EMEA, with a name on each so you can tell them apart.
That's it. No server, no crontab, no keeping a machine awake. If you schedule things in Zapier or Power Automate today, see how Callables compares with Zapier and Power Automate.
3. See every run
Scheduled runs show up in your run history like any other run. Each one keeps its inputs, logs and result, so when something looks off, you open the run and read what happened. Each schedule shows when it ran last and when it runs next, and a toggle pauses it without deleting it.
Prefer to keep the schedule in code?
If the schedule belongs with the code, say a nightly cleanup that should always exist, declare it in the decorator:
# Every day at 2am UTC
@deploy(title="Nightly cleanup", schedule="0 2 * * *")
def nightly_cleanup(dry_run: bool = False) -> str:
...
It's created when you push, and changing or removing the line updates or removes the schedule on the next push. Code schedules use UTC, and every input needs a default.
Good to know
- Every input needs a value. A schedule runs with the inputs saved on it, so fill in any required ones before you click Schedule….
- Functions that take a file upload can't be scheduled, since nobody is there to pick the file.
- Schedules run as the person who made them, and stop if that person can no longer run the function. Someone leaving the team doesn't leave a job running under their name.
A schedule is the same function your team already runs from a form, just on a timer. Write the function once, push it, and pick a time.
Turn your scripts into tools your team can run
Add @deploy to a Python function and push. Your team gets a form, schedules and run history.