Index Ai Academy
Phase 1 · Building with LLMs · Tools & agents

LLMs in automations

Put models inside scripts and workflow tools that run on their own.

About 30 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

Why it matters

The biggest time savings from AI usually aren't in a chat window. They're in automations that run while you do something else. Sorting every new lead as it arrives. Tagging last week's reviews every Monday. Drafting product descriptions for every new product. Running with nobody watching changes the engineering. Nobody is there to retry a failed request or notice a half-finished job, so the code has to handle it.

What you'll be able to do

  • Describe where LLM steps fit in scheduled scripts, webhooks and no-code workflow tools
  • Retry rate-limit and server errors with exponential backoff, and never retry your own bugs
  • Process a batch so one failure doesn't stop the run and a rerun doesn't repeat work
  • Know when to use the Message Batches API for large, non-urgent jobs

What it covers

  1. Where AI steps live
  2. Retry the right errors
  3. Batches that survive failure
  4. Backoff waits and a safe rerun
  5. Big jobs that can wait: the Message Batches API
  6. Don't rerun from the start
  7. Keep a record per item

Where AI steps live

Think of a dishwasher on a timer. You load it, set it, and go to bed. It has to cope on its own if something goes wrong, because you're not there.

AI steps run unattended in three common places:

  • Scheduled scripts. A Python file that runs every morning on a timer, such as cron, a GitHub Actions schedule or a hosting platform's scheduler. "Summarize yesterday's campaign results and post them to Slack."
  • Event triggers. Something happens, such as a form is submitted, an order ships or a ticket arrives. A webhook (a web address that gets called when the event happens) runs your code on that one item. "Classify this lead and route it."
  • No-code workflow tools. Tools such as Zapier, Make and n8n have AI steps that call a model between other steps. Everything in this lesson applies there too. The tool hides the code, not the ways things fail.

Each is the same request you already know, run with nobody watching. So three things matter more than in a chat:

  • structured output, so the next step can act on fields instead of reading prose,
  • error handling, so short-lived failures don't lose work,
  • a record of what was done.

Lead triage, unattended

The practice API has eight inbound leads. A triage step sorts each one into fields the workflow can act on:

import json
import requests

SCHEMA = {
    "type": "object",
    "properties": {
        "intent": {"type": "string", "enum": ["wholesale", "order_support", "returns", "product_question", "other"]},
        "priority": {"type": "string", "enum": ["high", "normal", "low"]},
    },
    "required": ["intent", "priority"],
    "additionalProperties": False,
}

def triage(lead):
    response = requests.post("https://api.anthropic.com/v1/messages",
        headers={"x-api-key": "added-by-the-academy-server", "anthropic-version": "2026-10-21"},
        json={"model": "claude-haiku-5-5", "max_tokens": 100,
              "system": "Triage inbound messages for an outdoor clothing shop. Priority is high for business orders of 20+ items.",
              "messages": [{"role": "user", "content": f"From: {lead['name']} ({lead['company'] or 'no company'})\n{lead['message']}"}],
              "output_config": {"format": {"type": "json_schema", "schema": SCHEMA}}},
        timeout=60)
    response.raise_for_status()
    text = "".join(b["text"] for b in response.json()["content"] if b["type"] == "text")
    return json.loads(text)

leads = requests.get("https://api.gradient.test/v1/leads").json()["data"]
for lead in leads[:3]:
    print(lead["id"], triage(lead), "|", lead["message"][:50])
It runs right here in your browser, with nothing to install.

Without an API key, the simulator fills every field with its first allowed value. So every lead comes back as wholesale and high. With a key, you'll see the routing a real model chooses.

Quick check
Why does an unattended triage step use structured output instead of a free-text reply?
  1. Structured output is cheaper per token
  2. The next step can act on exact fields like intent and priority instead of reading prose
  3. Free text isn't allowed in automations
  4. It makes the model faster
Show the answer

The next step can act on exact fields like intent and priority instead of reading prose

With nobody watching, the next step must route reliably. A field that's always one of a few allowed values is easy to act on; a paragraph isn't.

Retry the right errors

Requests to any API sometimes fail for reasons that fix themselves:

  • you hit a rate limit, a cap on how many requests you can send per minute (status 429),
  • the service is briefly overloaded (529 on Anthropic's API, or 503),
  • a server error (500, 502).

The standard response is to wait and retry, waiting longer each time: 1 second, then 2, then

  1. That's called exponential backoff, because the wait doubles each time. Real code also adds

a little random extra wait, called jitter, so a thousand scripts don't all retry at the same instant. And if the API sends a retry-after header, wait that long.

Never retry a 400 (bad request), 401 or 403 (problems with your key) or 404 (not found). The request itself is wrong, so it will fail the same way every time. Retrying just wastes time and hides the bug.

import requests

for code in [429, 503, 400]:
    response = requests.get(f"https://api.gradient.test/v1/status/{code}")
    try:
        response.raise_for_status()
    except requests.exceptions.HTTPError as e:
        retry = e.response.status_code in {429, 500, 502, 503, 529}
        print(code, "retry" if retry else "give up and report it")

It prints 429 retry, 503 retry and 400 give up and report it.

Note: the official Anthropic SDKs already retry these errors, twice by default, with backoff. You write retries yourself when you call the HTTP API directly, as these lessons do, or to wrap a whole multi-step job.

Quick check
What does this print?
waits = [2 ** n for n in range(4)]
print(sum(waits))
  1. 15
  2. 16
  3. 4
  4. 8
Show the answer

15

Waits of 1, 2, 4 and 8 seconds add up to 15. Each one doubles: that's exponential backoff.

Try it now

Retrying a permanent error only wastes time and money.

Batches that survive failure

Two more habits make a batch job safe to leave running:

  • Isolate failures. Wrap each item in its own try. One broken lead should produce one error line in the report, not stop the other 999.
  • Make reruns safe. Record the ids you've finished, and skip them next time. If the job crashes at item 600, the rerun starts at 601. It doesn't email those 600 customers again. A job that's safe to rerun like this is called idempotent.
Worked example

Backoff waits and a safe rerun

Two ideas in a few lines. First, the waits for four retries with exponential backoff: 2 to the power of 0, 1, 2 and 3 seconds. Second, a rerun that skips the reviews already done.

done = {"rev_01", "rev_02"}
todo = ["rev_01", "rev_02", "rev_03", "rev_04"]
waits = [2 ** attempt for attempt in range(4)]
print(waits)
print([r for r in todo if r not in done])
In the lesson you can change this and run it.

It prints [1, 2, 4, 8]: each wait doubles. Then ['rev_03', 'rev_04']: the rerun only works on the two reviews that weren't finished. In a real job, done would be saved to a file or database after each item, so it survives a crash.

Quick check
Your nightly script gets a 400 error from the API on one review. What should it do?
  1. Record the error for that review and continue with the rest; don't retry it
  2. Stop the whole job immediately
  3. Retry the entire batch from the start
  4. Retry it with backoff until it works
Show the answer

Record the error for that review and continue with the rest; don't retry it

A 400 means the request is wrong, so retrying won't help. Isolate it and keep going.

Big jobs that can wait: the Message Batches API

Sometimes thousands of items can wait, such as tagging a year of reviews or rewriting 5,000 product descriptions. Send them as one batch instead of one request at a time. Anthropic's Message Batches API works through them in the background at half the normal price. Results arrive within 24 hours, and most batches finish much sooner:

batch = requests.post("https://api.anthropic.com/v1/messages/batches", headers=HEADERS, json={
    "requests": [
        {"custom_id": review["id"],
         "params": {"model": "claude-haiku-5-5", "max_tokens": 50,
                    "messages": [{"role": "user", "content": review["text"]}]}}
        for review in reviews
    ]
}).json()
# later: check the batch's status, then download results matched up by custom_id

Each request carries your own custom_id, so you can match each result back to its row. (The academy's proxy only passes on single messages, so this one is for your own projects.)

For marketers: a good first automation is small and boring. A daily script that tags new reviews and emails you the negative ones. Get retries, logging and safe reruns right on that, then scale up.

Quick check
You need to tag 20,000 old reviews, and the results aren't needed until next week. What's the best fit?
  1. 20,000 separate requests as fast as possible
  2. The Message Batches API: half price, results within 24 hours
  3. Wait until a faster model comes out
  4. A chat window, pasting the reviews in by hand
Show the answer

The Message Batches API: half price, results within 24 hours

Big, non-urgent jobs are exactly what batches are for. You pay half and don't have to manage rate limits yourself.

Don't rerun from the start

If a job crashes at item 800 of 1,000, rerunning everything repeats 800 side effects, such as emails sent twice. Save progress per item, and let one bad item fail on its own without stopping the rest.

Keep a record per item

Writing each item's outcome to a file as you go means you can see what failed, and rerun only those items once you've fixed the cause.

Practice what you just read

  • 4 code examples you can edit and run
  • a practice exercise that checks your code
  • a 5-question quiz
  • 4 review cards for spaced repetition

Create a free account

Try a quiz question

Which errors should an automation retry with backoff?

  1. 429, 500, 502, 503 and 529
  2. 400 and 404, since the request may work on a later try
  3. 401 and 403, since a key may have only just been refused
  4. All errors, since a retry costs little and often works

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson