# Automating recurring work with the OpenAI stack
That weekly competitive brief you write every Monday morning, the one where you skim five competitor blogs, summarize pricing changes, and paste it into an email, can run itself while you sleep. The trick is to stop treating ChatGPT as a place you visit and start treating the OpenAI APIAPIApplication Programming Interface: a standardised interface that lets applications communicate and exchange data without knowing each other's internal workings.View full definition → as a component you schedule. This lesson turns one repeated chat into a hands-off pipelinepipelineAll active sales opportunities across the stages of the sales process, together with their combined potential value and probability of closing.View full definition →, with the guardrails that keep it from getting expensive or embarrassing.
Before writing code, know that OpenAI ships a no-code option. Inside ChatGPT, scheduled tasks let you ask the assistant to run a prompt on a recurring schedule and notify you. That is genuinely useful for personal reminders and lightweight digests. See Scheduled tasks in ChatGPT.
But scheduled tasks live inside your ChatGPT account. They cannot easily commit to a repo, write to your data warehousedata warehouseA central repository that consolidates data from many source systems into a structured, query-optimized store designed for analytics, reporting, and business intelligence.View full definition →, or send mail through your company's transactional provider. The moment you need real integration, version control, or a service account that is not tied to your personal login, you move to the APIAPIApplication Programming Interface: a standardised interface that lets applications communicate and exchange data without knowing each other's internal workings.View full definition → plus an external scheduler.
The decision rule:
The rest of this lesson is the second path.
Our example: every Monday at 6am, fetch recent competitor content, produce a structured brief, render it as an email, and send it.
A scheduled job is just four stages glued together:
1. Gather the inputs (RSS feeds, a few URLs, last week's brief).
2. Generate the brief via the APIAPIApplication Programming Interface: a standardised interface that lets applications communicate and exchange data without knowing each other's internal workings.View full definition →.
3. Validate the output against a schemaschemaA schema is the formal blueprint that defines how data is structured, named, typed, and related within a database, file, or message.View full definition →.
4. Deliver it (email, Slack, a committed file).
The model only owns stage 2. Treating gather, validate, and deliver as plain code (not model calls) is what makes the pipelinepipelineAll active sales opportunities across the stages of the sales process, together with their combined potential value and probability of closing.View full definition → cheap and predictable.
In a chat you read prose and move on. In a pipelinepipelineAll active sales opportunities across the stages of the sales process, together with their combined potential value and probability of closing.View full definition →, the next stage is code that needs to *parse* the result. If the model returns slightly different shapes week to week, your email renderer breaks.
Use Structured Outputs to force the model to return JSON that matches a schemaschemaA schema is the formal blueprint that defines how data is structured, named, typed, and related within a database, file, or message.View full definition → you define. This is not prompt-begging ("please return JSON"); the APIAPIApplication Programming Interface: a standardised interface that lets applications communicate and exchange data without knowing each other's internal workings.View full definition → constrains generation to your schemaschemaA schema is the formal blueprint that defines how data is structured, named, typed, and related within a database, file, or message.View full definition →. Read Structured Outputs.
Here is the core call using the Responses API, which is OpenAI's current primary interface for new builds. Note the text.format block pinning the response to a schemaschemaA schema is the formal blueprint that defines how data is structured, named, typed, and related within a database, file, or message.View full definition →.
import os
from openai import OpenAI
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
SCHEMA = {
"type": "object",
"properties": {
"summary": {"type": "string"},
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"competitor": {"type": "string"},
"headline": {"type": "string"},
"why_it_matters": {"type": "string"},
"source_url": {"type": "string"},
},
"required": ["competitor", "headline", "why_it_matters", "source_url"],
"additionalProperties": False,
},
},
},
"required": ["summary", "items"],
"additionalProperties": False,
}
def build_brief(gathered_text: str) -> dict:
resp = client.responses.create(
model="gpt-4.1-mini",
input=[
{"role": "system", "content": "You are a competitive analyst. "
"Only use facts present in the provided sources. If a claim is "
"not supported, omit it. Never invent pricing or dates."},
{"role": "user", "content": gathered_text},
],
text={"format": {"type": "json_schema", "name": "brief",
"schema": SCHEMA, "strict": True}},
)
return resp.output_parsedThree deliberate choices:
gpt-4.1-mini here) handles summarization fine. Reserve frontier models for tasks that actually need reasoning. This is your single biggest cost lever.Do not paste five full blogs into the prompt and pay for tokenstokensA token is the basic unit of text that language models process, often a word fragment, whole word, or punctuation mark rather than a single character.View full definition → you do not need. Fetch each feed, keep only entries newer than your last run, and trim each article to its lead section. Your gathered_text becomes a compact, dated digest. Less context means lower cost, faster runs, and fewer chances for the model to wander.
This is also where you store state: write last_run.json after each successful run so next week you only process *new* items. A pipelinepipelineAll active sales opportunities across the stages of the sales process, together with their combined potential value and probability of closing.View full definition → that re-summarizes the same articles every week is both wasteful and noisy.
Keep the model out of delivery entirely. Take the validated JSON and render it with a template, then send through whatever your org already uses.
def render_email(brief: dict) -> str:
lines = [f"<p>{brief['summary']}</p>", "<ul>"]
for item in brief["items"]:
lines.append(
f"<li><b>{item['competitor']}:</b> {item['headline']} "
f"— {item['why_it_matters']} "
f"<a href='{item['source_url']}'>source</a></li>"
)
lines.append("</ul>")
return "".join(lines)Send via your transactional email provider's SDK or SMTP. The point: the model produced *structured facts*, and deterministic code produced the *artifact*. If the email layout needs to change, you edit a template, not a prompt.
Now make it recurring. Two clean options.
On any always-on Linux box, crontab -e:
# Monday 06:00, write logs, never let a crash go silent
0 6 * * 1 cd /opt/briefs && /opt/briefs/.venv/bin/python run.py >> /var/log/brief.log 2>&1 || curl -s "$ALERT_WEBHOOK" -d "brief job failed"Simple, but you own the server, the secrets file, and the uptime.
Continuous integration runners are free for light scheduled jobs, secrets are managed, and every run is logged. CI here just means a hosted environment that runs your script on a trigger.
name: weekly-brief
on:
schedule:
- cron: "0 6 * * 1" # Mondays 06:00 UTC
workflow_dispatch: {} # lets you run it manually to test
jobs:
brief:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -r requirements.txt
- run: python run.py
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
EMAIL_API_KEY: ${{ secrets.EMAIL_API_KEY }}workflow_dispatch is the underrated line: it gives you a "Run workflow" button so you can test the whole pipelinepipelineAll active sales opportunities across the stages of the sales process, together with their combined potential value and probability of closing.View full definition → on demand without waiting for Monday. Secrets live in the repo's encrypted settings, never in code.
An unattended job that calls a paid APIAPIApplication Programming Interface: a standardised interface that lets applications communicate and exchange data without knowing each other's internal workings.View full definition → and emails your team needs limits. Set them before you ship, not after a surprise bill.
max_output_tokens. A brief does not need 4,000 tokenstokensA token is the basic unit of text that language models process, often a word fragment, whole word, or punctuation mark rather than a single character.View full definition →.source_url per item (the schemaschemaA schema is the formal blueprint that defines how data is structured, named, typed, and related within a database, file, or message.View full definition → already enforces this). An item with no source is a red flag your code can drop.items is empty, send "no notable changes this week" rather than an empty shell, and if it is suspiciously long, flag for review.Knowledge check
1. According to the lesson's decision rule, which scenario calls for using ChatGPT scheduled tasks rather than the API plus an external scheduler?
2. In the four-stage pipeline described, which stage is the only one owned by the model?
3. In the OpenAI API, what is the primary purpose of supplying a JSON schema with a structured outputs request?
4. Select ALL correct answers about why the lesson recommends moving from ChatGPT scheduled tasks to the API plus an external scheduler.
Sélectionnez toutes les réponses correctes.
5. Select ALL correct answers about the four stages of the automated pipeline as described in the lesson.
Sélectionnez toutes les réponses correctes.
The single-call design above is the right starting point, and it covers a surprising amount of recurring work. ReachReachThe number of unique people exposed to your message in a given period. Unlike impressions, reach counts each person once, no matter how often they see it.View full definition → for more structure only when the task genuinely needs it.
If the job must decide *which* sources to pull or call live systems (a pricing APIAPIApplication Programming Interface: a standardised interface that lets applications communicate and exchange data without knowing each other's internal workings.View full definition →, an internal search), give the model function calling so it can request those actions, and your code executes them. The model proposes; your code disposes. This keeps the model from touching anything you have not explicitly allowed.
For multi-step jobs (gather, draft, critique its own draft, revise, then format), the OpenAI Agents SDK gives you a clean way to orchestrate steps, tools, and handoffs without hand-rolling the control loop. Use it when your pipelinepipelineAll active sales opportunities across the stages of the sales process, together with their combined potential value and probability of closing.View full definition → has real branching logic. Do not reachreachThe number of unique people exposed to your message in a given period. Unlike impressions, reach counts each person once, no matter how often they see it.View full definition → for it to summarize one feed; that is overengineering, and every extra model call is latency and cost.
The ChatGPT agent can browse and operate inside a session to complete tasks, and Codex can write and run code for you. Both are excellent for *building and prototyping* this pipelinepipelineAll active sales opportunities across the stages of the sales process, together with their combined potential value and probability of closing.View full definition → interactively. But the production version of a recurring job should be deterministic code under version control and a scheduler, not an interactive agent session you have to babysit. Build with the agent; ship with the APIAPIApplication Programming Interface: a standardised interface that lets applications communicate and exchange data without knowing each other's internal workings.View full definition →.
Your run.py is now just the four stages in sequence:
def main():
state = load_state("last_run.json")
gathered = gather_sources(feeds, since=state["last_run"])
if not gathered.strip():
notify("No new competitor content this week.")
return
brief = build_brief(gathered)
if not brief["items"]:
notify("No notable changes this week.")
return
send_email(render_email(brief))
save_state("last_run.json", now())
if __name__ == "__main__":
main()Readable, testable, and boring in the best way. The model is one line in the middle. Everything around it is plain, debuggable code, which is exactly the property you want in something that runs without you watching.
workflow_dispatch button to test on demand. Use cron only when you control the box.