Skip to content

Sales Guy

Sales Guy runs full B2B outbound cadences from inside your LinkWorld workspace. You give it an Ideal Customer Profile (ICP) and a list of leads; it researches each company, drafts personalized email touches from your own M365 mailbox, paces sends to protect your domain reputation, triages replies, and pushes qualified prospects into your CRM. No separate seat, no CSV export to a third-party SDR tool, no copy-paste between systems.

  • Multi-touch email cadences — a configurable 5-touch sequence (intro → follow-up → value-proof → case-study → breakup) runs on a daily heartbeat. Each touch is personalized from the lead’s enrichment data; no spray-and-pray.
  • ICP-Chat campaign setup — you describe your target buyer in natural language; the sdr agent walks you through the missing criteria and creates the campaign.
  • Three lead-source paths — pasted CSV, manual company list (with per-lead enrichment via the researcher agent), or bulk search via ProxyCurl when configured.
  • Inbox-aware reply triage — every inbound email that matches a cadence thread gets auto-classified (positive / negative / OOO / info-request / other). Positive replies are drafted as approval- pending records you eyeball before sending; opt-outs hard-suppress.
  • Domain-reputation guards — per-user daily cap (default 20) + hourly burst limit (3) + tenant-configured business-hours window. send_touch returns deferred=True instead of sending if any cap fires.
  • Cross-app handoff to Pipeline — qualified leads land as Pipeline contacts; deal stages auto-react to reply verdicts.
  1. Connect M365 in Settings → Integrations. Sales Guy uses your real Outlook mailbox to send touches + read replies — the mail.read, mail.send, calendar.read, calendar.write scopes prompt at activation time.
  2. Activate Sales Guy from the Marketplace. Grant the requested scopes when the consent dialog appears.
  3. (Optional) Add API keys via the Settings tab:
    • proxycurl_key — needed for LinkedIn profile enrichment + bulk lead search. Without it, CSV-paste and manual paths still work.
    • hunter_key — verifies email deliverability before send. Without it, sends go out without a deliverability pre-check.
  4. Tune the send caps in the same Settings tab if 20/day isn’t right for you. Defaults protect a cold-warmed sender; raise only after you’ve checked the domain’s reputation in Microsoft’s Sender Score / Google Postmaster.
  5. Install Pipeline (recommended) so qualified leads + deal stages land in a CRM you can actually look at. The bilateral cross-app wire auto-approves at activation time when both apps are installed.

Open the Sales Guy chat panel (right-side chrome) and describe your target:

“Let’s start a campaign — DACH B2B SaaS companies, 50–200 employees, decision-maker is the Head of Sales or VP Sales”

The sdr agent enters ICP-Chat mode: it asks 1-2 follow-up questions at a time (geography, exclusions, trigger signals like recent funding or hires), summarizes back when ready, and asks for confirmation. Reply “yes” or “create it”:

“yes, name it DACH SaaS VPs”

create_campaign runs, the agent returns the campaign id and asks how you want to load leads. Status is draft until leads exist.

The simplest path. Paste rows directly into chat, any CSV-ish format the LLM can read works:

email,name,company,role
[email protected],Alice Schmidt,Scaleup GmbH,VP Sales
[email protected],Bob Lehner,Growth AG,Head of Sales
[email protected],Carla Mayer,Boostly SE,Sales Director

“import these into DACH SaaS VPs”

import_leads_to_campaign dedupes on (campaign_id, email), returns {created: N, skipped: M, total: T}. Idempotent — pasting the same rows twice safely no-ops the duplicates.

Importing leads — path 2: Manual companies + researcher

Section titled “Importing leads — path 2: Manual companies + researcher”

When you have target companies but no contact list, name them:

“I want to target Personio, HelloFresh, and Celonis. Find me their Head of Sales.”

The sdr delegates to the researcher agent (per-lead enrichment via ProxyCurl + Hunter). Researcher returns 3-5 facts per company + 2-3 per persona + one personalized opening hook. sdr then calls add_lead for each.

Section titled “Importing leads — path 3: ProxyCurl bulk search”

When configured, you can describe an ICP and let ProxyCurl find matching profiles in bulk. (Wired via enrich_lead per-profile today; leads.search platform skill for true bulk is on the roadmap — Issue #78.)

Once a campaign has leads, you can ask explicitly:

“send the first touch to [email protected]

send_touch runs the guard chain: suppression check → business hours → daily cap → hourly cap → email_send from your M365 mailbox → records the touch + emits touch_sent. Returns {sent: true, used: N, cap: 20, ...} or {deferred: true, reason: ...}.

For the autonomous run, just let the daily cadence heartbeat do its thing — fires at 09:00 (tenant-local UTC), walks every active campaign + lead, decides per-lead whether to send today.

When someone replies to a cadence touch, the platform routes the inbound to Sales Guy’s on_inbound handler. The agent:

  1. Matches the inbound to a prior touch via channel_thread_id
  2. Classifies the reply: positive / negative / ooo / info_request / other
  3. Acts per verdict:
    • positive → drafts a meeting-request reply, queues it as draft_reply with status=pending_approval. You see it in the Inbox tab.
    • negative → emits opt_out_recorded, suppresses in Pipeline, does NOT auto-reply.
    • ooo → snoozes the cadence by 7 days.
    • info_request / other → logs the reply; next heartbeat decides what to do.

You approve / discard drafts manually — Sales Guy never auto-sends risky content.

When sdr qualifies a lead (after positive reply or manual qualification), it calls register_qualified_lead. That fires three things:

  1. Posts the contact to Pipeline via the cross-app RPC contract (contact_create: email=..., name=..., company=...). Pipeline returns the contact_id.
  2. Emits lead_qualified for any other subscribed apps (reporting, analytics, your own webhook bot).
  3. Returns the Pipeline contact_id so subsequent send_touch calls can carry it as contact_id.

The full contract is in docs/strategy/sales-guy-pipeline-contract.md. Sales Guy degrades gracefully when Pipeline isn’t installed — pipeline_rpc returns a soft forbidden, the lead stays Sales-Guy-local, and the local suppression list still works.

  • Cadence ticks daily at 09:00 UTC — adjust the business_hours install_setting if your tenant is in a non-UTC zone (full timezone support lands in a later milestone).
  • 20 sends/user/day default cap — raise via the daily_send_cap_per_user install_setting only after warming the sending domain. Domain-reputation damage takes weeks to recover.
  • 3 sends/hour burst cap — fingerprint protection, not configurable today.
  • Every personalized body must reference a verifiable fact — the sdr agent refuses generic flattery; this is a hard rule in its prompt, not a policy you toggle.
  • Risky outbound (quotes, contracts, meeting confirms) never auto-sends — even if sdr drafts it, it lands as pending_approval and you sign off in the Inbox tab.
  • All app-private state lives in ctx.kv — campaigns, leads, touches, replies, drafts, suppression. Tenant-scoped, isolated from other tenants on the same instance.

“The agent didn’t import my CSV” — paste should be inside the same chat turn that says “import these”. If you paste first and ask later, the prior turn’s context is what the agent acts on; try again with both in one message.

“send_touch keeps returning deferred” — check the reason:

  • outside_business_hours → your business_hours window blocks current UTC time
  • daily_cap_reached → wait until tomorrow OR raise the cap
  • hourly_cap_reached → wait 1 hour
  • suppressed_local → the email is on your suppression list; remove it via discard_draft_reply flow if intended

“My Pipeline contact didn’t appear” — check that Pipeline is installed AND active on your tenant, AND that the cross-app wire shows “approved” in Workspace Control. Without the wire, pipeline_rpc returns forbidden and the lead stays Sales-Guy-local — Sales Guy’s own records aren’t lost, just not synced.

“Replies aren’t being triaged” — verify M365 is connected with mail.read granted, and the tenant has opted into app_lifecycle_fanout so inbound emails reach Sales Guy’s on_inbound handler (Workspace Control → Channels). Without fanout, the platform routes inbound to the default Steward agent and Sales Guy never sees the reply.

“Domain reputation is dropping” — Sales Guy can’t fix bad infrastructure. Verify SPF/DKIM/DMARC, switch to a warmed secondary sending domain (see Sending at scale for the playbook), and reduce daily_send_cap_per_user until deliverability recovers.

This section is auto-generated from the app’s manifest. It updates whenever the app publishes a new version.

  • mail.read
  • mail.send
  • calendar.read
  • calendar.write
  • tasks.create
KeyLabelTypeRequired
proxycurl_keyProxyCurl API keysecretno
hunter_keyHunter.io API keysecretno
daily_send_cap_per_userMax emails per user per daynumberno
business_hoursSending window (tenant-local)select_or_customno
  • Sales Guy (default)
  • Lead Researcher
  • list_app_records — Read-only view into the app’s private ctx.kv records. record_type
  • create_campaign — Create a new outbound campaign with an ICP description + filter
  • import_leads_to_campaign — Batch import N leads to a campaign in one call. Pass leads as a
  • add_lead — Manually add ONE lead to a campaign. Idempotent on
  • send_touch — Execute ONE outbound email touch. Enforces daily/hourly (scopes: mail.send)
  • register_qualified_lead — Push a qualified lead to the Pipeline app (idempotent on email).
  • approve_draft_reply — Approve a drafted reply (from on_inbound) and send it via (scopes: mail.send)
  • discard_draft_reply — Discard a drafted reply without sending. Marks status=‘discarded’
  • lead_qualified
  • meeting_booked
  • opt_out_recorded
  • touch_sent