LLMs in automations
Put models inside scripts and workflow tools that run on their own.
Create a free account Try part of it here
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
- Where AI steps live
- Retry the right errors
- Batches that survive failure
- Backoff waits and a safe rerun
- Big jobs that can wait: the Message Batches API
- Don't rerun from the start
- 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])
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.
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
- 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.
waits = [2 ** n for n in range(4)] print(sum(waits))
Show the answer
15
Waits of 1, 2, 4 and 8 seconds add up to 15. Each one doubles: that's exponential backoff.
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.
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])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.
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.
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
Try a quiz question
Which errors should an automation retry with backoff?