Tutorials Builder
How to build a personal AI agent for daily productivity (note and task triage)
Build an agent that triages your notes into tasks, references, and archives — runnable today with zero API keys, upgradeable to any LLM, and scalable from personal to team use.
The gateway drug of agentic AI isn’t a chatbot — it’s the first piece of your own recurring drudgery an agent takes over. For most people that’s triage: the pile of notes, saved links, and half-thoughts that accumulate faster than they get sorted. We’ll build a triage agent you can run today, then talk about the honest path from personal script to team tool.
Step 1 — The shape of a productivity agent
Every useful productivity agent has the same three parts:
- A collection point it drains (a folder, an inbox, a channel).
- A brain that decides what each item is and where it goes.
- A digest so you trust it without re-checking its work.
That third part is what separates agents people keep using from agents people quietly abandon. Never skip the digest.
Step 2 — Build it
Create triage.py. As always on this site, the brain starts as a
deterministic stand-in so the system works before any API key exists:
import shutil
from datetime import date
from pathlib import Path
INBOX = Path("inbox")
DESTINATIONS = {"task": "tasks", "reference": "reference", "archive": "archive"}
def classify(text: str) -> tuple[str, str]:
"""The brain. Swap this one function for an LLM call later.
Returns (category, one_line_summary)."""
lowered = text.lower()
first_line = text.strip().splitlines()[0][:80] if text.strip() else "(empty note)"
if any(k in lowered for k in ("todo", "deadline", "remind", "by friday", "must ")):
return "task", first_line
if any(k in lowered for k in ("how to", "docs", "reference", "recipe", "https://")):
return "reference", first_line
return "archive", first_line
def triage() -> str:
moves: list[tuple[str, str, str]] = []
for note in sorted(INBOX.glob("*.md")) + sorted(INBOX.glob("*.txt")):
category, summary = classify(note.read_text(encoding="utf-8"))
dest = Path(DESTINATIONS[category])
dest.mkdir(exist_ok=True)
shutil.move(str(note), dest / note.name)
moves.append((note.name, category, summary))
lines = [f"# Triage digest — {date.today().isoformat()}", ""]
if not moves:
lines.append("Inbox was empty. Nothing to do.")
for name, category, summary in moves:
lines.append(f"- **{name}** → `{DESTINATIONS[category]}/` — {summary}")
digest = "\n".join(lines) + "\n"
Path("digest.md").write_text(digest, encoding="utf-8")
return digest
if __name__ == "__main__":
print(triage())
Seed it and run:
mkdir -p inbox
echo "TODO: renew passport, deadline is Friday" > inbox/passport.md
echo "How to tune pgvector indexes: https://example.com/guide" > inbox/pgvector.md
echo "random shower thought about hexagons" > inbox/thought.txt
python triage.py
Three notes leave inbox/, land in tasks/, reference/, and
archive/, and digest.md tells you exactly what happened. The whole
loop — collect, decide, act, report — is running.
Step 3 — Give it a real brain
classify() is the only thing that changes. With any LLM — Claude, GPT,
Gemini, or a local model behind an
OpenAI-compatible endpoint — the swap looks like:
Prompt: You are a note-triage assistant. Classify the note into exactly
one of: task | reference | archive. Reply as JSON:
{"category": "...", "summary": "<one line, max 80 chars>"}
Note:
<note text>
Keep the contract identical (category + summary), validate the JSON, and
fall back to archive on any parse failure — a triage agent that
mis-files into archive is annoying; one that crashes at 6 a.m. is dead.
Run it on a schedule (cron, Task Scheduler) and the drudgery is gone.
Step 4 — Extend it toward real life
Each extension is one more tool, same skeleton: a calendar category
that drafts (never sends) an invite file; a collection point that pulls
from your email’s export folder; a weekly roll-up that summarizes the
digests. When the agent needs to reach real apps — your task manager, your
email — that’s exactly the REST-to-MCP wrapper
pattern, which keeps credentials out
of the brain entirely.
Step 5 — From personal to team (the honest path)
The jump from “my script” to “our agent” is bigger than it looks: your teammates’ notes are data you now process, mistakes become visible, and “works on my machine” stops being acceptable. The rule of thumb: a personal agent needs a digest; a team agent needs a digest, an owner, and an undo. The full progression — identity, secrets, review gates, cost — is the adoption ladder piece.
Troubleshooting
Everything lands in archive/
With the keyword brain, that’s expected for notes without trigger words — it’s the safe default, not a bug. With an LLM brain, check your JSON validation isn’t silently falling back on every note (log parse failures — a 100% fallback rate means your prompt or extraction is broken).
shutil.Error: Destination path ... already exists
You re-ran triage with a filename that was already filed. Dedupe on
move — append a timestamp to the name (passport-20260705.md) —
and the agent becomes safely re-runnable.
Was this guide useful?
Thanks — noted. It shapes what gets written next.
newsletter
One practical agentic-AI guide in your inbox. No news, no hype.
Tutorials and decision frameworks as they ship. Unsubscribe anytime.