Messaging
LeadHunter is a communication log, not a sending engine. You talk to the account through the channel you already use — Instagram, LinkedIn, X, email, WhatsApp, phone, SMS — then paste the exchange into LeadHunter so the AI, the scoring, and the team-wide conversation history all have full context.
This is intentional. Sending is hard to do well across every channel an outbound team actually uses, and LeadHunter would compete badly with the dedicated tool for each. Keeping LeadHunter out of the sending path means it’s compatible with every channel you’ll ever care about, including the ones that don’t exist yet.
The conversation thread
Section titled “The conversation thread”Each campaign-account (the join row between a campaign and an account — see Campaign → Campaign accounts) has its own conversation thread. Open any account in a campaign and you see the per-campaign timeline; open the account detail page instead and you see conversations aggregated across every campaign that account has been in, so the history follows the organisation even when the campaign changes.
The campaign-wide outreach inbox is a third view: open a campaign and switch to the Outreach tab to see every conversation across every account in that campaign, sorted by what needs your attention (recent inbound first, then drafts you haven’t sent yet).
What a message carries
Section titled “What a message carries”| Field | What it holds |
|---|---|
direction | outbound (you sent) or inbound (they sent). |
channel | Which surface the message lived on. Eight choices: email, linkedin, instagram, twitter_x, whatsapp, phone_call, sms, other. The per-message channel is what the cohort funnel attributes against later, so picking the right one matters. |
generation_mode | How the text was produced — one of five (see below). |
status | draft (scratch — doesn’t move the funnel) or sent (logged as out / in). |
original_text | Authoritative version: what the lead actually sent or received, in their language. |
original_language | ISO 639-1 code of original_text. |
translated_text | Optional reader-friendly view of the same content in another language. |
translated_to_language | ISO code of translated_text. |
sent_at | When the message was sent. For modes other than Log sent, defaults to “now”. For Log sent, you set it explicitly to when it really went out. |
created_by | The user who logged the message. |
| Audit | created_at, updated_at. |
There’s no separate “subject” or “thread id” — threading is implicit (every message attached to the same campaign-account belongs to the same thread).
The five drafting modes
Section titled “The five drafting modes”| Mode | Direction | Use when |
|---|---|---|
| AI draft | outbound | First outbound message in this thread. AI writes a fresh draft. |
| AI continue | outbound | Continuing a thread that already has history. AI reads the prior messages and proposes the next move. |
| Type & translate | outbound | You wrote the message yourself in your language. LeadHunter shows the translation into the account’s language and sends the translation. |
| Log sent | outbound | You already sent it elsewhere (your own email client, on LinkedIn directly, …). Paste the text to record it. |
| Inbound paste | inbound | The account replied. Paste their reply; LeadHunter shows you the translation into your reading language. |
How modes populate the two text fields
Section titled “How modes populate the two text fields”Knowing which mode wrote which field helps when re-reading old threads:
| Mode | original_text | translated_text |
|---|---|---|
| AI draft / AI continue / Log sent | The sent message (account’s language) | (empty) |
| Type & translate | The translated/sent message (account’s language) | Your original draft (your language) |
| Inbound paste | The account’s reply (their language) | Translation into your reading language |
The asymmetry on Type & translate is the one users miss most: original_text is the message that actually went out, not your scratchpad draft. If you go back six months and read a translated outbound, the version that left your hands is the canonical one.
AI drafting and translation cost
Section titled “AI drafting and translation cost”Every AI draft, AI continue, Type & translate, and Inbound paste message triggers an LLM call (translation in/out, prior-thread summarisation when there’s history). The cost is small per message — a fraction of a cent on Gemini Flash by default — but attributes per (Campaign, Account) under API costs. Log sent doesn’t call the LLM (you provide the final text directly), so it’s free.
If translation fails (rare; usually a transient provider error), the message is still saved with original_text only — translated_text stays empty and you can retry from the message panel.
Goal-aware drafting
Section titled “Goal-aware drafting”The drafter writes a different kind of message depending on the campaign’s goal. Same product, same account — but a sales campaign opens with a cold pitch, a press campaign opens with a newsworthy angle, a recruiting campaign asks the candidate for 15 minutes about a specific role, a partnership pitch proposes a collab shape, an investor approach leads with a one-line traction signal. The 0–10 scoring scale and the typed reasons stay consistent across goals; what shifts is the message’s voice and call-to-action.
Three things compose into the draft prompt, in order:
- Message intent — derived from
Campaign.goal. Reads as “write a cold sales opener — introduce the product, name one specific thing about the lead, end with a soft ask for a short call” for sales, “a short press pitch — newsworthy angle first, why-now hook, no sales language” for press, and so on for the eleven goals. - Target persona + product context — the (Product, Goal) ICP is rendered into the prompt so the model has the persona definition the campaign rates against.
- Account context + thread history — the account itself, plus the
role_in_campaignoverride if set (see below).
If the campaign’s goal changes mid-flight, the next draft you generate uses the new intent immediately. Existing already-drafted messages don’t auto-rewrite.
Per-campaign role override
Section titled “Per-campaign role override”Sometimes the same Account plays different roles in different campaigns. A SaaS company is your customer (sales), but their CTO is also a regular podcast guest (press), and the head of engineering recently spoke at your conference (event). Each campaign needs the message to read with the right voice.
Setting role_in_campaign on a CampaignAccount tells the drafter: “for this campaign, treat this account as <role> regardless of whatever primary tags it carries elsewhere.” The drafter respects the lens — a press campaign with role_in_campaign='press' produces a press pitch even when the account’s primary relationship_types say client. Blank means “use the account’s tags as-is.”
Setting it is a click in the campaign’s accounts panel: each row’s chip cluster shows a faint dashed + role affordance when no override is set, or a colored chip with the override label when one is set. Clicking opens a popover with all 13 relationship-type options — pick one to apply, or pick “Use the account’s tags (no override)” to clear.
This is the right escape hatch when one account legitimately wears multiple hats and you don’t want to blur them across campaigns.
DNC and per-purpose opt-outs
Section titled “DNC and per-purpose opt-outs”Language resolution
Section titled “Language resolution”Outbound messages render in the account’s language if you’ve set one. Otherwise LeadHunter walks a fallback chain until it finds a setting:
Account.language → Campaign.communication_language (per-campaign override) → Product.communication_language (product default) → User.communication_language (your personal default) → EnglishThe campaign-level override means you can run one campaign of the same product in Spanish (for the Spain market) and another in English (for the UK) without duplicating the product. See Campaign → Language.
Your reading language — what LeadHunter translates inbound text into — is your User-level communication language. It’s independent of the UI language, so you can run the dashboard in English while writing to and reading from accounts in Spanish.
Status auto-promotion
Section titled “Status auto-promotion”Logging a message as sent advances the pipeline automatically:
- Outbound sent → campaign-account
outreach_statusbecomessent(orbounced/responded), and the dashboard funnel counts the account as initiated. - First outbound on a
prospectaccount → account-levelstatusadvances tocontactedwithsource: campaign_outreachin the audit trail. - Inbound paste → campaign-account
outreach_statusbecomesresponded; the dashboard funnel counts the account as a response, cohort-attributed back to the original initiation date.
All forward-only. Already-customer accounts don’t regress when you log a follow-up; a second outbound doesn’t re-fire the prospect → contacted promotion.
Drafts (status='draft') don’t fire any of this. They’re scratch space — useful while you iterate, but they don’t move the funnel until you flip them to sent.
When the lead reaches out first
Section titled “When the lead reaches out first”The auto-promotion model assumes you initiate, then they reply — the common outbound case. For inbound-acquired accounts (Adwords form-fill, Instagram DM, referral), the account is created with an acquisition_channel and starts at status: contacted automatically before any message is logged. See Track inbound leads.
When the first message on such an account is your reply rather than their initial reach-out, log their initial message as an Inbound paste first (with the right sent_at), then your reply as AI continue or Type & translate. The funnel attribution works correctly either way — initiation is your first outbound, response is their first inbound; their original outreach pre-dating LeadHunter doesn’t count as a response.
Two layers of opt-out apply to outbound message logging:
- Blanket DNC — outbound to a
status='do_not_contact'account is blocked on every campaign goal. The UI shows a clear error and points at the account’s status history so the operator can see when and why the flag was set. No override. - Per-purpose DNC — outbound is blocked when the campaign’s
goalis in the account’sdo_not_contact_purposeslist. An account that opted out ofsalescan still receiveresearchinterview asks orpresspitches; an account that opted out ofpresscan still receive sales messages. The list is the GDPR / CASL-shaped consent surface, distinct from the blanket flag. Recorded viaPOST /api/accounts/{id}/record-dnc/(or whichever UI control surfaces it). See Account → Per-purpose opt-outs.
Other relationship-type guardrails (competitor, personal_network, soft blocks like supplier/investor/press/analyst/candidate) only apply to campaign bulk-add — not to logging individual messages on accounts already in a campaign. The rationale: if it’s in the campaign, it cleared the bulk-add filters.
Read next
Section titled “Read next”- Log a conversation — the operator-side walkthrough with a worked outbound → reply → continue example.
- Campaign — the language-resolution chain in full, plus the outreach guardrails.
- Account — the row whose lifecycle status the message log moves.