AI-powered LinkedIn outreach message generator
Project description
tmessage
A CLI tool that generates personalized LinkedIn outreach messages using AI. Built for sales and account teams who need to send high-quality, contextually relevant messages at scale — without spending 10 minutes researching each person manually.
Write-up
Demo
Loom walkthrough link here
Installation
Requirements
Install via Homebrew
brew install pipx
pipx install tmessage-cli
First run
On first run, tmessage will prompt you to enter your API keys. These are saved to ~/.config/tmessage/.env and reused on every subsequent run — you only need to do this once.
Usage
tmessage
The tool will prompt you for:
- Name — the person's first and last name
- LinkedIn description — paste the job description section from their LinkedIn profile, then press
Ctrl+Dwhen done
tmessage then runs a three-stage pipeline and outputs a ready-to-send LinkedIn message. Press y when prompted to generate another message, or n to exit.
How it works
Pipeline overview
LinkedIn description paste
│
▼
┌─────────────────────┐
│ Stage 1: Extraction │ Qwen3-30B
│ company, title, │
│ key_facts, │
│ persona_inference │
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ Stage 2: Research │ Tavily Search
│ title relevance │
│ + company search │
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ Stage 3: Message │ DeepSeek-V3.2
│ generation │
└─────────────────────┘
Stage 1 — Person extraction
The user pastes the raw LinkedIn job description block. A fast, lightweight model (Qwen3-30B) extracts structured information:
company_name— extracted from the pasted textjob_title— extracted from the pasted textkey_facts— a list of concrete responsibilities and activities, close to paraphrasing. Each fact describes what the person does, not how they do it.persona_inference— a list of inferred characteristics about how this person operates (e.g. hands-on, technically engaged, builds from scratch). Critically, this list is only populated when the inference is directly supported by specific language in the source text. If nothing is confidently inferable, this returns an empty list.
Design decision: the persona_inference constraint — preferring honest-but-generic over confident-but-wrong — is a core principle of the tool. A message that admits it doesn't know much about someone is better than one that fabricates detail and sounds off.
Stage 2 — Company research
The job title is first distilled to its most AI/tech-relevant component (e.g. "VP of Generative AI Marketing" → "Generative AI"). This focused query is then used to search Tavily for recent, role-specific company news and initiatives — scoped to the last 365 days to keep results current.
Design decision: searching by distilled title rather than full title produces more relevant results. A full title like "Vice President & Global Head of Generative AI // Marketing Transformation Office" retrieves noisier results than "Generative AI."
Stage 3 — Message generation
A larger, higher-quality model (DeepSeek-V3.2-fast) writes the final message using:
- Structured person context from Stage 1
- Tavily search results from Stage 2
- A library of real example messages to match tone and style
The model follows explicit rules: only reference facts supported by the provided context, don't fabricate a Tavily use case if one isn't evident, and write under 100 words with a low-pressure ask. If search results aren't relevant, it falls back to a generic message rather than forcing a connection that isn't there.
Design decision: two separate models for extraction vs. message generation. Extraction is a structured, deterministic task — a smaller, faster model handles it well. Message generation requires more nuance and writing quality, so a stronger model is used only where it matters.
Design philosophy
- Prefer honest-but-generic over confident-but-wrong. The tool will not fabricate relevance or invent personal detail. A generic message is better than a weird one.
- Minimize user input. The user provides a name and a LinkedIn paste — nothing else. All structure is inferred.
- Degrade gracefully. If search returns nothing useful, the tool still produces a message. It doesn't stall or crash.
- Speed over completeness where possible. Smaller models are used for simpler tasks to keep the pipeline fast enough for batches of 50+ people.
Tech stack
- LangChain — LLM orchestration
- Nebius — model hosting (Qwen3-30B, DeepSeek-V3.2-fast)
- Tavily — web search API
- Typer — CLI framework
- Rich — terminal formatting
Project structure
tmessage/
├── tmessage/
│ ├── __init__.py # main pipeline and CLI entrypoint
│ └── example_messages.txt # message style templates
├── pyproject.toml
├── requirements.txt
└── README.md
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tmessage_cli-0.1.2.tar.gz.
File metadata
- Download URL: tmessage_cli-0.1.2.tar.gz
- Upload date:
- Size: 12.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
46aa8fbd9d873c08fec5a6b3b1358980ae0a63276972ddf3177377a161e45178
|
|
| MD5 |
de81ac95806c7a3403d8d3e9d35116ca
|
|
| BLAKE2b-256 |
c4bdd64eaac196c4c44243683e0d70997cb9faa968128bd8170188bd286cb058
|
File details
Details for the file tmessage_cli-0.1.2-py3-none-any.whl.
File metadata
- Download URL: tmessage_cli-0.1.2-py3-none-any.whl
- Upload date:
- Size: 11.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
882b2606a139961bf44b07b2eb1a82b67dda3a522a05ba1affd16ec73ce8dad6
|
|
| MD5 |
61f7abc04c9595178ff35c31cca1eacc
|
|
| BLAKE2b-256 |
5a3c628b3e1def4d17e51300fb5390abcffec91020a1795127e4aec9f96c8b38
|