# Run LinkedIn outreach at a safe pace

You have access to your own LinkedIn session, your cold email
sequencer, a team messaging tool, and three tables you can read and
write. Run this **[several times each weekday, for example five slots
spread two hours apart]** for **[the outreach queue of people you
want to reach on LinkedIn]**.

## 0. Set up (skip if you're already connected)

If you don't already have live tool access for this, connect it
first. This prompt is only as real as the tools behind it:

1. Get a Tulina account: book a demo at https://tulina.ai/book.
2. Add Tulina's MCP server to your assistant: https://mcp.tulina.ai/mcp
3. In Tulina, connect your own API keys for the tools this
   prompt uses: your LinkedIn account, your cold email sequencer, and
   your team's messaging tool.

Once connected, your assistant has real tool access and the rest of
this prompt runs as written.

## 0b. Already on Tulina? Three things around the run

An account and the MCP server are not the whole setup. Two of these come
before the run below and one after it, and together they turn this from a
one-off answer into a process your workspace keeps:

1. **Activate this process's connectors first.** Call
   `oto_connector(op="list")`, match it against the tools named in step 3
   above, and `oto_connector(op="select", name="...")` every one that
   isn't active yet. Selecting a connector does not mount its tools in the
   conversation you're already in — reach them through `oto_call` for this
   run, or open a fresh conversation once they're all on.
2. **Attach the work to an existing project.** `oto_project(op="list")`
   shows your active org's projects: pick the one this work belongs to
   rather than opening another, and keep its id. The project is where this
   process, the tables it writes to and the connectors it uses hang
   together.
3. **When the run is done, save it as a process — with its graph.** Write
   the body with `oto_procedure(op="set", ...)`, then attach it with
   `oto_project(op="link", project_id=..., target_type="procedure",
   target_ref="<your slug>")`. The body has to carry a drawing: read
   `oto_guide(op="read", slug="procedure-flowchart")` and follow it
   exactly — ONE untagged fenced block in that grammar, opening with the
   trigger and a quoted example of what you'd type to start a run. Tulina
   parses that drawing back into the graph it renders as the process's
   default view, which is what makes it come out in the same style as
   every other process in the app; a drawing the grammar can't read falls
   back to raw characters instead. Saving a process needs org-admin
   rights — without them, hand the finished body to someone who has them.

## 1. Check the account before any send

### The tables this run keeps

- **The governor**: one row holding `status` (active, hold or
  paused), `level`, `invites_per_day`, `messages_per_day`,
  `level_since`, `hold_until`, `last_eval`, `last_reason`, and a
  `history` field that gets one dated line per change and is never
  rewritten.
- **The outreach queue**: one row per person, keyed on the
  normalized LinkedIn URL, with the person's provider id once known,
  a connection note (200 characters at most), a first message and a
  follow-up, written in advance and **sent verbatim, never edited by
  the run**. Its LinkedIn stage moves queued → invited → connected →
  first message sent → follow-up sent; any of them → replied; invited
  → withdrawn; or skipped. Each move stamps its own timestamp. When
  the person is also in an email sequence, the row carries that
  lead's id and campaign.
- **The stale invitation backlog**: pending invitations older than
  **[30 days]** waiting to be withdrawn, with a status of to
  withdraw, withdrawn, gone or error.

**This is the only thing allowed to send from the account
automatically.** Any other LinkedIn campaign becomes a new set of
rows in the queue, under the same budget, never a second sender on
the same seat.

Every slot starts here, and protecting the account comes before
volume. When in doubt, do less and say why in the report.

- Read the governor. `status = paused` means no LinkedIn write at
  all in this run: stop and report that a person has to set it back
  to active. `status = hold` runs normally at the current level; hold
  only blocks promotion.
- Check the LinkedIn session's status. Anything other than alive:
  set the governor to paused with the reason and the date, append a
  history line, and stop.
- If an earlier slot today stopped on a block, this slot writes
  nothing to LinkedIn.
- Weekends and public holidays: do nothing.

## 2. Set the day's budget from account health

Once a day, in the first slot, before any send, review the level and
stamp `last_eval` with today's date. The ladder is **[your levels,
for example 5, 8, 12, 15 and 20 invitations a day, with messages at
the same figures]**. Never skip a level and never go above the top
one.

- **Demote** one level (never below the first) when, since the last
  review, a run stopped on a limit-type error (429, "limit", "too
  many", "weekly"), or when acceptance on invitations sent **[7 to
  21 days]** ago fell under **[10%]** with at least **[20]** of them.
  Set `level_since` to today, set `status = hold` with `hold_until`
  **[7 days]** out, and append the reason to the history.
- **Pause** everything when any run reported a checkpoint, a
  captcha, a verification request, a restriction or a session that is
  no longer alive. All writes stop until a person resumes them. The
  run never sets `status` from paused back to active itself.
- **Promote** one level only when every test passes: at least
  **[5]** weekdays at the current level and any hold has expired; no
  blocked run in the last 7 days; acceptance on invitations sent 7 to
  21 days ago of at least **[20%]** when there are 10 or more of them
  (with fewer, skip this test); a stale backlog small enough for the
  level you are moving to (**[the smaller the backlog has to be, the
  higher the level]**); and at least twice the new daily cap still
  queued, since there is no reason to go faster with nothing to send.
- Otherwise leave the level alone and write the test that failed into
  `last_reason`.

Each slot then works within its share of the day: invitations and
messages each capped at the daily figure divided by the number of
slots, rounded up, withdrawals at **[6]** per slot, profile lookups
at twice the slot's invitation cap. The daily caps are counted from
the queue's own timestamps for today.

## 3. Record replies and acceptances

Reads only, plus one email-side pause.

- **Email replies.** For each email campaign the queue's people are
  in, list the leads that replied (also those marked interested or
  not interested). Match them to queue rows and mark them replied
  with the time, unless they already are.
- **LinkedIn replies.** Read the first page of the inbox. For each
  row that is invited, connected or mid-sequence, find its thread by
  the stored chat id or the attendee's provider id. If the last
  message's sender id is not your own account's id, the person
  replied: mark the row replied with that message's time, store the
  chat id, and **pause that person's lead in the email sequence**.
  Never trust the message's "is sender" flag; it has been seen false
  on every thread of an account, including the account's own.
- **Acceptances.** Read the first page of your connections, newest
  first. A connection matching an invited row, on provider id or on
  the slug of its LinkedIn URL, moves to connected with the
  connection's own date.
- Add a dated note to each row saying which channel the reply came
  from.

## 4. Send the due messages

Up to the slot's message cap, within the day's cap.

1. Follow-ups first: rows whose first message went out at least
   **[6 days]** ago get the follow-up, in the existing thread when
   there is one, otherwise to the person's provider id.
2. Then first messages: connected rows, oldest connection first.
   Store the chat id that comes back.

Re-read each row just before sending it. A row that became replied
or excluded since the slot started is skipped.

## 5. Invite after a fresh profile check

Before the first invitation of the slot, read the first page of
invitations the account has sent and count the ones created today.
That count plus this slot's planned invitations must stay within the
daily cap; lower this slot's number if not. This is what catches any
other sender on the account.

Take queued rows in **[your priority order, for example people with
no email address first, then by fit score with the freshest leads
ahead of the older backlog]**. For each:

1. Read the profile by its public identifier. The identifier that
   comes back must be the one you asked for: a mismatch is a redirect
   onto someone else. Mismatch or not found → skipped, "profile not
   resolved".
2. Clearly left the company → skipped and excluded, with the reason
   and the date, and their email lead paused.
3. Already a first-degree connection → connected, nothing to send.
   An invitation already pending → invited, nothing to send.
4. Otherwise send the invitation with the stored note, and stamp the
   row invited with the provider id, the time and the invitation id
   when one comes back.

## 6. Withdraw stale pending invitations

At most the slot's withdrawal cap.

1. Your own rows first: invited more than **[21 days]** ago with an
   invitation id → withdraw it and mark the row withdrawn. "Not
   found" means it was answered: check the profile once, connected if
   first degree, otherwise skipped as declined.
2. Then the backlog, oldest first: withdraw, mark withdrawn with the
   time; already gone → gone; any other error → error with a note,
   and stop this step for the slot.
3. Once a week, in the first slot, refill the backlog: page through
   the account's sent invitations with the returned cursor, at most
   **[8 pages]**, stopping at the first page whose old invitations are
   all already listed, and add each invitation older than [30 days]
   that is not there yet, keyed on its invitation id.

A long tail of pending invitations is one of the things that holds an
account back, which is why the promotion test in step 2 reads the
backlog's size.

### Pacing and stop rules, for every step

- Never two LinkedIn writes back to back: put a read or a table write
  between any two sends, invitations or withdrawals. Never send in
  parallel calls.
- Any error mentioning 429, "limit", "too many" or "weekly": stop
  every LinkedIn write and report the run as blocked, quoting it. The
  next daily review demotes.
- Any error mentioning a checkpoint, a captcha, verification, a
  restriction or a dead session: stop, set the governor to paused
  with the reason, append the history, report blocked.
- An invitation refused because of its note is never resent without
  the note: stop and report it.
- Never retry a failed LinkedIn write in the same slot. Never catch
  up a missed slot or a missed day.

## 7. Post the weekly recap

Once a week, after the last slot of **[the last working day]**, read
the tables and the week's runs and post **one** message to **[your
team's sales channel]**. Read-only everywhere else.

- Count with aggregates, never by pulling every row: invitations
  this week and to date; acceptances this week and to date;
  acceptance rate on invitations sent at least 7 days ago ("too
  early" under 10 of them); messages this week (first messages plus
  follow-ups); replies this week and to date, and the reply rate on
  people who received a first message ("too early" under 10); people
  still queued, and the weeks of work that leaves at the current daily
  cap; stale invitations withdrawn this week and left.
- Replies this week by name, company and channel, at most **[8]**, then
  "+N more".
- The level and its daily caps, with any change this week from the
  governor's history and its reason.
- Health: green when active with no blocked run this week, amber on
  hold or after a limit block, red when paused, with one line saying
  why when it is not green.
- Numbers in bold, no commentary, no advice, under **[900]** characters.
  A metric that could not be computed shows "n/a"; never estimate
  one. If the post is refused, report it; never post twice.

## Never

Send anything that is not stored in the queue, edit the stored copy,
contact a row that replied, was skipped, withdrawn or excluded, start
or resume anything in the email sequencer (pausing a lead on a reply
is its only write there), exceed the governor's caps or top level, or
lift a pause.

## Output

Report, per slot: the governor's level and caps and any change with
its reason, invitations sent (who), messages sent (who, first or
follow-up), acceptances, replies by channel, withdrawals (yours and
backlog), the account's invitations today, skips with their reasons,
the queue's stage counts and the backlog left. Only actions the tools
confirmed.
