Skip to content

Custom questions

Your agent asks questions two ways: objectives configured on the agent (every call of that version asks them), and per-call questions attached to a single POST /v1/calls launch. Both record answers through the same tool, land in the same SurveyResponse, and arrive in the same webhooks.

Objectives live on the agent definition and freeze into each published version. A scored question carries a scoring block:

{
"objectives": [
{
"id": "csat_overall",
"prompt": "How satisfied were you with your visit, one to five?",
"scoring": {
"scale": "csat",
"mapping": { "1": 1, "2": 2, "3": 3, "4": 4, "5": 5 },
"weight": 1
}
},
{
"id": "would_return",
"prompt": "Would you come back?",
"scoring": {
"scale": "ordinal",
"points": 3,
"mapping": { "no": 1, "maybe": 2, "yes": 3 }
}
},
{ "id": "verbatim", "prompt": "Anything else you want us to know?" }
]
}

Four scales: csat (1–5), nps (0–10), boolean ("true"/"false" keys), and ordinal (declare points and a mapping with exactly that many choices). A question with no scoring block is unscored - the answer is still recorded verbatim. A scored question requires an id (answers key on it) and question text.

The shape is contract-enforced: a malformed block - an unknown scale, mapping keys outside the scale’s answer set, ordinal without a matching points - is a 422 at write time and a 409 objective_scoring_invalid at publish, with errors[] naming each broken field. It cannot silently publish and yield score: null.

An objective can also declare capture_type as string, number, or boolean for information extracted into post-call summaries. For example:

{
"id": "years_experience",
"prompt": "How many years of driving experience do you have?",
"capture_type": "number"
}

Typed captures require a unique id or legacy name and question text. In the portal objective editor, choose Text, Number, or Yes or no under Captured answer type. Unspecified preserves the existing behavior. This setting controls summary extraction; it does not alter survey scoring or make an assignment or hiring decision.

Where automatic summaries are enabled, missing or refused answers remain absent; an explicit no or zero remains a captured answer. Replies with incorrect types are rejected and retried instead of silently converting strings into answers. Declaring a type does not enable automatic summaries for a deployment without a qualified model.

POST /v1/calls accepts up to 10 additional questions and up to 20 context facts for that one call:

{
"agent_id": "agnt_01J9…",
"contact_id": "cont_01J9…",
"questions": [
{
"id": "q_delivered",
"prompt": "Did yesterday's delivery arrive on time?",
"scoring": { "scale": "boolean", "mapping": { "true": 1, "false": 0 } }
}
],
"context": { "order_number": "A-1009", "delivery_date": "yesterday" }
}

The rules:

  • Additive only. A per-call question may not reuse an id the version’s objectives already carry - 409 call_question_conflict. The published, evaluation-gated objectives are never overridden per call.
  • The version must grant record_survey_answer. Otherwise the answers would have no tool to record them - 409 call_questions_unsupported at launch, not a silent no-op after the call.
  • Context keys are snake_case strings, ≤500 characters each, ≤8 KiB total. Platform-authored keys (current_datetime, contact_name, …) are refused - you cannot shadow what the platform tells the agent.

Per-call questions ride outside the agent’s cached prompt prefix, so they add no latency penalty, and they score into the same SurveyResponse as version objectives.

  • Push: survey.completed and call.ended webhooks carry answers and score inline - see Receive results.
  • Pull: GET /v1/survey-responses?call_id=… returns {answers: {id: {answer, verbatim_quote, confidence}}, score, completion_status}.
  • Aggregate: GET /v1/campaigns/{id}/survey-rollup for campaign questions.

The score is computed once, at call completion, from the exact question set the call ran - deterministic and reproducible; editing the agent later never restates a past response.