MCP App Store
by KeenEthics

Overview

lemlist is a sales engagement platform for running multichannel outreach across email, LinkedIn, calls, WhatsApp, and SMS from a single sequence. Campaigns and sequences: create campaigns, draft and preview personalized multichannel sequences step by step, set up A/B variants and AI-generated variables, validate a campaign before launch, and start, pause, or resume it. Leads and prospecting: search your existing campaign leads, add or import leads, update their data and custom variables, and search the lemlist leads database to build new lists. Enrichment tools can look up a lead's professional email and phone number, individually or in bulk, using enrichment credits from your plan. Signals and watch lists: build watch lists that track buying signals on companies you care about, browse the signal library, and turn a watch list into a campaign. CRM and contacts: create and update contacts and companies, organize them into lists, import them from CSV, and push campaign leads into your contact database. Conversations and activity: read your unified inbox, follow up on conversations, reply to a lead, review call activity and outcomes, and work through your task list. Deliverability and sending infrastructure: check the deliverability and DNS health of your sending domains, review and configure DNS records, run a deliverability audit and inbox placement tests, and connect, test, and manage the mailboxes you send from. Reporting: track how campaigns perform with detailed stats and reports across channels. Account: review your team, users, sending settings, unsubscribe list, and webhooks. All actions run on your own lemlist workspace with your own data, using the permissions granted to the connected account.

Tools

add_campaign_senders

ChatGPT
Add one or more team members to the sender pool of a campaign, or add channels to senders already in the pool. It never removes a sender or a channel: a sender already in the pool keeps its channels and gains the listed ones. Idempotent. Keywords: add a sender, assign a teammate to a campaign, add Ben to the campaign, send from this mailbox too. Use when the user asks about: - Adding a teammate to a campaign ("add Ben to the Q4 campaign"): one entry with the userId and no channels enables the channel types that the steps require, healthy channels first, exactly as the editor toggle does - Adding a teammate with chosen channels ("add Ben with the mailbox sales@acme.com only"): pass channels with the ids from get_senders - Enabling one channel type on many senders at once (channels on several entries) Before any write: call get_senders with the campaignId. It lists every team member who can send, in the pool or not (inPool), with the user ids, the channel ids, the aliases, the health of each channel and requiredChannelTypes, the channel types that the steps require. Never guess a user id, a mailbox id, a WhatsApp account id or an alias. Load load_skill("senders") first: it tells which tool fits the request, when to change the sender strategy, and how to read the diagnostics. Contract rules reproduced by the backend: - A campaign with a dynamic sender strategy (contactOwner, leadImporter, customField:<field>) has a read-only pool that the leads fill: every pool write is refused. Switch the strategy with set_campaign_sender_strategy first, only when the user wants it. - A channel type is accepted only when a non-forced step of the campaign uses it: no WhatsApp channel without a WhatsApp step. Add the step first with add_sequence_step: it enables the type on each pool sender who owns an account of it, and lists the enabled types in senderPoolChange. - Every entry must be a team member who owns the listed channels; an alias must be one of the aliases of the mailbox. - LinkedIn has no channel to pick: a sender in the pool is enabled on LinkedIn. Never list linkedin with an id. - A pool with more than one sender needs the emailPro plan; the mailboxes above the plan allowance are dropped. - A write runs on a running campaign: the leads not contacted yet get a new sender from the pool, the contacted leads keep theirs. - Every write answers "data": the pool after the change, with the diagnostics of each sender and channel. Read them and report the errors to the user. To define the whole pool at once, use replace_campaign_senders. To add one channel to one sender, enable_campaign_sender_channels does the same with a single entry. No user confirmation required.

add_companies_to_list

ChatGPT
Add existing CRM companies to a company list. Use when the user asks about: - Organizing companies or accounts into lists (e.g. "Target accounts", "Churn risk") - Grouping the companies returned by search_companies Contract rules reproduced by the backend: - company ids come from search_companies; the list id comes from get_company_lists — never invent one - the list must exist, hold companies (not contacts), and not be dynamic - companies already in the list are silently skipped, never duplicated - at most 1000 companies per call

add_contacts_to_list

ChatGPT
Add existing CRM contacts to a contact list. Use when the user asks about: - Organizing contacts into lists (e.g. "Nurturing", "Hot leads") - Grouping the contacts returned by search_contacts Contract rules reproduced by the backend: - contact ids come from search_contacts; the list id comes from get_contact_lists — never invent one - the list must exist, hold contacts (not companies), and not be dynamic - contacts already in the list are silently skipped, never duplicated - at most 1000 contacts per call

add_leads_to_campaign

ChatGPT
Add one or more leads to a campaign in a single call (max 100). Requires campaignId + a non-empty leads array. Each lead must have at least one identifying field (email, firstName, lastName, companyName, phone, linkedinUrl, linkedinUrlSalesNav, or customVariables). Server-side bulk dedup runs during the import — do NOT call search_campaign_leads or search_contacts to pre-check before this tool. The response reports each existing lead with outcome="skippedAlreadyInCampaign" (or "skippedDuplicateCrossCampaign" when deduplicate=true), so the LLM has full visibility without an extra round-trip. Invalid leads are reported in the results array with outcome="missingIdentifier", "forbiddenCustomKey", etc. — they do not abort the rest of the batch. Company matching: send companyDomain alongside companyName whenever you can (the work email usually gives it away). A name on its own only matches companies that have no domain, so omitting the domain creates a SECOND company with the same name and moves the contact onto it. Enrichment options (findEmail, verifyEmail, linkedinEnrichment, findPhone) and deduplicate apply to every lead in the batch. ⚠️ Enrichment COSTS CREDITS per successfully added lead — warn the user before enabling.

add_sequence_step

ChatGPT
REQUIRES CONFIRMATION: Add a step to an ALREADY CREATED campaign sequence (modification only). For initial campaign creation use save_campaign_from_proposal instead — it materializes the whole tree atomically. Supported types: email, linkedinSend, linkedinInvite, linkedinVisit, linkedinFollow, linkedinEndorse, linkedinLikeLastPost, linkedinCommentLastPost, linkedinWithdrawInvitation, linkedinVoiceNote (LinkedIn voice message. Two INDEPENDENT settings: recordMode = how the audio is produced, manual = the "Mark as manual" toggle. Set recordMode="ai" to generate the audio from a script via ElevenLabs: pass the script in message (full Liquid syntax) and the voice in voiceId (call list_voice_profiles first). When NO AI voice is available, set manual: true so the step becomes a per-lead recording task and the campaign stays launchable: do NOT set a voiceId and the script is optional (message is then only the optional LinkedIn invite-fallback text). Do NOT create a non-manual voice note with recordMode="manual" and no audio - the auto-send path still requires a recorded file at launch), manual, phone, whatsappMessage, sms, conditional, sendToAnotherCampaign, api, enrichment (⚠️ COSTS CREDITS: automatically enriches leads at this sequence step using the specified workflowIds; requires MULTI plan). By default the step is appended at the end; pass index (zero-based) to insert at a specific position (e.g. index=0 to insert before the first existing step). For YES/NO branching use type="conditional" with conditionKey and delayType. delayType is ALWAYS required: use "within" or "waitUntil" for action conditions (hasScore, emailsOpened, emailsClicked, emailsUnsubscribed, meetingBooked, linkedinInviteAccepted, linkedinOpened, aircallDone), use "within" with delay 0 for field-check conditions (hasEmailAddress, hasLinkedinUrl, hasPhoneNumber). For hasScore: use scoreComparator ("$gte"/"$lt") and scoreThreshold (0-100). For hasEmailAddress: use emailStatusFilter (["deliverable","risky","undeliverable","unverified"]). For aircallDone ("Call status", branch on native lemlist call dispositions): use callStatusFilter (call status KEYS, e.g. ["connected-positive","no-answer"]; keys are team-specific, call get_call_statuses first; omit to match any completed call). For customLeadInfo (branch on any custom lead variable): use customField (variable name, e.g. "secteur"; prefix "fields." for a contact field), customOperator ("equal"/"contains"/"empty"/"notEmpty"), customValue (required for equal/contains), with delayType="within" and delay 0. To move a lead between campaigns (e.g. when a LinkedIn invitation is not accepted within 7 days), use type="conditional" with conditionKey="linkedinInviteAccepted" and delayType="waitUntil", then add a type="sendToAnotherCampaign" step (with targetCampaignId) inside the NO branch. Beware of cycles between campaigns. For type="api" (outbound webhook to Zapier/Make/n8n/Pipedream/etc.): provide method (GET or POST only, iso with the lemlist UI) and url. The URL supports Liquid templating against lead variables. The request body for POST is automatically the lead data — custom bodies and custom headers are not supported. Response returns sub-sequence IDs for YES/NO branches. Also returns the full updated step list in steps (each with its real stepId, sequenceId, type, delay). Reuse those ids for any follow-up edit in the same turn — do NOT call get_campaign_context to rediscover steps. REQUIRES userConfirmed=true. Fails on running campaigns.

add_unsubscribe

ChatGPT
Add one or more email addresses OR whole email domains to the unsubscribe blocklist in a single call. Blocks all future campaign sends. Use delete_unsubscribe to reverse. Accepted values (each entry in `emails`): - An email address, e.g. "john@acme.com" — blocks that address only. - A domain starting with "@", e.g. "@acme.com" — blocks every lead whose email is on that domain. Batch semantics: - One call handles up to 1000 targets. Always pass ALL the values the user asked to block in a single call. Do NOT call the tool once per email. - Duplicates and values that are already unsubscribed are silently ignored server-side (no per-item failure). - Use the "@domain.com" form when the user wants to stop contacting everyone at a company (e.g. "block @luxottica.com").

bulk_enrich_data

ChatGPT
Enrich one or more contacts (max 500). ASYNC — lemAgent clients receive a 'dataRef' to pass to bulk_get_enrichment_results. External MCP clients receive an inline 'results' array of { enrichmentId?, error?, metadata? } to poll individually. ALL options COST CREDITS per item. Types: find_email, find_phone, verify, linkedin_enrichment. Submitting an enrichment does NOT mean data will be found. An enrichment can complete with nothing found. Always confirm the actual result via bulk_get_enrichment_results (check each 'summary') before telling the user any data was found.

bulk_get_enrichment_results

ChatGPT
Poll one or more enrichments in one call. Provide exactly one of: a dataRef returned by bulk_enrich_data (first poll), the nextPollRef from a previous result (subsequent polls), OR a plain enrichmentIds list — not both. Each call returns ONLY the enrichments that completed or failed in this poll. Use nextPollRef as the dataRef input for the next call. Repeat until pending=0. When pending=0, the dataRef contains ALL completed rows from every poll — pass it to display_table to show the full results in one table. Intermediate polls return a dataRef with only the newly completed rows for incremental display. A completed enrichment (status "done") can still return NO data. Inspect each result's 'summary' before reporting anything: when 'summary.noDataFound' is true (or the only fields present are 'emailNotFound'/'phoneNotFound'), tell the user that profile was "not found". Never claim an email, headline, or company was found, and never narrate fields that are absent from the 'summary'. An empty results table means nothing was found, not success. lemAgent clients get compact summaries + dataRef + nextPollRef; external MCP clients get full 'data' and 'input' inline per entry.

calculate_infrastructure

ChatGPT
Compute cold-email infrastructure sizing from campaign inputs. Returns: - Peak daily volume + injection rate - Mailbox count (raw and with safety buffer) - ESP split (Google / Microsoft / SMTP) - Domains needed per seat (cap: 3 mailboxes per domain) - Warmup timeline (rampup + 14-day stabilization) - Lemlist slot cost (included + extras at $9/month) - Brand-consistent domain name suggestions - Mailbox naming proposals per SDR (when sdrs provided), with SDR userId echoed back in sdrPlans[].userId if it was passed in All math is deterministic — pure domain function, no LLM math. Use in Step 2 of the outreach-infra skill, after gathering campaign inputs via ask_question.

call_api

ChatGPT
⚠️ REQUIRED FIRST STEP: You MUST call load_skill("api-reference") once in the current session before call_api. The server rejects every call_api until then. load_skill("api-reference") returns the live catalog of available endpoints (cached 1h from the OpenAPI spec). Without it the model cannot know which paths exist; with it, call_api is unlocked for the session. ⚠️ FALLBACK ONLY — Use a dedicated tool whenever one exists in your tools list. Call call_api only when no dedicated tool matches. Common operations are covered by dedicated tools — scan your tools list first for matches in these categories before reaching for call_api: - Campaigns: list, details, stats, reports, create, start/pause, readiness - Leads: add to campaign, update, enrich, search within a campaign - Sequences: list steps, add step, update step (with preview) - Contacts & lists: search, create, manage CRM lists - Inbox: list/read conversations, send email/LinkedIn/SMS/WhatsApp - Team & user: team info, members, settings, connected channels - Enrichment: single / bulk / lead; poll results - Webhooks & unsubscribes: list, create, delete - People database: filter options, search, push to contacts Strict server-side validation: - Endpoints covered by a dedicated tool are rejected — the error names the tool to use. - If that tool is not in your tools list (this connector exposes a narrowed set), the call runs instead and the result carries a hint naming the tool and the bucket that would provide it. Relay that hint to the user: they can widen their connector URL to get the dedicated tool. - Endpoints not found in the lemlist OpenAPI spec are rejected — suggestions of similar spec endpoints are returned in the error. Genuine use cases (no dedicated tool yet): - Campaign schedules: GET /api/campaigns/:campaignId/schedules, POST /api/campaigns/:campaignId/schedules/:scheduleId (associate), DELETE /api/campaigns/:campaignId/schedules/:scheduleId (remove), GET /api/schedules, POST /api/schedules (create) - Email templates - Any documented endpoint without a dedicated tool Safety rules: - GET requests execute immediately - POST, PUT, PATCH, DELETE require confirmed: true after user approval Full API reference: https://developer.lemlist.com/api-reference Example — replace a campaign schedule: 1. POST /api/campaigns/cam_xxx/schedules/skd_new (associate new) 2. DELETE /api/campaigns/cam_xxx/schedules/skd_old (remove old)

check_domain_health

ChatGPT
Check DNS health for email sending domains (MX, SPF, DMARC, blacklists). Returns score (0-100), per-check status, and DNS fix records.

clear_sequence_step_sender

ChatGPT
Remove the forced sender of one sequence step. The step then uses the campaign sender pool again. Use when the user asks to: - Stop sending a step from a specific team member or account - Put a step back on the campaign sender strategy Contract rules reproduced by the backend: - A LinkedIn or WhatsApp voice note step always has a forced sender: use set_sequence_step_sender to change it instead. - The channel type of the step comes back into the pool, as in the campaign editor. senderPoolChange lists the channel types that the change enabled or disabled. It does not tell whether each sender has an account of the type: read get_campaign_senders and report each error diagnostic. - On a running campaign, the pending tasks of the step move to a sender of the pool.

configure_domain_dns

ChatGPT
Write DNS records for a team-owned domain. Two modes: - Provide records: replaces the domain's full record set (SPF / DMARC / MX / CNAME etc.). - Provide dkimRecord: appends the DKIM TXT record only (after mailbox provisioning). Use in Step 4 (initial SPF/DMARC/MX) and Step 6 (DKIM after Google/M365 gives the public key) of the outreach-infra skill.

connect_email_account

ChatGPT
Connect a custom SMTP/IMAP email account for sending and receiving emails. For Google/Microsoft, use the UI instead. - You need SMTP credentials for sending and IMAP credentials for receiving - Ports are typically 587 (SMTP) and 993 (IMAP) with TLS enabled - The sender email will be verified before the account is connected

copy_campaign_leads

ChatGPT
Copy every lead from one campaign into another, in a single call, by re-importing their variables. Prefer this over composing search_campaign_leads + add_leads_to_campaign whenever the user wants to copy / recreate / move / duplicate the leads of a whole campaign into another campaign (e.g. "copy the leads from campaign A into campaign B", "put everyone from the old campaign into the rebuilt one"). Composing the two other tools drops fields (Sales Nav URL, phone, custom variables) and creates new empty contacts — this tool does not. Use when the user asks about: - Copying/recreating a whole campaign's leads into another campaign - Repopulating a freshly duplicated campaign (duplication never copies leads) from the original Contract rules reproduced by the backend: - Works by lead id server-side: it re-imports every variable (linkedinUrlSalesNav, phone, custom fields) and re-resolves the CRM contact from the identity variables (email, LinkedIn URL), so an identified lead reuses its existing contact (no empty duplicate). - Both campaigns must belong to the current team; sourceCampaignId and targetCampaignId must be different. - ADDS to the target (source campaign is left untouched). Leads already in the target follow the same dedup rules as a manual push.

create_ai_variable_prompt

ChatGPT
Create a NEW AI variable column (an AI-generated column in the Leads table: icebreaker, opener, company research, etc.) on a campaign. Provide the column name, the AI-generation prompt, and optionally the model. The column is created team-owned and editable, wired into the campaign so it shows up in the Leads table and generates for leads. Use {{variable}} placeholders in the prompt to reference existing fields or columns. The variable name must be unique on the campaign — creating a duplicate is rejected. To edit an existing column's prompt instead, use update_ai_variable_prompt; to read existing columns, use get_ai_variable_prompts. Creating the column only sets it up — it does NOT generate values. Values are generated per lead later (from the Leads table UI or on import) and that is what consumes credits, not this call. Pass autofill:true to turn the column's auto-generate toggle on, so every lead added or imported to the campaign afterwards (CSV import, CRM sync, manual add) generates it automatically; it stays off by default. To scrape LinkedIn profiles for the column, pass tools:["enrich-linkedin"] AND reference {{linkedinUrl}} in the prompt; enabling any tool forces the premium lemlist model and lemlist-credit billing (per lead, at generation). Use when the user asks to: - Add a new AI column / AI variable to a campaign's leads - Generate a new piece of AI content (research, icebreaker, summary) per lead - Have an AI column filled automatically for every lead imported into a campaign This mutates campaign configuration, so confirm the column name and prompt with the user before calling.

create_campaign_folder

ChatGPT
Create one or more campaign folders that share the same parent, in a single call. Use when the user asks about: - Adding one or several folders at the same level (pass them all in names). - Creating sub-folders under an existing folder (pass its parentId). For a nested chain (A > B > C) call once per level instead, threading each returned folderId as the next parentId. Contract rules reproduced by the backend: - Each name must be unique among folders that share the same parent. - Depth is capped at 15 levels, 50 folders per level, 500 folders per team. - Best-effort: valid names are created even if others fail; the response lists created and failed (with a per-name reason).

create_campaign_with_sequence

ChatGPT
Create campaign. If subject AND body are provided, creates the first email step. If omitted, creates an empty sequence (use add_sequence_step to add a condition or any step type as the first step). Call add_sequence_step for each additional step. Supports Liquid syntax, including spin for variations (only when the user asks for them): {% spin %}{% variation %}Hello{% variation %}Hi{% endspin %} — each variation must be genuinely different wording, never the same text repeated.

create_company_list

ChatGPT
Create a new static company list in the team's CRM. Use when the user asks about: - Grouping companies or accounts into a new list that does not exist yet - Preparing a destination before add_companies_to_list Contract rules reproduced by the backend: - the list is always static; filter-driven (dynamic) lists are built in the web app, not here - the list starts empty — add its members with add_companies_to_list - check get_company_lists first: a list with that name may already exist

create_contact_list

ChatGPT
Create a new static contact list in the team's CRM. Use when the user asks about: - Grouping contacts into a new list that does not exist yet - Preparing a destination before add_contacts_to_list or push_contacts_from_people_database Contract rules reproduced by the backend: - the list is always static; filter-driven (dynamic) lists are built in the web app, not here - the list starts empty — add its members with add_contacts_to_list - check get_contact_lists first: a list with that name may already exist

create_enrichment_waterfalls

ChatGPT
Create one or more custom enrichment waterfalls in ONE call, each serving the leads whose country is in 'conditions.country.in' (ISO 3166-1 alpha-2). A country may belong to only one waterfall per type (email, phone); leads with no country or an unassigned country use the default waterfall of the type. 'providers' is the execution order (first tried first); providers left out are appended disabled. Names are unique within a type, and a type holds at most 20 custom waterfalls. Run list_enrichment_waterfalls first to see the countries already taken and the legal provider ids. Entries are applied independently: 'results' reports the created waterfall or an error per name. Team-shared and free (no credits).

create_or_update_company

ChatGPT
Create a new company in the user's Lemlist company database, or update an existing one (upsert). Requires a name plus at least one identifier: domain, linkedinUrl, or linkedinUrlSalesNav. If a company with the same domain, LinkedIn URL, or Sales Navigator URL already exists, it will be updated instead of creating a duplicate. Returns the company data and whether it was created or updated.

create_or_update_contact

ChatGPT
Create a new contact in the user's Lemlist contact database, or update an existing one (upsert). Provide at least one identifier: email, linkedinUrl, or contactId. Use when the user asks to: - Create a contact from an email or LinkedIn URL - Update fields on an existing contact (standard fields AND custom fields) Identifiers: - email / linkedinUrl: match an existing contact (updated instead of duplicated) or create one if none matches. - contactId (ctc_xxx): update that exact contact in place, update-only. Prefer it when search_contacts already resolved the contact. Custom fields go in the "fields" object; call get_contact_fields_schema first for the valid names. Returns the contact data and whether it was created or updated.

create_people_database_persona

ChatGPT
Create a People Database persona for the current team. Use when the user asks about: - Saving a set of People Database filters as a reusable audience - Defining an ICP / persona to reuse later Contract rules reproduced by the backend: - name must be non-empty and unique within the team (duplicates are rejected) - mode must be "leads": company personas are not supported yet - every filterId must be an exact id from the People Database filter catalog (GET /api/database/filters) - filters gated by a plan the team does not have are silently dropped ⚠️ A persona only SAVES filters for reuse — it does NOT export or push results to the CRM. To USE the results (add to a contact list, push to a campaign), run lemleads_search (or, in the People Database panel, people_database_nl_to_filters) with this persona via personaId, then push_contacts_from_people_database. Returns the created persona id (pdp_xxx).

create_tasks

ChatGPT
Create one OR many standalone tasks (opportunities) on contacts, companies or campaign leads: to-dos listed on the Tasks / Focus page, each assigned to a teammate and due at its own date, for a person to complete. Use when the user asks about: - Creating a follow-up, reminder or to-do task on one or several contacts, companies or leads - Planning a call, or an email / LinkedIn / WhatsApp message to send later from the Tasks page - Assigning a task to a teammate with a due date and a priority Pass every task in ONE call through tasks[] (1 to 100), each with its own record, title, type, content, priority, due date and assignee. The result is always the aggregate { total, succeeded, failed, results[] }: each entry carries the task's index in tasks[], its record ID, and either opportunityId or an error code. Contract rules reproduced by the backend: - Exactly one of contactId, companyId or leadId per task. A lead task is linked to the lead's contact, shows the lead's campaign as its Task source, and follows its own due date and assignee alongside the lead's sequence. - A company task can only be of type "manual". - message holds the instructions of the task; content is the message to send (email, linkedinSend, whatsappMessage only); subject is email only. - assignedTo must be a teammate (list them with get_users); it defaults to the current user. - dueDate defaults to now; a past date creates a task that is already due. - A task that breaks one of the rules above rejects the whole call before any task is created: fix it and send the full list again. - A task whose record or assignee is not found fails on its own (error code in its results[] entry); the other tasks are still created. - A message task holds the message for later: it is sent with send_task, once the user approves. After the call: - Link every created task as [title](task://opp_xxx), with the exact title you sent and its opportunityId. - Retry ONLY the failed indexes: calling again with the whole list creates the successful tasks a second time.

create_upload_url

ChatGPT
Get a presigned URL to upload a file from the user's machine to lemlist storage, then reference it by key. Use this to AVOID retyping large file contents through the conversation. Flow: 1. Call this tool with purpose, fileName, and the EXACT fileSize in bytes. 2. Upload the raw file to the returned uploadUrl with an HTTP PUT, e.g.: curl -T leads.csv -H "Content-Type: <contentType>" "<uploadUrl>" 3. Pass the returned uploadKey to the tool that consumes it: - purpose="leadsCsv" → import_leads_to_campaign - purpose="contactsCsv" → import_contacts_from_csv - purpose="companiesCsv" → import_companies_from_csv - purpose="csvData" → read_uploaded_csv The URL is short-lived (expiresInSeconds). fileSize MUST be the real byte size of the file — it is signed into the URL, so a mismatch makes the upload fail, and a size above the limit is rejected up front.

create_watch_list

ChatGPT
Create a new watch list for the current team. Use when the user asks about: - Starting to monitor a new signal type - Setting up a watch list end-to-end (create + configure + activate) - Creating a draft watch list that the user will configure later in the UI Contract rules reproduced by the backend: - activate=true ⇒ segmentType AND signalProcessingType must be set - signalProcessingType="create_opportunity" ⇒ signalOpportunityTemplate required (and forbidden otherwise) - personaId is accepted ONLY on a company watch list with signalProcessingType="push_to_campaign" (any other config is rejected), and only for teams in the personas beta - personaId is optional there: without it the signals are still recorded, they simply source no contact, so nothing reaches the campaign - get a personaId from list_people_database_personas (or create_people_database_persona); never invent one - Only segmentType="all" is accepted via API (CRM lists and CSV imports are not)

create_webhook

ChatGPT
Create a webhook for real-time campaign activity notifications. Max 200 per account, no duplicate URLs. Filter by activity type (emailsSent, emailsOpened, emailsReplied, etc.) and/or campaignId.

delete_campaign_folder

ChatGPT
Delete one or more campaign folders. Campaigns are NEVER deleted — each folder's campaigns and sub-folders move up one level (to the parent folder, or to the root if the folder was at the top). Use when the user asks about: - Removing one or several folders while keeping their campaigns (pass them all in folderIds). Contract rules reproduced by the backend: - Deleting a folder is blocked if a sub-folder would collide, by name, with a folder already present in the parent — rename the conflicting sub-folder first. - Best-effort: each id is handled independently; the response lists deleted and failed (with a per-id reason).

delete_campaign_leads

ChatGPT
Permanently delete leads from ONE campaign in a single bulk call. Irreversible: it also removes each lead's activity and task history in that campaign. Use when the user asks about: - Removing a specific set of leads from a campaign at once (e.g. after moving or segmenting them elsewhere) - Cleaning leads out of a campaign without calling the unitary delete endpoint one lead at a time Two-phase confirmation flow (mandatory): 1. First call WITHOUT confirmed (or confirmed=false): returns the count and a sample of the exact leads that will be deleted, with status="awaiting_confirmation". Show these to the user and get explicit approval via ask_question. 2. Once the user has approved, call again with confirmed=true to actually delete. Contract rules reproduced by the backend: - Scoped to a SINGLE campaignId; a lead id is unique to one campaign (campaignId + email). - Every leadId MUST belong to campaignId. Ids from another campaign (or already deleted) are rejected with an error, not silently skipped. Re-fetch the ids via search_campaign_leads on the target campaign. - At most 1000 lead ids per call. - Never call with confirmed=true without explicit user approval.

delete_company

ChatGPT
REQUIRES CONFIRMATION: Delete a lemlist company. Only the lemlist record is removed — no CRM-side propagation. REQUIRES userConfirmed=true. By default the call fails with COMPANY_HAS_CONTACTS if contacts are still attached; pass force=true to detach them (their companyId is unset) before deleting. Typical use: as the final step of the duplicate-remapping flow — after reassigning contacts of a duplicate to the canonical company (via create_or_update_contact with companyId), drop the now-empty duplicate.

delete_contact

ChatGPT
REQUIRES CONFIRMATION: Delete a lemlist contact by its ID (ctc_xxx) or email. Only the lemlist record is removed — no CRM-side propagation. Deletion cascades to the contact's leads, opportunities, list memberships, inbox conversations and activities. REQUIRES userConfirmed=true. Fails with CONTACT_DELETE_BLOCKED when the contact cannot be deleted right now (e.g. an enrichment is still running — retry once it finishes).

delete_enrichment_waterfalls

ChatGPT
Delete one or more custom enrichment waterfalls in ONE call; their countries fall back to the default waterfall of the type. Default waterfalls (default_email, default_phone) cannot be deleted — use reset_enrichment_waterfalls. Ids are applied independently: 'results' reports 'deleted: true' or an error per waterfallId. Team-shared and free (no credits).

delete_memory

ChatGPT
Delete a memory by topic. Use this when: - The user asks you to forget something - Information is explicitly outdated or incorrect - The user says "that's no longer true" or "forget about..."

delete_people_database_persona

ChatGPT
Delete a People Database persona of the current team. The deletion is permanent. Use when the user asks about: - Removing a persona they no longer need - Cleaning up duplicate or outdated personas Important: - Ask the user to confirm before calling this tool; there is no undo. - Use list_people_database_personas to resolve a persona name into its id. - An unknown id, another team's persona, and the auto-generated system persona all fail the same way (not found) — none of them can be deleted here.

delete_sequence_step

ChatGPT
REQUIRES CONFIRMATION: Delete a step from a campaign sequence. Use this to remove a step that was added by mistake or is no longer needed (e.g. a wrongly-placed warm-up step). Cascades sub-sequences for conditional steps. Returns the full updated step list in steps (each with its real stepId, sequenceId, type, delay). Reuse those ids for any follow-up edit in the same turn — do NOT call get_campaign_context to rediscover steps. REQUIRES userConfirmed=true. Fails on a running campaign. Fails once leads have entered the campaign, whatever its state: pausing does not lift that, use skip_step_for_everyone instead.

delete_unsubscribe

ChatGPT
Remove one or more email addresses OR whole email domains from the unsubscribe list in a single call. Re-enables the targets to be contacted again in future campaigns. Accepted values (each entry in `emails`): - An email address, e.g. "lead@example.com" — re-enables that address only. - A domain starting with "@", e.g. "@example.com" — re-enables every address on that domain (only removes a domain-level block; per-address blocks stay). Batch semantics: - One call handles up to 1000 targets. Always pass ALL the values the user asked to re-subscribe in a single call. Do NOT call the tool once per email. - Values that weren't unsubscribed (or are protected internal blocks) are silently ignored server-side. Use this when: - Leads were unsubscribed by mistake - A lead wants to re-subscribe to your campaigns - You need to re-enable communication with specific emails or domains - Testing campaigns with your own email address Important notes: - Once removed, the emails can receive campaigns again - This does NOT automatically re-add the leads to any campaign - The emails will need to be added to campaigns manually - Use with caution — respect unsubscribe preferences Example: { "emails": ["lead@example.com", "@partner.com"] } Common workflow: 1. Check if the emails are unsubscribed (via search_campaign_leads) 2. Call delete_unsubscribe with the full list to remove them from the unsubscribe list 3. Add the leads back to a campaign if needed IMPORTANT: Do NOT pass confirmed=true on your first call. Use the returned emails to ask the user to confirm via ask_question, then call again with confirmed=true.

delete_watch_list

ChatGPT
Delete a watch list. The backend performs a soft delete (status set to "delete") and stops processing signals for this watch list immediately. Use when the user asks about: - Removing a watch list they no longer want to monitor - Cleaning up outdated or duplicate watch lists Two-phase confirmation flow: 1. First call WITHOUT confirmed (or confirmed=false): returns the watch list details and status="awaiting_confirmation". Use these details to ask the user to confirm via ask_question. 2. Once the user has explicitly approved, call again with confirmed=true to actually delete. Important: - Never call with confirmed=true without explicit user approval. - Use list_watch_lists first if you need to resolve a name into a watch list ID. - The watch list is soft-deleted; its signals are not removed.

delete_webhook

ChatGPT
Delete a webhook from your lemlist account. The webhook will stop receiving notifications immediately. Use this when: - Removing outdated integrations - Cleaning up unused webhooks - Replacing a webhook (delete old, create new) - Webhook URL has changed and needs to be recreated Important notes: - Once deleted, the webhook cannot be recovered - The webhook will immediately stop receiving notifications - Use get_webhooks to find the webhook ID first Example: { "webhookId": "hoo_abc123xyz" } Common workflow: 1. Call get_webhooks to list all webhooks 2. Find the webhook ID you want to delete 3. Call delete_webhook with that ID

disable_campaign_sender_channels

ChatGPT
Disable one or more channels on one sender of a campaign, and keep the person: a mailbox, a WhatsApp account, a phone number. The other channels of the sender stay active. When the last channel leaves, the sender leaves the pool. Keywords: remove this mailbox from the campaign, stop sending from ana@old.com, drop Ana's WhatsApp on this campaign. Use when the user asks about: - Removing one channel of a sender from a campaign ("remove the mailbox ana@old.com from the Q4 campaign") - Stopping one channel type for a sender (the type without id disables every channel of the type) To remove the person, use remove_campaign_senders. Call get_campaign_senders first: it gives the channel ids of the pool. Two-phase confirmation flow: 1. First call WITHOUT confirmed: the backend counts the contacted leads that keep a sender or a channel this change removes (impact.contactedLeads). Above 0 the call writes nothing and answers status="awaiting_confirmation" with "data" (the pool after the change) and the count: show them to the user, ask with ask_question, then call again with confirmed=true and the SAME input. At 0 the call writes at once. 2. Never call with confirmed=true without explicit user approval. Contract rules reproduced by the backend: - A campaign with a dynamic sender strategy (contactOwner, leadImporter, customField:<field>) has a read-only pool that the leads fill: every pool write is refused. Switch the strategy with set_campaign_sender_strategy first, only when the user wants it. - A channel type is accepted only when a non-forced step of the campaign uses it: no WhatsApp channel without a WhatsApp step. Add the step first with add_sequence_step: it enables the type on each pool sender who owns an account of it, and lists the enabled types in senderPoolChange. - Every entry must be a team member who owns the listed channels; an alias must be one of the aliases of the mailbox. - LinkedIn has no channel to pick: a sender in the pool is enabled on LinkedIn. Never list linkedin with an id. - A pool with more than one sender needs the emailPro plan; the mailboxes above the plan allowance are dropped. - A write runs on a running campaign: the leads not contacted yet get a new sender from the pool, the contacted leads keep theirs. - Every write answers "data": the pool after the change, with the diagnostics of each sender and channel. Read them and report the errors to the user. - linkedin cannot be disabled alone: a sender in the pool is enabled on LinkedIn. Remove the sender instead.

disconnect_email_account

ChatGPT
Disconnect email account. Stops sending immediately. Cannot be undone. Use get_user_channels to find account ID.

disconnect_enrichment_provider

ChatGPT
Remove the team's saved API key for an external enrichment provider (Lusha, Apollo): lemlist stops calling it and it is dropped from the waterfalls. Only a connected external provider can be disconnected; anything else returns an error. Team-shared.

display_leads

ChatGPT
Show leads in an interactive workspace table. Users can select, filter, and push to campaigns. For the two-step pattern (lemleads_search in companies mode with description -> display_leads), you MUST pass the returned dataRef to this tool. The server resolves it to the full company set server-side and injects a currentCompanyByIds filter so the leads table queries leads at those companies. This persists in the workspace item (no 30-min TTL concern). Do NOT copy the companyIds array into a currentCompanyByIds filter inline — emitting hundreds of IDs inline risks truncation; pass them via the companyIds field instead and the server builds the filter for you. FOLLOW-UP TURNS over an existing companies table ("find CEOs of those companies", "now show me leads from this list", "and the marketing directors"): the workspace already holds the company set, so DO NOT re-run lemleads_search. Recovery order: 1. If the companies-mode lemleads_search dataRef from earlier is still in your context AND was issued in the last ~30 minutes, pass it as dataRef. 2. Otherwise, call get_workspace_items({ itemType: "companies" }), read data.companyIds from the workspace_companies item, and pass that array via the companyIds field on this tool. The server builds currentCompanyByIds for you (no truncation risk). Re-running lemleads_search to "re-verify the count" loses the curated set and is what produces the bad / off-topic lead lists. Otherwise (no prior companies table involved), call display_leads directly with filters only — the table loads data dynamically. Pass any combination of filters, dataRef, and companyIds together; they are AND-combined server-side. To modify an existing LEADS search: get_workspace_items -> display_leads with updated filters + replaceItemId. Only pass replaceItemId when the target item is itself a leads table — never to convert a companies/messages/other item into leads. To show a different artifact type alongside, omit replaceItemId so a new item is appended.

display_leads_page

ChatGPT
Fetch one page of People Database leads for the workspace-leads table. App-only: called by the leads grid iframe (pagination, select-all), not by the agent. Returns the raw Elastic hits the iframe transforms into rows, the total count, and the already-in-campaign / already-in-contacts maps.

display_table

ChatGPT
Display a data table in the workspace panel. CRITICAL RULES — follow exactly or the table will be empty: 1. dataRef: If the source tool returned a dataRef string, pass it as dataRef (preferred — avoids large data transfer). The rows will be resolved automatically from the stored reference. 2. rows: Only if no dataRef is available. Pass the ORIGINAL data array from the tool result AS-IS. NEVER construct row objects manually or map/transform the array. 3. columns: Use suggestedColumns from the source tool result directly when available. NEVER invent column keys — they must match actual property names in the row objects. PREFERRED: dataRef=result.dataRef, columns=result.suggestedColumns, groupKey=result.groupKey, tableType=result.tableType FALLBACK: rows=result.campaigns, columns=result.suggestedColumns WRONG: rows=result.campaigns.map(c => ({sentCount: c.stats.sentCount})) — this strips data Always forward groupKey and tableType from the source tool when available.

duplicate_campaign

ChatGPT
Duplicate a whole campaign server-side: the copy gets the same sequence, step for step, with every step's content carried over as-is. ALWAYS prefer this over rebuilding a campaign step by step whenever the user wants to copy / clone / duplicate / recreate an existing campaign, or start a new one "like" an existing one (e.g. "duplicate my Q3 campaign for the DACH market", "same campaign but for CTOs"). Reading a campaign then replaying its steps through add_sequence_step drops everything the read does not surface (LinkedIn invitation notes, images, videos, A/B variants, plain-text flag). This tool copies the underlying templates, so it drops nothing. To adapt the copy afterwards, duplicate first, then edit the new campaign's steps with update_sequence_step. Never reconstruct from scratch to change a few messages. Use when the user asks about: - Duplicating / cloning / copying an existing campaign - Starting a campaign from an existing one, for another market, persona or language - Re-running a past campaign on a new audience Contract rules reproduced by the backend: - Copies the sequence, its steps and their content, plus the campaign's schedules. Does NOT copy leads: pair with copy_campaign_leads when the user wants the same audience. - Send stats start at zero on the copy, and it is named "<source name> Copy" unless you pass a name. - Requires the createCampaign permission. - The source campaign is never modified.

enable_campaign_sender_channels

ChatGPT
Enable one or more channels on one sender of a campaign: a second mailbox, the WhatsApp account, a phone number. The other channels of the sender do not change. Also sets the send-as alias of a mailbox. A sender outside the pool joins it with the listed channels. Idempotent. Keywords: add Ana's WhatsApp on the campaign, send from this mailbox too, change the sending alias, send as hello@acme.com. Use when the user asks about: - Adding one channel to a sender already in the pool ("add Ana's WhatsApp on the Q4 campaign") - Changing the alias a mailbox sends from ("Ana must send from hello@acme.com"): pass the email channel with its id and the alias - Enabling every account of a type on a sender: pass the type without id, the healthy accounts are enabled Before any write: call get_senders with the campaignId. It lists every team member who can send, in the pool or not (inPool), with the user ids, the channel ids, the aliases, the health of each channel and requiredChannelTypes, the channel types that the steps require. Never guess a user id, a mailbox id, a WhatsApp account id or an alias. Load load_skill("senders") first: it tells which tool fits the request, when to change the sender strategy, and how to read the diagnostics. Contract rules reproduced by the backend: - A campaign with a dynamic sender strategy (contactOwner, leadImporter, customField:<field>) has a read-only pool that the leads fill: every pool write is refused. Switch the strategy with set_campaign_sender_strategy first, only when the user wants it. - A channel type is accepted only when a non-forced step of the campaign uses it: no WhatsApp channel without a WhatsApp step. Add the step first with add_sequence_step: it enables the type on each pool sender who owns an account of it, and lists the enabled types in senderPoolChange. - Every entry must be a team member who owns the listed channels; an alias must be one of the aliases of the mailbox. - LinkedIn has no channel to pick: a sender in the pool is enabled on LinkedIn. Never list linkedin with an id. - A pool with more than one sender needs the emailPro plan; the mailboxes above the plan allowance are dropped. - A write runs on a running campaign: the leads not contacted yet get a new sender from the pool, the contacted leads keep theirs. - Every write answers "data": the pool after the change, with the diagnostics of each sender and channel. Read them and report the errors to the user. For several senders at once, use add_campaign_senders. To remove a channel, use disable_campaign_sender_channels. No user confirmation required.

enrich_lead

ChatGPT
Enrich existing campaign lead. ASYNC — poll with bulk_get_enrichment_results (pass enrichmentIds: [id]). For non-campaign contacts use bulk_enrich_data. ALL options COST CREDITS.

find_watch_list_linkedin_urls

ChatGPT
Find real, validated LinkedIn URLs to monitor for a watch list type. Returns LinkedIn URLs (person /in/ or company /company/ depending on the type), sourced and validated from live web search and grounded in the team's business context (AI Context Center). Use when the user asks about: - Pre-filling the LinkedIn URLs for a linkedinPeopleProfile, linkedinCompanyProfile, competitorConnections or competitorReactions watch list - Discovering relevant LinkedIn profiles or company pages to watch Important: - Only the 4 LinkedIn signal types are accepted; any other type is rejected. - Pass an optional "focus" to steer the search; otherwise it falls back to the type's name. - Returns an empty list when no relevant URLs are found. - Rate-limited per user per day (a limited number of calls).

generate_campaign_for_watch_list

ChatGPT
Generate an AI-powered outreach campaign for an existing watch list. Builds a campaign from a predefined sequence template, writes every step's copy with AI (steered by copyStyle and the watch list's signal), links it to the watch list, and returns the campaign with its generated sequences. Use when the user asks about: - Turning a watch list's signals into a ready-to-run outreach campaign - Auto-generating a sequence + AI copy targeted at what the watch list monitors Flow: - Run it on an existing watch list, or create one first (create_watch_list), then call this tool to generate its campaign. - autoLaunch=true launches the campaign immediately; false leaves it in draft. Important: - The watch list (watchListId) must exist for the current team. - reviewBeforeSend only applies when autoLaunch=true.

get_ai_variable_prompts

ChatGPT
Read the AI-generation prompts of a campaign's AI variable columns (the AI-generated columns in the Leads table: icebreakers, openers, contextual openers, etc.). Returns one entry per AI variable: its variable name, the prompt used to generate it, and the AI model. Prompts come back verbatim, with raw {{variable}} placeholders (e.g. {{firstName}}) left unresolved, since a prompt is a template applied across all leads (there is no single lead to resolve against). Use when the user asks to: - See how an AI column (icebreaker, opener, ...) is generated for a campaign - Review or audit the prompt behind a campaign's AI variables before editing it Read-only: no credits consumed, nothing is changed. To edit a prompt, use update_ai_variable_prompt; to add a new column, use create_ai_variable_prompt.

get_business_context

ChatGPT
Return the published business context for the current team — markdown describing the company, its value propositions, ICP, products, etc., generated from the team's website. Use BEFORE: - crafting any messaging, sequence step, or persona suggestion - recommending watch list types (so the AI knows what the team sells and to whom) - onboarding flows that need to reference the team's business Response shape: - data: null → no business context generated yet (suggest running the onboarding generation) - data: { markdown, sourceUrl, generatedAt, lastEditedAt, updatedAt }

get_call_activities

ChatGPT
List call activities from the lemlist dialer (cold calls), with filtering and pagination. Use when the user asks about: - Calls made or received through the lemlist dialer - Filtering calls by campaign, lead, contact, user, status/disposition, direction, or date range - Reviewing call outcomes, durations, or which leads were called Each call includes direction, duration, status/disposition, phone provider, whether a recording exists, the AI transcript summary, and the associated lead/contact/campaign/user IDs. Use get_call_details with an activityId to fetch the recording URL and full transcript for a single call.

get_call_details

ChatGPT
Get full details for ONE dialer call by its activity ID, including the recording URL and transcript. Use when the user asks about: - The recording or transcript of a specific call - The full context of a call (lead, contact, company, campaign, call note, disposition) Returns call metadata, the associated lead/contact/company/campaign/user, the manual call note (if any), and — unless includeRecording is false — a temporary recording URL plus the full transcript. Use get_call_activities first to find the activityId.

get_call_statuses

ChatGPT
List this team's configured call statuses (dispositions) — the valid KEYS for update_call_status. Use when: - BEFORE update_call_status, to pick a valid status key for this team (keys are team-specific: defaults plus any custom statuses) - The user asks which call dispositions / statuses exist Returns each status with its key (what update_call_status expects), label, and side effects: whether it marks the lead answered / positive / negative and whether it pauses campaigns.

get_campaign_details

ChatGPT
Get configuration and settings for one or more campaigns (timezone, emoji, labels, senders, sequences). For metrics use get_campaigns_stats, for email content use get_campaign_sequences. MUTUALLY EXCLUSIVE — pass EITHER campaignId (single) OR campaignIds (1-20, batch), never both and never neither. Sending both fields together is rejected with a validation error. When campaignIds is used, the result is an array in the same order; a campaign that fails to load carries an "error" field on its entry while the others still succeed; a malformed id rejects the whole call.

get_campaign_senders

ChatGPT
Read the sender pool of a campaign, as the campaign editor shows it: each sender with its enabled channels, the selected alias of each mailbox and the diagnostics, plus the sender strategy, the ESP matching and whether the pool is read-only. Keywords: campaign senders, who sends this campaign, from which mailbox, sender pool, sender errors of a campaign. Use when the user asks about: - Who sends a campaign, and from which mailboxes, WhatsApp accounts or phone numbers - Whether a campaign can send: an empty pool, a sender with an error, a required channel without an account - The current pool before a removal: remove_campaign_senders and disable_campaign_sender_channels take the ids from here Returns: campaignId, senderStrategy, espMatching, readOnly (true on a dynamic strategy: the leads fill the pool, no pool write is accepted), senders[] (userId, name, email, channels[] with type, id, label, alias, esp, diagnostics; and the sender diagnostics) and diagnostics (campaign scope, for example no_account_connected for a required channel type). An enabled channel of a type that no step uses carries channel_not_required: it sends nothing. LinkedIn appears only when a step uses it. Only the senders in the pool appear: to see the whole team and who could be added, use get_senders with the campaignId. Load load_skill("senders") to interpret the diagnostics.

get_campaign_sequences

ChatGPT
Get the email sequences and their content (subject, body) for one or more campaigns. Useful for reviewing copywriting and email flow. CALL THIS TOOL whenever the user asks to: - Audit / review / check a campaign against standards, best practices, a checklist, or a "cahier des charges" - Verify compliance of a campaign built via the MCP - Assess whether manual steps have the right title / priority - Inspect the full step-by-step structure with task-mode metadata (manual, priority, title), CC recipients, and condition branches This is the authoritative source for per-step data — prefer it over the sidebar campaign context for any compliance or standards audit. LinkedIn invitation notes: a step can carry a second note besides "message", returned as "altMessage" and "altMessagePremium", both omitted when empty. On linkedinInvite, altMessage is the PREMIUM note (300 chars) sent when the sender has LinkedIn Premium. On linkedinSend, altMessage is the out-of-network note sent to leads who are not 1st-degree connections (200 chars for a standard account) and altMessagePremium its premium variant (300 chars). Carry BOTH over when copying or recreating a campaign: they are separate fields of add_sequence_step, and dropping them silently loses content the source campaign sends. A/B tests: when a step runs an A/B test, its top-level subject/message are variant A. The parallel variant B is returned in a "variantB" object, and "abTest" carries the test state (true while running, "A"/"B" once a winner is picked). Covers email, linkedinInvite, linkedinSend, linkedinInMail steps. Absent when the step has no A/B test. Always check both variants before answering questions about an A/B step. Task-mode fields returned per step (only surfaced when the step is manual — either manual=true on a togglable channel type, or a natively-manual type like phone/manual; auto steps get none of them): - manual: true when a togglable channel step (email, linkedinSend, linkedinInvite, linkedinVoiceNote, whatsappMessage, sms) is set to "Manual step" instead of auto-send. Absent when automated. Natively-manual types (manual, phone) don't emit this — they're always manual. - priority: task priority (0=Low, 1=Medium, 2=High). Absent when unset (None). - title: task title displayed in the review UI. Transfer steps (type "sendToAnotherCampaign") return "targetCampaignId" (the campaign the lead is moved to) and "leadAction" — what happens to the lead in THIS campaign afterwards: "continue", "pause" or "stop". A step with no stored value reads back as "continue". MUTUALLY EXCLUSIVE — pass EITHER campaignId (single) OR campaignIds (1-20, batch), never both and never neither. Sending both fields together is rejected with a validation error. When campaignIds is used, the result is an array in the same order; a campaign that fails to load carries an "error" field on its entry while the others still succeed; a malformed id rejects the whole call.

get_campaigns

ChatGPT
List campaigns with search, filtering and sorting. - Use 'search' to find campaigns by name (recommended first approach) - Use 'labels' to filter by campaign labels/tags (OR semantics, accepts names or lab_ IDs) - Each campaign in the response includes its labels as an array of names - Results sorted by newest first by default - Returns dataRef for display_table (pass dataRef + suggestedColumns) - Use get_campaigns_stats for detailed metrics with date filtering.

get_campaigns_reports

ChatGPT
Get lifetime stats for MULTIPLE campaigns in one call. Returns metadata, sender info, and 65+ metrics per campaign. No date filtering - for time-based analysis use get_campaigns_stats.

get_campaigns_stats

ChatGPT
Get detailed stats for one or more campaigns, with optional time-period filtering. Returns lead funnel, message metrics (total + per-channel breakdown), and per-step breakdown. Omit startDate/endDate for all-time (lifetime) stats. Timezone & date bounds: - startDate/endDate accept TWO formats: 1. Calendar day "YYYY-MM-DD" — interpreted as start/end of day in the given timezone. 2. Full ISO 8601 datetime ("2026-04-01T00:00:00+05:30", "...Z") — used as-is, the timezone arg is ignored. - When YYYY-MM-DD is used and "timezone" is omitted, the tool resolves the timezone in this order: (1) the calling user's profile.timezone if set in their Lemlist preferences, (2) Europe/Paris. - Lemlist has no per-team timezone setting; the UI shows numbers using the BROWSER's local timezone. To exactly mirror what a user sees in the Lemlist UI, capture campaignReportsTimePeriod from the UI session and pass those ISO 8601 strings here — that removes timezone ambiguity entirely. Aggregation matches the Lemlist UI Campaign Reports tiles: - messageMetrics.sent ↔ "Messages sent" tile (events: emailsSent + linkedinSent + linkedinInMailSent + linkedinVoiceNoteDone + smsSent + whatsappMessageSent). - messageMetrics.delivered ↔ "Delivered" tile (emailsSentNotBounced + linkedinSent + linkedinInMailSent + linkedinVoiceNoteDone + smsDelivered + whatsappMessageDelivered). - leadMetrics.reached ↔ "Leads reached" tile (unique leads with a contact activity). - channelMetrics.linkedinInvitationAccepted ↔ "Invitation accepted" tile (linkedinInviteAccepted events). - *Percentage fields use the same 1-decimal rounding as the UI: Math.round(x / total * 1000) / 10. - messageMetrics.perChannel mirrors the UI channel dropdown (email / linkedin / sms / whatsapp / others). - messageMetrics.perChannel.linkedin.breakdown splits sent and notDelivered into messages / inMails / voiceNotes; each trio sums to the aggregated counter of the same name. - notDelivered counts FAILED send attempts (linkedinSendFailed / linkedinInMailFailed / linkedinVoiceNoteFailed). It is DISJOINT from sent, never a subset of it: per-sub-type failure rate = notDelivered[x] / (sent[x] + notDelivered[x]). - For LinkedIn, delivered repeats sent (same underlying events — LinkedIn has no separate delivery confirmation), so a LinkedIn delivery rate is always 100% and carries no information. Do not report one. - Note: this is the same pipeline as the UI Campaign Reports tile — DIFFERENT from the Reports Dashboard (/reports-dashboards/) which aggregates leads by first-contact date with a different formula. Comparing this MCP output to Reports Dashboard tiles WILL produce mismatches by design. Use when the user asks about: - Campaign performance (overall or over a specific period) - Comparing stats across multiple campaigns - Monthly/weekly campaign analysis - Multi-channel reporting (email vs LinkedIn vs SMS vs WhatsApp) - Any campaign metrics or analytics

get_company_lists

ChatGPT
Get available CRM company lists. Returns each list's id, name and dynamic flag. Use when the user asks about: - Which company/account lists exist - Finding a clt_xxx list id before add_companies_to_list or import_companies_from_csv Contract rules reproduced by the backend: - dynamic (filter-driven) lists are returned with dynamic: true; their membership is computed by their own sync, so add_companies_to_list and remove_companies_from_list reject them - at most 100 lists, newest first, no paging — narrow with search

get_condition_field_options

ChatGPT
List the exact values a customLeadInfo condition accepts as its customField, for one campaign. Returns: - contactCustomFields: the team's contact custom fields as { name, display, type } — a condition stores the name, bare or as variables.<name> - campaignVariableKeys: the extra keys this campaign's own leads carry (empty when no campaignId is given, or when the campaign has no leads yet) - leadVariables: the built-in lead variable keys every campaign carries, written like a custom field - contactFields: the contact fields that have no lead-variable form, each already carrying the fields. prefix (e.g. fields.leadStatus) — copy the value as listed; any other name goes bare or under variables. Use when: - Before propose_sequence, add_sequence_step, update_sequence_step or edit_condition_branches with a customLeadInfo condition: call it once for the campaign, then pick customField from what it returns - The user names a field in their own words and you need the key the campaign actually stores - A write came back rejected as an unknown field Contract rules reproduced by the backend: - customField is a lead variable or a contact custom-field NAME (the stored name, not its display label), written bare or as "variables.<name>" — the campaign editor writes "variables.". Use "fields.<name>" only for the contact fields that have no lead-variable form, listed by get_condition_field_options (leadStatus, industry, location…). A field that does not exist for the campaign is rejected, unless the campaign has no lead variables yet, in which case it is accepted and checked when the campaign launches. - A contact custom field is addressed by its name, never by its display label. A label matching exactly one field is rewritten silently; one matching several is rejected. - When you passed a campaignId and campaignVariableKeys came back empty, that campaign has imported no leads yet: any lead-variable name is accepted there and checked again when the campaign launches. With no campaignId the answer is team-level only — it carries no campaign's keys, so it cannot tell you whether a given lead variable exists on one. - Anything else absent from these lists is rejected, with the known keys echoed back.

get_contact_fields_schema

ChatGPT
Get the list of available fields and relations on a contact (lead) or a company. Returns: - standardFields: scalar fields always present on the entity (email, firstName, … / name, domain, …) - customFields: team-specific scalar fields defined by the user - relations: one-to-many relations available as 'include' on search_campaign_leads (contact only) - csvImportTargets: the field keys a CSV column can be mapped to for this entity Use this BEFORE calling search_campaign_leads to know which 'fields' (scalar) and 'include' (relations) values are valid, and BEFORE import_contacts_from_csv / import_companies_from_csv to build columnMapping.

get_contact_lists

ChatGPT
Get available CRM contact lists. Returns each list's id, name and dynamic flag. Use when the user asks about: - Which contact lists exist - Finding a clt_xxx list id before add_contacts_to_list, import_contacts_from_csv or push_contacts_from_people_database Contract rules reproduced by the backend: - dynamic (filter-driven) lists are returned with dynamic: true; their membership is computed by their own sync, so add_contacts_to_list and remove_contacts_from_list reject them - at most 100 lists, newest first, no paging — narrow with search

get_crm_import_filters

ChatGPT
List the filters of the team's connected CRM (HubSpot lists, Salesforce list views or reports, Pipedrive filters) that leads can be imported from into a campaign. Keywords: import from CRM, HubSpot list, Salesforce list view, Salesforce report, Pipedrive filter. Use when the user asks about: - Importing leads from their CRM (HubSpot, Salesforce, Pipedrive) into a campaign. - Which CRM lists / views / reports are available for a campaign. Contract rules: - The CRM is the one the team is connected to; nothing to pick. - Returns at most 100 filters; total counts every match. When total is higher, call again with search. - Pass the returned id (and the same filterType for Salesforce) to import_crm_leads_to_campaign. - Salesforce only: ownerScopeUsers lists the team members who have their own Salesforce account connected. They are the ONLY valid userId values for import_crm_leads_to_campaign; never offer any other team member.

get_domain_dns

ChatGPT
Read the current DNS records for a domain (MX, SPF, DMARC, DKIM, CNAME, A…). Use in Step 4 of the outreach-infra skill to audit DNS state before writing new records, and to confirm the apex A record is healthy.

get_inbox_conversation

ChatGPT
Get the conversation thread (all messages) for a specific contact. Returns activities across all channels: email, LinkedIn, WhatsApp, SMS. - Provide a contactId (from get_inbox_conversations results) - Set markAsRead=true to mark the conversation as read (this mutates state) - Results are paginated with page/limit - Email bodies come back as the last reply in plain text (quoted thread history stripped); 'messageTruncated'=true and 'messageLength' (plain-text length before the cap) flag a body that was cut. Call display_inbox_conversation to show the full thread in the UI. - When a message came back truncated and the user needs its complete or original content, call again with fullBody=true to get the untrimmed original HTML bodies. Do this only when actually needed — it greatly increases the response size. - Each reply activity may carry the AI sentiment of the lead's reply: 'aiLeadInterestLevel' (integer 0-6, where 0-1 = rejection, 2-3 = neutral/automated, 4-6 = active-to-strong interest), 'aiLeadInterestScore' (the same signal on a 0-1 scale), and 'aiLeadInterest' ('positive' when level >= 4, 'negative' when level <= 1, else 'neutral'). Use these to spot leads who replied negatively or are not interested (exclude them from further outreach) as well as interested ones. Only AI-scored replies carry these fields; all are null otherwise. This is the AI's read of one reply, not the lead's Interested / Not interested marking: to list conversations by that marking, call get_inbox_conversations with 'interestFilter'. - LinkedIn messages reach the inbox through a periodic sync, so this tool first syncs the LinkedIn inbox of the current user and the teammates whose inbox holds this conversation (the first 5) when their last sync is over 15 minutes old. 'linkedinSync' reports, per user, 'status' and 'lastSyncedAt' (ISO timestamp of the last sync): - synced: just synced, the LinkedIn data is current - recent: a sync ran in the last 15 minutes, the data is current enough - rate-limited: a sync was started a few minutes ago; 'lastSyncedAt' gives the age of the data - paused: LinkedIn syncs are paused for everyone after repeated LinkedIn errors; 'lastSyncedAt' gives the age of the data - syncing: LinkedIn is still syncing. Call again in about a minute if the user is waiting for a reply - no-linkedin: the user has no LinkedIn account connected - failed: LinkedIn could not be fully fetched; the data may miss the latest LinkedIn messages - lastSyncedAt=null means no sync in the last 2 hours, not "never synced" - When the user asks why a LinkedIn reply is missing, answer from 'linkedinSync' rather than guessing

get_inbox_conversations

ChatGPT
List inbox conversations for one or more users. Returns conversations with contact info and last message preview. - Use 'userId' for a single member's inbox, or 'userIds' (usr_xxx, from get_users) to fetch several members at once (e.g. three specific teammates). When both are omitted, defaults to the current user. - Use 'listId' to filter by type: myConversations (default), unRead, favorites, snoozed, archived, sentOnly, teamConversations, formerMembers (conversations left behind by removed teammates) - Use 'search' to find conversations by contact name or email - Use 'statusFilter' to filter by the team's contact status — the CRM lifecycle stage on the contact (default set: New, Lead, Contacted, Opportunity, Customer, Evangelist; each team renames it freely): - statusFilter.in: status names (case-insensitive) or ids to include - statusFilter.out: status names or ids to exclude - statusFilter.unclassified: true to return only contacts with no status (cannot be combined with in/out) - an unknown name returns an error listing the team's statuses — read it and retry with one of those, never invent a status - Use 'interestFilter' for the Interested / Not interested marking a user sets from the heart control. This is a SEPARATE axis from statusFilter: "Interested" is almost never a contact status: - interestFilter.in: ["interested"] keeps conversations whose contact has a lead marked interested; ["notInterested"] likewise - interestFilter.out: ["notInterested"] drops conversations whose contact has a lead marked not interested - interest lives on the lead, so it spans ALL of the contact's campaigns and does not narrow with campaignFilter - accepts the human labels too ("Interested", "Not interested"), normalized for you - this is the lead MARKING (set by hand from the heart control, or by the campaign's AI automation when it is enabled). It is NOT the AI sentiment of a single reply: for that, read 'aiLeadInterest' on the activities get_inbox_conversation returns. The two diverge when a campaign has no AI automation: the reply scored positive, the lead was never marked, and interestFilter returns nothing - Use 'campaignFilter' to isolate conversations by campaign (e.g. replies from one campaign): - campaignFilter.in: campaign ids (cam_xxx, from get_campaigns) to include - campaignFilter.out: campaign ids to exclude - Use 'tagFilter' to filter by the custom inbox tags the user created (the labels on conversations, e.g. "Apresentação - E-mail"): - tagFilter.in: tag names (case-insensitive) or label ids to include - an unknown tag returns an error listing the available tags - Use 'dateFilter' to restrict by last-activity date range (from/to, ISO 8601, both inclusive), e.g. conversations since January up to now - userIds and the status / interest / campaign / tag / date filters can all be combined in a single call - There is no company filter: the inbox cannot be filtered by the contact's company server-side (derive it from the results downstream if needed) - Results are paginated. Use 'page' and 'limit' to navigate. - LinkedIn messages reach the inbox through a periodic sync, so this tool first syncs the LinkedIn inbox of the users in 'userIds' (the first 5), or the 'userId' user (the current user by default) when 'userIds' is omitted — so a teamConversations read without 'userIds' does not sync teammates when their last sync is over 15 minutes old. 'linkedinSync' reports, per user, 'status' and 'lastSyncedAt' (ISO timestamp of the last sync): - synced: just synced, the LinkedIn data is current - recent: a sync ran in the last 15 minutes, the data is current enough - rate-limited: a sync was started a few minutes ago; 'lastSyncedAt' gives the age of the data - paused: LinkedIn syncs are paused for everyone after repeated LinkedIn errors; 'lastSyncedAt' gives the age of the data - syncing: LinkedIn is still syncing. Call again in about a minute if the user is waiting for a reply - no-linkedin: the user has no LinkedIn account connected - failed: LinkedIn could not be fully fetched; the data may miss the latest LinkedIn messages - …

get_inbox_placement_result

ChatGPT
Read the result of an inbox placement test started with run_inbox_placement_test. The test is asynchronous, so call this with the testId until state is terminal. Two terminal states: "NoResult" means the seed emails never reached the provider, so there is nothing to measure — stop polling and report the test could not run, never present it as 0% inbox. "Completed" returns the per-provider breakdown (Google / Microsoft / SMTP) of where the email landed (inbox / promotions / spam / unreceived percentages), the overall split, and a deterministic pattern — trust this field, do not re-derive the thresholds: - "content_borderline": Google/Microsoft inbox but SMTP in spam → fix content, not infra - "deeper_issue": Google/Microsoft also failing → warmup, blacklist, or egregious copy - "all_clean": no content issue, look elsewhere - "mixed": no single dominant pattern; report the per-provider numbers

get_lemleads_filters

ChatGPT
Get available filters for People Database searches. Call this FIRST before lemleads_search or display_leads/display_companies. Returns filter IDs with valid values.

get_message_template

ChatGPT
Read one library message template: name, subject, HTML body, cc, tags, creator and creation date. Use ONLY when the user explicitly asks about templates: - What a template says, or comparing templates by their body - Applying a template to a step or a reply, at the user's request: after list_message_templates, read the one picked here and copy its exact content into the step as is (subject and body). This fetch is mandatory: the messagePreview of the list is never a substitute for the body Only when the user explicitly asks for a template (by name, tag, author or "use a template"). Never list or apply templates on your own: writing or editing a step from a template is not the default, the default is to write the step yourself. Contract rules reproduced by the backend: - The body is HTML as stored and returned whole, with {{variables}} left raw: keep them as they are when reusing it. Pick the template from list_message_templates (name, subject, messagePreview) before fetching; messageLength there tells how heavy the fetch is. - Not found covers a deleted template, another team's template and a campaign step body alike.

get_senders

ChatGPT
List who can send in the team, and with which channels: every team member with the mailboxes (and their aliases), the LinkedIn profile, the WhatsApp accounts and the phone numbers they own, each with its id, its health diagnostics and, with a campaignId, whether the campaign pool enables it (inPool). Keywords: available senders, sender candidates, who can send, connected accounts of the team, mailbox id, sender health. Use when the user asks about: - Who in the team can send, or owns a given channel ("who has a connected WhatsApp account?") - Adding or changing senders on a campaign: call it FIRST, with the campaignId, to get the user ids, the channel ids, the aliases and requiredChannelTypes that add_campaign_senders, replace_campaign_senders and enable_campaign_sender_channels need - Why a sender cannot send: a disconnected mailbox, a missing seat, a LinkedIn profile linked on another account Returns: senders[] with userId, name, email, inPool, channels[] (type, id, label, aliases, esp, inPool, diagnostics) and diagnostics (user scope: license_missing, sender_paused). With a campaignId: campaignId, senderStrategy and requiredChannelTypes (the channel types that the steps use; only those can be enabled). On a dynamic strategy the list keeps the senders and the channels that the sender exclusion list in force (campaign override, else team) does not exclude: they are all in the pool. Load load_skill("senders") to interpret the diagnostics and to choose the next tool. To read the exact pool of a campaign (active channels, selected alias, campaign-level diagnostics), use get_campaign_senders.

get_sequence_step_sender

ChatGPT
Read the forced sender of one sequence step. Use when the user asks: - Who sends a given step, or from which mailbox, WhatsApp account or phone number - Whether a step uses the campaign sender pool or a pinned team member - Before set_sequence_step_sender or clear_sequence_step_sender, to show the current sender Contract rules reproduced by the backend: - The result has no sender when the step uses the campaign sender pool. - sender.channel is absent on a call, manual, API or phone step: those steps pin a user, not an account. - Get the step ids from get_campaign_sequences.

get_settings

ChatGPT
Retrieve settings for a given entity type. Available types: - "campaign": Behavior settings for a specific campaign — what happens on reply/link click/meeting booked (continue, pause, or stop the lead + propagate to company), tracking (opens/clicks/replies), sender strategy, AI features (reply scoring, actions on interested/not-interested leads with pause propagation), auto-review rules. Requires id (cam_xxx). - "sending": The user's ACTUAL daily email sending limits and capacity. Returns: each email mailbox with its dailyEmailLimit (max emails/day), and BCC address. Use this when the user asks about email sending volume, daily email limits, or how many emails they can send. No id needed. To CHANGE a mailbox's dailyEmailLimit, use update_settings with { type: "sending", id, dailyEmailLimit }. - "linkedinLimits": The user's daily LinkedIn action limits, per day: follows, invites, messages, profile visits, InMails, network checks, invitation withdrawals, last-post likes, endorsements, last-post retrievals and comments, plus the invite ramp-up (daily increase of the invitation limit, null when off). No id needed. The InMail limit is a daily throttle; the monthly InMail credits of the LinkedIn plan stay the hard cap. To CHANGE them, use update_settings with { type: "linkedinLimits", id: usr_xxx, ...limits } (the caller's own id is the userId returned here). - "team": Team configuration — custom tracking domain, active beta flags, connected CRM type. - "lemwarm": Warmup (lemwarm) configuration for one or more mailboxes — whether warmup is active, ramp-up speed, warmup-specific daily max, and a Warmup Score snapshot (score 0-100, lastAt) refreshed daily by a cron. The Warmup Score snapshot may be undefined for brand-new mailboxes. This is NOT about sending limits. Lemwarm is a separate feature that warms up a mailbox's reputation. Requires id (usm_xxx), OR ids (usm_xxx, 1-20) to fetch several at once — bulk returns an array in the same order with a per-mailbox "error" field on failures. IMPORTANT — "sending" vs "lemwarm" are DIFFERENT things: - "sending" = production sending capacity (how many emails the user can send per day). Use for questions about daily limits, volume, capacity. - "lemwarm" = email warmup strategy (a separate feature to build domain reputation). Use ONLY when the user specifically asks about lemwarm/warmup. Do NOT confuse the two. When asked about daily email limits or sending volume, ALWAYS use type="sending". Do NOT use this to check whether channels are connected — use get_user_channels for that. Do NOT use this to get campaign sequences or steps — use get_campaign_sequences for that. Do NOT use this to get campaign stats — use get_campaigns_stats for that.

get_snippet

ChatGPT
Read one snippet: name, HTML content, creator and creation date. Use ONLY when the user explicitly asks about snippets: - What a snippet contains - Inserting a snippet into a message, at the user's request: after list_snippets, read the one picked here and copy its exact content into the message as is. This fetch is mandatory: the contentPreview of the list is never a substitute for the content Only when the user explicitly asks for a snippet (by name, author or "use a snippet"). Never list or insert snippets on your own: composing a message from a snippet is not the default, the default is to write the message yourself. Contract rules reproduced by the backend: - The content is HTML as authored and returned whole, with {{variables}} left raw: keep them as they are when reusing it. Pick the snippet from list_snippets (name, contentPreview) before fetching; contentLength there tells how heavy the fetch is. - Not found covers a deleted snippet and another team's snippet alike.

get_statistics

ChatGPT
Retrieve statistics for one or more entities of the same type in a single call. Available types: - "lemwarm": Warmup email stats per mailbox over the last N days. Returns per-day counters (sent, inbox, spam) and aggregated totals over the period for each mailbox. Requires ids (usm_xxx). Only one type per call, but multiple ids (1-20) can be passed. Defaults to a 30-day window applied to every id. Returns an array with one entry per requested id (same order). Failed lookups carry an "error" field on their entry; other ids still succeed. Use this when the user asks about lemwarm performance, warmup volume, or spam rate for one or several mailboxes. Do NOT use to read warmup configuration — use get_settings type="lemwarm" for that. Do NOT use to read campaign performance — use get_campaigns_stats for that.

get_task_content

ChatGPT
Load the effective message content (subject + body) of a message-based task (opportunity) — the exact content the Focus-mode editor would show and that send_task would send. Use when the user asks about: - Previewing what an email / LinkedIn / WhatsApp task will actually send - Checking whether a task already has content before sending or editing it Where the content comes from: - The per-lead draft override if one exists, otherwise the task's template. This is the real outgoing body — get_tasks rows carry no body, and that must NOT be read as "empty". What it returns: - 'subject'/'message': the effective content ('subject' only for email tasks). - 'hasContent': false ONLY when the body is genuinely empty (the real "no content" case). - 'sendable': true when the content is ready for an immediate send_task (message-based, NON-manual task with a body — and a subject for email). Status, contact and sender are still validated by send_task itself. - 'schedulable': true when the task is a manual campaign step of a message type — it cannot be sent immediately, but send_task will schedule it via the campaign and mark it done ("Schedule & mark done"). A message task is either sendable or schedulable, never both. Read-only: it never edits or sends. Only email / linkedinSend / whatsappMessage tasks carry content; other types return empty content and sendable=false.

get_tasks

ChatGPT
Search the team's tasks (the Tasks page) with the GET /api/v2/tasks params: call/phone, manual, LinkedIn, email, SMS and WhatsApp tasks, open or already done. Use when the user asks about: - What is still to do ("what tasks do I have", "my high-priority calls this week") → status:["pending"] - What was already done, on a contact, a company, by a rep or team-wide ("what did we complete on this account last month") → status:["done"], sortBy:"handledOn" - Tasks of a campaign, owner, contact, company, type or date range - Which message tasks can be sent now, before send_task → status:["due","upcoming"], type:["email","linkedinSend","whatsappMessage"] Contract rules reproduced by the backend (field rules are in each field's description): - Omitting 'status' returns every status, closed tasks included. - 'total' is the exact count of matches — read it directly, never page through the list to count. page × limit is capped at 10000: past that, narrow the filters. - The list is an index refreshed within seconds: a task closed a moment ago may still show as pending. Right before a bulk send_task, re-list, and never re-send a task already sent in this conversation. Each row carries _id, type, channel, status, priority, dueDate, completedAt, ownerName, contactId, companyId, campaignId and leadId — copilot gets these as a preview with the task 'name'; full rows (external clients, and behind the dataRef) add 'task' (the display_table Task cell) and the other task fields. Use contactId / companyId to chain to that record's history rather than searching it by name. 'filters' echoes the query actually run. To show the tasks in a table, call display_table with dataRef=result.dataRef, columns=result.suggestedColumns, tableType=result.tableType. Pass the dataRef — do NOT pass result.tasks as rows: 'tasks' is a preview for your reasoning only, and the full rows are resolved from the dataRef. IMPORTANT — rows carry no message body, and that does NOT mean the task is empty: the subject/body of email / linkedinSend / whatsappMessage tasks lives in the task's template. To preview what will be sent, call get_task_content; to send, call send_task.

get_team_info

ChatGPT
Get basic team info (ID, name, plan, credits remaining, team's company domain — used as the seed for outreach domain suggestions) and minimal identity of the caller (current user id + email). Pass includeMembers: true to also get the full team member list (id, name, email, role) in the same call. For richer per-user details call get_users with userIds: ["me"] for the caller, userIds: ["all"] for the full member list, or userIds: ["usr_xxx", ...] for one or more team members.

get_team_overview

ChatGPT
Account summary: campaign count by status. Use get_campaigns for the full list with names and details.

get_unsubscribes

ChatGPT
List unsubscribed emails with pagination. Use delete_unsubscribe to re-enable.

get_user_channels

ChatGPT
Check connected sending channels (email, LinkedIn, WhatsApp). Returns connection status, plan availability, and accounts. WhatsApp requires separate addon purchase. Use show_connect_channel to guide setup (one channel at a time).

get_users

ChatGPT
Get team member information in a single call. userIds is required; each item is either: - "all": expands to every team member, lightweight shape (id + name + email + role only) — use when the user wants the team roster. - "me": the caller, full profile. - "usr_xxx": an explicit team member id, full profile. Returns an array. For "all", one entry per team member. For "me"/"usr_xxx", entries are returned in input order; full-profile entries include role, LinkedIn connection status and limits, and connected mailboxes with their providers and limits. Failed lookups carry an "error" field on their entry; other ids still succeed. Use when the user asks: - "list the team", "who's on the team?" → { "userIds": ["all"] } (was get_team_members) - "who am I?" / "what's my email?" → { "userIds": ["me"] } - Details on specific team members → pass their usr_xxx ids (1-20) Notes: - Mixing "all" with specific ids is allowed but redundant: "all" already covers everyone; mixed calls return the "all" lightweight roster only. - For a minimal identity (id + email only) of the caller, get_team_info already returns it in one call — prefer that when you just need to know who you're talking to.

get_watch_list_filter_value

ChatGPT
Resolve the valid values for one or more watch list filterIds. ALWAYS call this tool to get correctly formatted values before passing them to create_watch_list or update_watch_list. Do not guess or hardcode values. filterId MUST come from list_watch_list_filters — call it first to discover which filterIds are valid for the signal type you are configuring. ALWAYS pass "type" (the signal type you are configuring): some filters resolve against a type-specific vocabulary, so omitting it can yield values the signal rejects. BATCH your lookups: pass every value you need across every filterId in a single call via "requests" (one entry per filterId, with all its queries grouped in "queries"). Do NOT call this tool repeatedly one value at a time. The backend automatically picks the right resolution strategy for each filter. Pass query strings when searching for specific values (e.g. job titles, companies, locations). Each filterId returns the deduplicated union of its queries' matches. An empty results array means no match — do not invent values. The returned matches are candidates, not a selection: a single query can resolve to many values. Do NOT pass all of them back — keep only the ones that actually fit the watch list you are configuring, and drop the rest. If an entry comes back with a non-empty "error" and results null, fix what it describes (e.g. provide a query) and call this tool once more for that filterId.

get_webhooks

ChatGPT
List all configured webhooks. Returns array with _id, targetUrl, createdAt, type, campaignId, isFirst.

import_companies_from_csv

ChatGPT
Import companies into a company list from an uploaded CSV. Use when the user asks about: - Loading a CSV/spreadsheet of companies or accounts into a company list - Bulk-adding companies to the CRM Company columns are mapped to their bare keys here (name, domain, industry, …) — the company-prefixed form belongs to a contact CSV. Flow (3 steps, in order): 1. create_upload_url → get an uploadUrl + uploadKey 2. PUT the raw CSV to uploadUrl 3. this tool with the uploadKey Contract rules reproduced by the backend: - the list must exist, hold this entity type, and not be dynamic (filter-driven lists are filled by their own sync) - uploadKey must come from create_upload_url; a made-up key is rejected - at least one column must be mapped, and no mapping may target a reserved key (_id, teamId, …) - a "customField" target only lands if that custom field already exists on the team - every imported entity — created, updated or unchanged — is added to the list A target that matches no field is re-resolved server-side from the header and a sample value (so an export header like companyWebsite still fills companyDomain). Whatever it corrects comes back as repairedColumns; anything it cannot place comes back as ignoredColumns — those columns were NOT imported, so check them before reporting success. An owner column (ownerId, companyOwnerId — see get_contact_fields_schema) holds a team member email or usr_ id per row. It is re-resolved only when its header names an owner and its cells hold emails or usr_ ids: map it explicitly. An empty cell means no owner (a matched entity loses its owner only under updateStrategy "overwrite"). Its cells naming no team member come back as unknownOwners (one entry per column, with a count): tell the user those rows kept their previous owner, or got you as the owner when created.

import_contacts_from_csv

ChatGPT
Import contacts into a contact list from an uploaded CSV. Use when the user asks about: - Loading a CSV/spreadsheet of people into a contact list - Bulk-adding contacts to the CRM without starting a campaign (To put leads into a campaign instead, use import_leads_to_campaign.) A contact CSV can also carry the company columns: map them to the company-prefixed keys (companyName, companyDomain, …) and the company is created and linked in the same pass. A row whose company domain or company LinkedIn URL is invalid is rejected, contact included, and comes back in errors (companyDomainInvalid, …): fix the cell and import that row again. So is a row whose company columns point at two different existing companies (conflictingCompanies): keep the identifiers of one company only. Flow (3 steps, in order): 1. create_upload_url → get an uploadUrl + uploadKey 2. PUT the raw CSV to uploadUrl 3. this tool with the uploadKey Contract rules reproduced by the backend: - the list must exist, hold this entity type, and not be dynamic (filter-driven lists are filled by their own sync) - uploadKey must come from create_upload_url; a made-up key is rejected - at least one column must be mapped, and no mapping may target a reserved key (_id, teamId, …) - a "customField" target only lands if that custom field already exists on the team - every imported entity — created, updated or unchanged — is added to the list A target that matches no field is re-resolved server-side from the header and a sample value (so an export header like companyWebsite still fills companyDomain). Whatever it corrects comes back as repairedColumns; anything it cannot place comes back as ignoredColumns — those columns were NOT imported, so check them before reporting success. An owner column (ownerId, companyOwnerId — see get_contact_fields_schema) holds a team member email or usr_ id per row. It is re-resolved only when its header names an owner and its cells hold emails or usr_ ids: map it explicitly. An empty cell means no owner (a matched entity loses its owner only under updateStrategy "overwrite"). Its cells naming no team member come back as unknownOwners (one entry per column, with a count): tell the user those rows kept their previous owner, or got you as the owner when created.

import_leads_to_campaign

ChatGPT
Import leads into a campaign from a CSV uploaded with create_upload_url — the fast path for files of hundreds/thousands of leads (no lead data passes through the conversation). Flow: 1. create_upload_url({ purpose: "leadsCsv", fileName, fileSize }) → uploadUrl + uploadKey 2. PUT the file to uploadUrl (curl -T) 3. import_leads_to_campaign({ campaignId, uploadKey, columnMapping }) columnMapping maps each CSV header to a lead field (read the file's header row to build it). The server downloads, parses, dedups, optionally enriches, and bulk-inserts in one pass, then returns compact counters ({ total, added, updated, skipped, failed }) plus a capped list of failure rows — NOT the full list of imported leads. ⚠️ Enrichment options (findEmail, verifyEmail, linkedinEnrichment, findPhone) COST CREDITS per lead — warn the user before enabling.

launch_campaign

ChatGPT
Launch a DRAFT campaign: review and send its already-scanned leads now, and optionally enable auto-review so future leads launch on their own. Keywords: launch campaign, start draft, review scanned leads, go live, send campaign. Use when the user asks about: - Launching / going live with a draft campaign that has leads ready to send. - Reviewing and sending the scanned leads sitting in a campaign. - Making a campaign send new leads automatically as they are added (auto-review). To resume a PAUSED campaign use set_campaign_state (action "start"); this tool is for a draft that has never sent. In an interactive UI, prefer review_and_launch_leads so the user can review leads visually; use this tool for auto-review, large backlogs (>100 scanned), or a non-interactive launch. Two-phase confirmation flow: 1. First call WITHOUT userConfirmed (or userConfirmed=false): returns status="awaiting_confirmation". Ask the user to confirm launching, and also ask whether to enable auto-review for future leads, then call again. 2. Once the user has explicitly approved, call again with userConfirmed=true and autoReview set to the user's choice to review and send. Contract rules: - Sends real messages to leads — requires explicit user confirmation. - Launching always reviews and sends the current scanned backlog, regardless of autoReview. - autoReview is an explicit, user-chosen option that governs ONLY future leads; it defaults to off. Ask the user before enabling it. - Only leads in the "scanned" state are launched; leads already reviewed are left untouched (idempotent on re-run). - Leads still enriching or missing required AI variables are skipped, not sent. - The campaign must have a sequence and no blocking step errors.

list_campaign_folders

ChatGPT
List all campaign folders for the current team, with their hierarchy (parentId). Use when the user asks about: - Seeing their campaign folder structure. - Finding a folder's ID before renaming / moving / deleting it, or before filing campaigns into it. The hierarchy is expressed by parentId (absent = root-level folder).

list_domains

ChatGPT
List all domains owned by the team (registrar, status, mailbox count). Use to audit the team's existing sending surface before proposing new purchases in the outreach-infra skill.

list_enrichment_providers

ChatGPT
List the enrichment provider catalog — every provider the team can use in its waterfalls. Each entry has 'id' (the legal waterfall provider id), 'displayName', 'types' (email and/or phone it can serve) and 'source' ('internal' = lemlist credits, 'external' = the team's own API key / BYOK); external providers also carry 'connected' (whether the team saved an API key). Read this before connect_enrichment_provider to get the connectable external provider ids and their state, or before update_enrichment_waterfalls for the legal provider ids. Team-shared and free (no credits).

list_enrichment_waterfalls

ChatGPT
List the team's enrichment waterfalls — the ordered providers lemlist tries when finding an email or phone. Returns the default waterfall of each type (email, phone) plus any custom ones; each provider comes with its execution 'order', 'enabled' flag and 'source' ('internal' = lemlist credits, 'external' = the team's own API key). Read this before update_enrichment_waterfalls to get the current order and the legal provider ids. Team-shared and free (no credits).

list_mailboxes

ChatGPT
List the team's mailboxes with their IDs (dem_xxx), email, status, and currently assigned SDR (assignedToUserId). Optionally filter by domainId. Use to audit which user owns each mailbox — and as the discovery step before update_mailbox when the user wants to re-assign mailboxes (e.g. when an outreach infrastructure was provisioned without per-SDR assignToUser and every mailbox ended up on the API key owner). Each mailbox also carries a deterministic ageDays (no date math needed), and the response includes a providerMix summary { google, microsoft, smtp, total, smtpSharePct, smtpHeavy } where smtpHeavy is true when SMTP > 70% (structurally disadvantaged). Trust these computed fields.

list_message_template_tags

ChatGPT
List the distinct tags used on the team's library message templates, alphabetically. Use ONLY when the user explicitly asks about templates or their tags: - How the templates are organised, or which tags exist before filtering list_message_templates - Resolving a tag the user names approximately into the stored one Never call it on your own to look for a template the user did not ask for. Contract rules reproduced by the backend: - Tags are stored trimmed and lowercased; pass them as returned to list_message_templates. - userIds narrows to the tags of those creators' templates.

list_message_templates

ChatGPT
List the team's library message templates: whole reusable messages (a subject and an HTML body) saved from the Templates page or an editor, usable as a step on any channel, an inbox reply or an extension message. Use ONLY when the user explicitly asks about templates: - Which templates the team has, or finding one by name, tag or author - Applying a template to a step or a reply, at the user's request: list here, pick the right one from name, subject and messagePreview, read it with get_message_template, then copy its content into the step Only when the user explicitly asks for a template (by name, tag, author or "use a template"). Never list or apply templates on your own: writing or editing a step from a template is not the default, the default is to write the step yourself. Contract rules reproduced by the backend: - Returns summaries: no body, but messagePreview (the first characters of the body as plain text, variables kept) and messageLength (characters of the HTML body a fetch returns). Pick a template from name, subject and messagePreview, then call get_message_template for the body; messageLength tells how heavy that fetch is. messagePreview is for choosing only: it is plain text, cut, and NEVER the content to write into a step. Always fetch the single template to get the exact HTML body. - search matches the name only, case-insensitive, accents folded on a c e i n o u y. - tags is any-of; userIds is any-of and is a creator filter (usr_xxx from get_users). - Newest first; pages of at most 100. A team can hold hundreds of templates, so do the finding yourself: narrow first with search (a word of the name), tags (read the existing tags with list_message_template_tags) and userIds (resolve the author with get_users), then page with page=2, 3... up to pagination.totalPages when the match is not on the first page or the user wants the whole set. Never stop at page 1 and conclude a template does not exist. - Campaign step bodies are never listed here: read a campaign's steps with get_campaign_sequences.

list_people_database_personas

ChatGPT
List the People Database personas saved by the current team. A persona is a named, reusable set of People Database filters (an audience definition). Call this FIRST whenever a persona id is needed — a persona id (pdp_xxx) can only be obtained from this tool or from create_people_database_persona. Use when the user asks about: - Which personas / saved audiences the team has - Applying "our ICP" or a persona by name (resolve the name to its id here) - Picking the persona to attach to another resource Each entry returns _id, name, mode, filters, createdAt and updatedAt.

list_saved_searches

ChatGPT
List the team's saved People Database searches, sorted by name. Optionally filter by mode. Use when the user asks about: - Their saved searches ("my saved searches", "do I have a saved search for X") - Reloading, renaming, updating or deleting a saved search by name (resolve the name to its id here) Each entry returns _id, name, mode, filters, createdAt and lastResultsCount. Re-run one with lemleads_search using its mode and filters.

list_snippets

ChatGPT
List the team's snippets: reusable HTML fragments (a signature, a paragraph, a call-to-action) that are inserted INSIDE a message, unlike a message template which is a whole message. Use ONLY when the user explicitly asks about snippets: - Which snippets the team has, or finding one by name or author - Inserting a snippet into a message, at the user's request: list here, pick the right one from name and contentPreview, read it with get_snippet, then copy its content into the message Only when the user explicitly asks for a snippet (by name, author or "use a snippet"). Never list or insert snippets on your own: composing a message from a snippet is not the default, the default is to write the message yourself. Contract rules reproduced by the backend: - Returns summaries: no content, but contentPreview (the first characters of the content as plain text, variables kept) and contentLength (characters of the HTML content a fetch returns). Pick a snippet from name and contentPreview, then call get_snippet for the content; contentLength tells how heavy that fetch is. contentPreview is for choosing only: it is plain text, cut, and NEVER the content to insert into a message. Always fetch the single snippet to get the exact HTML content. - search matches the name only, case-insensitive, accents folded on a c e i n o u y. - userIds is any-of and is a creator filter (usr_xxx from get_users). - Sorted by name; pages of at most 100. A team can hold many snippets, so do the finding yourself: narrow first with search (a word of the name) and userIds (resolve the author with get_users), then page with page=2, 3... up to pagination.totalPages when the match is not on the first page or the user wants the whole set. Never stop at page 1 and conclude a snippet does not exist.

list_voice_profiles

ChatGPT
List the AI voice profiles available for LinkedIn AI voice note steps (recordMode="ai"): lemlist default voices and the team's own cloned voices. Use this BEFORE setting a voice on a linkedinVoiceNote step. Pass the returned voiceId to add_sequence_step (or in a propose_sequence step) together with recordMode="ai" and the script in message. Only ids returned here are accepted; a voiceId from another team is rejected.

list_watch_list_filters

ChatGPT
Return, for each watch list type, the filters is allowed to set. Each filter entry includes: - filterId (e.g. "title", "companyIndustries", "location") - name (human-readable label) - properties: which sides (in / out) the form exposes for this filter - required (optional): sides that must be non-empty for the watch list to be valid - requiresQuery (optional): when true, get_watch_list_filter_value MUST be called with a query for this filter; when absent/false, omit the query to get the full set Use BEFORE create_watch_list or update_watch_list to pick valid filterId values and to know which "in"/"out" sides to populate. Omit "type" to get the matrix for all types.

list_watch_list_library

ChatGPT
List the catalog of watch list signal types available on the platform. Use BEFORE create_watch_list to: - know which "type" values are valid (e.g. "companyIsHiring", "jobChange", "linkedinKeywords") - understand what each type monitors (title, description) Each entry includes type, title and description.

list_watch_list_signals

ChatGPT
List the signals captured by watch lists for the current team, with filtering and pagination. Use when the user asks about: - Reviewing newly received signals - Filtering signals by type, status, watch list, or date range - Paginating through historical signals Each signal includes its watch list, type, computed status, received date, and the enriched entity (contact or company).

list_watch_lists

ChatGPT
List watch lists of the current team, with optional type/status filters and pagination. Use when the user asks about: - Browsing the watch lists configured on the team - Finding a watch list by type (e.g. all hiring watch lists) - Filtering watch lists by status (active, draft, inactive, etc.)

load_skill

ChatGPT
Load specialized guidance for a specific domain. Returns the skill content to guide your responses. When to load campaign-builder: - User wants to create, design, write, optimize, or troubleshoot campaigns - User asks about email/LinkedIn limits, warmup, deliverability - User needs help with sequence patterns When to load copywriting: - Before writing any outreach messages (emails, LinkedIn, WhatsApp) - When user asks for help improving existing copy - When creating campaign sequences with messages When to load api-reference: - Mandatory before `call_api` — the server rejects call_api until this skill is loaded in the current session - Loading the skill returns the live catalog of available endpoints (method + path + description), cached 1h from the OpenAPI spec - When unsure whether an operation is supported by the API When to load lead-search: - ALWAYS load before searching for leads or companies in People Database - EXCEPT in the People Database scope ("Ask lemAgent" panel): there the page runs the search — use people_database_nl_to_filters, not this skill - User wants to find leads, build lead lists, search companies - Before calling lemleads_search or display_leads/display_companies - Contains all filter definitions — no need to call get_lemleads_filters after loading this skill When to load campaign-analysis: - User asks to analyze or compare existing campaigns and the leads inside them - Which leads are in several campaigns at once (overlap, "in both X and Y"), or dedup/compare leads across campaigns - This is search_campaign_leads territory; building a campaign is campaign-builder, People Database prospecting is lead-search When the user mentions buying / ordering domains or mailboxes — ASK FIRST, do NOT auto-load outreach-infra: The intent is ambiguous. Ask the user which they want, then branch: - Buy directly — they already know what they want and just want to place the order. Do NOT load outreach-infra. Drive the purchase tools directly: calculate_infrastructure → search_domains → purchase_domain → provision_mailboxes, respecting each tool's confirmation step. - Help setting up their infrastructure — they want guidance on how much to buy, ESP matching, DNS, or warmup planning. Load outreach-infra for the full multi-step plan. If the load fails because the feature is not enabled for the team, offer the direct purchase path instead. When to load outreach-infra: - User wants help sizing / planning / setting up cold-email sending infrastructure - User asks about how many mailboxes / domains they need - User mentions DNS setup, ESP matching, or warmup orchestration - Any request that implies multi-step deliverability planning When to load deliverability-audit: - User wants to audit / review / improve the deliverability of an existing account - User reports a drop: reply/open rate fell, emails landing in spam, spam complaints, bounces rising - User says their domains are "burned" or asks how to recover a sending domain - User wants to check warmup scores, sending limits, custom tracking domains, or run an inbox placement test - Any "why are my emails going to spam" / "is my sending setup healthy" request - This is the run/optimize counterpart to outreach-infra (build): load it to analyze a setup that is already running Main skill sections (campaign-builder only): - No section → Main workflow (campaign-builder.md) - sequence-patterns → Ready-to-use sequence templates - strategy → Strategy framework and ICP definition - channels → Channel constraints and daily limits

merge_companies

ChatGPT
REQUIRES CONFIRMATION: Merge up to 10 lemlist companys into one. The survivor (primaryId) keeps its id and every value it holds; its empty fields are filled from the other companys, which are then deleted. Everything attached to a deleted company (activities, tasks, list memberships, contacts) is moved to the survivor. When a CRM is connected, the survivor keeps its CRM link (or adopts the first linked company's when it has none) and receives the merged values there; lemlist never merges or deletes anything in the CRM itself, so the other companys' CRM records stay in the CRM, unlinked from lemlist. Use when the user asks about: - Deduplicating companys that describe the same organization - Cleaning up duplicates after an import or a CRM sync Two-phase confirmation flow: 1. First call WITHOUT confirmed (or confirmed=false): returns the resolved records with their names and status="awaiting_confirmation". Show them to the user and ask which one must survive. 2. Once the user has explicitly approved, call again with confirmed=true to run the merge. Contract rules reproduced by the backend: - A company is addressed by its lemlist id (cpn_xxx). - primaryId must not appear in secondaryIds, and each id is listed once - A company being enriched cannot be merged yet (ENTITY_MERGE_ENRICHMENT_IN_PROGRESS): retry once it finishes - The merge folds one record at a time and STOPS at the first failure: mergedIds are done, remainingIds still exist - There is no undo: deleted companys are gone

merge_contacts

ChatGPT
REQUIRES CONFIRMATION: Merge up to 10 lemlist contacts into one. The survivor (primaryId) keeps its id and every value it holds; its empty fields are filled from the other contacts, which are then deleted. Everything attached to a deleted contact (activities, tasks, list memberships, campaign leads) is moved to the survivor. When a CRM is connected, the survivor keeps its CRM link (or adopts the first linked contact's when it has none) and receives the merged values there; lemlist never merges or deletes anything in the CRM itself, so the other contacts' CRM records stay in the CRM, unlinked from lemlist. Use when the user asks about: - Deduplicating contacts that describe the same person - Cleaning up duplicates after an import or a CRM sync Two-phase confirmation flow: 1. First call WITHOUT confirmed (or confirmed=false): returns the resolved records with their names and status="awaiting_confirmation". Show them to the user and ask which one must survive. 2. Once the user has explicitly approved, call again with confirmed=true to run the merge. Contract rules reproduced by the backend: - A contact is addressed by its lemlist id (ctc_xxx) or by one of its email addresses. - primaryId must not appear in secondaryIds, and each id is listed once - Email addresses: the survivor keeps its own; a loser's addresses are dropped, unless the survivor has none (then it takes the first loser's) - A contact being enriched cannot be merged yet (ENTITY_MERGE_ENRICHMENT_IN_PROGRESS): retry once it finishes - The merge folds one record at a time and STOPS at the first failure: mergedIds are done, remainingIds still exist - There is no undo: deleted contacts are gone

move_campaign_folder

ChatGPT
Move a campaign folder (and its whole subtree) under a different parent folder, or to the root. Use when the user asks about: - Reorganizing their folder hierarchy. - Moving a folder out of its current parent (omit newParentId for the root). Contract rules reproduced by the backend: - A folder cannot be moved into itself or one of its own sub-folders. - The resulting depth must stay within 15 levels, and the target level within 50 folders. - The folder name must stay unique at the target level.

move_campaigns_to_folder

ChatGPT
Move one or more campaigns into a folder, or out to the root (the drag-and-drop equivalent). Use when the user asks about: - Filing campaigns into a folder. - Taking campaigns out of a folder (omit folderId to move them to the root). Contract rules reproduced by the backend: - Only campaigns owned by the current team are moved; unknown ids are skipped.

optimize_watch_lists

ChatGPT
Generate AI optimization recommendations for the team's existing watch lists. Analyzes active watch lists and returns concrete, ready-to-apply changes — each a filter diff (add/remove) on an existing watch list, with a short title and a rationale — tailored to the team's ICP and the user's stated goal. Use when the user asks about: - Improving, tuning or fixing the signal agents (watch lists) they already have - Why an agent returns too few, too many, or low-quality signals Important: - Requires the team's business context (AI Context Center) to be set. - Pass watchListIds to optimize specific agents; omit them to let the backend pick the most relevant active ones (capped by limit). - Returns recommendations only; applying them is a separate step.

people_database_search_count

ChatGPT
Use ONLY in the People Database scope. Return the exact number of People Database documents (leads or companies) that match a set of filters, without returning the documents themselves. Use it when: - The user asks how many people or companies match a set of criteria. - To refine a set of filters after people_database_nl_to_filters. - To size an audience before running a full search or building a campaign. - To compare how restrictive two filter sets are. Every filterId must be an exact id returned by people_database_nl_to_filters.

personalize_step_for_lead

ChatGPT
Personalize one step of a campaign sequence for a single lead: the lead receives this version of the message instead of the step's shared content, which stays untouched for every other lead. Same as "Personalize" in the lemlist review page. Use when the user asks to: - Rewrite or tweak an upcoming email, LinkedIn, SMS or WhatsApp message for one prospect before it is sent - Put one lead on version A or B of an A/B tested step Typical flow: get_campaigns -> search_campaign_leads (lead id) -> get_campaign_sequences (step id and current content) -> personalize_step_for_lead. Contract rules reproduced by the backend: - Supported steps: email, whatsappMessage, sms, linkedinInvite, linkedinSend, linkedinInMail. Not voice notes, not conditions. - Every content field is optional and overlays the lead's current version (its personalization, else the step's content); fields you omit are kept. Calling with only variant puts the lead on that version as is. - A personalized step is no longer A/B tested for that lead: it pins the lead to the variant it is written on. Switching the variant replaces the lead's personalization of the other variant. - Refused once the lead has reached the step (sent, skipped or ignored). For a task already created, use update_task_content on that task instead. - On a follow-up email, a non-empty subject starts a new email thread; omit subject to stay in the thread. - To undo, use reset_step_personalization.

preview_email

ChatGPT
Preview how an email step renders for a SPECIFIC lead. Compiles a sequence step's subject + body for one lead: the lead's {{variables}} are substituted and Liquid conditionals ({% if jobTitle contains "Founder" %}...{% endif %}) are evaluated, exactly like the lemlist UI email preview. Use when the user asks to: - See the final email a given lead will receive - Check how variables / Liquid resolve for a real lead - Spot empty variables or broken Liquid before sending Read-only: no credits consumed, nothing is sent. The lead's campaign is inferred from the lead unless campaignId is passed to override it.

preview_sequence_update

ChatGPT
SAFE READ-ONLY: Preview what would change in an email sequence step before applying modifications. Shows current vs proposed content and campaign status. Must call this before update_sequence_step.

propose_sequence

ChatGPT
Propose a sequence with full tree structure for user review. Displays visual tree in workspace. To update an existing sequence: get_workspace_items → propose_sequence with replaceItemId. Format: provide an array of sequences. The main sequence contains the root steps. Conditional steps reference sub-sequences by ID via their "conditions" array. Example with condition: - sequences: [ { _id: "seq_main", steps: [{ _id: "s1", type: "email", delay: 0, name: "Initial email" }, { _id: "s2", type: "conditional", delay: 3, delayType: "within", conditions: [{ sequenceId: "seq_yes", key: "emailsOpened", label: "Opened" }, { sequenceId: "seq_no", fallback: true, label: "Not opened" }] }] }, { _id: "seq_yes", steps: [{ _id: "s3", type: "linkedinSend", delay: 2, name: "LinkedIn follow-up" }] }, { _id: "seq_no", steps: [{ _id: "s4", type: "email", delay: 3, name: "Reminder email", subject: "Following up" }] } ] - mainSequenceId: "seq_main" CRITICAL RULES: - TREE STRUCTURE ONLY: each sub-sequence must be referenced by exactly ONE condition branch. NEVER reuse the same sequenceId in multiple conditions. If two branches need similar steps, duplicate the sub-sequence with different IDs (e.g. "seq_breakup_1", "seq_breakup_2"). - BRANCH CONVERGENCE: to converge multiple branches on a common closing step (e.g. an "Email Breakup"), place that step AFTER the conditional in the PARENT sequence — do NOT duplicate it in every branch. The engine resumes the lead at the parent's next step when a sub-sequence ends. Example: seq_main = [email, conditional(YES->seq_engaged, NO->seq_breakup), email "Final goodbye"] — the final email runs for both branches. - Max 10 levels of nested conditions (a conditional inside a sub-sequence of another conditional, chained up to 10 deep). Nest as deep as the campaign logic genuinely needs within that ceiling; prefer a flatter structure when it expresses the same flow. EDITING AN EXISTING CAMPAIGN (desired-state): when the user asks to review/edit a campaign you already have context for, the proposal must be the FULL intended sequence — describe what the campaign should look like AFTER the edit: - Keep a step: include it with existing: true and sourceStepId set to its real id (the "id:" shown in get_campaign_context). Do NOT recompose its message — its content is preserved automatically. - Replace a step: same existing: true + sourceStepId, plus the new content (bind a messageId via compose_messages) and/or a changed delay. - Add a step: include it with NO existing flag (a brand-new step), at the position you want. - Remove a step: simply OMIT it from the proposal (any live step you don't reference is deleted). On a DRAFT campaign with no launched leads, the full set of edits above is applied. On a RUNNING/PAUSED or lead-populated campaign, only non-destructive edits are accepted: you may replace a step's content and APPEND new steps after the last existing step, but you must keep every existing step (reference each with existing: true + sourceStepId) and may NOT insert a step between existing ones or remove one — for those, use add_sequence_step (with an explicit index) or delete_sequence_step. Condition types — every conditional step ALWAYS requires delay, delayType and a non-fallback branch with key: - Field-check (hasEmailAddress, hasLinkedinUrl, hasPhoneNumber): check lead data instantly. Use delayType: "within" and delay: 0. - Action (hasScore, emailsOpened, emailsClicked, emailsUnsubscribed, meetingBooked, linkedinInviteAccepted, linkedinOpened, aircallDone): track lead behavior over time. Use delayType: "within" (must occur within delay days) or delayType: "waitUntil" (wait indefinitely). - Async (linkedinNetworkCheck, hasWhatsappAccount): external API check. Use delayType: "within" and delay: 0. Step types: email (needs subject), linkedinVisit, linkedinFollow, linkedinInvite, linkedinLikeLastPost, linkedinCommentLastPost (AI-generated comment on the lead's last LinkedIn post; user reviews via Opportunity before publish), linkedinSend, linkedinEndorse, linkedinVoiceNote (Li…

provision_mailboxes

ChatGPT
Create mailbox orders on a team-owned domain. Call this IMMEDIATELY after purchase_domain for each domain — do not wait for the domain to become Active first. The mailbox type is locked by the domain's provider (set at purchase and irreversible): - "google" domain → Google Workspace mailboxes only - "m365" domain → Microsoft 365 mailboxes only - "infraforge" domain → SMTP mailboxes only Both domain and mailbox orders are processed asynchronously by background cron jobs (Ordered → Paid → Registered → Pending → Active). This can take minutes to hours (M365: up to 2 days). After calling this tool, tell the user to wait until their domains show as Active before proceeding with DNS configuration. DESTRUCTIVE — IRREVERSIBLE CHARGE. Creates real mailbox orders and bills the team. Only call after the user has explicitly confirmed the full infrastructure plan. Returns { added, mailboxes: [{ _id, email, assignToUser }] } — keep the _id (dem_xxx) of each created mailbox if any later update_mailbox call is anticipated (e.g. recovery, re-assignment).

purchase_domain

ChatGPT
Purchase (or transfer) a domain at the registrar. Charges the team via Stripe. DESTRUCTIVE — IRREVERSIBLE CHARGE. Only call after the user has explicitly confirmed the full infrastructure plan (Step 7 of the outreach-infra skill). The plan must include price, provider, and any pre-set DNS records. CRITICAL: The provider locks the mailbox type for this domain permanently. A domain purchased with "google" can ONLY have Google Workspace mailboxes. A domain purchased with "m365" can ONLY have Microsoft 365 mailboxes. A domain purchased with "infraforge" can ONLY have SMTP mailboxes. This choice is irreversible — you cannot change the provider after purchase. Choose carefully based on the ESP split from calculate_infrastructure. Registrant contact: the registrar requires a billing/registrant contact. On the team's FIRST purchase, pass billingContact (collected from the user). It is stored on the team and reused for later domains, so omit it once set. Purchasing without a contact (and none stored) fails with a clear error.

push_companies_from_people_database

ChatGPT
Push companies from the People Database into your CRM (optionally into a company list). Use when the user asks about: - Saving companies found in the People Database as CRM companies - Bulk-adding People Database companies to a company list - Importing the companies returned by display_companies or lemleads_search (companies mode) Contract rules reproduced by the backend: - pass ONE source: companyIds (an explicit selection), dataRef (a prior companies search) or filters - company ids are CoreSignal ids from lemleads_search (companies mode) or display_companies, never cpn_xxx - dataRef comes from a lemleads_search in companies mode; the backend pushes its matches up to the cap - filters are People Database company filters (from get_lemleads_filters) for a search you build directly - excludeCompanyIds drops specific companies from the set (e.g. "all these except X") - companies are matched and de-duplicated by CoreSignal id, so re-pushing never duplicates one - listId comes from get_company_lists; it must be a non-dynamic company list — a contact list is rejected - at most 1000 companies per call; a bigger search reports how many matched - to push People Database people to contacts instead, use push_contacts_from_people_database

read_help_center_article

ChatGPT
Read ONE lemlist help center article in full. search_help_center only ever returns part of an article; this returns the whole article. Use when: - A search hit came back with partial=true and the answer is not in the part you got - You are about to state a COUNT, a LIST or a TABLE from an article (supported countries, plans, limits, pricing, steps): an excerpt that stops mid-table is NOT the whole table - You are about to tell the user that a capability is unsupported on the strength of an article Contract rules reproduced by the backend: - articleId comes from search_help_center. Never invent one. - found=false means no published article matches that id — search again rather than answering from memory. - truncated=true on the result means even the full read hit the size cap, so the end of the article is still missing: say so instead of presenting what you have as complete.

recall_memory

ChatGPT
Retrieve memories from previous conversations. Use this when: - The user asks about something you discussed before - You need context about user preferences, ICP, or past decisions - The memory summary in your system prompt shows a relevant topic - The user says "remember when..." or "like we discussed..." If topic is provided, returns that specific memory. If omitted, returns ALL memories so you can pick the relevant ones. Sorted by most recently updated.

remove_campaign_senders

ChatGPT
Remove one or more senders from the sender pool of a campaign. Every channel of the sender leaves with it. The leads not contacted yet go to the remaining senders; the contacted leads keep their sender. Keywords: remove a sender, take Carl off the campaign, stop sending from this user. Use when the user asks about: - Removing a teammate from a campaign ("remove Carl from the Q4 campaign") - Removing a teammate who left the team or lost a seat To remove one channel and keep the person, use disable_campaign_sender_channels instead. Call get_campaign_senders first: it gives the user ids of the pool. A user outside the pool changes nothing and raises no error. Two-phase confirmation flow: 1. First call WITHOUT confirmed: the backend counts the contacted leads that keep a sender or a channel this change removes (impact.contactedLeads). Above 0 the call writes nothing and answers status="awaiting_confirmation" with "data" (the pool after the change) and the count: show them to the user, ask with ask_question, then call again with confirmed=true and the SAME input. At 0 the call writes at once. 2. Never call with confirmed=true without explicit user approval. Contract rules reproduced by the backend: - A campaign with a dynamic sender strategy (contactOwner, leadImporter, customField:<field>) has a read-only pool that the leads fill: every pool write is refused. Switch the strategy with set_campaign_sender_strategy first, only when the user wants it. - A channel type is accepted only when a non-forced step of the campaign uses it: no WhatsApp channel without a WhatsApp step. Add the step first with add_sequence_step: it enables the type on each pool sender who owns an account of it, and lists the enabled types in senderPoolChange. - Every entry must be a team member who owns the listed channels; an alias must be one of the aliases of the mailbox. - LinkedIn has no channel to pick: a sender in the pool is enabled on LinkedIn. Never list linkedin with an id. - A pool with more than one sender needs the emailPro plan; the mailboxes above the plan allowance are dropped. - A write runs on a running campaign: the leads not contacted yet get a new sender from the pool, the contacted leads keep theirs. - Every write answers "data": the pool after the change, with the diagnostics of each sender and channel. Read them and report the errors to the user.

remove_companies_from_list

ChatGPT
Remove existing CRM companies from a company list. Use when the user asks about: - Taking companies out of a list (clean up duplicates, drop a wrong segment) - Removing a subset of a list found with search_companies and its listId filter Contract rules reproduced by the backend: - this removes list MEMBERSHIP only; the companies themselves are never deleted - the list id comes from get_company_lists — never invent one - the list must exist, hold companies (not contacts), and not be dynamic - companies that are not in the list are silently skipped, not an error - at most 1000 companies per call

remove_contacts_from_list

ChatGPT
Remove existing CRM contacts from a contact list. Use when the user asks about: - Taking contacts out of a list (clean up duplicates, drop a wrong segment) - Removing a subset of a list found with search_contacts and its listId filter Contract rules reproduced by the backend: - this removes list MEMBERSHIP only; the contacts themselves are never deleted - the list id comes from get_contact_lists — never invent one - the list must exist, hold contacts (not companies), and not be dynamic - contacts that are not in the list are silently skipped, not an error - at most 1000 contacts per call

remove_watch_list_trigger_from_campaign

ChatGPT
Detach a watch list signal trigger from a campaign: its future signals stop feeding the campaign. Keywords: remove trigger, detach agent, stop feeding campaign, unlink watch list. Use when the user asks about: - Removing a trigger / an agent from a campaign - Stopping a watch list from pushing leads into a campaign Contract rules reproduced by the backend: - Leads already pushed stay in the campaign; the watch list itself is kept (delete_watch_list removes it) - The watch list goes back to signalProcessingType="manual" only when no other campaign still uses it - The campaign keeps its signal-based mode and its import options, so re-linking restores the previous setup - No user confirmation required

rename_campaign_folder

ChatGPT
Rename a campaign folder and/or change its color. Use when the user asks about: - Renaming a folder. - Changing a folder's color. Contract rules reproduced by the backend: - The new name must be unique among folders that share the same parent.

replace_campaign_senders

ChatGPT
Replace the whole sender pool of a campaign with the listed senders and channels. Every sender and channel outside the list leaves the pool; an empty list clears the pool. Keywords: set the senders, only Ana and Ben send this campaign, nobody else, reset the senders, clear the senders. Use when the user asks about: - Defining the complete list of senders ("Ana and Ben send the Q4 campaign, nobody else") - Clearing the pool of a campaign Use it only when the user names the whole list. For one change on the pool, prefer add_campaign_senders or remove_campaign_senders: a replace that forgets a current sender removes it silently. When only a channel of one sender changes, prefer enable_campaign_sender_channels or disable_campaign_sender_channels. Call get_campaign_senders first to know the current pool and tell the user who leaves. Before any write: call get_senders with the campaignId. It lists every team member who can send, in the pool or not (inPool), with the user ids, the channel ids, the aliases, the health of each channel and requiredChannelTypes, the channel types that the steps require. Never guess a user id, a mailbox id, a WhatsApp account id or an alias. Load load_skill("senders") first: it tells which tool fits the request, when to change the sender strategy, and how to read the diagnostics. Two-phase confirmation flow: 1. First call WITHOUT confirmed: the backend counts the contacted leads that keep a sender or a channel this change removes (impact.contactedLeads). Above 0 the call writes nothing and answers status="awaiting_confirmation" with "data" (the pool after the change) and the count: show them to the user, ask with ask_question, then call again with confirmed=true and the SAME input. At 0 the call writes at once. 2. Never call with confirmed=true without explicit user approval. Contract rules reproduced by the backend: - A campaign with a dynamic sender strategy (contactOwner, leadImporter, customField:<field>) has a read-only pool that the leads fill: every pool write is refused. Switch the strategy with set_campaign_sender_strategy first, only when the user wants it. - A channel type is accepted only when a non-forced step of the campaign uses it: no WhatsApp channel without a WhatsApp step. Add the step first with add_sequence_step: it enables the type on each pool sender who owns an account of it, and lists the enabled types in senderPoolChange. - Every entry must be a team member who owns the listed channels; an alias must be one of the aliases of the mailbox. - LinkedIn has no channel to pick: a sender in the pool is enabled on LinkedIn. Never list linkedin with an id. - A pool with more than one sender needs the emailPro plan; the mailboxes above the plan allowance are dropped. - A write runs on a running campaign: the leads not contacted yet get a new sender from the pool, the contacted leads keep theirs. - Every write answers "data": the pool after the change, with the diagnostics of each sender and channel. Read them and report the errors to the user. - Each entry resolves as an addition to an empty pool: an entry without channels gets the required channel types, healthy first.

report_unsupported_case

ChatGPT
Report a feature request or unsupported use case to the product team. Use this ONLY when the user's request is something lemlist should support but lemAgent cannot do yet, AND the user has agreed to have their feedback reported. Do NOT use for off-topic requests unrelated to lemlist. Always provide inputs in English, translate if needed.

reset_enrichment_waterfalls

ChatGPT
Reset one or more default waterfalls (default_email, default_phone) back to lemlist's managed behaviour in ONE call: lemlist picks the provider order again, as if nobody had edited it. Only default waterfalls can be reset; a custom id returns an error for that entry. Ids are applied independently: 'results' reports the reset waterfall or an error per waterfallId. Team-shared and free (no credits).

reset_step_personalization

ChatGPT
Remove a lead's personalization of one campaign step: the lead receives the step's shared message again, and A/B testing applies to it again. Same as "Reset override" in the lemlist review page. Use when the user asks to: - Undo a personalization made with personalize_step_for_lead or in the review page - Put a lead back on the campaign's standard message for a step Contract rules reproduced by the backend: - The personalized text is deleted and cannot be recovered: confirm with the user first. - Refused once the lead has reached the step (sent, skipped or ignored). - Idempotent: reset=false when the step was not personalized for that lead.

run_deliverability_audit

ChatGPT
Run the full deliverability audit for the current team and return the complete structured report (header, summary, every section, verdict). This IS the deliverability playbook — the same deterministic engine the lemgod audit page runs, so both surfaces always agree on the same account. Use when the user wants to: - Audit an account's deliverability, or diagnose why open/reply rates dropped or emails land in spam (the deliverability-audit skill) - Get a health check of a sending setup (DNS, warm-up reputation, sending volume, content, targeting) The report is READ-ONLY: it never modifies a mailbox, domain, or campaign, and sends nothing. It contains: - header: team, mailbox/domain/campaign counts, account age, lemwarm usage - summary: severity (healthy / minor / critical) with the criteria that set it, each section's status, key numbers (warm-up spam over the last 14 days per domain, cold bounce rate, cold volume) and a dated timeline (warm-up start, cold start, volume spikes, reputation drops, campaign launches) - sections: technical, coldSending, campaignSettings, content, warmup — each with a status (ok / minor / critical / skipped), findings grouped per sending domain, and "unknowns" (signals that could not be measured, with the reason); warmup adds a per-mailbox table (score, saturated flag, warm-up spam over the last 14 days, the 14 days before and after the first cold send) - verdict: the causes the evidence supports (NOT ranked — there is no root cause), each with its level and evidence; actions ordered most urgent first; an optional recovery plan (path A = refresh, with path B as its stricter fallback, or C = move to new domains); and a prose summary Contract rules reproduced by the backend: - All sections always run; a section that cannot run returns status "skipped" with a reason — never fabricate data for it. - An unknown is never "ok": present it as unknown and say why. - Severity, causes, actions and recovery are computed deterministically; trust them and present them, do not re-derive the thresholds, and do not rank the causes. - Inbox placement is NOT part of this tool — it sends a real email and is a separate, explicitly-confirmed action (run_inbox_placement_test).

run_inbox_placement_test

ChatGPT
Run an inbox placement (spam) test: send one email from a mailbox to ~25 seed inboxes across Google, Microsoft, and SMTP, to measure where it lands (inbox / promotions / spam) per provider. Use when the user wants to: - Test whether a campaign's content lands in spam (Phase 6 of the deliverability-audit skill) - Compare placement of two campaigns to quantify a content change - Diagnose a "Google fine but SMTP spam" content issue This SENDS A REAL EMAIL and consumes 100 credits. Confirm with the user first. Pull the real campaign subject + body (get_campaign_sequences / preview_email) so the test mirrors production content. The test is asynchronous: this returns a testId with state "Pending". Poll get_inbox_placement_result with that testId until it reports a terminal state — "Completed" to read the per-provider breakdown, or "NoResult" if the test never measured anything.

save_business_context

ChatGPT
Save the team's business context — overwrites the stored markdown document with the full version you provide. Use when the user adds, corrects, or removes business-context information (company, value props, ICP, products, etc.). Contract rules: - Call get_business_context FIRST and start from its current content. - Provide the COMPLETE updated document, never a fragment — what you send replaces everything. - Preserve existing information; only remove content the user explicitly asked to drop. - Integrate new facts into the relevant existing section rather than appending a duplicate section. - Plain markdown only (no HTML comments). Length must stay between 300 and 40000 characters.

save_memory

ChatGPT
Save a piece of information to persistent memory so you can recall it in future conversations. Use this when the user: - States a preference ("I prefer short emails", "always use formal tone") - Shares business context not in the Business Context ("we just signed a deal with Acme") - Corrects you ("no, our ICP is mid-market, not enterprise") - Mentions recurring processes ("we send campaigns every Monday") - Shares team/org knowledge ("Jean handles EMEA region") Do NOT save: - Ephemeral task details ("send this email now") - Information already in the Business Context - Sensitive data (passwords, API keys) The topic acts as a unique key: saving to an existing topic REPLACES the content entirely. IMPORTANT: Before updating an existing topic, ALWAYS recall it first to merge new info with existing content. Never overwrite blindly. Language: team-scoped memories MUST be written in English. User-scoped memories should use the user's language.

search_campaign_leads

ChatGPT
Find leads in your campaigns. Two modes: - Search by email or ID: provide email or id (and optionally campaignId to narrow down) - List leads in one or more campaigns: provide campaignId or campaignIds (no email or id) Optional filters (campaign-listing mode): - 'addedAfter' / 'addedBefore': filter by the date each lead was added to the campaign (returned as 'createdAt' on every lead). To count/list leads added on a given day, set addedAfter to that day and addedBefore to the next (dates are UTC). Optional enrichments (campaign-listing mode): - 'include': fetch notes / activities / other campaigns per lead - 'fields': fetch specific custom field values per lead — call get_contact_fields_schema first to know which names exist When 'include' is set, max limit is capped at 50. lemAgent clients receive a 'dataRef' (reference to stored data); pass it directly to display_table to render the results. External MCP clients receive an inline 'leads' array with the actual rows. Use get_campaigns first to find the campaignId if you only have the campaign name. Per-contact engagement state (each lead's current state, e.g. emailsSent, emailsOpened, emailsClicked, emailsReplied) is not returned by this tool. When a user wants that state as one column per contact in an export, point them to the campaign's "Leads list" tab, "Export as .CSV": that file has a 'lastState' column holding each lead's current state. The "Performance" tab export is a different file (per-step timestamps like sentAt1/openAt1, no consolidated state column). Never claim the state column was removed or renamed. For prospecting NEW leads, use lemleads_search instead.

search_companies

ChatGPT
Search your team's Lemlist companies. Returns a paginated list with each company's id, domain, name, owner, and a curated crmSync block describing how the record is synced to your active CRM (Hubspot, Salesforce, or Pipedrive). Use listId (from get_company_lists) to list the companies inside a given company list. Use the crmSyncStatus filter to find companies in a specific sync state — most notably unique_index_error_company to list lemlist companies blocked from syncing because another lemlist company already occupies their CRM record. For each result in that state, look at crmSync.errors[].metadata.alreadyExistingCompanyId — that is the canonical lemlist company you should remap contacts to before deleting the duplicate (see search_contacts with companyId, then delete_company). Each returned company also carries a fieldRejections array — values lemlist refused to write (which field, why, and conflictingRecordId for duplicates); use fieldRejectionReason to only return companies flagged with a given reason.

search_contacts

ChatGPT
Search or list your team's Lemlist contacts by name, email, contact list, or attached company. Returns matching contacts with their details (ID, name, email, phone, job title, company, campaign count). All filters are optional — calling the tool without any filter returns the paginated list of all contacts of the team (useful for discovery, e.g. obtaining a Record ID before associating it with another resource). Use this to find if a contact already exists in Lemlist before creating a new one. You can filter by a specific contact list using listId (get valid IDs from get_contact_lists first). Use notInAnyCampaign=true to find contacts that are not part of any campaign (orphan contacts). Use withPrimaryCompany=false to find contacts without an attached company, or withPrimaryCompany=true for only contacts linked to a company — cannot be combined with the company filters. To list contacts attached to a given company, use companyId (cpn_xxx) or one of companyDomain / companyLinkedinUrl / companySalesnavUrl — only one company filter at a time. URL/domain filters return an empty list if no company matches. lemAgent clients receive a 'dataRef' plus a truncated 3-field 'contacts' list (id, email, fullName). Pass the dataRef to display_table to render the full table. External MCP clients receive a 'contacts' array with the full contact fields inline. Each returned contact carries a 'fieldRejections' array — values lemlist refused to write (which field, why, and conflictingRecordId for duplicates); use fieldRejectionReason to only return contacts flagged with a given reason. For searching leads WITHIN campaigns, use search_campaign_leads instead. For prospecting NEW leads from the People Database, use lemleads_search instead.

search_domains

ChatGPT
Check availability of a domain at the registrar (and optionally return suggestions). Use in Step 3 of the outreach-infra skill — after calculate_infrastructure has produced a shortlist of brand-consistent candidates, call this tool for each candidate to confirm availability + pricing before presenting the plan to the user.

search_help_center

ChatGPT
Search the lemlist help center for official documentation and guides. Use this when you need to provide guidance on how to do something in lemlist that you cannot do directly via tools. Use when: - The user asks how to do something in the product that no tool performs for them - You are about to state that a lemlist capability does not exist (verify first, never deny from memory) Contract rules reproduced by the backend: - Each hit holds PART of an article, never the whole one: an excerpt cut at a fixed length, or the passages that matched your query joined by [...] with the text between them missing. - partial=true means text is missing from that article, so any list, table or count inside it is INCOMPLETE. Call read_help_center_article with that articleId before stating how many items exist, presenting a list as exhaustive, or denying a capability. - Hits are ranked by how close they are to your query, which does not mean they answer it. If the answer is not in what you got, say you could not find it rather than piecing one together. - Always cite the article link(s) in your answer. - Do NOT use this for questions you can answer from context or from another tool's result.

send_message

ChatGPT
Send a message from the inbox to a contact or lead. Pick the channel that matches the user's intent. Channels: - "email": send an email. Requires sendUserId, sendUserEmail, sendUserMailboxId, subject, message. Optional: cc[], replyToActivityId (act_xxx or "latest") to reply within an existing thread. - "linkedin": send a LinkedIn message. Requires sendUserId, message. Sender must have a connected LinkedIn account. - "sms": send an SMS. Requires sendUserId, from (sender phone number), message. - "whatsapp": send a WhatsApp message. Requires sendUserId, sendUserWhatsappAccountId, message. Recipient: every channel needs either contactId OR leadId. Use get_user_channels to find the right sendUserId/sendUserEmail/sendUserMailboxId/sendUserWhatsappAccountId. Notes: - For "email", plain-text messages have newlines auto-converted to <br>. HTML is supported. - For "whatsapp", the API may return a contactId derived from a leadId — it is surfaced in the response.

send_task

ChatGPT
Send one OR many message-based tasks now, OR — for a manual campaign step — schedule it via the campaign and mark it done (the "Send & mark done" / "Schedule & mark done" actions on the Tasks / Focus page). Use when the user asks to actually SEND an email, LinkedIn message or WhatsApp message task (not merely close it), or to schedule/complete a manual campaign step. Pass opportunityId as a single task ID or an array of task IDs. The same sender is applied to every task; the result is always the aggregate { total, succeeded, failed, results[] } — each entry carries channel/sent/scheduled or an error code, and a bad task fails on its own without failing the others. With an array this contacts MULTIPLE leads at once — only call it after the user has approved sending to all of them. To pick tasks for a bulk send, list them with get_tasks (status:["due","upcoming"], type:["email","linkedinSend","whatsappMessage"]) and re-list right before sending — non-message, already-closed or already-sent task ids are rejected here. What it sends: - The task's current content: the per-lead draft override if one exists, otherwise the task's template. Edit it first with update_task_content if needed. Contract rules reproduced by the backend: - Regular (non-manual) email, linkedinSend and whatsappMessage tasks are sent immediately; this requires the per-channel sender fields, a contact and non-empty content (email needs a subject). Returns sent=true, scheduled=false. - A manual campaign step is NOT sent immediately: it is handed back to the campaign to send at its scheduled time and marked done. Sender fields, contact and content are NOT required (the campaign step's own content is used at send time). Returns sent=false, scheduled=true. - Still only email, linkedinSend and whatsappMessage types are handled; phone, SMS, generic manual and other types are rejected — close those with update_task status="done". - The task must be open (due/upcoming); paused or already-closed tasks are rejected. - Sender details for a direct send: email needs sendUserEmail + sendUserMailboxId; whatsapp needs sendUserWhatsappAccountId; linkedin needs only sendUserId. Discover valid sender ids/mailboxes with get_users / get_user_channels. - This actually contacts the lead (now, or at the scheduled time) — only call it after the user has approved sending.

set_ab_variant

ChatGPT
Set or update the A/B test variant B of a sequence step. Use when the user asks to: - A/B test an email step (two subjects, two bodies, or both) - A/B test a LinkedIn invitation, typically variant A with a note vs variant B without one (pass message: "") - A/B test any other step type listed below - Add or edit the variant B of such a step Behaviour: - If the step has no A/B test yet, this enables it: variant B starts as a copy of variant A, then the fields you pass are applied on top. Fields you omit stay inherited from A. - If a variant B already exists, only the fields you pass are changed; the rest of B is left untouched. - Pass no field to just enable A/B with variant B identical to A. - Leads are then split between A and B until a winner is picked. Supported step types: email, linkedinInvite, linkedinSend, linkedinInMail (email steps are the ones carrying no explicit type). Contract rules reproduced by the backend: - Any other step type is rejected (SEQUENCE_AB_UNSUPPORTED_STEP). The public REST A/B endpoint carries the same limits. - message: "" is accepted on linkedinInvite only (SEQUENCE_AB_EMPTY_MESSAGE otherwise): everywhere else an empty message makes the send cron pause the whole campaign. For the same reason, enabling A/B on a step whose variant A has no message is rejected. - subject: a blank one is rejected on linkedinInMail (SEQUENCE_AB_EMPTY_SUBJECT): the InMail send cannot go out without it. - On a linkedinInvite, altMessage over 300 characters is rejected (SEQUENCE_AB_ALT_MESSAGE_TOO_LONG), and so is any altMessagePremium over the same limit. The standard note has a per-sender limit (200 by default) that is not enforced here: keep it short, the campaign editor flags an over-long note on either variant. - A running campaign can reject edits to steps that have already launched: pause it first.

set_campaign_sender_strategy

ChatGPT
Set the sender-assignment strategy for a campaign — the algorithm that decides which sender (user) is attached to each lead at launch. Keywords: sender strategy, dynamic sender assignment, round-robin sender, contact owner sender, custom variable sender, per-lead sender routing. Use when the user asks about: - Configuring HOW senders are assigned to leads (as opposed to WHICH users are in the send pool — that is set_campaign_senders). - Routing sends by a per-lead custom variable (e.g. "send from the account manager listed on each lead"). - Using the CRM contact owner as the sender. - Using the user who imported the lead as the sender. - Switching between random / round-robin and any of the dynamic strategies above. This tool decides HOW senders are picked. To change WHO is in the eligible pool, use set_campaign_senders. Contract rules: - "customField:<fieldName>" requires the field to exist as a team custom field of type "user", AND each lead must have variables.<fieldName> populated with a valid userId. Leads missing that value are silently dropped from launch with a "noAssignment" diagnostic (they will not be sent to). - Changing the strategy on a running campaign does NOT reassign already-resolved leads; it only applies to leads reviewed/launched after the change. - No user confirmation required.

set_campaign_senders

ChatGPT
Assign senders to a campaign. Senders are team members who will send the campaign's outreach messages. This defines the pool of eligible sender users (WHO). Use after validate_campaign_readiness detects missing senders: 1. Use get_users with userIds: ["all"] to list team members with IDs 2. Use ask_question to let the user choose which senders to use 3. Call this tool with the chosen sender IDs Each sender's connected channels (email mailboxes, LinkedIn, etc.) are automatically detected and configured. Related: to choose how a sender is assigned to each lead (random / round-robin, contactOwner, leadImporter, or by custom variable), use set_campaign_sender_strategy. Sender assignment is a campaign-level setting — it is NOT configurable per step.

set_campaign_state

ChatGPT
Start, pause, archive or unarchive a campaign. - Use action "start" to launch a draft or paused campaign → transitions to "running" - Use action "pause" to pause a running campaign → transitions to "paused" - Use action "archive" to archive any non-archived campaign → flag set to archived (running campaigns are paused first) - Use action "unarchive" to restore an archived campaign → flag cleared (campaign returns to its previous state, you must call "start" again to resume sending) - REQUIRES CONFIRMATION for "start" and "archive": You must ask the user to confirm before starting or archiving a campaign. Set userConfirmed=true only after user says yes. Pause and unarchive do NOT require confirmation.

set_sequence_step_sender

ChatGPT
Force the sender of one sequence step, instead of the campaign sender pool. Use when the user asks to: - Send one step from a specific team member or a specific mailbox, WhatsApp account or phone number - Change the sender of a LinkedIn or WhatsApp voice note step - Make one step always come from the same person, whatever sender the lead got Contract rules reproduced by the backend: - Email, LinkedIn, WhatsApp, SMS, call, manual, API and phone steps accept a forced sender. Delay, condition and other utility steps do not. - channel is required on an email, LinkedIn, WhatsApp or SMS step, and channel.type must match the step. channel is forbidden on a call, manual, API or phone step. - The user must be a team member who owns the account. An unhealthy account is accepted: the result carries its diagnostics. - An InMail step needs a sender with LinkedIn Premium. - The pool changes as in the campaign editor: the channels of a type that no other step uses are disabled. senderPoolChange lists the channel types that the change enabled or disabled. - On a running campaign, the pending tasks of the step move to the forced sender. - Get the step ids from get_campaign_sequences and the user ids from get_users.

skip_step_for_everyone

ChatGPT
Skip one step of a campaign sequence for EVERY lead: leads that have not reached it move on to the following step after that step's own delay, leads added to the campaign later never receive it, and the skipped step is recorded as "Step skipped" in the review and the reports. Campaign-wide and permanent. Use when the user asks to: - Drop a step from a running campaign for everyone (it no longer fits, it under-performs, the channel is not wanted anymore) without pausing or rebuilding the campaign - Free the sending capacity a step consumes, for all leads at once Two-phase confirmation flow (mandatory): 1. First call WITHOUT confirmed: returns status="awaiting_confirmation" with leadsImpacted (an upper bound: every lead not yet past the step) and destination (the step they continue on, or completed=true when it is the last one). Show both to the user and get explicit approval. 2. Once approved, call again with confirmed=true to skip. Typical flow: get_campaigns -> get_campaign_sequences (read the step ids) -> skip_step_for_everyone (preview) -> ask the user -> skip_step_for_everyone (confirmed=true). Contract rules reproduced by the backend: - A condition step is never skippable. - Leads that already ran the step are not affected; nothing already sent changes. - Idempotent: a step already skipped for everyone answers skipped=false (and the preview says alreadySkipped=true) rather than failing. - A skip cannot be undone, here or in the lemlist UI. To skip a step for one lead or a list of leads only, use skip_step_for_lead. - Never call with confirmed=true without explicit user approval.

skip_step_for_lead

ChatGPT
Skip one step of a campaign sequence for one lead or a list of leads (up to 100), in one call. Each lead is not stopped: the step never runs for it, it moves on to the following step after that step's own delay, and the skipped step is recorded as "Step skipped" in the review and the reports. Use when the user asks to: - Drop a step for one or several prospects it no longer fits: they replied, accepted the LinkedIn invite, or were reached on another channel - Move one lead, or a set of leads, past the step they are waiting on, without pausing or removing them Two-phase confirmation flow (mandatory): 1. First call WITHOUT confirmed: returns status="awaiting_confirmation" with, for the requested leads, how many will skip (toSkip), how many already had it (alreadySkipped), which ids were not found in this campaign (notFound), and where they continue (destination). A call of 20 leads or more carries "advice": tell the user, offer skip_step_for_everyone, and only call again with confirmed=true if they still want these leads only. The confirm answer repeats it; once the user chose these leads only, go on. 2. Once approved, call again with confirmed=true and the SAME leadIds to skip. Typical flow: get_campaigns -> search_campaign_leads (collect the lead ids) -> get_campaign_sequences (read the step id) -> skip_step_for_lead (preview) -> ask the user -> skip_step_for_lead (confirmed=true). Contract rules reproduced by the backend: - A condition step is never skippable. - Only leads that have NOT run the step are affected; nothing already sent changes. - Idempotent per lead: a lead already skipped answers status="already_skipped" ("skipped" when this call closed a task still waiting on the step); a lead not in the campaign answers "not_found"; neither fails the call. - A skip cannot be undone, here or in the lemlist UI. - Put every lead of the request in ONE call: never call this tool once per lead. For EVERY lead of the campaign, use skip_step_for_everyone. - Never call with confirmed=true without explicit user approval.

suggest_watch_lists

ChatGPT
Generate AI-suggested watch lists for the current team. Returns scored, ready-to-create watch list configurations — each with a relevance score, a reason and an estimated monthly signal volume — tailored to the team's ICP. Use when the user asks about: - Recommendations on which signals / watch lists to set up - Discovering the most relevant watch list types for their business Important: - Requires the team's business context (AI Context Center) to be set, otherwise it errors. - Consumes one daily AI suggestion session (the team has a limited daily quota). - Returns suggestions only; creating or activating them is a separate step (create_watch_list).

test_email_account

ChatGPT
Test SMTP/IMAP connectivity of an email account. No actual email sent. Use get_user_channels to find account ID.

transfer_campaign_leads_to_list

ChatGPT
Add every lead of a campaign to a CRM contact list, in one call. Each campaign lead is backed by a CRM contact; this resolves those contacts server-side and adds them to the list. You do NOT need to fetch or enumerate lead/contact IDs first. When to use: - User wants to move/copy a whole campaign's leads into a contact list (e.g. "add everyone in campaign X to my 'Hot leads' list") - After identifying a campaign and a target list, to populate the list from the campaign Parameters: - campaignId: Campaign ID in cam_xxx format (required). Get it from get_campaigns. - listId: Target contact list ID in clt_xxx format (required). Get it from get_contact_lists, or create one with create_contact_list. Never invent or guess a list ID. IMPORTANT: - This ADDS (copies) contacts to the list; it does not remove leads from the campaign or move them off any other list. - Contacts already in the list are silently skipped (idempotent, no duplicates). - The target list must be a static contact list; dynamic lists are rejected. - Leads that have no associated contact are skipped. Example: { "campaignId": "cam_abc123def456", "listId": "clt_xyz789" }

update_ai_variable_prompt

ChatGPT
Edit the AI-generation prompt of ONE AI variable column on a campaign (icebreaker, opener, contextual opener, etc.). Replaces the existing prompt entirely with the provided one. Use get_ai_variable_prompts first to read the current prompt and the exact variable names. Keep the {{variable}} placeholders (e.g. {{firstName}}) in the new prompt, and only reference variables or AI columns that already exist on the campaign. Only prompts owned by your team (the per-campaign copy) can be edited. Shared lemlist/system prompts are read-only and will be rejected. Pass tools to change the column's tools (e.g. tools:["enrich-linkedin"] to scrape LinkedIn — the prompt must reference {{linkedinUrl}}). Enabling any tool forces the premium lemlist model and lemlist-credit billing. Pass an empty array ([]) to remove all tools; the column reverts to your team's default model and billing (still charged in lemlist credits if your team has no own AI provider key, otherwise billed via your own API key). Omitting tools leaves the existing tools untouched and only replaces the prompt. Pass autofill to switch the column's auto-generate toggle: true fills this AI variable for every lead added or imported to the campaign afterwards (CSV import, CRM sync, manual add), false turns that off. Omitting autofill leaves the toggle as it is. Use when the user asks to: - Change how an AI column is generated for a campaign - Tweak / rewrite an icebreaker or opener prompt - Turn auto-generation on new/imported leads on or off for an AI column This mutates campaign configuration, so confirm the change with the user before calling.

update_call_status

ChatGPT
Set the disposition (call status) on a single dialer call activity. Use when the user asks to: - Log or correct the outcome of a call (e.g. after reading its transcript) - Back-fill a missing status on a call that was dialed but never dispositioned Typical flow: get_call_activities (find calls, spot empty statuses) → get_call_details (read the transcript) → get_call_statuses (get the valid keys) → update_call_status (set the key). Setting a status is NOT cosmetic: depending on how the team configured that status, it can mark the lead interested / not-interested and pause campaigns, and it syncs to the connected CRM — exactly like a rep clicking it in the UI.

update_enrichment_waterfalls

ChatGPT
Reorder or toggle providers, rename, or change the countries of one or more enrichment waterfalls in ONE call. Put every waterfall you want to change in 'updates'; never call this once per waterfall. For each entry, 'providers' is the execution order (first tried first); you may send a partial list and every provider left out is appended disabled, so the stored order always covers the whole catalog. 'conditions' replaces the countries of a custom waterfall; a country may belong to only one waterfall per type, and the default waterfall (the catch-all) takes no condition. Editing a default waterfall makes it follow your order instead of lemlist's — use reset_enrichment_waterfalls to hand it back. Run list_enrichment_waterfalls first for the current order, the taken countries and the legal provider ids. Entries are applied independently: 'results' reports the updated waterfall or an error per waterfallId. Team-shared and free (no credits).

update_lead

ChatGPT
Update standard fields on an existing lead (email, firstName, lastName, picture, phone, linkedinUrl, timezone, jobTitle, jobDescription, companyName, companyDomain). Requires leadId + at least one field to update. For custom variables, use update_lead_variables instead.

update_lead_variables

ChatGPT
Set custom variables on an existing lead (upsert). Requires leadId + variables (key-value pairs of non-empty strings). Automatically handles both updating existing variables and adding new ones. IMPORTANT: Do NOT pass standard lead fields as variables — the following keys are FORBIDDEN and will be rejected: email, firstName, lastName, picture, phone, linkedinUrl, timezone, jobTitle, jobDescription, linkedinVideoUrl, companyName, companyDomain. To update these fields, use the update_lead tool instead. Empty string values are rejected — omit the variable instead.

update_mailbox

ChatGPT
Update a team mailbox. Currently supports assignment to a specific SDR (lemlist user); the schema is extensible for future updates (settings, status, …). Idempotent — re-applying the same update is safe. Use in Step 5 of the outreach-infra skill once mailboxes have been provisioned, to wire each mailbox to its intended SDR before warmup starts.

update_sequence_step

ChatGPT
REQUIRES CONFIRMATION: Update a sequence step. For email/content steps: Call preview_sequence_update first, show the diff, then call this with userConfirmed=true. Pass cc to set the CC addresses on the step (replaces the current list; empty array clears them) — cc alone is a valid change, no preview needed for it. For conditional steps: Update conditionKey, delayType, or delay directly. No preview needed. For a customLeadInfo branch, pass customField/customOperator/customValue (omit the ones you keep — e.g. send only customValue to change the compared value). For an aircallDone ("Call status") branch, pass callStatusFilter (call status keys from get_call_statuses) to choose which dispositions trigger the YES branch. For api (webhook) steps: Update method (GET/POST) and/or url. The URL supports Liquid templating against lead variables. Returns the full updated step list in steps (each with its real stepId, sequenceId, type, delay). Reuse those ids for any follow-up edit in the same turn — do NOT call get_campaign_context to rediscover steps. Will fail if campaign is running.

update_settings

ChatGPT
Update settings for one or more entities. Batch counterpart of get_settings: pass "items", an array of updates (always an array, even for a single one). Each item is discriminated by "type". Supported item types: - { type: "lemwarm", id: usm_xxx, ... }: activate/pause warmup or update warmup parameters for ONE mailbox. - { type: "campaign", id: cam_xxx, settings: {...} }: update a campaign's behavior settings (mirrors get_settings type="campaign"). - { type: "sending", id: usm_xxx, dailyEmailLimit }: set the production daily email limit of ONE mailbox (mirrors get_settings type="sending"). - { type: "linkedinLimits", id: usr_xxx, ...limits }: set the daily LinkedIn limits of ONE team member (mirrors get_settings type="linkedinLimits"). Items are applied independently: one failing item does not abort the others. The response returns a per-item result array (results[]) with success/error for each, plus total/succeeded/failed counts. lemwarm item fields: - action: "start" activates warmup, "pause" stops it. Omit when only updating settings. - warmEmailMax / warmEmailRampup (0-40), internalCommunicationPercent / answerPercentage (0-100), warmUsePlaintext, warmDailyVarianceEnabled. Some require a team beta (see field hints). - userConfirmed: REQUIRED (true) when action="start". Ask the user to confirm before activating warmup. "pause" and settings-only updates do NOT require confirmation. - Provide at least one of action / warmEmailMax / warmEmailRampup / internalCommunicationPercent / warmUsePlaintext / answerPercentage / warmDailyVarianceEnabled. campaign item: settings carries any of these sections (provide only what you change; omitted flags keep their value): - aiFeatures: { scoreReplies, onInterestedReply, onNotInterestedReply }. Each on*Reply has { enabled, pauseAllCampaignsForLead, pauseCampaignForCompany, pauseAllCampaignsForCompany }. - tracking: { trackOpens, trackClicks, trackReplies }. - onReplied: { createNewTask, campaignProgress (continue|pause|stop), propagateProgressToCompany, disableOutOfOffice }. - onLinkClicked: { createNewTask, campaignProgress, propagateProgressToCompany, specificLinks }. - onMeetingBooked: { campaignProgress, propagateProgressToCompany }. - Provide at least one section. Campaign updates do NOT require user confirmation. sending item fields: - dailyEmailLimit: max campaign emails per day for that mailbox. 0 pauses sending. - This is production sending capacity, NOT warmup — use type="lemwarm" + warmEmailMax for warmup volume. - Requires the "editLimits" permission and a paid plan. On a free trial the value is capped down to the trial limit, so read results[].appliedSettings.dailyEmailLimit for what was actually stored. - One item per mailbox; batch several items to adjust a whole domain or pool at once. linkedinLimits item fields: - followLimit, inviteLimit, sendLimit, visitLimit, inMailLimit, networkCheckLimit, withdrawInvitationLimit, likeLastPostLimit, endorseLimit, commentLastPostRetrieveLimit, commentLastPostSendLimit: max LinkedIn actions per day for that user, named like get_users linkedIn. inviteRampUp: daily increase of the invitation limit until it reaches inviteLimit, null turns it off. Provide at least one; omitted ones keep their value. The InMail limit is a daily throttle: the LinkedIn plan's monthly InMail credits stay the hard cap. - id must be a member of the caller's team: check it is the user the request is about. get_settings type="linkedinLimits" only reads the caller's own limits. - Requires the "editLimits" permission and a paid plan; refused on a free trial. Values above the recommended maximum are stored and results[].warning says so — relay it. For sender configuration (pool + assignment strategy), use set_campaign_senders (pool) and set_campaign_sender_strategy (algorithm).

update_task

ChatGPT
Update a single editable field on one OR many tasks (opportunities) on the Tasks / Focus page. Use when the user asks about: - Reassigning a task to a teammate (or unassigning it) - Changing a task's priority (none/low/medium/high) - Snoozing a task to a later date - Marking a task done or ignored - Marking a task read or unread Pass opportunityId as a single task ID or an array of task IDs. The same field/value is applied to every task; the result is always the aggregate { field, total, succeeded, failed, results[] } — each entry carries updated or an error code, and a bad task fails on its own without failing the others. Contract rules reproduced by the backend: - Exactly ONE of assignedTo / priority / dueDate / status / read must be provided per call (applied to every task). - assignedTo must be a teammate of the current team, or "unassign". - A paused task cannot be marked "done" (use "ignored"); it also cannot be snoozed (resume it first). - dueDate must be a future ISO 8601 date. - status="done" ONLY marks the task complete — it does NOT send the message. For an email/LinkedIn/WhatsApp task where the message should actually be sent to the lead, use send_task (which sends the content AND marks done). Use status="done" here only to close a task without sending (the "mark as done only" action). For a manual campaign step, send_task schedules-and-marks-done (same end state as status="done"), so both paths converge. - To edit a task's subject or body, use update_task_content instead.

update_task_content

ChatGPT
Update the message content (subject and/or body) of a message-based task (opportunity). Use when the user asks about: - Rewriting or tweaking the body of an email, LinkedIn or WhatsApp task - Changing the subject line of an email task Contract rules reproduced by the backend: - Only message-based task types are supported: email, linkedinSend, whatsappMessage. - subject is only allowed on email tasks. - Provide at least one of subject or message; the untouched field is preserved. - Pass the task's type so the body is formatted for its channel (email body kept as HTML, LinkedIn/WhatsApp as plain text). - For campaign-step tasks the edit is stored as a per-lead override (the shared campaign template is untouched); standalone tasks edit their own template. - Needs an existing task. To personalize a campaign step the lead has not reached yet, use personalize_step_for_lead.

update_watch_list

ChatGPT
Update an existing watch list. Use when the user asks about: - Renaming or changing the emoji of a watch list - Adjusting filters of a watch list - Changing how signals are processed (manual, create_opportunity, push_to_campaign) Contract rules reproduced by the backend: - personaId is accepted ONLY on a company watch list with signalProcessingType="push_to_campaign" (any other config is rejected), and only for teams in the personas beta - personaId is optional there: without it the signals are still recorded, they simply source no contact, so nothing reaches the campaign - get a personaId from list_people_database_personas (or create_people_database_persona); never invent one

update_watch_list_signals

ChatGPT
Set the status of one or several watch list signals to "handled" or "ignored". Use when the user asks about: - Marking signals they have acted on as handled (e.g. after reaching out to the leads) - Ignoring / dismissing irrelevant signals to clean up the signals list - Bulk-updating many signals at once Resolve the signal ids with list_watch_list_signals first. Bulk-capable: pass every id in one call (up to 1000). "reason" is optional and only meaningful when status="ignored".

validate_campaign_readiness

ChatGPT
Check if a campaign is ready to launch by validating steps content, sender configuration, DNS health, daily limits, and more. Use BEFORE review_and_launch_leads to detect issues early. Returns: - "ready": campaign can be launched - "has_errors": campaign has errors — check the errors array for details. Senders can be fixed with set_campaign_senders, content errors must be fixed in the campaign editor

create_campaign_with_sequence

Claude

get_campaign_details

Claude

get_campaign_sequences

Claude

get_inbox_conversations

Claude

get_lemleads_filters

Claude

get_settings

Claude

load_skill

Claude

push_leads_to_contacts

Claude

search_help_center

Claude

Capabilities

WritesInteractive

Example Prompts

Click any prompt to copy it.

App Stats

179

Tools

3

Prompts

ChatGPT, Claude

Platforms

Category

CRM Records

Works with

ChatGPT
Claude

Data refreshed daily