Inspiration

Like most students applying for internships and jobs, I was sending out CVs constantly and losing track of what happened to them. Some companies replied fast, some replied months later, and most never replied at all. I found myself manually reopening my inbox again and again just to check for a "sorry, we decided to go with another candidate" — and even when I found one, I had to go update a spreadsheet by hand. That manual loop was the actual problem I wanted to kill: I never want to have to check "did I get rejected?" by hand again.

What it does

It's an autonomous job-application agent, built as a Streamlit app, that:

Sends your original, unmodified CV to a list of recipients with one click — the exact file you uploaded, never rewritten or "tailored," so what you see is exactly what the company receives. Automatically checks Gmail (IMAP) for replies to pending applications and flips the status to 🟢 Accepted or 🔴 Rejected based on the actual content of the reply — no manual work required. Treats total silence past a set number of days as a silent rejection too, since ghosting is itself an answer. Keeps a live tracking dashboard (total / accepted / pending / rejected) with manual override only as a safety net. Includes a real conversational assistant — powered by the Strands Agents SDK — that can search the web for companies/openings, draft a spontaneous application email tailored to a sector, or just have a normal conversation, instead of a rigid keyword-matched chatbot. How we built it Streamlit for the UI (CV upload, sidebar quick actions, live tracking table, chat). Strands Agents SDK for the conversational layer, with tool-calling so the LLM can decide to call search_web, draft_spontaneous_message, or check_replies instead of just replying with static text. Ollama as the local LLM backend for the agent, keeping everything free and privacy-friendly (no data leaves the machine for the chat part). ddgs for free, no-API-key web search to find companies and postings. Plain smtplib/imaplib for sending the CV by email and reading Gmail replies — no third-party email API needed. A simple JSON log as the tracking database, easy to inspect and back up. Challenges we ran into

Almost every real bug only showed up once I actually tested it against my own inbox, not in theory:

IMAP's SINCE filter is day-level, not time-level. A reply I'd received earlier the same day I sent a new application was still being picked up as if it were a response to that new application — causing a false "rejected" status one minute after sending. Fixed by re-checking the message's real timestamp in Python against the exact send time. Domain-based matching was too broad for public webmail. Matching replies by @gmail.com (or Yahoo/Outlook) meant matching literally any email from anyone using that provider, not just the company. Had to fall back to exact-address matching for public domains and only trust domain-wide matching for a company's own custom domain. Deciding to stop tailoring the CV. Early on the plan was to auto-rewrite the "Profile" section of the PDF per job offer using pdfplumber + reportlab overlays. It technically worked, but it meant the CV a company received was never exactly what I'd uploaded — which felt dishonest and fragile (layout detection could fail). Removing all of that in favor of always sending the original file, untouched, made the whole product simpler and more trustworthy. Making the chat feel like an assistant, not a form. The first version was a pure if/elif keyword router — it couldn't handle a simple "hi" or a correction like "you're wrong" without falling back to a canned help message. Rebuilding it around Strands' tool-calling with full conversation history fixed that while keeping the high-stakes actions (actually sending emails) on a separate, deterministic path for reliability. Accomplishments that we're proud of

Turning something that used to be a genuinely annoying manual chore — a nightly ritual of checking my inbox and updating a spreadsheet — into something that runs quietly in the background and only asks for my attention when there's a real update: an interview, a rejection, or nothing at all.

What we learned How to design an agent around tool-calling rather than hardcoded logic, and where it's still worth keeping a deterministic path for actions that must never fail silently (like actually sending an email). How much nuance is hidden inside something as "simple" as reading email replies automatically — timezone/date granularity, sender-address ambiguity, and keyword ambiguity all matter more than expected. That removing a feature (automatic CV tailoring) can be a bigger improvement than adding one, when it makes the product simpler and more honest. What's next Move the LLM backend from local Ollama to Amazon Bedrock via Strands for easier deployment and no local model requirement. Aggregate anonymized reply data across users (average response time, rejection/silence rate per company) to help candidates prioritize where they apply — without ever exposing anyone's individual emails.

Built With

  • ddgs
  • gmail
  • google-imap
  • imaplib
  • json
  • llama3.2
  • ollama
  • pandas
  • pypdf
  • python
  • smtplib
  • strands-agents
  • streamlit
  • streamlit-autorefresh
Share this project:

Updates

Submission history