Qualtrics Live-Survey Operations
SkillDev toolsOperate or audit a live Qualtrics survey via the v3 APIs without breaking fielding — publish gating, quotas, flow routing, embedded data, panel-vendor redirects, read-back verification, and a read-only pre-fielding audit. Use when publishing or patching a fielding instrument, when a quota counts but never blocks, when wiring panel-vendor redirects or flow gates, or when auditing a survey before launch (consent-before-anything gates, force-response completeness, quota and redirect checks, anti-bot instrumentation, and language-arm symmetry).
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Qualtrics Live-Survey Operations skill
What this skill tells your AI
The instructions your AI receives, as published by scdenney/open-science-skills in codex/qualtrics-ops/SKILL.md and read by ahel’s review.
Instructions
1. When to use
This skill covers operating a survey already built and imported into Qualtrics — publishing, quotas, flow routing, live text patches, panel-vendor integration, security options — on an instrument that is or will be fielding real respondents. It is not survey design or QSF construction; assume the survey definition already exists and the question is how to change it without breaking what respondents currently see.
Two modes. The default is operations: sections 2-8 below govern any write
to a live instrument. Audit mode ($qualtrics-ops audit) is the
read-only pre-fielding assessment in the section at the end — it produces
ranked findings and makes no changes, and any repair it surfaces comes back
through the operations sections as a separate, authorized change. The
write-safety rules in section 2 are the shared frame for both: audit mode
never writes at all, and operations never publish or activate by accident.
2. Non-negotiables
Backups first, always. Pull and save the survey definition (and flow and options as separate JSON) before any write. Qualtrics version restore exists but is coarse and carries data-risk caveats; your pre-change snapshot is the only precise recovery path, and the only one that covers objects (quota counters, options) outside version history.
Verify by read-back, never by absence of error. A 200 response proves the API accepted the request, not that respondents will see the result. After every write, GET the changed object back and confirm it matches intent.
The version list is the proof a change is live, not the publish-state flag. Question/block writes create no version-history entry, so publish-state can report "in sync" while the live version still lags behind your edit. The standard: your publish call created a NEW published version entry carrying your own description — and, because a description proves provenance rather than content, pair it with a read-back of the published content where the API exposes it.
One writer at a time. Confirm you are on the right survey, brand, and environment, that no one has the builder open concurrently, and that you know the rollback path before the first write. On an actively fielding instrument, prefer a change window; and remember the platform's own warning that deleting or restructuring questions and choices can invalidate already-collected data.
Assert everything untouched is unchanged, not just that your target changed — compare parsed structures, since the server may normalize ordering and defaults. Several of the traps below are full-replace endpoints that silently wipe sibling data; catching that requires diffing the whole object against the pre-change backup, not just checking the field you meant to edit.
3. Publish gating — two distinct failure modes
Most flow and options edits to an active survey are staged, not live,
until an explicit version publish — respondents keep getting the old version
while the API happily reads back your new one. (A few option keys apply
immediately, availability among them; treat "staged until published" as the
safe default assumption and verify per key.) Question/block writes are worse: they
create no list_versions entry at all, so publish-state can say "in sync"
immediately after a write that hasn't actually gone anywhere near respondents.
Stage all related writes for a change as one batch, verify the batch by
read-back, then publish ONCE, deliberately (forcing the publish if your client
supports it) — publishing after every individual write can expose respondents
to internally inconsistent intermediate versions. Reload the builder UI before
trusting its Draft/Published badge — it lags the API.
Publishing an inactive survey activates it. Treat activation as a separate, deliberate decision from publishing a change — have your API client require an explicit activation flag so a routine content publish can never launch fielding as a side effect.
4. Quota API
-
START HERE IF A QUOTA COUNTS BUT NEVER BLOCKS: check
ActionInfo.QuotaAction: "EndCurrentSurvey"only NAMES the action. The nestedActionInfoobject is what actually FIRES it. A quota written with an empty stub —"ActionInfo": {"Type": "BooleanExpression"}— counts every matching respondent flawlessly, forever, and acts on none of them: no termination, no screen-out, no error at write time, at publish time, or at runtime, and the builder UI renders the quota as normal. The platform's own serialization is:
"ActionInfo": {"0": {"0": {"ActionType": "EndCurrentSurvey", "Type": "Expression", "LogicType": "QuotaAction"}, "Type": "If"}, "Type": "BooleanExpression"}ActionTypemust repeat theQuotaActionvalue. This was shipped as an empty stub in a homegrown API client and made every quota it ever created inert across multiple live fielding surveys — the true reason quotas "didn't work", after days of chasing logic-dialect theories that were all downstream of it. If a quota'scountis incrementing, its matching logic is already correct and the problem is not the logic; go straight toActionInfo. -
Proven by controlled before/after, and this is the method to reuse. Do NOT try to diagnose quota enforcement on a clone of the real instrument — geo gates, duplicate-device gates, anti-automation paradata and session-resume cookies will each eject or short-circuit the walk, and stripping them piecemeal corrupts the flow (all of this was tried, and burned hours). Instead build a minimal disposable survey from scratch: three quota-bearing questions on page 1, one marker question on page 2 (reaching page 2 is the "not blocked" signal — a single-page survey cannot demonstrate blocking at all, because the respondent has already answered everything by the time the quota evaluates), plus TWO quotas: one the walk matches and one differing in a single condition as a negative control. Walk it once to fill the match quota, then walk again and observe. The control must stay at 0 — that is what proves the multi-condition AND is sound rather than collapsing. Change ONE variable per walk. In the reference case the immediately-preceding walk already had the group's
Selectedflag, canonical dialect andEndSurveyOptionsall in place and still reached page 2; populatingActionInfoalone flipped the identical walk to terminating at page 1. -
Ruled out along the way — do not re-chase these (each was a plausible theory that cost real time): the quota group's
Selectedflag (setting itTrueto match a working reference changed nothing on its own); the presence or absence ofEndSurveyOptions(worth having for theQuotaMetflag, but not what makes a quota fire); andOccurrences: 0as a way to express "already full" — that IS wrong and no platform-authored quota uses it (minimum observed across 49 reference quotas was 16), but with a brokenActionInfono limit value of any kind would have blocked anyone. UseOccurrences: 1when a cell is already over target and you want it shut: the first matching respondent tips it to full and every subsequent one is blocked. -
The actual root cause of "compound quota logic never fires" is a hand-authoring dialect mismatch, not a platform limit on condition count — and getting this wrong once already cost a full day of live-fielding churn across two surveys, so read this one carefully. Qualtrics's own internally-generated quota JSON uses
"Conjuction"(not"Conjunction"— a legacy misspelling baked permanently into the schema) as the key joining multiple conditions, and"q://{QID}/ChoiceTextEntryValue"(not"q://{QID}/TextEntry") as the operand/locator for a numeric comparison against a text-entry question, withChoiceLocatorduplicating the same string. Hand-write either one with the "obviously correct" English spelling or the intuitive-looking locator and the quota accepts the write (200 OK), reads back exactly as sent, renders correctly in the builder UI's condition editor, and then matches nobody, ever, with no error anywhere — because the engine simply doesn't recognize the key/locator and silently drops that half of the condition. This produced the exact symptom pattern that looks like "compound conditions never fire": single-condition quotas (noConjuctionneeded) counted correctly the whole time; every age-range and multi-choice-OR quota sat at zero, on multiple live production surveys, for over a week, undetected. The fix, once diagnosed, is to write compound conditions in the correct dialect, not to avoid them. A same-day rebuild using the verified dialect — including three- and four-condition flat AND groups spanning multiple different QuestionIDs (age range AND gender-selected AND region-selected in one group) — fired correctly and counted real respondents within the hour. Do not hand-guess this dialect from documentation or from what "looks right." Find a real, platform-exported QSF from the same account (or ask the user for one — even an unrelated old survey works) and diff your generatedLogicblock against its quota objects byte-for-byte before trusting anything with more than one condition; this is the single highest- leverage check available and takes minutes. Absent a reference file, the fallback is the advisor-consult pattern: hand a second, independent model the full evidence trail (what fired, what didn't, exact JSON of both) and ask it to reason from first principles rather than guessing again yourself — that is what actually surfaced this dialect mismatch after multiple failed self-directed attempts. -
LogicType: "EmbeddedField"quota conditions take the BARE field name asLeftOperand, not thee://Field/...piped reference — and getting this wrong is silent, like every other quota-dialect error. A platform-authored Cross quota carries{"LogicType": "EmbeddedField", "LeftOperand": "gc", "Operator": "EqualTo", "RightOperand": "1"}— just"gc". Writing"e://Field/gc"(the form that is correct everywhere else in Qualtrics: Branch logic, display logic, piped text, redirect URLs) produces a quota that accepts the write, reads back intact, and matches nobody. This was learned expensively: a Branch-precompute design (merge a value into one flat embedded field, then quota on equality) was built at ~130 quota objects across two live surveys, with the field VERIFIED correctly populated in the response export for real respondents, and every one of those quotas readcount: 0— because all of them used thee://Field/prefix. Do not conclude from a failure like that that embedded-field quota logic is unsupported; check the operand format against a platform-authored example first. Note also that when the goal is an interlock,LogicType: "Cross"(below) is the native mechanism and usually beats precomputing a merged field at all. -
LogicType: "Cross"is Qualtrics's native interlock quota, and it is a different object shape fromSimple— know which one you are reading. A Simple quota is one cell:Logicis a single expression tree (a dict) andOccurrencesis that cell's absolute target. A Cross quota is a whole grid:Logicis an ARRAY of logic sets, the engine crosses the sets to generate cells, andOccurrencesis the TOTAL across the grid. In a Cross quota theConjuctionkey does not hold"And"/"Or"— it holds that condition's percentage allocation ("27%","29%"), and each cell's effective target isOccurrences xthe product of its shares. So the same misspelled key means two completely different things depending on quota type; do not pattern-match one onto the other. Choosing between them: Cross expresses an age x gender x region interlock as ONE object instead of hundreds, which is decisive when building a grid up front. But because Cross targets are percentages of a total, it is poorly suited to rebasing a partially-collected field — "this cell has 33 slots left of 473" is trivial as a Simple quota's absoluteOccurrences: 33and awkward as a share of a total that is already half filled. Build with Cross; repair mid-field with Simple. -
Sidestep the whole range-comparison problem at design time by asking age as a categorical band question rather than a numeric text entry. A platform-authored reference survey that quotas cleanly on age does it with a multiple-choice item (18-29 / 30-39 / 40-49 / 50-59 / 60+) and plain
Selectedconditions — noChoiceTextEntryValuelocator, no>=/<=pair, no compound condition, nothing to get wrong. If the instrument is not yet fielded and the analysis does not need exact age, this is strictly the safer design. -
For a bilingual/multi-arm instrument, don't reflexively split every marginal quota by arm. If one arm carries the overwhelming majority of traffic (check the actual split from the response export, don't assume), splitting age/gender/etc. into one quota per arm doubles the object count and produces a nonsensical-looking result on screen (a demographic quota that appears to depend on survey language). Fold the minority arm into the majority arm's quota instead — check only the majority-language question, size it to the full combined remaining target — and accept that the minority arm isn't independently capped by that specific quota. Disclose the tradeoff; don't build the split by default.
-
Before trusting ANY quota with more than a trivial condition, prove the engine actually fires it: create it, then either drive one real or preview response through the matching path and confirm
countincrements, or — cheaper — diff itsLogicshape against a quota on the same live survey that is already demonstrably counting; identical shape, live proof either way. Note preview/import responses are not reliable for this: a response created viaPOST .../responses(import) does not trigger quota evaluation at all — that's expected, uninformative behavior, not a signal either way. -
A quota's
countnever back-counts responses collected before the quota existed. It only increments on new submissions from creation forward. On a survey that has been fielding for a while, a freshly created or freshly fixed quota reading 0 or low is not evidence it's broken — and, the more dangerous direction, it is also not evidence a previous broken quota didn't already let the sample run uneven. Either way,countcannot tell you the true current composition of an already-fielding survey. Pull the actual response export (POST .../export-responses) and recompute fill directly from respondents' real answers against your target grid — that's the only number that reflects who has actually been collected, and it's required reading before reporting "verified" on any quota fix applied mid-field. -
Quota creation's group auto-assignment is flaky, not just "always goes to the first group" — confirmed empirically across two surveys built with near-identical scripts. There is no field to target a specific group on write. In one run, three sequential
create_quota_groupcalls (Age, Gender, Region) resulted in Gender's quotas silently landing in the Age group while Region correctly got its own; in another run on a different survey, all three groups' quotas landed in the very first group regardless of creation order. Do not assume any particular assignment pattern, and do not rely on the UI's "Move to…" menu for anything beyond a handful of objects — it does not scale. Instead: create every quota first (accept whatever group it lands in), THEN read back the actual membership and fix it programmatically via the group PUT below. Order matters when fixing it: a quota already listed in group A's membership cannot be added to group B's membership directly — the API returnsESDEF44("already exists in Quota Group X"). PUT the source group first with a shrunk membership list (removing the quotas you're about to move), THEN PUT the destination group with them added. Verify final state by reading back every group's membership and matching quota names against your intended structure — the group's ownNamefield is not proof itsQuotasarray is what you think it is. -
The quota-group update endpoint is a full replace: omit the quotas array and the group's membership is silently wiped. Always resend the complete membership plus any fields the API requires on write but omits from its own list payload (e.g. a match-mode flag) — write-shape and read-shape are not the same contract.
-
Choice-based quota conditions need both the operand the evaluation engine reads AND the locator the editor UI renders its dropdown from. Write only the operand and the condition still works but displays as an empty "Select Choice…" in the UI — and a later UI-side save of that blank state can overwrite live quota logic with nothing.
-
Confirm the exact operator enum the API expects (vendors sometimes reject a plausible-looking synonym) rather than assuming from REST convention.
-
Give every hard quota its own
EndSurveyOptionsat build time. A quota created without one inherits the survey-level termination settings, which works — the respondent still exits and still hits whatever redirect the survey-levelEOSRedirectURLresolves to — but the resulting response row carries noQuotaMetflag, so afterwards you cannot tell a quota-terminated respondent from any other early exit except by inferring it from their answer pattern. On an instrument that also produces consent-refusal and screen-out rows of the same shape (demographics answered, no outcome data), that inference gets genuinely fiddly. The platform's own serialization for a hard quota looks like:"EndSurveyOptions": {"EndingType": "Advanced", "ResponseFlag": "QuotaMet", "Screenout": "Yes", "IgnoreResponse": "Yes", "AnonymizeResponse": "Yes", "CountQuotas": "No", "SurveyTermination": "Redirect", "EOSRedirectURL": "<vendor quota-full URL>"}Note this supersedes an earlier claim in this skill that the API accepts no per-quota redirect — a platform-exported reference survey carries
EOSRedirectURLinsideEndSurveyOptionson its quota objects, so the quota-full URL can live on the quota itself rather than being bracketed in the flow. Treat the old claim as untested rather than true; it dates from the same period as the compound-logic misdiagnosis above. -
The flow-bracket pattern remains the right retrofit when quotas are already live without
EndSurveyOptions: set an embedded field to the quota-full exit URL immediately BEFORE the block holding the quota-bearing questions, reset it to the screen-out URL immediately AFTER, and have the survey-level end-of-survey redirect read that field. Verify by mapping flow indices — the setter must precede the block, the reset must follow it. This gets the vendor disposition right (which is the billing-relevant half) even though the response row stays unflagged. RetrofittingEndSurveyOptionsonto many live quota objects is a mass mutation on a fielding survey; if the bracket is already correct, the remaining benefit is forensic tidiness only, and is usually not worth the write. -
Quota counts can retain stale values after response deletion even when the deletion call requests a decrement. Before a FIRST fielding wave, zero the counters explicitly rather than trusting the decrement flag; mid-study, reconcile in-progress sessions and prior-wave records first — resetting a live counter is destructive and needs explicit authorization.
-
Quota-list endpoints paginate at a small page size — always follow the next-page cursor, or an audit silently covers only the first page of quotas.
5. Flow mutation and routing placement
- Anchor routing gates on block descriptions, not on a data-capture node. A capture node's position can vary across instruments (some run it pre-consent, some post-), so a gate anchored to "wherever that node sits" can end up before consent on some builds — ethics-relevant if the gate is a termination. Anchor each gate type to a stable semantic point instead: consent-dependent gates immediately after the consent block; paradata-based gates after the point where every field they read is guaranteed to exist; questionnaire-anchored checks right after their own block, never held to end-of-survey (a late termination costs the respondent the whole length of interview for nothing).
- Know which multilingual architecture you have. Qualtrics' native translation layer keeps ONE block structure with per-language text; a branch-per-language build duplicates every block per arm. On the latter, duplicate each gate into every arm with a fresh flow-element ID — one gate does not cover all arms.
- Capture-before-gate order is load-bearing. The node that writes a field must precede every gate that reads it. Get the order wrong and the gate fails safe — no error, no fire, just silently dead — which is far more dangerous than a routing bug that throws.
- When the exit URL is carried in an embedded-data field consumed by the end-of-survey redirect, the node that SETS the field must precede the terminating element inside the gate — the termination ends flow evaluation, so anything ordered after it never executes.
- On paired-language (twin) instruments, never express a failure condition as "the correct option was not selected" — an unanswered field in the respondent's other language arm also satisfies "not selected" and routes out the wrong arm entirely. Express failure as positive selection of a wrong option instead.
- Guard any geolocation-based termination with an explicit "value present and not equal to the excluded value," not just "not equal to." An unresolved lookup (proxy, privacy relay, corporate VPN) must not silently satisfy a bare not-equal check and terminate a legitimate respondent.
- Reserve live termination for signals that cannot belong to a real, eligible respondent: ineligibility, duplicate device/session, a hard machine signature. Anything that is scored or graded on a continuum — a bot-risk score, a fraud score, an attention-check failure, response-speed outliers — belongs in analysis-side exclusion criteria, not a live termination branch, because a live gate can't be revisited once it has turned away a respondent.
6. Live text edits vs. source specs
When a survey is built from versioned source specs (YAML, a survey-builder config, etc.), a live typo or wording fix still often needs to go directly against the live question via a targeted patch — matching exact surrounding text and replacing only the intended span — rather than a full spec rebuild and repush, because a rebuild will clobber any manual formatting or ordering that was applied directly in the live tool since the last build. When syncing the fix back into the source spec afterward, match whitespace-insensitively: prose in structured source formats commonly soft-wraps, so a byte-for-byte diff against live text produces false mismatches.
Keep an explicit list of anything the build pipeline does not emit (e.g. quality-routing branches, vendor-specific disclosures added live) — a rebuild silently drops these, so they must be reapplied by hand after every rebuild and repush.
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 55
- Forks
- 3
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
qualtrics-ops- Source
- github.com/scdenney/open-science-skills