Short answer
Write a small script that does one bounded job, such as summarizing the latest items from a feed in a single API call, and let cron, the standard Unix scheduler, run it at a fixed time. Give the script hard limits (input size, output tokens, a timeout, a monthly token budget, a lock against overlapping runs and a kill switch) and keep the API key in an environment variable, never in the code.
Follow-up question How do you stop runaway costs and loops?
Some of the most useful AI tasks are boring and regular: a Monday digest of a news feed, a nightly summary of support tickets, a weekly tidy-up of meeting notes. You don't need a platform for that. On any Mac or Linux machine, or a small rented server, the classic tool is cron, a scheduler that runs commands at fixed times. Pair it with a short script that calls an AI model's API, and you have an assistant that works while you sleep.
The danger is the same feature: it works while you sleep. A schedule typo or a script that misbehaves can repeat thousands of times before you notice. So this guide builds the limits in from the first line.
How cron reads a schedule
Each job is one line in your crontab (cron table), which you edit with crontab -e. The line starts with five time fields, then the command:
# minute hour day-of-month month day-of-week command
0 7 * * 1 /home/me/digest/run.sh >> /home/me/digest/digest.log 2>&1
That means: at minute 0 of hour 7, on any date, in any month, on day-of-week 1 (Monday, since 0 is Sunday). In plain words, every Monday at 7:00. The >> digest.log 2>&1 part appends all output and errors to a log file so you can see what happened. Cron uses the machine's time zone, so check it on a server.
Notice how little separates this from disaster: * * * * * means every minute, which is 10,080 runs a week instead of one.
The script: one job, hard limits
This weekly digest reads a news feed (RSS, a standard format websites use to publish their latest items), keeps the ten newest headlines, and asks a model for a short summary in a single call. It uses the official openai Python library (version 1.x).
import fcntl, json, os, sys, urllib.request, xml.etree.ElementTree as ET
from datetime import date
from openai import OpenAI
FEED_URL = "https://example.org/news/rss.xml" # your feed
MAX_ITEMS = 10 # never send more than 10 headlines
MAX_INPUT_CHARS = 6000 # cap on text sent to the model
MONTHLY_TOKEN_BUDGET = 50_000
STATE = "usage.json"
if os.path.exists("STOP"): # kill switch
sys.exit("STOP file present; not running.")
lock = open("digest.lock", "w") # no overlapping runs
try:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
sys.exit("Previous run still active; exiting.")
month = date.today().strftime("%Y-%m")
usage = json.load(open(STATE)) if os.path.exists(STATE) else {}
if usage.get(month, 0) >= MONTHLY_TOKEN_BUDGET:
sys.exit("Monthly token budget reached; not calling the API.")
with urllib.request.urlopen(FEED_URL, timeout=20) as r:
root = ET.fromstring(r.read())
items = [(i.findtext("title") or "").strip() for i in root.iter("item")][:MAX_ITEMS]
text = "\n".join("- " + t for t in items)[:MAX_INPUT_CHARS]
client = OpenAI(timeout=60.0, max_retries=1) # key comes from OPENAI_API_KEY
response = client.chat.completions.create(
model="gpt-4o-mini",
max_tokens=400,
messages=[{"role": "user", "content":
"Summarize this week's headlines in 5 bullet points for a busy reader. "
"Use only the headlines given; do not add facts.\n\n" + text}],
)
usage[month] = usage.get(month, 0) + response.usage.total_tokens
json.dump(usage, open(STATE, "w"))
open(f"digest-{date.today()}.md", "w").write(response.choices[0].message.content)
print("Done. Tokens this month:", usage[month])
Read it as a list of guard rails. There is no loop around the API: one run, one request. The input is capped twice, by item count and by characters, and the output by max_tokens. The library's own safety settings are tightened: by default it waits up to 10 minutes per request and retries some failures twice, so timeout=60.0 and max_retries=1 keep a bad day short. The budget file stops the job before it calls the API once the month's allowance is spent. The token figures here are illustrative; set them from your own test runs.
Keep the key out of the code
The library reads your key from the OPENAI_API_KEY environment variable, which is what OpenAI recommends: code can be shared, the key cannot. The small wrapper that cron calls loads it from a private file:
#!/bin/sh
cd /home/me/digest || exit 1
. ./secrets.env # one line: export OPENAI_API_KEY="..."
exec /usr/bin/python3 digest.py
Make the wrapper executable (chmod +x run.sh), make secrets.env readable only by you (chmod 600 secrets.env), never commit it to a repository, and use a key created just for this job, so you can revoke it without breaking anything else.
Try it: Before adding the cron line, run ./run.sh by hand twice in a row, then create a file named STOP and run it again. You should get a digest file, a token count in usage.json that rises with each run, and then a clean refusal once the kill switch is in place.
How do you stop runaway costs and loops?
Count every way the job can repeat, and put a ceiling on each one. Schedules repeat: double-check the five fields, and watch the log after the first run. Retries repeat: each retry is another billed request, which is why this script allows only one. Overlapping runs repeat: if a slow run is still going when the next one starts, the lock file makes the second one exit. Your own code can repeat: if you later add a loop, for example one call per article, give it a fixed maximum number of passes and count tokens inside it.
Then add brakes outside the script. Check the Usage page of your OpenAI account regularly, and if your account offers spending limits or alerts, set them well above a normal month but far below a painful one. OpenAI's key-safety advice is clear about why: a leaked or misused key spends your quota, and the fix is to rotate it immediately. Finally, keep the kill switch simple enough to use from a phone: deleting the cron line with crontab -e, or creating the STOP file, ends the job at its next run. The cheapest runaway is the one that can't start.
Sources
- cron — Wikipedia, August 4, 2024
- OpenAI Python API library (README) — OpenAI (GitHub), September 17, 2024
- Best Practices for API Key Safety — OpenAI Help Center