hubagenticai

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.

updated 2026-07-05 ⏱ 40 min

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:

  1. A collection point it drains (a folder, an inbox, a channel).
  2. A brain that decides what each item is and where it goes.
  3. 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.

newsletter

One practical agentic-AI guide in your inbox. No news, no hype.

Tutorials and decision frameworks as they ship. Unsubscribe anytime.