Push prospects WITH their full prefilled sequence into our DB for a chosen sending account, deduped on profileUrl (re-ingesting updates rather than duplicates). `campaignName` SELECTS the campaign: a name that already exists on that account is added to, and a name that does not is created. The reply always says which one you got — `result.campaignName`, `result.campaignLaunchedAt` and `result.campaignCreated` — because a batch landing in a campaign you did not mean is otherwise invisible. Read `result.warnings`: the sharpest one says these prospects joined a campaign that is ALREADY LIVE, which means launch:false staged nothing. Each prospect carries connectionNote + firstMessage + followUp1..3/closer and optional metadata. startStage="connection" (default) enters the invite drip; startStage="message" (already a 1st-degree connection, e.g. an inbound invitation you accepted) NEVER sends an invite — and REQUIRES that the connection already exists. Nothing verifies it: if you are not 1st-degree when the first message comes due, the profile has no Message control, the send fails as "no message entry point", and that failure is PERMANENT — the prospect is retired having received nothing. A PENDING inbound invitation is NOT a connection, so accept it (li_accept_invitations, and check the reply says it was accepted) BEFORE staging anyone at the message stage — their first message is scheduled for connectionWaitHours (24h by default) after the campaign is launched, and with launch:false nothing is scheduled until you call li_launch_campaign. Someone already queued, invited or connected through this account's invite-only list (li_send_connection_now) is not invited twice: their invite is MOVED into this campaign with its state, the sequence attached, listed in `result.adoptedFromInviteOnly`. Replaces the skill's Airtable master sheet. Does not generate text — supply it.
Every confirmed ingest also returns `collisions`: who among these prospects had ALREADY been approached from the accounts you can see, measured just before the rows were written. A collision is NOT an error and NOTHING was refused — every prospect was imported. It is there because the cost lands later and invisibly: re-importing someone into the SAME campaign is idempotent, but importing them into a DIFFERENT campaign creates a second invite the drip attempts, and at send time LinkedIn answering "already invited" or "already connected" sends nothing while still spending a ~50s browser session the account then has to wait behind. Anything else sends a REAL second invite, charged against each account's rolling 7-day invite allowance this system enforces, set by the account's detected LinkedIn tier — 63-70 a week (5-10 a day) new or unverified, 90-100 a week (18-24 a day) established, 135-150 a week (25-30 a day) Premium, redrawn within the range each week. Our own policy, not a published LinkedIn limit; the daily band exists only to stop a burst. li_list_accounts gives each account's actual numbers. `collisions.summary` is one sentence safe to relay verbatim (it is folded into `result.plan` too), and the report is bounded by the accounts you can see and reports nothing about anything outside them (`collisions.scopeNote` says so), so a clean result means 'not in your pipeline', not 'nobody anywhere'. Do NOT call li_check_targets first to get it: this runs the same check, and it runs it knowing the campaign, so people already in that campaign read as an idempotent update here instead of as a clash elsewhere. That tool is for the list you have not decided to import yet.
ONE CALL, MANY CAMPAIGNS (up to 500 prospects): each prospect may name its own account (sessionLabel/sessionId), campaign (campaignId, or campaignName to find-or-create), strategy and ICP; a field it leaves out is inherited from the call. The batch is grouped by (account, campaign, strategy, ICP), each group is its own import with its own pacing sentence and collision report, and the reply's `campaigns` lists every campaign touched (created or existing, imported/updated/rejected) while `routed` gives each submitted row's campaign and outcome by position. A row naming a campaignId takes that campaign's account, strategy and ICP. `suppressed` and `rejectedRows` are lifted to the top level as one list. Everything is resolved before anything is written: an unknown account, campaign, strategy or ICP, or the same person routed to TWO places, refuses the whole call. launch:false stages every campaign it touches.
SHARED COPY: defaultSteps gives one text per field (connectionNote, firstMessage, followUp1..3, closer — the names a row's own copy uses) for every row that brings none. {first} is the only variable: the row's firstName, else the first word of name, filled in at import, so the stored text is what is sent. A row with no first name for a template using it is not imported and says why; `copy` reports per-step lengths and every row over a limit (a note over 300 is shortened, a message over the limit is refused).
The do-not-contact list is the one thing here that DOES refuse: a prospect on it (the team's own list or the install-wide one) is NOT imported, and comes back by name with the reason in `result.suppressed` (top-level `suppressed` when the batch was split). Relay that list — a person the user asked to reach who silently never entered the pipeline looks like a bug weeks later. Manage the list with li_suppress_profiles / li_list_suppressed / li_unsuppress_profile.
Engage-before-invite: a connection-stage prospect may carry engagePostUrl (one of their own posts, the one the note and first message mention). The sending account visits their profile and reacts to it first, and the invite follows about a day later, inside the window it goes out in (the person's own morning when the account times invites to each prospect, otherwise its sending hours; 18-30 hours with no window to aim at); about one in seven is only viewed. Reactions are capped at 15-25 a day per account (our policy, not LinkedIn's) and run in sending hours, so a large batch is engaged over days while everyone else keeps being invited. A post that is gone or not theirs releases the invite without a reaction. Message-stage prospects ignore engagePostUrl (`result.warnings` says so). `result.engagement` counts both, and the campaign contacts read shows each person's `engagement`.