Skip to content

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.

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).

FieldWhat it holds
directionoutbound (you sent) or inbound (they sent).
channelWhich 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_modeHow the text was produced — one of five (see below).
statusdraft (scratch — doesn’t move the funnel) or sent (logged as out / in).
original_textAuthoritative version: what the lead actually sent or received, in their language.
original_languageISO 639-1 code of original_text.
translated_textOptional reader-friendly view of the same content in another language.
translated_to_languageISO code of translated_text.
sent_atWhen 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_byThe user who logged the message.
Auditcreated_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).

ModeDirectionUse when
AI draftoutboundFirst outbound message in this thread. AI writes a fresh draft.
AI continueoutboundContinuing a thread that already has history. AI reads the prior messages and proposes the next move.
Type & translateoutboundYou wrote the message yourself in your language. LeadHunter shows the translation into the account’s language and sends the translation.
Log sentoutboundYou already sent it elsewhere (your own email client, on LinkedIn directly, …). Paste the text to record it.
Inbound pasteinboundThe account replied. Paste their reply; LeadHunter shows you the translation into your reading language.

Knowing which mode wrote which field helps when re-reading old threads:

Modeoriginal_texttranslated_text
AI draft / AI continue / Log sentThe sent message (account’s language)(empty)
Type & translateThe translated/sent message (account’s language)Your original draft (your language)
Inbound pasteThe 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.

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.

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:

  1. 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.
  2. Target persona + product context — the (Product, Goal) ICP is rendered into the prompt so the model has the persona definition the campaign rates against.
  3. Account context + thread history — the account itself, plus the role_in_campaign override 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.

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.

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)
→ English

The 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.

Logging a message as sent advances the pipeline automatically:

  • Outbound sent → campaign-account outreach_status becomes sent (or bounced/responded), and the dashboard funnel counts the account as initiated.
  • First outbound on a prospect account → account-level status advances to contacted with source: campaign_outreach in the audit trail.
  • Inbound paste → campaign-account outreach_status becomes responded; 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.

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 goal is in the account’s do_not_contact_purposes list. An account that opted out of sales can still receive research interview asks or press pitches; an account that opted out of press can still receive sales messages. The list is the GDPR / CASL-shaped consent surface, distinct from the blanket flag. Recorded via POST /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.

  • 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.