# 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_messages` and 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_events` for 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 as `contacted` with 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_contacts` scoped to the company, then `get_contacts` for the `contact_id` and `linkedin_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.
- **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_messages` with no recipient. The `conversations` list 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 `contacted` rows with channel `linkedin` whose text says a connection request was sent (the server writes "Sent a LinkedIn connection request"), minus anyone with a later `connected` or `connection_lapsed` event. Call `get_contacts` with those `contact_ids` for their `linkedin_url` (saved contacts, so nothing new is revealed), then `get_linkedin_messages` with each. That read also settles the request on the server: an accepted one gets a `connected` event, one no longer pending after a day gets `connection_lapsed`, with no logging from you. `connected` true and no messages from either side: accepted, nobody has spoken, a first DM is due. `connected` true with messages: in conversation. `invite_pending` true: 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, with `accept_linkedin_connection_request`; the server logs the `connected` event itself, and a `next_step` appears 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:

1. **Reply now**: interested, objection, wrong person, soft no. Replies come first; they are the campaign's return.
2. **First message**: accepted requests with an empty thread.
3. **Accept**: requests waiting on the user.
4. **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.
5. **Close**: silent after the nudge.
6. **Wait**: silent under 3 days, on any channel.
7. **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_event` kind `replied`, 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 and `get_contacts` shows it from now on. A dead number: kind `note`.
- 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_verification` and `email_verified_at` on the `get_contacts` row, and call `verify_emails` when the check is null, not `deliverable`, or older than about 30 days. `undeliverable` or `bounced` means another channel. `risky` is 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_message` with the recipient and the approved text. The server saves the person and logs the send as `contacted` itself; do not log it again, and follow `next_step` only when the answer says `event_logged` false. 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 `contacted` with channel `email` once 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 `contacted` with channel `phone` when they say they called, or `meeting` if 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 `undeliverable` or `bounced` address.
