+180 XP

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 API as a component you schedule. This lesson turns one repeated chat into a hands-off pipeline, with the guardrails that keep it from getting expensive or embarrassing.

Two ways to automate: in-product vs. the API

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 warehouse, 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 API plus an external scheduler.

The decision rule:

  • One person, light output, notify-me: ChatGPT scheduled tasks.
  • Shared output, real systems, auditable runs: API + cron or CI.

The rest of this lesson is the second path.

The pipeline, in four stages

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 API.
  3. Validate the output against a schema.
  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 pipeline cheap and predictable.

Why structured outputs are non-negotiable here

In a chat you read prose and move on. In a pipeline, 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 schema you define. This is not prompt-begging ("please return JSON"); the API constrains generation to your schema. Read Structured Outputs.

Building the brief generator

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 schema.

python
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_parsed

Three deliberate choices:

  • A small, cheap model (gpt-4.1-mini here) handles summarization fine. Reserve frontier models for tasks that actually need reasoning. This is your single biggest cost lever.
  • `strict": True` guarantees the JSON validates, so stage 4 can render without defensive parsing.
  • The system prompt forbids invention. Recurring jobs run unattended, so a hallucinated competitor price can sit in an executive inbox before anyone notices.

Gathering inputs without a giant context window

Do not paste five full blogs into the prompt and pay for tokens 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 pipeline that re-summarizes the same articles every week is both wasteful and noisy.

Delivery: render, then send

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.

python
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"&mdash; {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.

Scheduling it

Now make it recurring. Two clean options.

Option A: cron on a small server

On any always-on Linux box, crontab -e:

bash
# 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.

Option B: GitHub Actions (recommended for most teams)

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.

yaml
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 pipeline on demand without waiting for Monday. Secrets live in the repo's encrypted settings, never in code.

Schedule Python Scripts with GitHub Actions

Watch on YouTube

Guardrails that keep it safe and cheap

An unattended job that calls a paid API and emails your team needs limits. Set them before you ship, not after a surprise bill.

Cost guardrails

  • Set a usage limit in the API dashboard so a runaway loop cannot drain your account. See Usage limits.
  • Cap output tokens with max_output_tokens. A brief does not need 4,000 tokens.
  • Use a dedicated project and API key for this job. Per-project keys mean you can see exactly what this pipeline costs and revoke it independently.
  • Pick the cheapest model that passes your eval. Re-test occasionally; cheaper models keep improving.

Correctness guardrails

  • Ground the model in fetched sources only, and require a source_url per item (the schema already enforces this). An item with no source is a red flag your code can drop.
  • Add a sanity check between validate and deliver: if items is empty, send "no notable changes this week" rather than an empty shell, and if it is suspiciously long, flag for review.

Operational guardrails

  • Fail loudly. A silent cron job that stopped running three weeks ago is worse than no job. Ping a webhook on failure (see the cron example).
  • Retry transient errors with backoff. Network blips and rate limits happen; wrap the API call in a retry with a couple of attempts.
  • Log every run's token usage and cost, even on success, so you can spot drift.

Knowledge check

1. According to the lesson, what is the fundamental mindset shift required to automate a recurring task with the OpenAI stack?

2. A team needs an automated brief that commits to a shared repository, uses a service account not tied to a personal login, and produces auditable runs. Which approach does the lesson recommend?

3. In the four-stage pipeline (Gather, Generate, Validate, Deliver), why does the lesson insist that only stage 2 should be a model call?

MULTIPLE CHOICE

4. Select ALL correct statements about Structured Outputs as described in the lesson.

Select all the correct answers.

MULTIPLE CHOICE

5. Select ALL scenarios where ChatGPT scheduled tasks (the no-code option) are an appropriate choice according to the lesson.

Select all the correct answers.

Scaling up: when one prompt is not enough

The single-call design above is the right starting point, and it covers a surprising amount of recurring work. Reach for more structure only when the task genuinely needs it.

When to add tools

If the job must decide *which* sources to pull or call live systems (a pricing API, 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.

When to use the agents SDK

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 pipeline has real branching logic. Do not reach for it to summarize one feed; that is overengineering, and every extra model call is latency and cost.

A note on the ChatGPT agent and Codex

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 pipeline 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 API.

Putting it together

Your run.py is now just the four stages in sequence:

python
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.

Key Takeaways

  • Let the model own only generation. Gather, validate, and deliver belong in deterministic code. That separation is what makes scheduled jobs cheap and reliable.
  • Force Structured Outputs with `strict": True` so the next stage can parse results without defensive code or week-to-week breakage.
  • Schedule with GitHub Actions for teams: managed secrets, logged runs, and a workflow_dispatch button to test on demand. Use cron only when you control the box.
  • Set guardrails before shipping: a hard usage limit, a per-project API key, the cheapest model that passes your eval, capped output tokens, and a failure webhook so a dead job never goes unnoticed.
  • Build interactively, ship deterministically. Prototype with the ChatGPT agent or Codex, but run the recurring job as version-controlled code; reach for the Agents SDK only when the task has real multi-step branching.

What to do, from this lesson

These actions are compiled in the role's Playbook.

  • Write scheduled Tasks as complete standalone prompts specifying quiet-week behavior
  • Set hard billing limits with separate API keys per environment
  • Let the model own only generation; gather, validate, and deliver in code
See the full action playbook →