Split a List Across Experiment Arms
li_experiment_assignRun an A/B(/n) test without dealing people out by hand. An experiment has arms (key, strategy, engage on/off); this tool assigns each prospect to an arm SERVER SIDE and imports them, staged, through the same multi-campaign import as li_ingest_prospects: one campaign per (arm, ICP, account), named " | | ". Nothing sends until those campaigns are launched (the reply's launch.campaignIds).
ASSIGNMENT is stratified by ICP and account (stratifyBy) and deterministic: a hash of the experiment and the person, then the arm with the fewest people in that stratum, so arms stay balanced to within one person per stratum across calls. It is STICKY: someone assigned before keeps their arm and campaign. requirePostForArms keeps people without a reactable engagePostUrl out of those arms; they go to controlArm and are marked forced (left out of the comparison). An engage arm keeps each person's post and reacts before the invite; other arms drop it.
COPY per arm, most specific first: the row's armCopy[arm], the row's own text, armSteps[arm], then defaultSteps ({first} is the only variable, rendered at import).
First call: experiment {name, arms, ...}; later calls: experimentId. Up to 500 prospects. Dry-run unless confirmAssign=true; everything (accounts, ICPs, strategies, campaigns, copy) is checked before anything is written. Results: li_outreach_analytics(groupBy "arm", experimentId).
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| icpId | No | Default ICP label id for rows naming none | |
| icpName | No | Default ICP label by name | |
| armSteps | No | Shared text per step for ONE arm, keyed by arm; a step it does not name falls back to defaultSteps | |
| prospects | Yes | 1-500 people: profileUrl, their account and ICP (or the call's), text or armCopy, optional engagePostUrl | |
| sessionId | No | Default sending account id for rows naming none | |
| experiment | No | A NEW experiment. Sent again with the same name and arms, it is reused. | |
| defaultSteps | No | Shared text per step (connectionNote, firstMessage, followUp1, followUp2, followUp3, closer) for every arm, for each step its armSteps does not name | |
| experimentId | No | An experiment you already have (from an earlier reply); or give `experiment` | |
| sessionLabel | No | Default sending account for rows naming none | |
| confirmAssign | Yes | Must be true to write; false previews the split and the campaigns | |
| idempotencyKey | No | Same key on a retry after a timeout returns the stored answer (24h) |