Playbook

The How-To Guides

Tech Stack → Playbook. Six builds, each runnable end to end. No theory beyond what the build needs.

Every guide below follows the same shape: what you'll build, what you need first, numbered steps you can actually run, a "you know it worked when" check, and where to go next. Commands are for macOS/Linux terminals; Windows differences are flagged where they bite.

How-To 1: Run a local model tonight

What you'll build: A language model running entirely on your own machine — no account, no API key, no data leaving your laptop — that you can chat with in a terminal and call from code.

Prerequisites: A computer from the last ~5 years with 8 GB of RAM (16 GB is comfortable). About 20 minutes, most of it download time. No programming required until step 5.

Steps:

  1. Install Ollama. Go to ollama.com and download the installer for your OS, or on macOS/Linux:
curl -fsSL https://ollama.com/install.sh | sh

(General rule: read a script before piping it to your shell. This one is Ollama's official installer.)

  1. Confirm it's alive:
ollama --version

You should get a version number back. On macOS and Windows the installer also starts a background service; on Linux you may need ollama serve in a separate terminal.

  1. Pull a model sized for your RAM. Sensible first picks:
# ~8 GB RAM — small, fast, surprisingly capable
ollama pull llama3.2:3b

# 16 GB+ RAM — noticeably smarter
ollama pull qwen2.5:7b

The 3B model is roughly a 2 GB download; the 7B is roughly 4-5 GB ⚑ unverified. This is the slow step — get coffee.

  1. Chat:
ollama run llama3.2:3b

Type a question. Type /bye to exit. That's a language model running on your hardware, with the network cable conceptually unplugged.

  1. Now call it from code — Ollama exposes a local API on port 11434 automatically:
curl http://localhost:11434/api/generate -d '{
  "model": "llama3.2:3b",
  "prompt": "Explain what an API is in two sentences.",
  "stream": false
}'

Or from Python (pip install ollama):

import ollama
r = ollama.chat(model="llama3.2:3b",
                messages=[{"role": "user", "content": "Two-sentence summary of TCP vs UDP."}])
print(r["message"]["content"])
  1. Housekeeping commands you'll want: ollama list (what's on disk), ollama ps (what's loaded in memory), ollama rm <model> (delete).

If it breaks: "Connection refused" on the curl means the server isn't running — start ollama serve or relaunch the desktop app. A model that loads but crawls (multiple seconds per word) is too big for your RAM and is swapping to disk; drop to the next size down. On Windows, if ollama isn't found in a new terminal, close and reopen the terminal so PATH updates take.

You know it worked when: you can turn off Wi-Fi, run ollama run llama3.2:3b, ask it a question, and get an answer. That moment — a working model with the network off — is the entire point of this exercise.

Where to go next: Try a coding model (qwen2.5-coder:7b) against your daily coding questions. Put a UI on it with Open WebUI. Then run How-To 6 to measure honestly how your local model compares to a frontier model on your tasks — the answer is more nuanced than either camp will tell you.


How-To 2: Build your first RAG app

What you'll build: A script that answers questions about your documents by retrieving relevant chunks and feeding them to a model — retrieval-augmented generation, the architecture behind almost every "chat with your data" product.

Prerequisites: Python 3.10+, Ollama installed (How-To 1), and a folder of a few .txt or .md files you want to query. ~45 minutes.

Steps:

  1. Install dependencies and pull two models — one to embed, one to generate:
pip install chromadb ollama
ollama pull nomic-embed-text
ollama pull llama3.2:3b
  1. Understand the four moves before writing them. Embed: turn text into vectors — lists of numbers where similar meaning lands near similar numbers. Store: keep those vectors in a database built for nearest-neighbor lookups. Retrieve: embed the user's question, find the closest stored chunks. Generate: hand question + chunks to the model and let it answer from the chunks. That's RAG. Everything else is refinement.
  1. Create rag.py — ingestion first:
import os, glob
import chromadb
import ollama

client = chromadb.PersistentClient(path="./rag_db")
collection = client.get_or_create_collection("docs")

def chunk(text, size=800, overlap=150):
    chunks, i = [], 0
    while i < len(text):
        chunks.append(text[i:i + size])
        i += size - overlap
    return chunks

def ingest(folder):
    for path in glob.glob(os.path.join(folder, "*.txt")) + \
                glob.glob(os.path.join(folder, "*.md")):
        text = open(path, encoding="utf-8").read()
        for n, c in enumerate(chunk(text)):
            emb = ollama.embed(model="nomic-embed-text", input=c)["embeddings"][0]
            collection.add(ids=[f"{path}-{n}"], embeddings=[emb],
                           documents=[c], metadatas=[{"source": path}])
        print(f"ingested {path}")

Chunking with overlap matters: no overlap, and answers that straddle a chunk boundary get cut in half.

  1. Add retrieval and generation to the same file:
def ask(question, k=4):
    q_emb = ollama.embed(model="nomic-embed-text", input=question)["embeddings"][0]
    res = collection.query(query_embeddings=[q_emb], n_results=k)
    context = "\n\n---\n\n".join(res["documents"][0])
    sources = {m["source"] for m in res["metadatas"][0]}

    prompt = f"""Answer using ONLY the context below.
If the context doesn't contain the answer, say "Not in the documents."

CONTEXT:
{context}

QUESTION: {question}"""

    r = ollama.chat(model="llama3.2:3b",
                    messages=[{"role": "user", "content": prompt}])
    return r["message"]["content"], sources

if __name__ == "__main__":
    import sys
    if sys.argv[1] == "ingest":
        ingest(sys.argv[2])
    else:
        answer, sources = ask(" ".join(sys.argv[1:]))
        print(answer)
        print("\nSources:", *sources)

The "ONLY the context" instruction plus a permitted "Not in the documents" answer is your anti-hallucination seatbelt. It is not perfect. It is much better than nothing.

  1. Run it:
python rag.py ingest ./my_docs
python rag.py "What does the Q3 report say about hiring?"
  1. Test the seatbelt: ask something your documents definitely don't cover. You want "Not in the documents," not a confident invention.

If it breaks: Wrong or irrelevant answers almost always mean retrieval failed, not generation — print res["documents"][0] inside ask() and look at what actually came back. If the right chunk isn't in the top 4, your chunks are too big (the signal is diluted) or too small (the answer is split); tune size and re-ingest. If ingestion errors on a file, it's usually encoding — add errors="ignore" to the open() call and move on. A ChromaDB "dimension mismatch" means you changed embedding models mid-stream; delete ./rag_db and re-ingest, and remember the rule it just taught you: the embedder that stores must be the embedder that queries.

You know it worked when: the app correctly answers a question whose answer exists only in your documents — something the base model could not possibly know — and cites the right source file. And when it declines the question from step 6.

Where to go next: The quality levers, in rough order of payoff: better chunking (split on headings/paragraphs, not raw character counts), retrieving more candidates then re-ranking, a bigger generation model, and adding metadata filters (by date, by document type). When you outgrow this script, look at PDF parsing next — most real corpora aren't clean text files.


How-To 3: Set up Claude Code and work agentically

What you'll build: A terminal-based AI agent that can read your codebase, edit files, and run commands — with a project memory file and an MCP server extending what it can touch.

Prerequisites: A terminal, Node.js 18+ ⚑ unverified, a Claude subscription (Pro or Max) or Anthropic API key, and a real project directory to work in. ~30 minutes.

Steps:

  1. Install and authenticate:
npm install -g @anthropic-ai/claude-code
cd ~/your-project
claude

First run walks you through login in the browser. When you land at the prompt, you're in.

  1. Understand what changed. Chat AI answers questions about code you paste. Agentic AI acts: it reads files itself, edits them, runs your tests, reads the failures, and edits again. The loop — act, observe, correct — is the difference in kind. It's also why the permission prompts exist: the tool asks before editing files or running commands. Read those prompts for the first week; say yes deliberately, not reflexively.
  1. First task — pick something real but small:
Look at this project and tell me how it's structured. Then find any TODO
comments and list them with file paths.

Read-only, so no permissions needed. Watch it explore. Then a real one:

Add input validation to the signup handler. Reject malformed emails with
a 400. Follow the error-handling style already used in this file. Run the
existing tests when done.

Note what that prompt contains: a specific change, a defined behavior, a style anchor, and a verification step. That's the anatomy of a good agent task.

  1. Create the project memory file. Run /init inside Claude Code and it drafts a CLAUDE.md from your codebase — or write one by hand at the project root:
# CLAUDE.md

## Project
Flask API for invoice processing. Postgres via SQLAlchemy. Tests in /tests.

## Commands
- Run tests: pytest -x
- Lint: ruff check .

## Rules
- Never edit anything in /migrations
- All new endpoints need a test
- Use the existing error classes in app/errors.py, don't invent new ones

This file loads automatically at every session start. It's the difference between a contractor you re-brief every morning and one who remembers the house rules. Every recurring correction you find yourself typing belongs in this file.

  1. Add an MCP server. MCP (Model Context Protocol) is the plug standard for giving the agent new capabilities — databases, browsers, APIs. Example, a filesystem server scoped to a docs directory:
claude mcp add docs-fs -- npx -y @modelcontextprotocol/server-filesystem ~/my-docs

Restart Claude Code, then ask something that requires the new capability ("read my-docs/roadmap.md and compare it to what this repo actually implements"). claude mcp list shows what's connected. The same mechanism connects Postgres, GitHub, Slack, browsers — one protocol, hundreds of servers ⚑ unverified.

  1. Learn the three commands that matter early: /clear (wipe context between unrelated tasks — context bleed causes weird behavior), /compact (summarize a long session to reclaim context), and Esc (interrupt an agent that's headed somewhere wrong — you don't have to let it finish).

If it breaks: The most common early failure isn't technical — it's scope. An agent given "clean up this codebase" will roam; an agent given "rename these three functions and update their call sites" will finish. If it edited something it shouldn't have, that's a missing rule for CLAUDE.md, and git diff plus git checkout -- <file> is your undo (work in a git repo; the agent's safety net is version control, same as yours). If an MCP server doesn't appear after adding, check claude mcp list for an error state and confirm the underlying npx command runs on its own.

You know it worked when: Claude Code completes a multi-file change in your project — edit, test run, fix, green — with you approving steps rather than performing them. And when a new session, unprompted, follows a rule that exists only in your CLAUDE.md.

Where to go next: Custom slash commands (reusable prompt files in .claude/commands/), headless mode (claude -p "..." in scripts and CI), and hooks for automation. The deeper skill is task-shaping: agents excel at well-specified changes with a verification step, and flail at "make it better." Write tasks like step 3's, get results like step 3's. Cursor and GitHub Copilot offer overlapping agentic modes in an IDE if you want a comparison point.


How-To 4: Build a custom GPT or Claude Project for your team docs

What you'll build: A shared assistant that knows your team's documents — policies, runbooks, product specs — and answers from them, with instructions tuned to your team's needs. Two routes: OpenAI custom GPT or Claude Project. Same architecture, pick per your existing subscription.

Prerequisites: A paid plan (ChatGPT Plus/Team, or Claude Pro/Team — sharing with teammates requires the Team tiers on both ⚑ unverified), and your documents gathered as PDF, DOCX, TXT, or MD. ~30 minutes, most of it document curation.

Steps:

  1. Curate before you upload — this determines most of the outcome. Pick the 5-15 documents that answer your team's actual recurring questions. Kill duplicates and dead versions first: the assistant cannot tell the 2023 policy from the 2025 policy unless you delete one or say so. Garbage in still applies; it just answers politely now.
  1. Claude route: In the Claude app, Projects → New Project. Name it for its job ("Support Runbook Assistant," not "Team AI"). Upload documents to Project knowledge. OpenAI route: In ChatGPT, Explore GPTs → Create, then use Configure directly (skip the conversational builder — you'll want the precision). Upload files under Knowledge.
  1. Write the instructions. This is the same skill for both platforms, and it's worth ten minutes of care:
You answer questions for [TEAM] using the attached documents.

Rules:
- Answer from the documents. When you use one, name it ("per the Refund
  Policy v3..."). If the documents don't cover the question, say so
  plainly and suggest who to ask instead: [NAME/CHANNEL].
- If two documents conflict, quote both and flag the conflict. Do not
  silently pick one.
- Style: direct, short answers first, detail only on request.
- Common question shortcuts: "escalation" → escalation-matrix.md;
  "pricing exceptions" → discount-policy.pdf section 4.
- Never invent policy. A wrong-but-confident answer about [SENSITIVE
  AREA, e.g., refunds or HR] is the worst possible outcome.

The conflict rule and the "who to ask instead" fallback are what separate a useful team tool from a liability generator.

  1. Test adversarially before sharing. Ask: three questions the docs clearly answer (check the citations are right), one question the docs don't answer (must decline, not improvise), and one question where you know two docs disagree (must surface the conflict). If it fails, fix the instructions or the documents — usually the documents.
  1. Share it. Claude: invite teammates to the Project (Team plan). OpenAI: publish the GPT to your workspace, or "anyone with the link" — think before choosing the latter; your uploaded knowledge is effectively exposed through determined questioning, so nothing confidential goes in a link-shared assistant.
  1. Assign an owner and a revision habit. Documents rot. A quarterly calendar slot — replace stale files, re-run the step 4 test — is the entire maintenance plan, and skipping it is how these tools quietly become wrong.

You know it worked when: a teammate asks a real question, gets a correct answer citing the right document, without pinging the person who used to be the human FAQ. And when the off-topic question from step 4 gets a "that's not in my documents" instead of fan fiction.

Where to go next: When uploads-in-a-box stops scaling — too many docs, or you need live data — the next step is real RAG over your document store (How-To 2 is the miniature version) or an MCP connection to the systems where the truth actually lives. This how-to is the low-code on-ramp to that architecture.


How-To 5: Add AI to a spreadsheet workflow

What you'll build: A spreadsheet where a model processes rows automatically — categorize, extract, summarize, draft — turning a column of raw text into a column of structured output. The pattern behind half of real-world office automation.

Prerequisites: Google Sheets, an OpenAI or Anthropic API key (a real API key, not a chat subscription — different product, pay-per-use, a few dollars covers this whole exercise ⚑ unverified). ~40 minutes.

Steps:

  1. Pick a real column-shaped problem. Good fits: expense descriptions → categories; support tickets → topic + sentiment; job postings → extracted salary ranges; feedback comments → one-line summaries. The pattern: same instruction, applied per row, hundreds of times. That repetition is exactly what you're automating.
  1. Open your sheet → Extensions → Apps Script, and paste:
const API_KEY = PropertiesService.getScriptProperties().getProperty('ANTHROPIC_KEY');

function AI(instruction, input) {
  if (!input) return "";
  const payload = {
    model: "claude-3-5-haiku-latest",   // small + cheap; fine for row work
    max_tokens: 300,
    messages: [{
      role: "user",
      content: instruction + "\n\nINPUT:\n" + input +
               "\n\nReply with the answer only. No explanation."
    }]
  };
  const res = UrlFetchApp.fetch("https://api.anthropic.com/v1/messages", {
    method: "post",
    headers: { "x-api-key": API_KEY, "anthropic-version": "2023-06-01" },
    contentType: "application/json",
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  });
  const data = JSON.parse(res.getContentText());
  return data.content ? data.content[0].text.trim() : "ERROR: " + res.getContentText().slice(0, 120);
}

⚑ unverified

  1. Store the key properly — never in the sheet or the code. In Apps Script: Project Settings → Script Properties → add ANTHROPIC_KEY with your key. Anyone you share the sheet with can read formulas; script properties stay server-side.
  1. Use it as a formula. If your raw text is in column A:
=AI("Categorize this expense as one of: Travel, Software, Meals, Office, Other. One word only.", A2)

Drag down. Each cell makes one API call. For a few hundred rows this is fine; for thousands, you'd batch (step 6).

  1. Make the instruction strict, because spreadsheets need consistency more than brilliance. Enumerate the allowed outputs ("one of: X, Y, Z"), demand "one word only" or "format: YYYY-MM-DD", and give the model an out: "If the input is ambiguous, output UNCLEAR." Then filter for UNCLEAR and handle those twenty rows by hand — that division of labor is the whole game.
  1. Two production notes. Caching: Sheets recalculates formulas, which re-calls the API and re-bills you; once a column is done, copy → Paste special → values only. Rate limits: if you see errors on a big drag-down, add Utilities.sleep(200) before the fetch, or process in chunks.

If it breaks: #NAME? means Apps Script didn't save or you have a typo in the formula name. An ERROR cell containing "authentication" means the script property key name doesn't match the code or the key itself is wrong. Cells stuck on "Loading..." for minutes usually means you dragged over hundreds of rows at once — Sheets throttles custom functions; do 50-row batches. And if outputs drift from your allowed categories ("Meals & Entertainment" when you said "Meals"), tighten the instruction: models treat category lists as suggestions unless you add "exactly one of, verbatim."

You know it worked when: you drag the formula down 100 rows, watch the categories fill in, spot-check 15 of them and find at most one you'd dispute — and the ambiguous rows say UNCLEAR instead of guessing. Then you freeze the values and the job that used to eat an afternoon is a formula.

Pro alternative routes: No-code versions of this exist — Zapier and Make both chain "new row → AI step → write back," and Google's built-in Gemini functions ⚑ unverified are creeping into Sheets natively. The Apps Script route costs an hour once and gives you exact control of model, prompt, and spend.

Where to go next: The same fetch-call pattern works in Excel via Office Scripts, in Airtable via its scripting block, and anywhere else that runs JavaScript. When a workflow outgrows a sheet — needs multiple steps, approvals, or real volume — that's the moment to graduate to a small Python script with a queue, not a bigger spreadsheet.


How-To 6: Evaluate two models on YOUR task in 30 minutes

What you'll build: A mini eval harness — a script that runs the same test cases through two models, blinds the outputs, and lets you grade honestly. Ends the "which model is better" debate with data about your task instead of vibes about benchmarks.

Prerequisites: Python 3.10+, and any two models you can call — the example uses two local Ollama models; the swap-in for APIs is noted in step 5. A real task you actually use AI for. 30 minutes.

Steps:

  1. Write 10 test cases from your real work — actual emails you've summarized, actual functions you've asked about, actual questions from your domain. Ten real cases beat a hundred synthetic ones ⚑ unverified. Save as cases.jsonl:
{"id": 1, "prompt": "Summarize in 2 sentences: [REAL EMAIL TEXT]"}
{"id": 2, "prompt": "Extract the deadline and owner from: [REAL TEXT]"}
{"id": 3, "prompt": "Explain this error: [REAL ERROR YOU HIT]"}

Include at least two hard cases and one where you know the right answer cold — that one calibrates your grading.

  1. Create eval.py:
import json, random, time
import ollama

MODEL_A = "llama3.2:3b"
MODEL_B = "qwen2.5:7b"

def run(model, prompt):
    t0 = time.time()
    r = ollama.chat(model=model, messages=[{"role": "user", "content": prompt}])
    return r["message"]["content"], round(time.time() - t0, 1)

results = []
for line in open("cases.jsonl"):
    case = json.loads(line)
    out_a, t_a = run(MODEL_A, case["prompt"])
    out_b, t_b = run(MODEL_B, case["prompt"])
    # Blind: shuffle which model is shown first
    pair = [("A", MODEL_A, out_a, t_a), ("B", MODEL_B, out_b, t_b)]
    random.shuffle(pair)
    results.append({"case": case, "first": pair[0], "second": pair[1]})
    print(f"case {case['id']} done")

json.dump(results, open("results.json", "w"), indent=2)
  1. The shuffle is not decoration. If you know which output came from the model you already prefer, you will grade it kindly — everyone does. Blinding is the single feature separating an eval from a ritual.
  1. Grade. Create grade.py:
import json

results = json.load(open("results.json"))
scores = {}
for r in results:
    print("\n" + "=" * 60)
    print("PROMPT:", r["case"]["prompt"][:200])
    print("\n--- OUTPUT 1 ---\n", r["first"][2])
    print("\n--- OUTPUT 2 ---\n", r["second"][2])
    pick = input("\nBetter output? (1/2/tie): ").strip()
    winner = r["first"][1] if pick == "1" else r["second"][1] if pick == "2" else "tie"
    scores[winner] = scores.get(winner, 0) + 1

print("\nFINAL:", scores)

Run python grade.py and judge each pair on one question: which would I actually use? Not which is longer or more polite — which does the job.

  1. Read the result like an adult. 7-3 with ten cases is signal; 6-4 is noise — call it a tie or add ten more cases. Check the timing numbers in results.json too: a model that's slightly worse but three times faster wins many real workflows. To eval an API model against a local one, replace one run() body with an API call (Anthropic or OpenAI SDK, ~6 lines) — now you're measuring the actual trade: quality vs. cost vs. privacy, on your task.
  1. Keep cases.jsonl. When the next model ships, you re-run two scripts and know in half an hour whether it matters for you — while everyone else argues about benchmark charts. A private eval set is the single most reusable AI asset a practitioner can own.

If it breaks: If every case is a tie, your cases are too easy — add harder ones until the models separate. If grading feels arbitrary, you skipped defining what "better" means for your task; write one sentence per case type ("better = correct deadline extracted, nothing invented") before grading. And resist the urge to peek at results.json to see which model is which mid-grading — the moment you unblind, the eval is over.

You know it worked when: you have a score you'd bet on — and at least once, the blinded grading surprised you by favoring the model you expected to lose. That surprise is proof the harness is honest.

Where to go next: Scale the graded set with an LLM-judge (a third model grades against a rubric — see the rubric meta-prompt in the Prompt Library), add per-case categories so you can see where each model wins, and version your cases file in git. Congratulations: you now do evals, which puts you ahead of most teams shipping AI features ⚑ unverified.


The pattern behind all six

Notice what these builds share. Every one puts a verification step at the end — the model's output is checked, not trusted. Every one starts smaller than you think — one model, ten cases, one column, five documents. And every one produces a reusable asset: a local setup, a RAG script, a CLAUDE.md, a team assistant, a formula, an eval set. That's the difference between using AI and building with it: builders accumulate assets; users accumulate chat history.

Part of Tech Stack → Playbook. The Prompt Library covers what to say to these systems once they're running.

Some links on this page are affiliate links. If you sign up or buy through one, TheCatch.AI earns a commission at no extra cost to you. We list what we would use; the commission never decides the ranking, and nothing in Bubble Watch or Economics carries an affiliate link — analysis stays clean.