Screening Questions
Define screening questions to qualify, disqualify, or collect freeform responses from participants during application.
Screening questions are asked to participants during application. Most are multi-option questions that qualify or disqualify based on the chosen answer (design these so the qualifying answer is not obvious). You can also ask open-ended freeform questions (pick: "text") when you want a written response to review rather than an automatic qualify/reject.
Example
[
{
"key": "role_type",
"text": "Which best describes your current role?",
"pick": "one",
"answers": [
{ "text": "Individual contributor", "qualify_logic": "may" },
{ "text": "People manager", "qualify_logic": "may" },
{ "text": "Executive / C-suite", "qualify_logic": "may" },
{ "text": "Not currently employed", "qualify_logic": "reject" }
]
},
{
"key": "facility_size",
"text": "What is the approximate total square footage of facilities you currently oversee?",
"pick": "one",
"answers": [
{ "text": "I don't manage any facilities", "qualify_logic": "reject" },
{ "text": "Under 10,000 sqft", "qualify_logic": "reject" },
{ "text": "10,000 - 50,000 sqft", "qualify_logic": "may" },
{ "text": "50,001 - 100,000 sqft", "qualify_logic": "may" },
{ "text": "100,001 - 500,000 sqft", "qualify_logic": "must_one_of" },
{ "text": "Over 500,000 sqft", "qualify_logic": "must_one_of" }
]
},
{
"key": "team_size",
"text": "How many direct reports do you have?",
"pick": "one",
"answers": [
{ "text": "1-5", "qualify_logic": "may" },
{ "text": "6-15", "qualify_logic": "may" },
{ "text": "16-50", "qualify_logic": "may" },
{ "text": "Over 50", "qualify_logic": "may" }
]
}
]The key field is a client-defined identifier for the question. It is required when the question is referenced by a quota.
Whether or not you send one, every question reads back with a key, and that same value is returned as screening_answers[].key on GET /submissions/{submissionId}. That is how you join an answer to the question that produced it. Treat the value as opaque: when you omit key we assign one, and the format is not part of the contract.
Question Fields
| Field | Type | Required | Description |
|---|---|---|---|
key | string | no | Client-defined identifier. Required when referenced by quotas or display conditions. |
text | string | yes | The question text shown to participants (plaintext). |
question_rich_text | string | no | Optional Markdown (GFM) display copy. See Rich Text Prompts. |
pick | string | yes | Question type: one, any, boolean, text, or grid. |
answers | array | conditional | Two or more answer options. Required for choice questions (one, any, boolean); omit for text and grid. |
answers[].text | string | yes | The answer text shown to participants. |
answers[].qualify_logic | string | yes | Qualification behavior: may, must, must_one_of, reject, or review. |
answers[].allow_free_text | boolean | no | Reveals an "Other, please specify" write-in box when this answer is selected. Applies to pick one / any. |
min_qualifying | integer | no | Multi-select (any) only. Require at least this many must_one_of answers (see Qualify Logic). |
grid | object | conditional | Required for pick: "grid". Rows, columns, and per-cell qualify logic (see Grid Questions). |
display_condition | object | no | Show this question only when earlier answers match (see Ask Only If). |
conditional_rules | array | no | Cross-question consistency / anti-falsification checks that reject or flag for review (see Consistency Checks). |
skip_rules | array | no | Branching: jump to a later question (or end the screener) when this question's answers match (see Branching). |
allow_paste | boolean | no | Open-ended (text) only. Whether participants may paste into the field. Pasting is blocked by default. |
Question Types
pick | Description |
|---|---|
one | Single select -- participant picks exactly one |
any | Multi select -- participant picks one or more |
boolean | Yes / No |
text | Open-ended -- participant types a freeform response (no options) |
grid | Matrix -- one shared set of columns chosen per row |
Open-Ended Questions
Set pick: "text" for a freeform response. Open-ended questions have no answers and no qualifying logic, so they do not auto-screen participants. Submissions are routed to manual review for you to read the response and approve or reject. Quotas cannot target open-ended questions.
[
{
"key": "biggest_challenge",
"text": "What is the biggest challenge you face managing your facilities today?",
"pick": "text"
}
]Qualify Logic
| Value | Meaning |
|---|---|
may | Does not affect qualification |
must | Participant must select this exact answer (valid with one or any) |
must_one_of | At least one answer in this any-of group must be selected |
reject | Selecting this disqualifies the participant |
review | Selecting this routes an otherwise-qualified applicant to manual review instead of auto-inviting (auto-invite studies only) |
Note: On multi-select (
pick: "any"),mustmeans this exact answer is required, whilemust_one_ofmeans at least one of a group. Usemustfor each individually-required option (e.g. "must have run Google Ads") andmust_one_offor an any-of group (e.g. "Meta OR TikTok").
Single-select collapse: on
pick: "one"andpick: "boolean"only one answer can be picked, somay/must/must_one_ofall persist as the same qualifying disposition (and read back asmust). Onlyrejectandreviewchange the outcome on a single-select — usepick: "any"when you need a distinct disposition per answer.
On multi-select, min_qualifying raises the "at least one of" bar for the must_one_of group: with three must_one_of answers and min_qualifying: 2, a participant must select at least two of the three to qualify. Omitted or 1 keeps the default "at least one of" behavior. It is ignored for other pick types.
Grid Questions
Set pick: "grid" for a matrix question that asks the same set of choices about several items. Omit answers and provide grid instead. Per-cell qualify_logic uses the same values as answers.
Use grid.pick to control how many columns a participant may choose per row: "one" (default) is a single column per row, and "any" lets them select any number of columns per row (a "select all that apply per row" matrix).
| Field | Type | Required | Description |
|---|---|---|---|
grid.pick | string | no | one (default, single column per row) or any (multiple per row). |
grid.rows[].text | string | yes | Row label (e.g. a platform name). |
grid.rows[].key | string | no | Stable row key; defaults to r0, r1, ... Used by cell_actions. |
grid.columns[].text | string | yes | Column label shared across every row. |
grid.columns[].value | string | no | Stable column value; defaults to c0, c1, ... |
grid.cell_actions[] | array | no | Per-cell qualify logic. Omit a cell for neutral (may). |
cell_actions[].row | string | yes | A row key from grid.rows. |
cell_actions[].column | string | yes | A column value from grid.columns. |
cell_actions[].qualify_logic | string | yes | may, must, must_one_of, or reject. |
[
{
"key": "platforms",
"text": "How active are you on each platform?",
"pick": "grid",
"grid": {
"rows": [
{ "key": "youtube", "text": "YouTube" },
{ "key": "tiktok", "text": "TikTok" }
],
"columns": [
{ "value": "active", "text": "Active" },
{ "value": "never", "text": "Never" }
],
"cell_actions": [
{
"row": "youtube",
"column": "active",
"qualify_logic": "must_one_of"
},
{ "row": "tiktok", "column": "active", "qualify_logic": "must_one_of" }
]
}
}
]Ask Only If (Display Conditions)
Use display_condition to show a question only when earlier answers match -- for example, ask a Meta-specific follow-up only if the participant said they use Meta. Each condition references an earlier question by its key and one of its answers (for a grid source, use "Row: Column"). Conditions are combined with join (and / or). A condition that references a later question returns a BAD_REQUEST error.
| Field | Type | Required | Description |
|---|---|---|---|
display_condition.conditions[] | array | yes | One or more conditions. |
conditions[].screening_question | string | yes | The key of an earlier question. |
conditions[].answer | string | yes | An answer of that question. For a grid source use "Row: Column". |
conditions[].operator | string | no | eq (answered that value, default) or ne (did not). |
display_condition.join | string | no | and (default) or or. |
[
{
"key": "platforms",
"text": "Which platforms do you use?",
"pick": "any",
"answers": [
{ "text": "Meta", "qualify_logic": "may" },
{ "text": "TikTok", "qualify_logic": "may" }
]
},
{
"key": "meta_spend",
"text": "What is your monthly Meta ad spend?",
"pick": "one",
"answers": [
{ "text": "$10k+", "qualify_logic": "must" },
{ "text": "Under $10k", "qualify_logic": "reject" }
],
"display_condition": {
"conditions": [
{
"screening_question": "platforms",
"answer": "Meta",
"operator": "eq"
}
],
"join": "and"
}
}
]Rich Text Prompts
Set question_rich_text to render the prompt with Markdown (GitHub-flavored: bold, bullet lists, links). text stays required and remains the plaintext source of truth used by the voice agent and search, so keep the two in sync — question_rich_text only changes how the prompt is displayed to participants.
[
{
"key": "compliance",
"text": "Which compliance frameworks has your team completed in the last 12 months?",
"question_rich_text": "Which compliance frameworks has your team **completed** in the last 12 months?\n\n- Count only audits that closed\n- Exclude in-progress engagements",
"pick": "any",
"answers": [
{ "text": "SOC 2", "qualify_logic": "must_one_of" },
{ "text": "ISO 27001", "qualify_logic": "must_one_of" },
{ "text": "None of these", "qualify_logic": "reject" }
]
}
]Consistency Checks (Conditional Rules)
conditional_rules catch inconsistent or low-quality answers that per-answer qualify logic can't express on its own — for example, a participant who claims a senior role but selects none of the tasks that role would actually do. When a rule's conditions match, it either rejects the participant or flags the submission for human review.
Unlike a display_condition (which references an earlier question to gate visibility), a consistency-check condition may reference any question by key, or omit screening_question to test an answer on the question the rule lives on (a self-check). Write one all-and rule per pattern you want to catch.
| Field | Type | Required | Description |
|---|---|---|---|
conditional_rules[].conditions[] | array | yes | One or more conditions (up to 26). |
conditions[].screening_question | string | no | The key of the question to read. Omit to test an answer on THIS question (a self-check). |
conditions[].answer | string | yes | An answer by its text. For a grid source use "Row: Column". |
conditions[].operator | string | no | eq (selected that answer, default) or ne (did not). |
conditional_rules[].join | string | no | and (default) or or. |
conditional_rules[].outcome | string | yes | reject screens the participant out; review flags the submission for a human when it matches. |
[
{
"key": "role",
"text": "What is your role?",
"pick": "one",
"answers": [
{ "text": "Store Manager", "qualify_logic": "may" },
{ "text": "Associate", "qualify_logic": "may" }
]
},
{
"key": "tasks",
"text": "Which of these do you do day to day?",
"pick": "any",
"answers": [
{ "text": "Approve schedules", "qualify_logic": "may" },
{ "text": "Manage the P&L", "qualify_logic": "may" },
{ "text": "None of these", "qualify_logic": "may" }
],
"conditional_rules": [
{
"conditions": [
{ "screening_question": "role", "answer": "Store Manager" },
{ "answer": "None of these", "operator": "eq" }
],
"join": "and",
"outcome": "review"
}
]
}
]The second condition omits screening_question, so it tests an answer on the tasks question the rule lives on: a "Store Manager" who also picks "None of these" is flagged for review.
Branching (Skip Rules)
skip_rules let a question jump the participant forward — to a later question, or straight to the end of the screener — based on how they answered it. A rule tests the answers of the question it lives on, and the target must be a later question (the screener only walks forward). Not available for pick: "text" or "grid".
| Field | Type | Required | Description |
|---|---|---|---|
skip_rules[].conditions[] | array | yes | One or more conditions (up to 26) testing THIS question's answers. |
conditions[].screening_question | string | yes | The key of the question this rule lives on. |
conditions[].answer | string | yes | One of that question's answers (its text; "Yes"/"No" for pick: "boolean"). |
conditions[].operator | string | yes | eq (answered that value) or ne (did not). |
skip_rules[].join | string | yes | and or or. |
skip_rules[].target_question | string | null | yes | key of a later question to jump to; null ends the screener. |
[
{
"key": "uses_product",
"text": "Do you currently use the product?",
"pick": "boolean",
"answers": [
{ "text": "Yes", "qualify_logic": "may" },
{ "text": "No", "qualify_logic": "may" }
],
"skip_rules": [
{
"conditions": [
{
"screening_question": "uses_product",
"answer": "No",
"operator": "eq"
}
],
"join": "and",
"target_question": null
}
]
},
{
"key": "plan_tier",
"text": "Which plan are you on?",
"pick": "one",
"answers": [
{ "text": "Free", "qualify_logic": "may" },
{ "text": "Pro", "qualify_logic": "may" }
]
}
]A participant who answers "No" skips straight to the end; everyone else continues to the plan-tier question.
Reading Screen-Out Rates
GET /opportunities/{opportunityId} returns screening_stats on a live opportunity: one entry per question, keyed by the same key you authored.
{
"screening_question": "years_experience",
"graded": 412,
"rejected": 321,
"rejection_rate": 0.78,
"answers": [
{ "text": "0-10", "selected": 91, "share": 0.22 },
{ "text": "Over 10 years", "selected": 321, "share": 0.78 }
]
}graded counts the finished screeners that were graded on this question, which is not the same as how many people answered it. A screener with skip_rules or a display_condition stops grading at the first failure, so an answer the evaluator never reached carries no verdict and is excluded here.
That distinction is what makes rejection_rate useful: it is the share of the population that could have been rejected on this question, not the share of everyone who applied. A question late in a branching screener can show a high rate on a small graded count, which means it is strict among the few who got that far, not that it is your main bottleneck. Sort by rejected when you want the question costing you the most applicants, and by rejection_rate when you want the strictest one.
answers[].share is out of the same graded count, so a lopsided split is the signal that a question is narrower than intended. In the example above, widening the accepted range on years_experience is worth more than touching any other question.
Launching Without a Screener
A screener is recommended, because it decides who reaches the paid task. Some studies need none, for example when the task screens participants itself. For those, your organization can launch opportunities with no screening_questions once Terac has enabled it. Ask your Terac contact to turn it on.
With no screener, everyone who matches your filters goes straight to the task, so the launch asks for three things:
- Every task that delivers the work, such as an interview, activity or file upload, has
review_type: "manual_review". Nobody is paid until you approve their submission withPOST /submissions/{submissionId}/approve, and acompletedredirect moves a submission into review instead of paying it. A task that only confirms a step, such as signing a document, can stay as it is, as long as one task waits for your review. customer_screening_reviewisauto_invite. Holding applicants for review needs screening answers to review them on.- The launch says so explicitly:
POST /opportunities/{opportunityId}/launch
{ "without_screener": true }Without without_screener, a draft with no screener is refused with 412 and a message naming what to do next. The flag is ignored on an opportunity that has screening questions, and it never launches a screener that nobody can pass.
If Terac runs its AI screening interview on the opportunity, applicants are still screened and reviewed before they reach the task, so only the third item applies.
The dashboard offers the same launch. Its builder asks you to confirm it, sets each Needs approval task to manual_review as it launches, and keeps applicants on automatic invites while there is no screener. See Launching Without a Screener in the researcher docs.
When your task screens participants itself, send the screened_out redirect for anyone it turns away (see External Surveys & Redirect URLs). They are rejected without pay, and their place opens for the next participant.