For agents: the plain-markdown version of this recipe is at /recipes/follow-up.md.
Follow up with contacts
A step-by-step recipe for an AI agent using the BaseBara connector. Follow it when the user wants to continue a conversation that has already started: with one person they name, or with everyone from a campaign or a stretch of outreach, on LinkedIn, by email, or on the phone. One person is a queue of one. A campaign is a queue of many. The steps are the same; the queue is worked one person at a time.
Tools used: get_linkedin_messages, send_linkedin_message, send_linkedin_connection_request, accept_linkedin_connection_request, find_contacts, get_contacts, verify_emails, create_contacts, log_contact_event, list_contact_events.
Use when
- One person: the user says "follow up with Dana", "did she reply", "nudge him", "what did I say to", or pastes a reply they got on any channel.
- A catch-up: the user asks what is new on LinkedIn, who wrote, who accepted, or wants to go through their inbox without naming anyone.
- After a campaign: the user says "follow up on the campaign", "who went silent", "second touches", "work the replies", or opens a new sitting after the outreach-campaign recipe has sent a wave.
Not for: the first message to someone (call reach_out), or a list nobody has contacted yet (call run_outreach_campaign).
What BaseBara can and cannot see
- LinkedIn: BaseBara reads the thread with
get_linkedin_messagesand sends the reply. - Email and phone: BaseBara cannot read the user's inbox or call log. Look at the other tools in the chat first. A mail connector (Gmail, Outlook, a CRM's mail tool) can search for the thread with the contact's address and send the reply in it; a calendar connector shows whether a meeting was booked; a CRM connector may hold the call notes. Use them in place of asking the user. When none is connected, the history comes from
list_contact_eventsfor what the user or a teammate logged, plus the user themselves, who pastes the reply or says how the call went, and the reply goes out from their own client or phone. Either way the reply and the send are logged in BaseBara. - The record of who was contacted:
list_contact_events. Every send the reach-out and campaign recipes make is logged there ascontactedwith its channel, so the follow-up queue can be rebuilt from it in any later conversation.
Inputs to collect
- Scope: who this is about.
- One person: a name plus company, a LinkedIn URL or slug, or an email address. For a name, run
find_contactsscoped to the company, thenget_contactsfor thecontact_idandlinkedin_url. - A list: the people the user pastes, or a campaign described by its slice and when it started. Default window: since the first send, or 30 days.
- Nobody named: the catch-up. Everything new on LinkedIn, plus replies by email when a mail connector is in the chat.
- One person: a name plus company, a LinkedIn URL or slug, or an email address. For a name, run
- The channel or channels the conversation is on. Infer from how the user describes it; ask only when unclear.
- The goal: book a call, send something, get a referral, keep the door open. For a campaign, the goal is the campaign's ask.
- Today's budget, when the queue has more than a few people: how many sends this sitting. The campaign recipe's daily batch applies (10 to 15 LinkedIn DMs, 15 to 25 emails, gaps between every two).
Steps
1. Build the queue
For one named person, the queue is that one row and this step is the read: go straight to 1d. Otherwise assemble the queue from three sources.
1a. The roster, from the log. Call list_contact_events with kinds contacted, replied, meeting, note, bounced, connected, connection_lapsed, scope "mine", and the window. For a pasted list, pass their contact_ids instead. Group the events by contact and keep the newest per person. That gives each row a last touch, its channel, and whether anything has come back since. People whose newest event is replied or meeting are already in conversation. People whose newest event is contacted are waiting on a reply. A connected event is the server's record that a connection request was accepted; connection_lapsed that it was declined, withdrawn, or expired, which takes them out of the LinkedIn sweep. A note saying they asked not to be contacted takes them out. A bounced event means that address is dead. The row stays only if the person has another channel.
1b. LinkedIn, three sweeps. Draft only where a message makes sense; nothing is marked read, accepted, or sent until the user says so.
- Unread messages. Call
get_linkedin_messageswith no recipient. Theconversationslist has every thread with unread messages, newest first: who (name, headline,public_identifier), whether they are connected, and the unread messages. Match each to a roster row where one exists; a reply from someone not on the roster is still a row. - Requests the user sent that have been accepted. From the roster, take the
contactedrows with channellinkedinwhose text says a connection request was sent (the server writes "Sent a LinkedIn connection request"), minus anyone with a laterconnectedorconnection_lapsedevent. Callget_contactswith thosecontact_idsfor theirlinkedin_url(saved contacts, so nothing new is revealed), thenget_linkedin_messageswith each. That read also settles the request on the server: an accepted one gets aconnectedevent, one no longer pending after a day getsconnection_lapsed, with no logging from you.connectedtrue and no messages from either side: accepted, nobody has spoken, a first DM is due.connectedtrue with messages: in conversation.invite_pendingtrue: still waiting. Under 3 days, skip; at 3 days or more it is a nudge row, and the nudge goes by email or phone with the request left pending. Both false: declined or withdrawn, leave them alone. Probe only the people on the roster, never the user's whole contact list. - Requests waiting on the user. The same inbox call returns
connection_requests: people waiting for the user to accept, with the note they attached. For each, one line on whether accepting makes sense: a prospect, a customer, a peer, or a real note is a yes; an unknown recruiter or a vendor pitch is the user's call. Accept only the people the user names, withaccept_linkedin_connection_request; the server logs theconnectedevent itself, and anext_stepappears only when it could not. Never accept a whole list on your own.
1c. Email and phone. With a mail connector in the chat, get the roster's email rows' addresses with get_contacts and search the mailbox for replies and bounces from them since the send. With a calendar connector, check for meetings booked with them. Without either, ask the user once, for the whole list: who replied, who bounced, who they spoke to.
1d. Read each thread that needs it. For anyone about to be written to, read the conversation itself before drafting. LinkedIn: get_linkedin_messages with the recipient, for whether they are connected, whether a request is pending, and the recent messages oldest first. Email: the thread in the mail connector, or the reply the user pastes. Phone: the user's sentence on how the call ended. If the person also has a linkedin_url, read that thread too, whatever channel the conversation is on.
Never draft a follow-up before reading what has happened. The next message continues the conversation. It never restarts it.
2. Classify every row
| State | Signals | Next action |
|---|---|---|
| Interested | A question, "tell me more", asks for pricing or a call | Short positive reply. Propose two time windows or a concrete next step. |
| Objection | Price, fit, timing, "we use X", "not now" | One sentence acknowledging it, one specific reframe, one small next step. |
| Wrong person | "Not me", "talk to X" | Thank them, ask for the right name. Then find that person with find_contacts. The referral is the best outcome here. |
| Soft no / vague | "Send me something", "maybe later" | One short value-add tied to something specific. Do not pile on. |
| Stop | "Please don't message me", "remove me", "unsubscribe" | No reply. Tell the user to mark them do-not-contact. "Not interested" is an objection, not a stop. |
| Accepted, no message yet | LinkedIn request accepted, empty thread | First DM, written from the reach-out drafting rules. Never "thanks for connecting". |
| No reply yet | Only the user's messages, on any channel | Follow-up only if 3 or more days have passed, and on a different channel from the unanswered touch when the person has one. Shorter than the last message, new angle, stands alone. After the second silence, the close. |
| Closed out | The 25 to 50 word close already went | Nothing. Honor it. |
| Voicemail, no call back | The user left a message, nothing came back | Treat as no reply. The next touch is short and on a written channel, so they can answer on their own time. |
| Bounced or wrong address | The email came back, the number was dead, or get_contacts shows email_verification undeliverable or bounced |
Nothing to follow up on that channel. get_contacts again for another channel, or find_contacts for a current colleague. |
| Not connected, request pending | invite_pending true |
Under 3 days: wait. At 3 days: leave the request pending and send the follow-up by email or phone, without mentioning the request. If they accept later, the thread continues on LinkedIn from wherever the conversation got to. |
| Not connected, no request | connected false, invite_pending false |
Offer send_linkedin_connection_request. The follow-up goes after acceptance. |
If a reply is ambiguous, ask the user which bucket fits. Do not guess.
3. Show the queue and plan the sitting
For one person, this is a one-line read and you move on. For a list, show the queue grouped by action before drafting anything:
- Reply now: interested, objection, wrong person, soft no. Replies come first; they are the campaign's return.
- First message: accepted requests with an empty thread.
- Accept: requests waiting on the user.
- Nudge: silent 3 or more days, not yet closed out, including pending connection requests. The nudge goes on a different channel from the unanswered touch.
- Close: silent after the nudge.
- Wait: silent under 3 days, on any channel.
- Out: stops, bounces, closed-out, declined.
Ask the user which groups to work today and confirm the budget. Order the sends: replies, then first messages, then nudges, then closes. If the queue exceeds the budget, the rest is tomorrow's sitting; say so.
4. Log what happened, per person
Record the reply before drafting, so the team sees it on the contact. Use the channel the reply came in on: linkedin, email, or phone.
- Interested, objection, soft no, wrong person:
log_contact_eventkindreplied, with the channel and text as close to their words as possible. - A call that happened, or a call booked: kind
meeting. - Stop: kind
note, text "asked not to be contacted" with the channel. Tell the user to mark them do-not-contact. - Bounced email: kind
bounced, with the address in the text. This marks the address undeliverable for everyone, so no teammate tries it andget_contactsshows it from now on. A dead number: kindnote. - Accepted, no reply yet, request pending, not connected: nothing to log. Acceptance and lapse are the server's own events.
If the person is not saved, log_contact_event answers not_in_my_contacts. Call create_contacts with the basebara_contact_id and re-send the event, without asking the user.
5. Draft, one person at a time
Two to four sentences. Plain text, no markdown, no signature. One idea per message. One ask, or none. On email, keep the existing subject line; a reply never gets a new one. On LinkedIn, no subject-line thinking at all. For a call-back, write the two sentences the user will open with.
- Reference what they actually said, in their words where possible.
- A follow-up to silence brings something new: a sharper point, a relevant example, a useful asset. If there is nothing new, skip it.
- In a queue, every nudge is written for its person. Two people getting the same follow-up text is the same defect as in the campaign, and on LinkedIn the server will refuse it anyway.
- A reply keeps the conversation on the channel it came in on. Silence rotates it: after 3 days without an answer, the next touch goes on a different channel the person has (email after a silent DM or a pending connection request, LinkedIn after a silent email, phone when both are quiet), stands alone, and never mentions the unanswered one. Someone with a single channel gets the follow-ups there.
- Before a rotation onto email, or a first email to someone who accepted a request, check the address. Read
email_verificationandemail_verified_aton theget_contactsrow, and callverify_emailswhen the check is null, notdeliverable, or older than about 30 days.undeliverableorbouncedmeans another channel.riskyis the user's call. - The last follow-up in a cadence is a 25 to 50 word close that leaves the door open and pitches nothing. Once sent, honor it.
6. Show, then send, then the next person
Show the exact text and the one-line reasoning. Wait for approval. A one-word "go" per message is fine once the queue is agreed; silence is not.
- LinkedIn: call
send_linkedin_messagewith the recipient and the approved text. The server saves the person and logs the send ascontacteditself; do not log it again, and follownext_steponly when the answer saysevent_loggedfalse. The server paces sends to protect the user's own LinkedIn account: one message per person per day, limits per hour and per day, and the same text to only a few people. A pacing refusal is final. Report the reason and the wait. Do not retry, and do not move the message to another channel to get around it. - Email: with a mail connector in the chat, send the approved text from it as a reply in the existing thread, and log
contactedwith channelemailonce it confirms. Without one, hand the user the text to send from their own client, and log it when they say it went out. - Phone: hand the user the opener. Log
contactedwith channelphonewhen they say they called, ormeetingif they spoke.
Then move to the next row. Do not fire the next send in the same breath; the user's reading time is the gap. Stop when the budget is spent, even if the user says "keep going", and say what the next sitting holds.
7. End of a sitting
For a queue, reprint it with what moved: who was answered, who got a first message, who was nudged, who is now out, and who is waiting with a date. The events are the record; the reprinted queue is the view. The next sitting starts again at step 1 and rebuilds the queue from the log.
Output shape
One person:
THREAD with <Name>, <Title> at <Company> LinkedIn — connected ✓ (or: Email — 2 sent, 1 reply · Phone — voicemail Oct 1)
Sep 28 you: <first line of your message>
Oct 1 them: "Thanks — we're using Outreach today, not sure it's worth switching."
READ Objection: incumbent tool.
REPLY (<channel>)
<the text, exactly as it will go out>
NEXT Approve to send. If no reply by Oct 8, one short close.
A queue (campaign or catch-up), before any drafting:
FOLLOW-UP QUEUE <slice or "inbox catch-up"> 14 people · budget today 10
REPLY NOW (3)
Dana Ruiz, RevOps @ Acme — email — Oct 14: "send me the comparison" → interested
Sam Ortiz, VP Sales @ Beta — LinkedIn — Oct 13: "not now, maybe Q1" → objection
Jo Park, Sales Ops @ Zeta — LinkedIn — Oct 12: "talk to our RevOps lead" → wrong person, referral
FIRST MESSAGE (2) accepted your request, empty thread
Priya Nair, CRO @ Gamma — requested Oct 9, connected Oct 12
Lee Chen, Director Sales Ops @ Delta — requested Oct 10, connected Oct 14
ACCEPT (1)
Alex Kim, Head of Growth @ Epsilon — note: "saw your post on outbound" → yes, short reply
NUDGE (4) silent 3+ days, one follow-up each
...
WAIT (3) 2 requests pending · 1 emailed yesterday
OUT (1) 1 bounce → bounced logged, try LinkedIn next sitting
NEXT Work the first three groups today (9 sends), nudges tomorrow? Drafts come one at a time.
Rules
- Read the conversation before writing. Always. On LinkedIn that is the thread; on email and phone it is the mail or calendar connector when one is in the chat, otherwise the logged events plus what the user tells you.
- One person or many, the queue is worked one row at a time, each draft written for its person and approved before it goes.
- Replies before nudges. A reply is the point of the outreach; a nudge can wait a day.
- Never send a reply the user has not approved.
- Three days of silence on a first touch, including a connection request, means the next touch goes on another channel the person has. Leave a pending request pending, and never mention it in the other channel's message.
- A reply pins the conversation to its channel. Only silence rotates it.
- "Not interested" is an objection. Only an explicit "stop" is a stop.
- A wrong-person reply is a referral. Act on it.
- A pacing refusal is final. Report it, and end the sitting if it was the daily cap.
- Respect the budget. A queue of thirty is three sittings, not one burst.
- Another connector can read and send; BaseBara is still where every reply and send is logged.
- Log the reply before you draft, with its channel. A LinkedIn send is logged by the server; an email or phone send by you, after it goes. A bounce is logged as
bounced, never as a note. - Check the address before a touch moves onto email. Never email an
undeliverableorbouncedaddress.