Campaign Tools — MCP Tool Reference¶
Campaign tools manage the lifecycle of survey campaigns: creating campaigns, assigning interviewers and respondent pools, distributing workload, and sending invitations. Many operations use a batch response pattern with added/skipped/reasons fields.
list_campaigns¶
List all campaigns, optionally filtered by project, name, or status.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
project_id |
string |
No | — | Filter by project ID |
name |
string |
No | — | Filter by campaign name |
status |
string |
No | — | Filter by status |
limit |
integer |
No | 100 |
Maximum results to return |
Returns: { items: [...], count, limit }
get_campaign¶
Get a campaign by ID.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
campaign_id |
string |
Yes | — | Campaign UUID |
Returns: Campaign object or { error }.
create_campaign¶
Create a new campaign. Campaign mode is automatically determined based on whether interviewers are assigned.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string |
Yes | — | Campaign display name |
project_id |
string |
Yes | — | Parent project ID |
questionnaire_id |
string |
Yes | — | Questionnaire to use |
status |
string |
No | "draft" |
Initial status: draft, active or scheduled. Anything else is refused |
admits_volunteers |
boolean |
Yes | — | Whether people never drawn into the campaign's pool may take part (walk-ins, the open link). No default: state it. |
offers_ai_channel |
boolean |
Yes | — | Whether respondents may also take the survey in their own AI assistant. No default: state it. |
intro_text |
string |
When offers_ai_channel is true |
— | Who is collecting the data and why, shown word for word before the first question |
consent_text |
string |
When offers_ai_channel is true |
— | What the respondent agrees to, shown word for word; a fixed paragraph about the AI provider is always added after it |
Returns: Created campaign object.
update_campaign changes status only along the campaign's lifecycle: draft → active or scheduled; scheduled → active or draft; active → paused, completed or aborted; paused → active, completed or aborted; completed or aborted → draft. Any other move, or an unknown status, is refused and nothing is changed.
update_campaign also takes offers_ai_channel, intro_text and consent_text, each optional and kept from the stored campaign when omitted. The texts are checked against the result: turning the assistant on for a campaign that already holds both texts needs no restatement, and blanking a text while the assistant is offered is refused. Turning the assistant off stops every pairing code not yet used; respondents already connected can still finish.
add_interviewers_to_campaign¶
Add interviewers (Users with interviewer role) to a campaign. Uses the batch response pattern.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
campaign_id |
string |
Yes | — | Campaign UUID |
user_ids |
string[] |
Yes | — | List of user IDs to add as interviewers |
Returns:
{
"campaign_id": "...",
"added": ["uid1"],
"already_present": ["uid2"],
"skipped": [{"id": "uid3", "reason": "User not found"}],
"total_added": 1
}
Skip reasons: "User not found", "No interviewer role".
remove_interviewers_from_campaign¶
Remove interviewers from a campaign.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
campaign_id |
string |
Yes | — | Campaign UUID |
user_ids |
string[] |
Yes | — | List of user IDs to remove |
Returns:
update_campaign_questionnaire¶
Change the questionnaire assigned to a campaign.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
campaign_id |
string |
Yes | — | Campaign UUID |
questionnaire_id |
string |
Yes | — | New questionnaire UUID |
Returns:
{
"campaign_id": "...",
"old_questionnaire_id": "...",
"new_questionnaire_id": "...",
"success": true,
"warning": "Campaign has existing surveys — they reference the old questionnaire"
}
Existing Surveys
The warning field appears if the campaign already has surveys. Existing surveys retain references to the old questionnaire.
assign_respondents_to_interviewer¶
Assign specific respondents to a specific interviewer within a campaign for workload distribution.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
campaign_id |
string |
Yes | — | Campaign UUID |
interviewer_id |
string |
Yes | — | Interviewer user UUID |
respondent_ids |
string[] |
Yes | — | List of respondent IDs to assign |
Returns:
unassign_respondents_from_interviewer¶
Remove respondents from an interviewer's assignment. Respondents remain in the campaign pool but become unassigned.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
campaign_id |
string |
Yes | — | Campaign UUID |
interviewer_id |
string |
Yes | — | Interviewer user UUID |
respondent_ids |
string[] |
Yes | — | List of respondent IDs to unassign |
Returns:
get_interviewer_workload¶
Get the workload for a specific interviewer in a campaign — assigned respondents and survey completion status.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
campaign_id |
string |
Yes | — | Campaign UUID |
interviewer_id |
string |
Yes | — | Interviewer user UUID |
Returns:
{
"campaign_id": "...",
"interviewer_id": "...",
"assigned_respondents": [
{"id": "...", "name": "...", "email": "...", "age": 35, "gender": "female", "location": "Berlin"}
],
"survey_status": {
"respondent_id": {"survey_id": "...", "status": "completed", "completed": true}
},
"statistics": {
"total_assigned": 10,
"completed": 3,
"in_progress": 2,
"not_started": 5,
"completion_rate": 0.3
}
}
get_unassigned_respondents¶
Get respondents in a campaign not yet assigned to any interviewer.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
campaign_id |
string |
Yes | — | Campaign UUID |
Returns:
{
"campaign_id": "...",
"unassigned_count": 5,
"unassigned_respondents": [
{"id": "...", "name": "...", "email": "...", "age": 28, "gender": "male", "location": "Munich"}
]
}
assign_pool_to_campaign¶
Assign a respondent pool to a campaign. Links the pool so bulk_create_surveys can create surveys for all pool respondents.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
campaign_id |
string |
Yes | — | Campaign UUID |
pool_id |
string |
Yes | — | Respondent pool UUID |
Returns:
{
"campaign_id": "...",
"pool_id": "...",
"pool_name": "Urban Millennials",
"respondent_count": 200,
"previous_pool_id": null
}
get_campaign_pool¶
Get the respondent pool assigned to a campaign.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
campaign_id |
string |
Yes | — | Campaign UUID |
Returns (pool assigned):
{
"campaign_id": "...",
"pool_id": "...",
"pool_name": "...",
"pool_description": "...",
"respondent_count": 200,
"respondent_ids": ["..."],
"source_type": "strategy",
"source_strategy_id": "...",
"quality_score": 0.95,
"has_pool": true
}
Returns (no pool): { campaign_id, pool_id: null, has_pool: false, message: "..." }
send_campaign_invitations¶
Send survey invitation emails to campaign respondents. Sends real emails with magic link access.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
campaign_id |
string |
Yes | — | Campaign UUID |
respondent_ids |
string[] |
No | — | Specific respondents to invite |
send_to_all_uninvited |
boolean |
No | false |
Send to all uninvited respondents |
Real Emails
This tool sends actual emails. You must specify either respondent_ids or send_to_all_uninvited=true.
Returns:
{
"campaign_id": "...",
"results": {
"sent": [{"respondent_id": "...", "name": "...", "email": "...", "survey_id": "...", "survey_url": "..."}],
"skipped": [{"respondent_id": "...", "name": "...", "reason": "No email address"}],
"failed": [{"respondent_id": "...", "name": "...", "reason": "Email send failed: ..."}]
},
"summary": {"sent": 8, "skipped": 1, "failed": 1}
}
When the campaign offers the AI assistant, every invitation email adds a paragraph pointing the respondent to the AI option on their survey page, and each sent entry also carries connector_url, the respondent connector address.
Eligibility requirements per respondent:
- Must have an email address
notification_preferencemust not be"none"- Survey must not be already completed or in progress
- The campaign must accept them. Inviting somebody it never drew into its pool needs a campaign that admits volunteers and is actively fielding — the same gate a walk-in meets. Their survey is then recorded as a volunteer rather than as a drawn case, so the dataset can still tell the two apart.
Skip reasons: "Respondent not found", "No email address", "Opted out of notifications", "Survey already completed", "Survey already in_progress", plus a refusal naming whichever half of the admission gate said no — the campaign does not take walk-ins, or it is not currently being fielded.
Chain Tools¶
Two of the sequences above are also available as a single call. They are not shortcuts that skip anything: each step is the same tool documented on this page, so the same records are written, the same audit events land, and any refusal a raw tool would give you is given here unchanged — plus stopped_at_step, naming which step a stop came from. Nothing is undone, so a chain that stops partway leaves what the earlier steps built and you continue with the individual tools.
A chain checks your entitlement for every step before it builds anything. If your subscription does not cover a later step you are refused up front rather than handed a half-built fielding.
prepare_campaign_fielding¶
Run a campaign's whole fielding chain: create the sampling strategy, generate the respondent pool, assign the pool to the campaign, publish (and pin) the questionnaire version the campaign will field, and create the surveys.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
campaign_id |
string |
Yes | — | Campaign to prepare. It must already name the questionnaire you intend to field |
admits_volunteers |
boolean |
Yes | none | This fielding's volunteer-admission policy, stated explicitly. There is no default, and it must match the campaign's own setting — change that with update_campaign first if it is wrong |
strategy_name |
string |
Yes | — | Name for the sampling strategy this run creates |
target_size |
integer |
Yes | — | The pool draw's requested size: respondents to draw, before oversampling (> 0). Passed to the draw as requested_size and stored on the pool |
factors |
object[] |
No | [] |
Demographic distribution factors the strategy grades against |
selection_algorithm |
string |
No | "greedy" |
Selection algorithm for the pool draw ("greedy" or "random_constrained") |
oversample_factor |
number |
No | 1.0 |
Oversampling multiplier for the pool draw (≥ 1) |
quality_metric |
string |
No | "rmse" |
Metric the strategy grades fit with |
require_email |
boolean |
No | false |
The pool draw admits only respondents that have an email address |
pool_name |
string |
No | — | Name for the generated pool; defaults to a label derived from the strategy |
The strategy this run creates holds only factors and quality_metric. The draw settings (target_size, oversample_factor, selection_algorithm, require_email) go to the pool step and are stored on the pool, exactly as a direct generate_pool_from_strategy call would store them.
Returns: ready_to_invite, the strategy_id, pool_id, pool_respondent_count, qml_version_id and surveys_created / surveys_skipped it produced, plus next_step. On a stop: stopped_at_step with the refusing step's own error and detail, and whatever earlier steps had already built.
It stops before the invitations go out
The chain never calls send_campaign_invitations. Recruitment is the one irreversible, externally visible act in the sequence — mail cannot be unsent — so it stays a separate decision you make after reading what the chain built.
Why the publish step matters. A hand-built chain routinely forgets to publish, and an unpinned campaign resolves the questionnaire's head at read time — so an edit made mid-fielding silently changes the instrument under respondents who have already answered.
Re-running draws again. Each run creates a new strategy and pool and rebinds the campaign to it (a pool is single-use), then fans out over the new pool, skipping respondents that already hold a survey for this campaign. Against a roster larger than target_size a second run therefore adds newly drawn respondents rather than replacing anyone.
simulate_campaign_responses¶
Create the campaign's surveys and then generate synthetic responses over them. See mass_fill_surveys for what each distribution mode and persona profile does.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
campaign_id |
string |
Yes | — | Campaign whose surveys are created and then filled |
distribution |
string |
No | "realistic" |
"realistic", "random", "stratified", or "llm" |
max_surveys |
integer |
No | — | Cap on how many of the campaign's surveys are filled |
profiles |
string[] |
No | ["random"] |
Persona names cycled over respondents |
concurrency |
integer |
No | 1 |
Parallelism for the fill's inner loop |
Returns: surveys_created / surveys_skipped, and a task_id for the fill with status: "pending". Poll it with get_task_status exactly as you would a mass_fill_surveys run. On a stop: stopped_at_step with the refusing step's error and detail, and task_id: null.
Error Handling¶
Common Errors¶
| Scenario | Response |
|---|---|
| Campaign not found | {"error": "Campaign {id} not found"} |
| Interviewer not in campaign | {"error": "Interviewer {id} is not assigned to this campaign"} |
| No questionnaire/pool | {"error": "Campaign has no questionnaire assigned"} |
Batch Operation Pattern¶
Most campaign tools that accept lists of IDs return structured results rather than failing on individual items:
{
"added": ["id1"],
"already_present": ["id2"],
"skipped": [{"id": "id3", "reason": "User not found"}],
"total_added": 1
}
This pattern applies to: add_interviewers_to_campaign, remove_interviewers_from_campaign, assign_respondents_to_interviewer, unassign_respondents_from_interviewer, and send_campaign_invitations.
Each skipped/failed item includes a reason field explaining why it was excluded from the operation.