macOS · Python · CLI · LangChain · Tavily

tmessage

A CLI tool that generates personalized LinkedIn outreach messages. Paste a job description, get a grounded message in under a minute.

Install
$brew install pipx $pipx install tmessage-cli $tmessage
The Problem

I spent too long writing LinkedIn messages

The tradeoff is brutal — send a generic message in 30 seconds, or spend 10 minutes researching someone and write something actually personalized. Neither felt right.

I wanted a CLI tool that could do both: research the person, understand the company, and write a grounded message that doesn't sound like it came from a template.

Design

Starting with the right decisions

The obvious first idea was: take a LinkedIn URL, scrape the profile, research the company, generate a message. Clean. Simple. Wrong.

LinkedIn aggressively blocks scraping. So I made the first real design decision: the user pastes the job description directly. One copy-paste, and the tool gets far better signal than any scraper could return reliably.

Don't make the agent guess at something the user already has in front of them.

For users without a job description, the tool degrades gracefully across three tiers:

Tier 1
Job description provided → extract directly from text
specific
Tier 2
No description → search Tavily for the person
grounded
Pipeline

Three stages, two models

Stage 1
Extraction
Parses the job description into key_facts (grounded responsibilities) and persona_inference (only when directly supported by text — empty list is fine). Also generates a disambiguated search_query.
Qwen3-30B
Stage 2
Research
Searches Tavily using the disambiguated query + distilled title. Scoped to the last 365 days for recency.
Tavily
Stage 3
Generation
Writes the message from structured context + search results + example templates. An eval loop scores the output and regenerates if it fails — up to 3 retries.
Llama-3.3-70B

Extraction and evaluation use a smaller, faster model. Message generation uses a larger one — only where writing quality actually matters.

Bug

The wrong Greenlight

The most embarrassing bug in testing: I ran the tool on someone who worked at Greenlight — the fintech app that teaches kids about money. The tool came back with a message about AI-ready data center infrastructure.

It had found Greenlight Data Centers. A completely different company.

query: "Greenlight Senior ML Engineer"
✗ Greenlight Data Centers — AI-ready infrastructure provider
✓ Greenlight — fintech app for kids and families

The root cause was the search query. Naive {company} {title} was ambiguous enough to pull the wrong result. The fix: have the extraction model generate a disambiguated query as part of its output — "Greenlight fintech kids finance app" instead of just "Greenlight". One extra field, dramatically better retrieval.

Evaluation Loop

The evaluator kept failing the wrong things

After generating a message, a second model call scores it against a checklist: under 100 words, no buzzwords, low-pressure ask, claims grounded in context, no fabricated Tavily use cases. Fails → regenerate with critique, up to 3 retries.

Calibrating this was the most iterative part. The first version kept flagging messages for mentioning Tavily — reasoning it wasn't in the search results, so any reference was "unsupported." Which is technically true and completely misses the point.

Tavily is the product being pitched. It's always going to be in the message. Only flag specific claims about the prospect's stack that aren't grounded in context.

After that calibration, the loop started catching the right things — like when the generator implied a prospect was using "search and retrieval layers" with no evidence, or wrote about data center infrastructure for someone at a fintech app.

Reflection

What I learned

01

Match the model to the task

Using a large model for everything is the lazy path. Smaller models for structured tasks, larger only where writing quality matters.

02

Retrieval is a context engineering problem

The wrong Greenlight bug wasn't a Tavily problem — it was a query problem. The information to fix it was already in the job description.

03

Evaluators need to understand the task

A checklist without intent will flag the wrong things. The evaluator needed to know that Tavily mentions are always valid — that took iteration.

04

Honest-but-generic beats confident-but-wrong

Empty persona inference list. Generic fallback message. A weird, confident hallucination is worse than a forgettable generic one.