AI Tips Ladder · Full Guides
The complete walkthroughs from that post
TUE · AUG 04 · ED 580
BEGINNER
AUTOMATION
01 · Your Most-Repeated Prompt Is A Zap
A prompt you retype three times a week is roughly 15–20 minutes a month of copying, pasting and re-explaining context, and the output quality drifts every time you reword it. Turning it into a Zap fixes the wording once and runs it on arrival, so the answer is waiting in your inbox instead of behind a tab switch. Use it when the input is something that already lands somewhere machine-readable — an email, a form response, a new spreadsheet row — not when you need to think before pasting.
- 01Find the repeat: open chatgpt.com and scroll the sidebar over the last 7 days, or go to Settings → Data controls → Export data, open the emailed conversations.html and search it. Pick the one prompt whose wording you've retyped 3+ times.
- 02Rewrite it as a template in a plain text file: keep the role, the rules and the output format, delete anything true of only one instance (names, dates, that one client), and leave exactly one marker <<INPUT>> where the pasted text went.
- 03Name the trigger — the thing you always paste. New Gmail email, new Typeform/Google Form response, new Google Sheets row. If the input has no home yet, stop and give it one; a Zap needs a source.
- 04In Zapier click + Create → Zaps (New Zap). In the trigger step search your app, pick the event (for Gmail prefer New Email Matching Search with a search string like from:billing@vendor.com has:attachment over plain New Email), connect the account, then Continue → Test trigger so you pull one real record.
- 05Add the action step: search ChatGPT and choose the Conversation event (or search AI by Zapier if you'd rather not supply your own OpenAI key). Connect the account — the ChatGPT app asks for an OpenAI API key from platform.openai.com, billed separately from Zapier.
- 06Paste the template into the User Message / Prompt field. Delete the literal <<INPUT>> text, leave the cursor there, click the field's data picker (the icon at the right edge of the field) and insert the trigger field — for Gmail that is 1. Body Plain, not Body Html. Set Model to a current model and leave Memory Key empty so runs don't bleed into each other.
- 07Add a third step to deliver the result: Gmail → Send Email, Slack → Send Channel Message, or Google Sheets → Create Spreadsheet Row. In its body, insert the AI step's output field (labelled Reply or Response in the picker).
- 08Click Test step on each step in turn, read the actual output, fix the prompt wording, retest, then Publish and toggle the Zap on. Check Zap History (zapier.com → Zap History) after the first real trigger fires.
You are helping me with a task I do repeatedly. Follow these rules exactly.
ROLE: <one line — e.g. "You are a support lead triaging inbound customer email.">
TASK: <one line — the single thing to do with the input below.>
RULES:
- Use only information present in the INPUT. If something is missing, write "not stated" rather than guessing.
- Do not add preamble, apologies, or an offer to help further.
- Keep the total response under <N> words.
OUTPUT FORMAT (return exactly this shape, nothing else):
Summary: <one sentence>
Action needed: <yes/no> — <what, if yes>
Owner: <name or "not stated">
Draft reply:
<3 sentences max>
INPUT:
<<INPUT>>
Watch out: Zapier does not interpret {{placeholder}} — text in double braces is sent to the model literally. The placeholder is only a marker for you; in the Zap you must delete it and insert the real field from the step's data picker. · Zaps poll rather than listen — free and Starter plans check every 15 minutes, and each check that finds a record consumes a task. A broad trigger like New Email fires on everything; narrow it with a Gmail search string, a label, or a Filter step before the AI action, or you'll burn your monthly task quota on newsletters. · Test trigger pulls one recent sample and the prompt gets tuned to that one shape. Before publishing, test against a short and a messy example too — an email with a long quoted thread, or a form entry with a blank field — since that's where the fixed output format breaks.
INTERMEDIATE
TOOLS
02 · Your Whisper Call Is One Word Outdated
whisper-1 mangles exactly the words that matter most in a transcript — product names, client names, people's names, internal acronyms — and every downstream summary inherits the error. gpt-transcribe is a drop-in on the same /v1/audio/transcriptions endpoint with lower word error rate, and it accepts a `keywords` list plus a `prompt` describing the recording, so you can hand it your vocabulary instead of hoping. Use it for meeting notes, sales calls and voice memos; keep whisper-1 anywhere you need word timestamps or subtitle files.
- 01Find every call site: run `grep -rn --include=*.py --include=*.ts --include=*.js -e 'whisper-1' -e 'audio.transcriptions' -e 'audio/transcriptions' .` from your repo root (add `--include` globs for other languages you use).
- 02Upgrade the SDK before editing anything, or the new params will throw: `pip install -U openai` (Python) or `npm i openai@latest` (Node). Confirm with `pip show openai` / `npm ls openai`.
- 03Triage the hits: at each call, check `response_format` and `timestamp_granularities`. Anything using `verbose_json`, `srt`, `vtt`, or word/segment timestamps must stay on `whisper-1` — gpt-transcribe returns json/text only. Swap only the plain-text paths.
- 04Create one shared vocabulary constant in a module both your code and your teammates can edit — a flat list of product names, client names, people's names, and acronyms, spelled and capitalised exactly as you want them to appear.
- 05In each remaining call, change `model="whisper-1"` to `model="gpt-transcribe"` and add `prompt=<one or two sentences describing what the recording is>`, `keywords=<your vocabulary list>`, and `languages=["en"]` (or your actual language codes).
- 06A/B it before you ship: pick a real recording that contains at least five of your jargon terms, transcribe it once with each model, write both to files, and run `diff old.txt new.txt` to confirm the proper nouns actually improved rather than assuming.
- 07Gate the rollout with an env var — read the model id from `OPENAI_TRANSCRIBE_MODEL` defaulting to `gpt-transcribe` — so you can revert a single deploy variable if a specific accent or audio profile regresses.
- 08Re-check anything that assumed the old per-minute rate: gpt-transcribe bills at $0.0045/min vs whisper-1's $0.006/min, so cost estimates, usage alerts and client billing math all need updating.
# pip install -U openai
from openai import OpenAI
client = OpenAI() # reads OPENAI_API_KEY from the environment
# One place to edit. Spell these EXACTLY as you want them in the transcript.
VOCAB = [
"Acme Analytics", # your product names
"Northwind Trading", # your client names
"Priya Raghunathan", # people whose names get mangled
"ARR", "MSB", "webhook", "Postgres", # jargon and acronyms
]
CONTEXT = (
"Internal product and sales meeting. Speakers use the company, client, "
"person and technical names given in keywords. Transcribe those terms "
"with exactly that spelling and capitalisation."
)
with open("meeting.m4a", "rb") as f:
result = client.audio.transcriptions.create(
model="gpt-transcribe", # was: "whisper-1"
file=f,
prompt=CONTEXT, # unstructured context about the recording
keywords=VOCAB, # literal expected terms — the real lever
languages=["en"], # expected language codes
)
print(result.text)
Watch out: The response shape is only the same for plain transcripts. gpt-transcribe supports json/text — `verbose_json`, `srt`, `vtt` and `timestamp_granularities` are whisper-1-only, so any code reading `.segments`, `.words` or writing subtitle files will 400 or come back empty. Leave those call sites on whisper-1. · `prompt` and `keywords` are hints, not a find-and-replace. A short, real list works; a bloated one makes the model insert names into audio that never contained them, so keep it to terms that genuinely appear and re-listen to a sample before trusting it. · `keywords` and `languages` are newer parameters — an older pinned openai SDK will raise a TypeError or silently drop them. Upgrade the SDK (or pass them via `extra_body`) and verify on one file rather than assuming they took effect. The 25 MB / ~mp3, mp4, mpeg, mpga, m4a, wav, webm file limits are unchanged, so existing chunking code still applies.
ADVANCED
TOOLS
03 · Give Claude Code A House Style
A CLAUDE.md is auto-loaded into context at the start of every Claude Code session in that directory, so the model already knows your package manager, test command, and forbidden patterns before you type anything. Without it you re-explain the same project facts each session and keep correcting the same wrong assumptions (wrong test runner, new files in the wrong directory, `any` types). Worth doing once per repo, and worth updating the first time you catch yourself correcting Claude on something it should have known.
- 01Open a terminal at the repo root (the directory containing .git) and run `claude` to start an interactive session.
- 02Type `/init` and press Enter. Claude scans the repo and writes a draft CLAUDE.md at the root. Wait for it to finish, then open the file and read it — the draft is descriptive (what it found) and usually needs your rules added.
- 03Edit CLAUDE.md so the top holds the facts a new contributor would ask for: language/framework versions, package manager (npm vs pnpm vs yarn — Claude guesses wrong constantly), and the exact commands for install, dev server, test, single-test, lint, and typecheck. Paste real commands you have actually run, not idealized ones.
- 04Add a `## Rules` section with 3-5 hard 'never do this' lines drawn from mistakes you have already had to correct. Write them as imperatives with a reason: 'Never commit to main — always branch first', 'Never add a dependency without asking', 'Never use `any`; this repo runs tsc --strict'.
- 05Test it in a fresh session: exit Claude, run `claude` again, and ask 'What are the test and lint commands for this repo, and what are the hard rules?' If the answer is wrong or vague, the file is unclear — fix the wording, not the prompt.
- 06Commit and push it so teammates inherit it: `git add CLAUDE.md && git commit -m "Add CLAUDE.md project instructions" && git push`. Anyone who clones the repo now gets the same briefing.
- 07Keep it current with the `#` shortcut: mid-session, type `#` followed by the rule (e.g. `# always run pnpm typecheck before committing`) and Claude appends it to CLAUDE.md for you. Use `/memory` to open memory files in your editor and prune stale lines.
- 08Optional: put personal preferences that should not be in the shared repo (verbosity, your editor, your own shortcuts) in `~/.claude/CLAUDE.md` instead — it applies to every project on your machine and is never committed.
# CLAUDE.md
## Stack
- TypeScript 5.x, Next.js 15 (App Router), React 19
- Postgres via Drizzle ORM
- Package manager: pnpm (NOT npm — lockfile is pnpm-lock.yaml)
## Commands
- Install: `pnpm install`
- Dev server: `pnpm dev` (port 3000)
- Test all: `pnpm test`
- Test one file: `pnpm test path/to/file.test.ts`
- Typecheck: `pnpm typecheck`
- Lint + fix: `pnpm lint --fix`
## Layout
- `app/` routes and server components
- `lib/` shared logic — put new business logic here, not in components
- `db/schema.ts` single source of truth for tables; migrations via `pnpm db:generate`
## Rules
- Never commit directly to `main` — create a branch first.
- Never install a new dependency without asking me first.
- Never use `any` or `@ts-expect-error`; the build runs `tsc --strict`.
- Never edit files in `generated/` — they are regenerated by `pnpm db:generate`.
- Always run `pnpm typecheck && pnpm test` before saying a task is done.
## Conventions
- Named exports only; no default exports.
- Tests live next to source as `*.test.ts`.
Watch out: Every line costs context on every single session, so a 500-line CLAUDE.md crowds out the actual code. Keep it under roughly 100 lines; if a section only matters for one subsystem, put a CLAUDE.md in that subdirectory instead — it loads only when files there are touched. · The /init draft is a description of what the scanner saw, not a set of rules, and it goes stale silently. Vague entries ('write clean code', 'follow best practices') are ignored in practice — only specific, checkable instructions change behavior. · It is a committed plaintext file: no API keys, tokens, connection strings, or internal URLs. For anything machine-specific or private, use ~/.claude/CLAUDE.md, or a gitignored file pulled in with an `@path/to/file.md` import line.
PRO
AGENTS
04 · Cache The System Prompt, Save 90%
Cached prefix tokens are billed at ~0.1x the normal input rate, so an agent that resends the same 5k-token rulebook every turn pays roughly 10% for that portion after the first call (output tokens are unaffected). The first call costs 1.25x to write the cache, so it pays for itself from the second call onward within the 5-minute TTL. Use it whenever a large, byte-identical block — system prompt, tool definitions, retrieved docs, few-shot examples — is resent across requests; skip it if the prompt differs from the first token every time.
- 01Inventory what goes into your prompt and label each piece: never changes (rules, schemas, tool defs), changes per session (user profile), changes per turn (the question). Confirm the stable part clears the minimum cacheable prefix — 512 tokens on claude-opus-5, 1024 on claude-opus-4-8/claude-sonnet-5, 4096 on claude-opus-4-6 and claude-haiku-4-5. Below the minimum nothing caches and you get no error.
- 02Reorder the request so stable content physically precedes volatile content. The API renders tools -> system -> messages, and the cache is a prefix match on exact bytes, so one changed byte invalidates everything after it.
- 03Strip silent invalidators out of the prefix: grep your prompt-building code for datetime.now()/Date.now(), uuid4()/crypto.randomUUID(), json.dumps() without sort_keys=True, iteration over a set, and f-strings injecting session or user IDs into the system prompt. Move each to the end of the prompt or make it deterministic.
- 04Put cache_control {"type":"ephemeral"} on the LAST stable block — usually the final system text block, which caches tools+system together. Max 4 breakpoints per request. If you don't need fine placement, pass cache_control={"type":"ephemeral"} as a top-level arg to messages.create() and the SDK marks the last cacheable block for you.
- 05Run the same request twice and print response.usage: first call should show cache_creation_input_tokens > 0, second should show cache_read_input_tokens > 0 with input_tokens down to just the volatile remainder. Total prompt size = input_tokens + cache_creation_input_tokens + cache_read_input_tokens.
- 06If cache_read_input_tokens stays 0 across identical calls, dump the fully rendered request body (json.dumps(kwargs, sort_keys=True)) for two consecutive calls and diff them — the first differing byte is your invalidator.
- 07For multi-turn agents, additionally set cache_control on the last content block of the most recently appended turn so the growing conversation caches incrementally. In long tool-calling turns, add an intermediate breakpoint about every 15 blocks — a breakpoint only looks back 20 content blocks for a prior entry.
- 08If your traffic is bursty with gaps longer than 5 minutes, switch to cache_control {"type":"ephemeral","ttl":"1h"}. The write costs 2x instead of 1.25x, so it needs 3+ reads to break even versus 2 for the default.
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY
SYSTEM_RULES = open("rules.md").read() # your ~5k-token static rulebook
def ask(question: str):
return client.messages.create(
model="claude-opus-5",
max_tokens=1024,
# tools render BEFORE system; keep the list stable and sorted by name
system=[
{
"type": "text",
"text": SYSTEM_RULES, # frozen: no timestamps, no user IDs
"cache_control": {"type": "ephemeral"}, # add "ttl": "1h" for bursty traffic
}
],
# everything volatile lives AFTER the breakpoint
messages=[{"role": "user", "content": question}],
)
first = ask("Summarize rule 3.")
second = ask("Now summarize rule 7.")
for label, r in (("first", first), ("second", second)):
u = r.usage
print(
f"{label}: write={u.cache_creation_input_tokens} "
f"read={u.cache_read_input_tokens} uncached={u.input_tokens}"
)
# Expect: first -> write>0, read=0
# second -> write=0, read>0 (cache hit confirmed)
Watch out: Anything dynamic in front of the breakpoint kills it silently: a "Current date: ..." line, a request UUID, or json.dumps() with unstable key order changes the prefix bytes on every call, so you pay the 1.25x write premium forever and never get a read. · Prefixes under the model's minimum (512-4096 tokens depending on model) are not cached and return no error — just cache_creation_input_tokens: 0. Same for a breakpoint placed after the varying part of the prompt: every request writes its own entry and nothing is ever read. · Changing the tool list, reordering tools, or switching models mid-conversation invalidates the entire cache (tools render at position 0, and caches are model-scoped). Also, N parallel requests fired at once all miss — an entry is only readable once the first response starts streaming, so send one request, wait for the first token, then fan out.
🛠️ We build the AI systems & automations behind tips like these.
Follow @conon.ai for the daily brief — news + tips, every day.