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.