Every week you probably spend hours doing the same small jobs by hand: sorting an inbox, reading invoices to copy three numbers into a spreadsheet, tagging support messages, or renaming files. None of it is hard. It is just slow, and it never ends. The reason it resists the old style of automation is that the inputs are messy and need a little judgement. A rule like "if the subject contains invoice, file it" breaks the moment a real human writes "Re: your bill from last month."
This guide shows you how to build a small Python program that does these jobs for you by leaning on an AI model for the judgement part. The shape is always the same and only four steps long: read your inputs, ask an AI to classify or transform each one, write the results somewhere useful, and put it on a schedule so it runs without you. You will write real, runnable code in each step, see a complete worked example you can copy, and learn how to fix the handful of errors that trip up beginners.
You do not need a computer science background. If you have followed Python AI Fundamentals for Non-Developers far enough to run a script and call an API once, you have everything you need here. Where a task needs deeper knowledge (cleaning the data first, or understanding how an API charges you), this guide links to the section that covers it.
Who this is for and what you will build
You are a good fit for this guide if you do a recurring task that involves reading text a person wrote and deciding what to do with it. The classic example, and the one we build toward, is inbox sorting: unread mail comes in, the AI labels each message as urgent, newsletter, invoice, or general, and the labels get written to a log you can act on. The exact task does not matter. Swap emails for support tickets, PDF invoices, or rows in a spreadsheet and the code barely changes.
The skill you are really learning is how to split a chore into two kinds of work: the parts a computer can do with fixed rules, and the parts that need a human-style read of messy text. Plain Python is perfect for the fixed parts — listing files, looping, writing a CSV — and it is free and instant. The AI handles only the judgement: "is this email urgent?", "what is the total on this invoice?", "is this review positive or negative?". Keeping that split clear is what makes your automation cheap, fast, and easy to debug, because most of the program is ordinary code you can read at a glance.
It also helps to know when not to reach for an AI at all. If a task has a clean rule — "move every file older than 30 days into an archive folder" — a few lines of plain Python beat an AI on speed, cost, and reliability every time. Save the model for the moments where a rule would need a hundred exceptions to work. A good test: if you can describe the decision to a coworker in one sentence and they would always agree on the answer, write a rule; if reasonable people would sometimes disagree, that is judgement, and that is where the AI earns its place.
By the end you will have a single Python file that reads a batch of inputs, sends each to an AI model, saves the answers, and runs itself every morning. From there, the dedicated Python Script to Automate Email Sorting guide takes the same pattern all the way to a live Gmail inbox.
Prerequisites
You need Python 3.10 or newer. Check what you have:
python3 --version
Create a project folder, make an isolated virtual environment (a private copy of Python so these packages do not collide with anything else on your machine), and install the four libraries this guide uses:
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install openai python-dotenv schedule httpx
Here is what each one does: openai is the official SDK for talking to the AI model, python-dotenv loads your secret key from a file, schedule runs your job on a timer, and httpx is a modern HTTP client used if you ever call an API the SDK does not cover. If installation as it stands fails, the Setting Up Python for AI section walks through OS-specific fixes.
Next, store your API key. Never paste a key directly into your code, because anyone who sees the file then has your key. Create a file named .env in your project folder:
OPENAI_API_KEY=sk-your-real-key-here
Then immediately add .env to your .gitignore so the key is never committed or shared:
echo ".env" >> .gitignore
That .gitignore line is the single most important habit in this guide. A leaked key can run up real charges on your account. To understand how keys, models, and billing actually work, read Understanding LLM APIs.
Step 1: Read your inputs
Every automation starts by pulling the work into Python as a plain list of small text records. Keep each record short — a subject line, one row, one file's text — because the AI charges by how much text you send and reads faster when there is less of it.
To keep this step easy to test, we read from a folder of .txt files. Each file is one item to process. Later you can replace this function with one that reads a live inbox or a spreadsheet; the rest of the program will not care where the text came from.
from pathlib import Path
def read_inputs(folder: str) -> list[dict]:
"""Load every .txt file in a folder as a list of records."""
records = []
for path in sorted(Path(folder).glob("*.txt")):
text = path.read_text(encoding="utf-8", errors="ignore").strip()
if text:
records.append({"id": path.name, "text": text})
return records
if __name__ == "__main__":
items = read_inputs("inbox")
print(f"Loaded {len(items)} items to process")
Make a folder named inbox, drop a couple of .txt files into it, and run the script. You should see a count of how many it found. A few details in this small function are worth understanding because they save you pain later. Path(folder).glob("*.txt") finds every text file without you having to know their names in advance, and wrapping it in sorted() makes the order predictable so two runs behave the same way. The errors="ignore" argument tells Python to skip stray characters instead of crashing — real-world files are full of odd bytes from copy-pasted symbols and foreign keyboards, and a skipped character is far better than a failed run. The if text: check quietly drops empty files so you never waste an AI call on nothing.
Notice too that each record is a small dictionary with an id and the text. That id is your thread back to the original — the filename here, but a message ID or a database key in a real system — so that when the AI returns an answer you always know which item it belongs to. Keep your records small and consistent in shape, and every later step stays simple. If your inputs are spreadsheets or messy exports rather than tidy text files, run them through the Data Cleaning for AI steps first, because the cleaner the text you hand the model, the more accurate and cheaper its answers become.
Step 2: Call an AI to classify or transform each item
This is where the judgement happens. You send the model a short instruction (a prompt) plus the item's text, and it sends back an answer. The trick to making this reliable inside a program is to ask for a predictable, machine-readable shape — here, a single JSON object — rather than a friendly paragraph.
We use the openai SDK and ask the model to return JSON with response_format. Setting a low temperature (how much randomness the model uses) keeps answers consistent, which is exactly what you want when the same input should always get the same label.
import os
import json
from openai import OpenAI
from dotenv import load_dotenv
load_dotenv()
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
def classify(text: str) -> dict:
"""Ask the AI to label one item and return a parsed dict."""
prompt = (
"Classify the message below into exactly one category: "
"'urgent', 'newsletter', 'invoice', or 'general'. "
"Reply with JSON shaped like {\"category\": \"...\", \"reason\": \"...\"}.\n\n"
f"Message:\n{text[:1500]}"
)
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0,
response_format={"type": "json_object"},
)
return json.loads(response.choices[0].message.content)
Several choices in this function are doing real work. The text[:1500] cap protects you from sending a giant document by accident, which would slow the call down and run up cost — the model is paid per chunk of text, called a token, so trimming directly saves money. Setting temperature=0 removes randomness, which means the same email gets the same label every time; that consistency is exactly what you want for sorting, even though you would raise it for creative rewriting. The response_format={"type": "json_object"} line tells the model to reply with valid JSON instead of a chatty sentence, so json.loads can turn the answer straight into a Python dictionary your code can use.
The prompt itself deserves care, because it is the difference between an automation you trust and one you constantly babysit. Notice that it names the exact categories allowed, shows the precise JSON shape to return, and labels the user's text clearly so the model never confuses your instructions with the content. Asking for a short reason alongside the category is a cheap trick that pays off twice: it nudges the model to think before answering, which improves accuracy, and it gives you a human-readable note to check when a label looks wrong. To go deeper on writing prompts that hold their shape and on how models and pricing work, see Prompt Engineering Basics and Understanding LLM APIs.
Step 3: Write the results somewhere useful
A classification you cannot see is wasted work. Write each result to an output the rest of your tools can read. A CSV file is the friendliest choice: it opens in any spreadsheet and is trivial to append to as new items arrive.
import csv
from pathlib import Path
def write_results(rows: list[dict], outfile: str = "results.csv") -> None:
"""Append classified rows to a CSV, writing the header once."""
fields = ["id", "category", "reason"]
file_exists = Path(outfile).exists()
with open(outfile, "a", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=fields)
if not file_exists:
writer.writeheader()
writer.writerows(rows)
Opening the file in append mode ("a") means each run adds to the history instead of erasing it, and the file_exists check writes the column header only the first time so you do not get a header line buried in the middle of your data. The newline="" argument is a small but important detail on Windows: without it the CSV module inserts blank rows between every record. Once results land in a CSV you can open it in any spreadsheet, filter by category, and act on the urgent items first — the automation has done the reading for you.
Writing to a file is the safest place to start because it never fails silently and you can always inspect it. If your real output needs to land in a CRM, a Slack channel, or another web service, the same idea applies — you swap the CSV writer for an API call and keep the rest of the loop untouched. That swap-one-part habit is what makes these pipelines durable: each step has one job, so you can change where the data comes from or where it goes without rewriting the logic in between. The Building AI-Powered Business Applications track covers those downstream integrations in depth.
Step 4: Schedule it so it runs on its own
The point of automation is that you stop touching it. Wrap the three steps above into one function, then trigger that function on a timer. The schedule library is the simplest option for a machine you keep running:
import schedule
import time
def run_cycle() -> None:
items = read_inputs("inbox")
rows = []
for item in items:
result = classify(item["text"])
rows.append({"id": item["id"], **result})
write_results(rows)
print(f"Processed {len(rows)} items")
schedule.every().day.at("08:00").do(run_cycle)
while True:
schedule.run_pending()
time.sleep(60)
This keeps Python running and fires run_cycle every morning at 08:00. The while True loop with time.sleep(60) simply wakes up once a minute, asks whether any job is due, and goes back to sleep — light enough that you can leave it running all day. The catch is that it only works while that program stays alive: close the terminal or shut the laptop and the schedule stops. That is fine for testing, but not for something you depend on.
For anything important, a system cron job is more robust because it is run by the operating system itself and survives reboots. On Mac or Linux, type crontab -e and add a line like 0 8 * * * /path/to/.venv/bin/python /path/to/script.py, which runs the script every day at 08:00 without any terminal open. The five fields before the command are minute, hour, day-of-month, month, and day-of-week, so 0 8 * * * reads as "minute 0 of hour 8, every day." Cron starts your script in a bare environment with hardly any settings, which is the single most common reason scheduled jobs mysteriously do nothing: relative paths like inbox point somewhere unexpected. Always use absolute paths to both the Python executable and your script, and add a line such as os.chdir(Path(__file__).parent) at the top so the script always runs from its own folder and finds both your inbox and your .env. If you would rather run on a machine you do not own, a small always-on cloud server runs the same cron line so your automation works even when your computer is off.
Parameter reference
These are the settings you will adjust most often as you adapt the code to your own task.
| Parameter | Type | Default | Effect |
|---|---|---|---|
model | str | "gpt-4o-mini" | Which AI model answers. Smaller models are cheaper and faster; larger ones reason better on hard inputs. |
temperature | float | 0 | Randomness of the answer. Keep at 0 for consistent classification; raise toward 1 only for creative rewriting. |
response_format | dict | {"type": "json_object"} | Forces valid JSON output so your parsing step does not break. |
text[:1500] | int slice | 1500 | Maximum characters sent per item. Lower it to cut cost; raise it if items get cut off. |
schedule.every().day.at() | str | "08:00" | When the loop runs. Accepts 24-hour "HH:MM" times. |
time.sleep(60) | int | 60 | Seconds between schedule checks. 60 is plenty; lower values waste CPU. |
Troubleshooting
openai.AuthenticationError: Incorrect API key provided— Your key is wrong, expired, or not being loaded. Confirm.envsits in the folder you run from and that the line readsOPENAI_API_KEY=sk-...with no quotes or spaces. The Fix the 401 Unauthorized Error in OpenAI Python guide covers every cause.openai.RateLimitError: Rate limit reached— You sent requests faster than your plan allows, or your free credits ran out. Add a shorttime.sleep(1)between items in the loop, or follow Fix the 429 Rate-Limit Error in Python to add automatic retries.json.decoder.JSONDecodeError: Expecting value— The model returned text that is not valid JSON. Keepresponse_format={"type": "json_object"}, lowertemperatureto0, and wrapjson.loadsin a try/except so one bad item does not stop the batch. See Fix JSONDecodeError with AI API Responses in Python.This model's maximum context length is ... tokens— You sent an item that is too long. Trim it harder with a smaller slice such astext[:800], or split the document into parts. The Fix the Context-Length-Exceeded Error in Python guide explains the math.ModuleNotFoundError: No module named 'openai'— Your virtual environment is not active, so Python cannot see the package. Runsource .venv/bin/activate(Windows:.venv\Scripts\activate) and reinstall, then checkwhich python3points inside.venv.- Cron job runs but does nothing — Cron starts in a bare environment with the wrong working directory, so relative paths like
inboxand.envare not found. Use absolute paths everywhere, or addos.chdir(Path(__file__).parent)at the top of your script so it always runs from its own folder.
Worked example: a complete inbox classifier
This single file ties all four steps together. Save it as automate.py, put a few .txt files in an inbox folder, and run python automate.py for an immediate pass — or uncomment the schedule block to leave it running every morning.
import os
import csv
import json
import time
from pathlib import Path
from openai import OpenAI
from dotenv import load_dotenv
load_dotenv() # read OPENAI_API_KEY from .env (keep .env in .gitignore)
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
def read_inputs(folder: str) -> list[dict]: # Step 1: load each .txt file as one record
records = []
for path in sorted(Path(folder).glob("*.txt")):
text = path.read_text(encoding="utf-8", errors="ignore").strip()
if text:
records.append({"id": path.name, "text": text})
return records
def classify(text: str) -> dict: # Step 2: ask the AI for a JSON label
prompt = (
"Classify the message into exactly one category: "
"'urgent', 'newsletter', 'invoice', or 'general'. "
"Reply as JSON: {\"category\": \"...\", \"reason\": \"...\"}.\n\n"
f"Message:\n{text[:1500]}"
)
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0,
response_format={"type": "json_object"},
)
return json.loads(response.choices[0].message.content)
def write_results(rows: list[dict], outfile: str = "results.csv") -> None: # Step 3: save to CSV
fields = ["id", "category", "reason"]
new_file = not Path(outfile).exists()
with open(outfile, "a", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=fields)
if new_file:
writer.writeheader()
writer.writerows(rows)
def run_cycle() -> None: # Step 4: one full pass, ready to schedule
rows = []
for item in read_inputs("inbox"):
try:
rows.append({"id": item["id"], **classify(item["text"])})
except Exception as error: # one bad item must not stop the batch
print(f"Skipped {item['id']}: {error}")
write_results(rows)
print(f"Processed {len(rows)} items into results.csv")
if __name__ == "__main__":
run_cycle()
# import schedule
# schedule.every().day.at("08:00").do(run_cycle)
# while True:
# schedule.run_pending()
# time.sleep(60)
Next steps
You now have a working loop. Here is how to grow it:
- Point it at real email. Follow the Python Script to Automate Email Sorting guide to replace the
inboxfolder with a live Gmail account over IMAP. - Feed it cleaner data. If your inputs are spreadsheets or exports, the Cleaning CSV Data with Pandas for AI walkthrough gets them ready before the AI sees them.
- Tighten your prompts. Better instructions mean more accurate labels and fewer parsing errors; the Prompt Engineering Basics section shows how.
Back to Python AI Fundamentals for Non-Developers.
Related guides
- Python Script to Automate Email Sorting — take this pattern all the way to a live inbox.
- Data Cleaning for AI — prepare messy inputs so your automation stays accurate.
- Understanding LLM APIs — how keys, models, and billing work behind the AI step.
- Setting Up Python for AI — install Python and your tools the right way.
- Python AI Fundamentals for Non-Developers — the full beginner track this section belongs to.