Survey Tools — MCP Tool Reference¶
Survey tools handle the survey execution lifecycle: creating survey sessions, navigating questionnaire flows, submitting responses, and generating synthetic test data. Comment items are automatically skipped — callers never receive them.
list_surveys¶
List surveys, optionally filtered by questionnaire, campaign, respondent, interviewer, or status.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
questionnaire_id |
string |
No | — | Filter by questionnaire ID |
campaign_id |
string |
No | — | Filter by campaign ID |
respondent_id |
string |
No | — | Filter by respondent ID |
interviewer_user_id |
string |
No | — | Filter by assigned interviewer (User) |
status |
string |
No | — | Filter: "pending", "in_progress", or "completed" |
expand |
boolean |
No | false |
Populate nested questionnaire/interviewer/respondent/campaign objects |
cursor |
string |
No | — | Pagination cursor from a previous page |
limit |
integer |
No | 50 |
Page size (max 100) |
Returns: { data: [...], next_cursor, has_more } — cursor-paginated. Each survey carries status plus a derived, read-only completed boolean (true exactly when status is "completed"); status is the single source of truth, and there is no completed filter or input anywhere on the survey surface.
update_survey¶
Update a survey's mutable fields. The bound entities (questionnaire, respondent, interviewer, campaign) are immutable here — delete and recreate the survey to rebind them.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
survey_id |
string |
Yes | — | Survey UUID |
status |
string |
No | — | New status; setting "completed" is how completion is expressed (there is no separate completed input) |
qml_state |
object |
No | — | Advanced: replace the survey's flow state |
current_block_id |
string |
No | — | Advanced: reposition the flow pointer |
current_item_id |
string |
No | — | Advanced: reposition the flow pointer |
Returns: the updated survey object (same shape as list_surveys entries, completed derived from status).
create_survey¶
Create a new survey for a respondent.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
questionnaire_id |
string |
Yes | — | Questionnaire to use |
respondent_id |
string |
Yes | — | Respondent UUID |
interviewer_user_id |
string |
No | — | For phone/in-person interviews. Omit for self-completion |
campaign_id |
string |
No | — | Associate with a campaign |
Returns: { id, questionnaire_id, interviewer_user_id, respondent_id, campaign_id, mode }
bulk_create_surveys¶
Create surveys for all respondents in a campaign's assigned pool. Skips respondents who already have surveys.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
campaign_id |
string |
Yes | — | Campaign UUID |
Returns:
{
"status": "success",
"campaign_id": "...",
"created_count": 45,
"skipped_count": 5,
"surveys": [{"id": "...", "respondent_id": "..."}]
}
Requires: Campaign must have both a questionnaire and a respondent pool assigned.
get_survey_current_item¶
Get the current question in a survey. Initializes the survey state on first call.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
survey_id |
string |
Yes | — | Survey UUID |
Returns (question): the current item, its response options folded in, and the control type — a single round-trip carries everything needed to answer.
{
"survey_id": "...",
"status": "in_progress",
"item": {
"id": "q_gender",
"kind": "Question",
"text": "What is your gender?"
},
"control_type": "radio",
"options": [
{"value": "male", "text": "Male"},
{"value": "female", "text": "Female"},
{"value": "other", "text": "Other"}
],
"required": true
}
For a range control, options carries a single range descriptor:
Text controls (editbox, textarea) return an empty options list. Control types: radio, dropdown, checkbox, slider, switch, editbox, textarea.
Returns (complete): { survey_id, status: "completed", message: "..." }
Auto-Comment Skipping
Comment items (informational text) are transparently skipped — up to 50 consecutive Comments are auto-processed. Callers only receive actionable items (Question, QuestionGroup, MatrixQuestion).
Option extraction is folded in — there is no separate options tool
The legacy get_question_options tool was removed. Its payload (options + control_type) now folds directly into get_survey_current_item's response, so a single call per question is enough.
submit_survey_response¶
Submit an answer for the current question and advance to the next item.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
survey_id |
string |
Yes | — | Survey UUID |
item_id |
string |
Yes | — | Item ID being answered |
outcome |
any |
Yes | — | Response value (type depends on control) |
Returns:
status is "in_progress" or "completed". next folds in the following step (same shape as get_survey_current_item) so you don't need a separate fetch — it is null once the survey completes. Trailing Comment items are auto-processed after each submission.
Call get_survey_current_item first
The survey must be initialized before submitting responses. If not initialized, returns: {"error": "Survey not initialized. Call get_survey_current_item first."}
Answers submitted here are recorded as machine-produced
A survey that has had an invitation sent cannot be answered through this tool at all — that is refused outright, so no live fielded interview can be written to programmatically. What remains is testing and simulation, so every answer submitted here is recorded as machine-produced and its response time is excluded from speeder detection.
finish_survey¶
Explicitly mark a survey as completed. Use for edge cases where auto-completion doesn't trigger.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
survey_id |
string |
Yes | — | Survey UUID |
Returns: { status: "success" | "already_complete", survey_id, message }.
mass_fill_surveys¶
Enqueue an asynchronous background task to fill all pending surveys in a campaign with synthetic test responses. Do not use on campaigns with real respondent data.
The tool returns a task_id immediately; the generation loop runs as a background job on Portor's own worker. Poll get_task_status until the task reaches a terminal state: completed (every survey filled), partial (some surveys filled, others failed — e.g. bad respondent data or a transient QML error), or failed (zero surveys filled — either a pre-run validation error such as a missing campaign, or every survey in the run failed; last_error explains why). The three states are mutually exclusive: any mix of successes and failures lands on partial, never failed.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
campaign_id |
string |
Yes | — | Campaign UUID |
distribution |
string |
No | "realistic" |
Strategy: "realistic", "random", "stratified", or "llm" |
max_surveys |
integer |
No | — | Limit surveys to fill. null = all pending |
profiles |
string[] |
No | ["random"] |
Persona profiles, assigned round-robin |
concurrency |
integer |
No | 1 |
Per-task thread-pool size for the inner loop |
Returns (synchronously, after enqueue validation):
{
"task_id": "f5b6c7d8-...",
"status": "pending",
"campaign_id": "...",
"distribution": "realistic",
"profiles": ["young_single", "retired"]
}
Poll get_task_status(task_id) until state is completed, partial, or failed. status_text surfaces live progress (e.g. "47 of 200 surveys processed") and, on a terminal state, a fill summary (e.g. "47 of 200 surveys filled (3 failed)"); last_error carries a sanitized reason on failure. To retry after a partial result, re-invoke mass_fill_surveys on the same campaign — the fill only processes surveys that are not yet completed, so already-filled surveys are skipped.
Distribution Modes¶
| Mode | Engine | Speed | Description |
|---|---|---|---|
realistic |
Weighted random | Fast | Profile-aware demographic weighting (default) |
random |
Uniform random | Fast | Pure random, no demographic influence |
stratified |
Demographic strata | Fast | Quota-balanced responses |
llm |
Claude Haiku API | ~1s/question | AI-generated, persona-consistent responses. Requires ANTHROPIC_API_KEY |
Persona Profiles¶
Assigned round-robin to respondents. Available profiles:
| Profile | Age | Income | Behavior |
|---|---|---|---|
young_single |
25–35 | $65k | Progressive, tech-savvy, favors middle options |
married_family |
30–50 | $80k | Practical, value-oriented |
retired |
65+ | $55k | Conservative, favors early/traditional options |
high_income |
45 | $150k+ | Quality-focused, favors premium options |
random |
40 | $70k | No particular bias |
Item Kind Handling¶
Answers are numeric codes (a choice control's labels map integer codes to display strings; the code is what is submitted) — free text only for the textarea control. See Survey Presentation Format for the full contract.
| Item Kind | Outcome Format | Example |
|---|---|---|
Comment |
null (auto-skipped) |
Informational text |
Question |
Single number: answer code / numeric value / switch 1-0 (Checkbox: bitmask int; Textarea: string) |
2, 42, true |
QuestionGroup |
List of numbers, one per sub-question — all from the group's single shared control | [1, 3, 2] |
MatrixQuestion |
Nested list of numbers, rows × columns — one shared control | [[1, 2], [3, 1]] |
bulk_delete_surveys¶
Delete multiple surveys by filter criteria. Uses a two-step workflow: call with dry_run=true first to preview what will be deleted, then call again with dry_run=false to execute.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
campaign_id |
string |
Yes | — | Delete surveys in this campaign |
status |
string |
No | — | Only target surveys with this status: "pending", "in_progress", or "completed" |
dry_run |
boolean |
No | true |
Preview mode — returns count and sample without deleting |
Dry run returns:
{
"dry_run": true,
"count": 25,
"sample": [{"id": "...", "respondent_id": "...", "status": "pending", "campaign_id": "..."}],
"message": "Would delete 25 surveys. Call again with dry_run=False to execute."
}
Execute returns: { dry_run: false, deleted: 25 }
Destructive Operation
This tool permanently deletes survey records. Always preview with dry_run=true first.
Error Handling¶
Common Errors¶
| Scenario | Response |
|---|---|
| Survey not found | {"error": "Survey {id} not found"} |
| Campaign not found | {"error": "Campaign {id} not found"} |
| Questionnaire not found | {"error": "Questionnaire {id} not found"} |
| Survey not initialized | {"error": "Survey not initialized. Call get_survey_current_item first."} |
| No pool assigned | {"error": "Campaign has no respondent pool assigned"} |
| No incomplete surveys | {"status": "no_surveys", "message": "No incomplete surveys found in campaign"} |
Survey State Machine¶
[Not Started] → get_survey_current_item → [In Progress]
[In Progress] → submit_survey_response → [In Progress | Completed]
[In Progress] → finish_survey → [Completed]
- Initialization: First call to
get_survey_current_itemloads the QML and creates the survey state - Auto-completion: Surveys complete automatically when the last item is answered
- Comment skipping: Comment items are transparently processed (up to 50 consecutive) — callers never receive them
- Persistence: Every response is immediately persisted to the database