Operate 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).
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 nested
ActionInfo object 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:
ActionType must repeat the QuotaAction value. 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's count is incrementing, its matching logic
is already correct and the problem is not the logic; go straight to
ActionInfo.
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
Selected flag, canonical dialect and EndSurveyOptions all in place and
still reached page 2; populating ActionInfo alone 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 Selected flag (setting it
True to match a working reference changed nothing on its own); the
presence or absence of EndSurveyOptions (worth having for the QuotaMet
flag, but not what makes a quota fire); and Occurrences: 0 as 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 broken
ActionInfo no limit value of any kind would have blocked anyone. Use
Occurrences: 1 when 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, with ChoiceLocator duplicating 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
(no Conjuction needed) 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
generated Logic block 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 as
LeftOperand, not the e://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 read count: 0 — because all of them used the e://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 from Simple — know which one you are reading.
A Simple quota is one cell: Logic is a single expression tree (a dict)
and Occurrences is that cell's absolute target. A Cross quota is a whole
grid: Logic is an ARRAY of logic sets, the engine crosses the sets to
generate cells, and Occurrences is the TOTAL across the grid. In a Cross
quota the Conjuction key does not hold "And"/"Or" — it holds that
condition's percentage allocation ("27%", "29%"), and each cell's
effective target is Occurrences x the 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 absolute Occurrences: 33 and 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
Selected conditions — no ChoiceTextEntryValue locator, 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 count increments, or —
cheaper — diff its Logic shape 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 via POST .../responses (import) does not trigger quota
evaluation at all — that's expected, uninformative behavior, not a signal
either way.
A quota's count never 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, count cannot
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_group
calls (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 returns ESDEF44 ("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 own Name field is not proof its Quotas array
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 EndSurveyOptions at 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-level EOSRedirectURL resolves to — but the resulting response row
carries no QuotaMet flag, 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:
Note this supersedes an earlier claim in this skill that the API accepts
no per-quota redirect — a platform-exported reference survey carries
EOSRedirectURL inside EndSurveyOptions on 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. Retrofitting EndSurveyOptions
onto 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.
7. Panel-vendor integration basics
Redirect logic for panel-vendor traffic follows a stable pattern regardless of
vendor: a pre-consent screen-out value, a terminal complete value, and an
end-of-survey redirect to whichever URL parameter carries the vendor's
completion redirect — typically piped from an embedded-data field the vendor's
entry link populated. Bracket any quota-bearing block with a quota-full
redirect variant so respondents who close out a quota mid-survey get routed to
the vendor's quota-full endpoint rather than falling through to a generic
completion or termination redirect.
URL query parameters resolve into piped references at session start
regardless of where (or whether) the embedded-data declaration sits in the
flow — live-verified against redirect pipes. Declare the field anyway:
declaration is what makes the value reliably typed, saved, and exported, and
downstream logic easier to read.
(Panel vendors vary — a generic panel-vendor redirect endpoint is the concept
that matters here, not any particular vendor's API shape.)
Before enabling ANY quota's hard-terminate action (EndCurrentSurvey) on a
survey running through a panel vendor, check prior vendor correspondence for
an explicit statement of which real-time termination paths are in use.
Vendors are sometimes told directly — in an email, not just implied by
default config — that a given exit status (a quota-full redirect code,
say) is deliberately not used, with real-time termination limited to a
named, narrower set of conditions (duplicate device, geo-ineligibility,
automation detection). Flipping a quota to hard-terminate is, from inside
Qualtrics, a purely internal config change with a green checkmark and no
warning — but it silently starts exercising a redirect path the vendor's
system was told to expect never to see. This is a compliance question, not
a technical one, and the fix isn't a Qualtrics setting: read the actual
correspondence (search for the vendor's redirect status codes by name, not
just "quota") before assuming a hard quota is safe to activate on a live
vendor-sourced field.
8. Security options
Treat the survey's security/options block as read-modify-write: fetch the
full current object, change only the target keys, and write the whole object
back — then assert every key you did not intend to touch is byte-identical to
the pre-change value. Options endpoints are as prone to full-replace semantics
as the quota-group endpoint above, and a security setting silently reset to a
default (e.g. a fraud-detection threshold, a ballot-box-stuffing prevention
flag) is the kind of regression that goes unnoticed until an incident, not at
write time.
After any live toggling of quota actions or repeated publishes on a
fielding survey, verify no real respondent was actually affected — don't
just reason about it. Pull the response export, filter to sessions with
StartDate inside the affected window, and check Progress/Finished
across all of them. A clean 100/Finished=True for every session is
checkable proof nobody was cut off mid-survey by a config change in flight;
don't rely on "the targets never should have hit zero" reasoning alone when
the actual data is one export call away.
Audit mode (audit)
Run this mode when the task is to assess a survey rather than change it,
invoked as $qualtrics-ops audit. It reads
the live instrument the way it will actually run, not the way its build files
say it should, and it produces findings, never fixes.
Audit mode is read-only on the survey definition.GET everything;
PUT/POST nothing. If the platform offers a no-op write check for token
scope, that is the only write. Every repair the audit surfaces goes back
through the operations sections above as a separate, explicitly authorized
change — with its own backup, read-back, and publish-with-proof — rather than
being folded into the audit. The single exception is the optional browser walk
(Phase H), which generates test responses: data-plane writes with their own
cleanup obligations, and possibly test hits on a vendor dashboard, so it is
opt-in and announced, never silent.
The API mechanics the audit reads through are documented above and not
repeated here: what proves a change is actually live (§3), quota object shapes,
logic dialect, and list pagination (§4), flow anchoring and capture-before-gate
order (§5), the vendor redirect pattern and query-parameter resolution (§7),
and options read semantics (§8). The audit checks the live objects against
those standards; it does not restate how to write them.
When to run an audit
Immediately before a soft launch or full launch; after any live patch to a
fielding instrument; when a vendor reports a broken redirect or "different
content"; when handed an unfamiliar survey to take over. Inputs: API
credentials and the survey id; ideally also the pre-registration or PAP (for
the report-only-vs-terminating posture), the vendor's integration sheet
(redirect URLs, ID parameter name), and the quota targets. A browser
automation tool enables Phase H; without it, run A–G and say so in the report.
Fielding now happens in an environment where AI agents complete surveys at
scale and pass conventional attention checks (documented since 2025 in
peer-reviewed and platform validations), panel vendors bill on redirect
passbacks, and platforms silently stage rather than publish edits. Each of
those failure classes is invisible in a casual preview and cheap to catch here.
Audit posture
Evidence or it didn't happen: every PASS cites the object read back (flow
element, option key, quota logic), never the absence of an error.
The registered design wins. Where a PAP declares an item report-only, a live
branch that terminates on it is a blocking finding even if well-built.
Phase A — identity and publish state
Confirm the survey id, name, and active/inactive state match intent. An
inactive instrument scheduled for launch is fine; an active one nobody meant
to open is a finding.
Apply the §3 standard: the working definition and the published version must
match, proven by the version list rather than an in_sync flag.
Staged-but-unpublished edits to a fielding survey are a blocking finding.
Response settings that shape the data: partial-response window, multiple-
submission prevention, anonymization/IP recording, link type, expiration —
and whether in-progress respondents stay pinned to the version they started.
Phase B — consent before anything
The first substantive screen a respondent reaches is consent (or a language
selector whose every arm leads first to consent).
Nothing evaluates or acts before affirmative consent: no terminating gates,
no quality branches, no telemetry collectors on or before the consent page.
(If the approved protocol places a minimal eligibility screener before
consent, audit that instead for authorization, minimization, and whether
pre-consent data are retained.)
Location/device capture nodes may write earlier (platforms populate them at
session start), but every branch that reads them must sit after consent.
Decline path: declining consent must route to the vendor's screen-out (or the
study's stated exit), not dead-end or count as a complete.
Consent text ↔ configuration consistency, both directions: if invisible
scoring or fingerprinting is enabled (reCAPTCHA, device checks), the text
discloses it; if the text promises skippable questions, optional questions
actually exist. A consent page describing a survey that isn't this one is a
finding whichever direction the drift runs.
Phase C — question integrity
Force-response completeness: enumerate every question; classify descriptive
(no answer possible), forced, requested, and unvalidated. The check is
consistency, not a universal forced-by-default norm (optional is often the
right call for sensitive items): every unvalidated answerable item must be
one the design names optional, and if any exist, Phase B's
consent-consistency check must see them.
Attention and manipulation checks: present where the design says, and their
consequence (terminate vs record-only) matches the registration. In the
current environment, terminating on an attention check screens out humans
while catching almost no agents — flag it as a design smell even when it
matches the PAP.
Multilingual instruments: first identify which architecture you have (§5).
Under the native translation layer, audit the translations for coverage;
under a branch-per-language build, every item, choice set, validation
setting, and embedded JS must exist symmetrically in each arm. A check
present in one arm only, or logic testing "correct option NOT selected" on a
twin build, is a blocking finding.
Phase D — flow structure
Walk the full flow tree, at every nesting depth:
Block order matches the intended instrument; randomizers present with the
intended settings (even presentation, subset size).
Capture-before-gate holds for every embedded-data field (§5). A guarded
condition on a never-yet-written field is silently dead — it fails safe,
which is exactly why nobody notices.
Terminating branches: condition logic decodes to the intended trigger; inner
flow sets the exit redirect before the End-of-Survey element; unique flow
IDs throughout; the terminal "completion" redirect node is the last element.
On branched (language/arm) instruments, structural checks run per arm, not
once globally.
Phase E — vendor integration
Redirect pattern (§7): a pre-consent default carrying the screen-out URL, a
terminal overwrite carrying the complete URL, end-of-survey set to redirect
to the piped field. Early leavers must exit as screen-outs, completers as
completes, quota-fulls (if hard quotas exist) as quota-fulls — each URL
byte-exact against the vendor's sheet.
The vendor's respondent-ID parameter is captured as embedded data and echoed
back on every exit path, including declines. Because query parameters resolve
regardless of declaration (§7), treat a missing declaration as a minor
finding and a wrong parameter name as fatal.
Enumerate which vendor endpoints can receive traffic and which are dead by
design, and check that against what the vendor was told in writing (§7). A
quality or quota-full endpoint the vendor expects to fire, wired to nothing,
is a relationship problem waiting for fieldwork.
Phase F — quotas
Decode every quota's logic against the live question's choices and audit it
against the RATIFIED grid, not an assumed one: marginal-family designs
should partition each frame exactly once with per-family targets summing to
the commissioned N; interlocked or deliberately overlapping designs have
their own intended structure (check the multiple-match setting, and identify
Simple vs Cross before reading Logic at all — §4). Screening
categories ("I don't live here") belong to no quota either way.
Hard vs soft actions match the ratified design; group labels say which is
which truthfully. For any quota whose action is EndCurrentSurvey, read
ActionInfo and confirm it is populated with the nested firing shape (§4) —
an empty stub counts every match and blocks nobody, and no other symptom
ever surfaces.
Verify each quota's condition dialect against §4 (Conjuction,
ChoiceTextEntryValue, bare field names for EmbeddedField operands, both
the evaluation operand and the UI ChoiceLocator). A quota reading count: 0
on a fielding survey is a dialect finding until proven otherwise, and
pagination must be followed or the audit silently covers page one only.
All counts are zero before fielding (test responses leave phantom counts even
after deletion-with-decrement — read the actual counters). On an instrument
that has already been fielding, count cannot tell you composition at all;
recompute fill from the response export (§4).
Phase G — anti-automation layer
Platform toggles (bot-detection scoring, device fingerprinting, geo capture)
are on if the design says so — and disclosed per Phase B.
Behavioral instrumentation (interaction paradata, honeypots, page timers) is
present on the pages the design instruments, in every language arm.
The live-terminating set is restricted to signals that cannot plausibly be a
real person: ineligibility, duplicate device, machine signature. Anything
scored or graded (bot-score thresholds, fraud scores, speed cutoffs,
attention items) belongs to analysis, not to a live gate (§5) — a scored live
gate is a finding.
Phase H — browser walk (optional, needs browser automation)
Use the LIVE distribution link, never the preview (preview banners change
rendering and skip embedded-data population). Append a test value for the
vendor ID parameter.
Walk at minimum: one decline (assert the screen-out redirect fires with the
ID echoed), one complete per language arm (assert the complete redirect), one
mobile-viewport pass (conjoint tables and stacked layouts render; nothing
clips). Where feasible add: the quota-full path, one pass per experimental
arm, a missing-vendor-ID entry, and validation/back-button behavior on one
forced item.
Confirm no screen precedes consent, and that the consent page renders in the
right language for each arm.
Clean up: delete the test responses with quota decrement, then re-read quota
counts (Phase F) — and note that in-progress partials usually cannot be
deleted via API and must expire or be cleared in the UI.
Audit report
Rank findings blocking / major / minor, each with the evidence read back
and the phase that produced it. State explicitly: live version vs working
version; which phases ran (and that H was skipped, if it was); which findings
the registered design forces you to leave alone. End with the test-response
cleanup confirmation if Phase H ran.
Quality Checks
Pre-change backup saved (survey definition + flow + options as separate
JSON) before any write
Publish issued after every quota/question/flow write, with activation
(allow_activation/equivalent) triggered only at deliberate launch, never
as a side effect
Change verified live via list_versions showing a version with your own
description — not via publish-state or the builder UI badge alone
Full object read back post-write; every untouched key confirmed
byte-identical to the backup
Quota-group and options writes sent as complete objects (full
membership / full key set), never partial
Any quota with more than one condition is diffed byte-for-byte against
a real, platform-exported QSF's quota Logic shape (Conjuction
spelling, ChoiceTextEntryValue locator) before being trusted —
never hand-authored from the "obviously correct" spelling
Any LogicType: "EmbeddedField" quota condition uses the BARE field
name as LeftOperand ("gc"), never the e://Field/ piped form that
is correct everywhere else in Qualtrics
Quota type identified before reading or writing Logic: Simple =
one cell, dict-shaped logic, absolute Occurrences; Cross = a grid,
array-of-logic-sets, Occurrences is the total and Conjuction holds
a percentage share, not And/Or
Fill verified against a real response export, never against count
alone, on any survey that was already fielding before the quota was
created or fixed
Every hard quota's ActionInfo is POPULATED with the nested
{"0":{"0":{"ActionType":<same as QuotaAction>,"Type":"Expression", "LogicType":"QuotaAction"},"Type":"If"},"Type":"BooleanExpression"}
shape -- an empty {"Type":"BooleanExpression"} stub yields a quota
that counts every match and blocks nobo
…(truncated)
1---2name: qualtrics-ops3description: Operate 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).4---56# Qualtrics Live-Survey Operations78## Instructions910### 1. When to use1112This skill covers **operating** a survey already built and imported into13Qualtrics — publishing, quotas, flow routing, live text patches, panel-vendor14integration, security options — on an instrument that is or will be fielding15real respondents. It is not survey design or QSF construction; assume the16survey definition already exists and the question is how to change it without17breaking what respondents currently see.1819Two modes. The default is **operations**: sections 2-8 below govern any write20to a live instrument. **Audit mode** (`$qualtrics-ops audit`) is the21read-only pre-fielding assessment in the section at the end — it produces22ranked findings and makes no changes, and any repair it surfaces comes back23through the operations sections as a separate, authorized change. The24write-safety rules in section 2 are the shared frame for both: audit mode25never writes at all, and operations never publish or activate by accident.2627### 2. Non-negotiables2829**Backups first, always.** Pull and save the survey definition (and flow and30options as separate JSON) before any write. Qualtrics version restore exists31but is coarse and carries data-risk caveats; your pre-change snapshot is the32only *precise* recovery path, and the only one that covers objects (quota33counters, options) outside version history.3435**Verify by read-back, never by absence of error.** A 200 response proves the36API accepted the request, not that respondents will see the result. After37every write, GET the changed object back and confirm it matches intent.3839**The version list is the proof a change is live**, not the publish-state40flag. Question/block writes create no version-history entry, so publish-state41can report "in sync" while the live version still lags behind your edit. The42standard: your publish call created a NEW published version entry carrying43your own description — and, because a description proves provenance rather44than content, pair it with a read-back of the published content where the API45exposes it.4647**One writer at a time.** Confirm you are on the right survey, brand, and48environment, that no one has the builder open concurrently, and that you know49the rollback path before the first write. On an actively fielding instrument,50prefer a change window; and remember the platform's own warning that deleting51or restructuring questions and choices can invalidate already-collected data.5253**Assert everything untouched is unchanged**, not just that your target54changed — compare parsed structures, since the server may normalize ordering55and defaults. Several of the traps below are full-replace endpoints that56silently wipe sibling data; catching that requires diffing the whole object57against the pre-change backup, not just checking the field you meant to edit.5859### 3. Publish gating — two distinct failure modes6061Most flow and options edits to an active survey are **staged**, not live,62until an explicit version publish — respondents keep getting the old version63while the API happily reads back your new one. (A few option keys apply64immediately, availability among them; treat "staged until published" as the65safe default assumption and verify per key.) Question/block writes are worse: they66create no `list_versions` entry at all, so publish-state can say "in sync"67immediately after a write that hasn't actually gone anywhere near respondents.68Stage all related writes for a change as one batch, verify the batch by69read-back, then publish ONCE, deliberately (forcing the publish if your client70supports it) — publishing after every individual write can expose respondents71to internally inconsistent intermediate versions. Reload the builder UI before72trusting its Draft/Published badge — it lags the API.7374Publishing an **inactive** survey activates it. Treat activation as a75separate, deliberate decision from publishing a change — have your API client76require an explicit activation flag so a routine content publish can never77launch fielding as a side effect.7879### 4. Quota API8081- **START HERE IF A QUOTA COUNTS BUT NEVER BLOCKS: check `ActionInfo`.**82 `QuotaAction: "EndCurrentSurvey"` only NAMES the action. The nested83 `ActionInfo` object is what actually FIRES it. A quota written with an84 empty stub —8586 "ActionInfo": {"Type": "BooleanExpression"}8788 — counts every matching respondent flawlessly, forever, and acts on none89 of them: no termination, no screen-out, no error at write time, at publish90 time, or at runtime, and the builder UI renders the quota as normal. The91 platform's own serialization is:9293 "ActionInfo": {"0": {"0": {"ActionType": "EndCurrentSurvey",94 "Type": "Expression",95 "LogicType": "QuotaAction"},96 "Type": "If"},97 "Type": "BooleanExpression"}9899 `ActionType` must repeat the `QuotaAction` value. This was shipped as an100 empty stub in a homegrown API client and made **every quota it ever created101 inert** across multiple live fielding surveys — the true reason quotas102 "didn't work", after days of chasing logic-dialect theories that were all103 downstream of it. If a quota's `count` is incrementing, its matching logic104 is already correct and the problem is not the logic; go straight to105 `ActionInfo`.106- **Proven by controlled before/after, and this is the method to reuse.**107 Do NOT try to diagnose quota enforcement on a clone of the real108 instrument — geo gates, duplicate-device gates, anti-automation paradata109 and session-resume cookies will each eject or short-circuit the walk, and110 stripping them piecemeal corrupts the flow (all of this was tried, and111 burned hours). Instead build a **minimal disposable survey from scratch**:112 three quota-bearing questions on page 1, one marker question on page 2113 (reaching page 2 is the "not blocked" signal — a single-page survey cannot114 demonstrate blocking at all, because the respondent has already answered115 everything by the time the quota evaluates), plus TWO quotas: one the walk116 matches and one differing in a single condition as a negative control.117 Walk it once to fill the match quota, then walk again and observe. The118 control must stay at 0 — that is what proves the multi-condition AND is119 sound rather than collapsing. Change ONE variable per walk. In the120 reference case the immediately-preceding walk already had the group's121 `Selected` flag, canonical dialect and `EndSurveyOptions` all in place and122 still reached page 2; populating `ActionInfo` alone flipped the identical123 walk to terminating at page 1.124- **Ruled out along the way — do not re-chase these** (each was a plausible125 theory that cost real time): the quota group's `Selected` flag (setting it126 `True` to match a working reference changed nothing on its own); the127 presence or absence of `EndSurveyOptions` (worth having for the `QuotaMet`128 flag, but not what makes a quota fire); and `Occurrences: 0` as a way to129 express "already full" — that IS wrong and no platform-authored quota uses130 it (minimum observed across 49 reference quotas was 16), but with a broken131 `ActionInfo` no limit value of any kind would have blocked anyone. Use132 `Occurrences: 1` when a cell is already over target and you want it shut:133 the first matching respondent tips it to full and every subsequent one is134 blocked.135- **The actual root cause of "compound quota logic never fires" is a136 hand-authoring dialect mismatch, not a platform limit on condition count —137 and getting this wrong once already cost a full day of live-fielding churn138 across two surveys, so read this one carefully.** Qualtrics's own139 internally-generated quota JSON uses `"Conjuction"` (not `"Conjunction"` —140 a legacy misspelling baked permanently into the schema) as the key joining141 multiple conditions, and `"q://{QID}/ChoiceTextEntryValue"` (not142 `"q://{QID}/TextEntry"`) as the operand/locator for a numeric comparison143 against a text-entry question, with `ChoiceLocator` duplicating the same144 string. Hand-write either one with the "obviously correct" English145 spelling or the intuitive-looking locator and the quota accepts the write146 (200 OK), reads back exactly as sent, renders correctly in the builder UI's147 condition editor, and then matches nobody, ever, with no error anywhere —148 because the engine simply doesn't recognize the key/locator and silently149 drops that half of the condition. This produced the exact symptom pattern150 that looks like "compound conditions never fire": single-condition quotas151 (no `Conjuction` needed) counted correctly the whole time; every152 age-range and multi-choice-OR quota sat at zero, on multiple live153 production surveys, for over a week, undetected. **The fix, once154 diagnosed, is to write compound conditions in the correct dialect, not to155 avoid them.** A same-day rebuild using the verified dialect — including156 three- and four-condition flat AND groups spanning multiple different157 QuestionIDs (age range AND gender-selected AND region-selected in one158 group) — fired correctly and counted real respondents within the hour.159 **Do not hand-guess this dialect from documentation or from what "looks160 right."** Find a real, platform-exported QSF from the same account (or ask161 the user for one — even an unrelated old survey works) and diff your162 generated `Logic` block against its quota objects byte-for-byte before163 trusting anything with more than one condition; this is the single highest-164 leverage check available and takes minutes. Absent a reference file, the165 fallback is the advisor-consult pattern: hand a second, independent model166 the full evidence trail (what fired, what didn't, exact JSON of both) and167 ask it to reason from first principles rather than guessing again yourself168 — that is what actually surfaced this dialect mismatch after multiple169 failed self-directed attempts.170- **`LogicType: "EmbeddedField"` quota conditions take the BARE field name as171 `LeftOperand`, not the `e://Field/...` piped reference** — and getting this172 wrong is silent, like every other quota-dialect error. A platform-authored173 Cross quota carries `{"LogicType": "EmbeddedField", "LeftOperand": "gc",174 "Operator": "EqualTo", "RightOperand": "1"}` — just `"gc"`. Writing175 `"e://Field/gc"` (the form that is correct everywhere else in Qualtrics:176 Branch logic, display logic, piped text, redirect URLs) produces a quota177 that accepts the write, reads back intact, and matches nobody. This was178 learned expensively: a Branch-precompute design (merge a value into one179 flat embedded field, then quota on equality) was built at ~130 quota180 objects across two live surveys, with the field VERIFIED correctly181 populated in the response export for real respondents, and every one of182 those quotas read `count: 0` — because all of them used the `e://Field/`183 prefix. Do not conclude from a failure like that that embedded-field quota184 logic is unsupported; check the operand format against a platform-authored185 example first. Note also that when the goal is an interlock, `LogicType:186 "Cross"` (below) is the native mechanism and usually beats precomputing a187 merged field at all.188- **`LogicType: "Cross"` is Qualtrics's native interlock quota, and it is a189 different object shape from `Simple` — know which one you are reading.**190 A Simple quota is one cell: `Logic` is a single expression tree (a dict)191 and `Occurrences` is that cell's absolute target. A Cross quota is a whole192 grid: `Logic` is an ARRAY of logic sets, the engine crosses the sets to193 generate cells, and `Occurrences` is the TOTAL across the grid. In a Cross194 quota the `Conjuction` key does not hold `"And"`/`"Or"` — it holds that195 condition's **percentage allocation** (`"27%"`, `"29%"`), and each cell's196 effective target is `Occurrences x` the product of its shares. So the same197 misspelled key means two completely different things depending on quota198 type; do not pattern-match one onto the other. Choosing between them:199 Cross expresses an age x gender x region interlock as ONE object instead200 of hundreds, which is decisive when building a grid up front. But because201 Cross targets are percentages of a total, it is poorly suited to rebasing202 a partially-collected field — "this cell has 33 slots left of 473" is203 trivial as a Simple quota's absolute `Occurrences: 33` and awkward as a204 share of a total that is already half filled. Build with Cross; repair205 mid-field with Simple.206- **Sidestep the whole range-comparison problem at design time by asking age207 as a categorical band question rather than a numeric text entry.** A208 platform-authored reference survey that quotas cleanly on age does it with209 a multiple-choice item (18-29 / 30-39 / 40-49 / 50-59 / 60+) and plain210 `Selected` conditions — no `ChoiceTextEntryValue` locator, no211 `>=`/`<=` pair, no compound condition, nothing to get wrong. If the212 instrument is not yet fielded and the analysis does not need exact age,213 this is strictly the safer design.214- **For a bilingual/multi-arm instrument, don't reflexively split every215 marginal quota by arm.** If one arm carries the overwhelming majority of216 traffic (check the actual split from the response export, don't assume),217 splitting age/gender/etc. into one quota per arm doubles the object count218 and produces a nonsensical-looking result on screen (a demographic quota219 that appears to depend on survey language). Fold the minority arm into the220 majority arm's quota instead — check only the majority-language question,221 size it to the full combined remaining target — and accept that the222 minority arm isn't independently capped by that specific quota. Disclose223 the tradeoff; don't build the split by default.224- Before trusting ANY quota with more than a trivial condition, prove the225 engine actually fires it: create it, then either drive one real or preview226 response through the matching path and confirm `count` increments, or —227 cheaper — diff its `Logic` shape against a quota on the same live survey228 that is *already* demonstrably counting; identical shape, live proof either229 way. Note preview/import responses are not reliable for this: a response230 created via `POST .../responses` (import) does not trigger quota231 evaluation at all — that's expected, uninformative behavior, not a signal232 either way.233- **A quota's `count` never back-counts responses collected before the quota234 existed.** It only increments on new submissions from creation forward.235 On a survey that has been fielding for a while, a freshly created or236 freshly fixed quota reading 0 or low is not evidence it's broken — and,237 the more dangerous direction, it is also not evidence a *previous* broken238 quota didn't already let the sample run uneven. Either way, `count` cannot239 tell you the true current composition of an already-fielding survey.240 Pull the actual response export (`POST .../export-responses`) and recompute241 fill directly from respondents' real answers against your target grid —242 that's the only number that reflects who has actually been collected, and243 it's required reading before reporting "verified" on any quota fix applied244 mid-field.245- **Quota creation's group auto-assignment is flaky, not just246 "always goes to the first group" — confirmed empirically across two247 surveys built with near-identical scripts.** There is no field to target a248 specific group on write. In one run, three sequential `create_quota_group`249 calls (Age, Gender, Region) resulted in Gender's quotas silently landing in250 the Age group while Region correctly got its own; in another run on a251 different survey, all three groups' quotas landed in the very first group252 regardless of creation order. Do not assume any particular assignment253 pattern, and do not rely on the UI's "Move to…" menu for anything beyond a254 handful of objects — it does not scale. Instead: create every quota first255 (accept whatever group it lands in), THEN read back the actual membership256 and fix it programmatically via the group PUT below. **Order matters when257 fixing it**: a quota already listed in group A's membership cannot be added258 to group B's membership directly — the API returns `ESDEF44` ("already259 exists in Quota Group X"). PUT the *source* group first with a shrunk260 membership list (removing the quotas you're about to move), THEN PUT the261 destination group with them added. Verify final state by reading back every262 group's membership and matching quota names against your intended263 structure — the group's own `Name` field is not proof its `Quotas` array264 is what you think it is.265- The quota-group update endpoint is a **full replace**: omit the quotas array266 and the group's membership is silently wiped. Always resend the complete267 membership plus any fields the API requires on write but omits from its own268 list payload (e.g. a match-mode flag) — write-shape and read-shape are not269 the same contract.270- Choice-based quota conditions need both the operand the evaluation engine271 reads AND the locator the editor UI renders its dropdown from. Write only272 the operand and the condition still *works* but displays as an empty273 "Select Choice…" in the UI — and a later UI-side save of that blank state274 can overwrite live quota logic with nothing.275- Confirm the exact operator enum the API expects (vendors sometimes reject a276 plausible-looking synonym) rather than assuming from REST convention.277- **Give every hard quota its own `EndSurveyOptions` at build time.** A quota278 created without one inherits the survey-level termination settings, which279 works — the respondent still exits and still hits whatever redirect the280 survey-level `EOSRedirectURL` resolves to — but the resulting response row281 carries **no `QuotaMet` flag**, so afterwards you cannot tell a282 quota-terminated respondent from any other early exit except by inferring283 it from their answer pattern. On an instrument that also produces284 consent-refusal and screen-out rows of the same shape (demographics285 answered, no outcome data), that inference gets genuinely fiddly. The286 platform's own serialization for a hard quota looks like:287288 "EndSurveyOptions": {"EndingType": "Advanced",289 "ResponseFlag": "QuotaMet",290 "Screenout": "Yes", "IgnoreResponse": "Yes",291 "AnonymizeResponse": "Yes", "CountQuotas": "No",292 "SurveyTermination": "Redirect",293 "EOSRedirectURL": "<vendor quota-full URL>"}294295 Note this **supersedes an earlier claim in this skill that the API accepts296 no per-quota redirect** — a platform-exported reference survey carries297 `EOSRedirectURL` inside `EndSurveyOptions` on its quota objects, so the298 quota-full URL can live on the quota itself rather than being bracketed in299 the flow. Treat the old claim as untested rather than true; it dates from300 the same period as the compound-logic misdiagnosis above.301- **The flow-bracket pattern remains the right retrofit** when quotas are302 already live without `EndSurveyOptions`: set an embedded field to the303 quota-full exit URL immediately BEFORE the block holding the quota-bearing304 questions, reset it to the screen-out URL immediately AFTER, and have the305 survey-level end-of-survey redirect read that field. Verify by mapping flow306 indices — the setter must precede the block, the reset must follow it. This307 gets the vendor disposition right (which is the billing-relevant half) even308 though the response row stays unflagged. Retrofitting `EndSurveyOptions`309 onto many live quota objects is a mass mutation on a fielding survey; if the310 bracket is already correct, the remaining benefit is forensic tidiness only,311 and is usually not worth the write.312- Quota counts can retain stale values after response deletion even when the313 deletion call requests a decrement. Before a FIRST fielding wave, zero the314 counters explicitly rather than trusting the decrement flag; mid-study,315 reconcile in-progress sessions and prior-wave records first — resetting a316 live counter is destructive and needs explicit authorization.317- Quota-list endpoints paginate at a small page size — always follow the318 next-page cursor, or an audit silently covers only the first page of quotas.319320### 5. Flow mutation and routing placement321322- **Anchor routing gates on block descriptions, not on a data-capture node.**323 A capture node's position can vary across instruments (some run it324 pre-consent, some post-), so a gate anchored to "wherever that node sits"325 can end up before consent on some builds — ethics-relevant if the gate is a326 termination. Anchor each gate type to a stable semantic point instead:327 consent-dependent gates immediately after the consent block; paradata-based328 gates after the point where every field they read is guaranteed to exist;329 questionnaire-anchored checks right after their own block, never held to330 end-of-survey (a late termination costs the respondent the whole length of331 interview for nothing).332- Know which multilingual architecture you have. Qualtrics' native333 translation layer keeps ONE block structure with per-language text; a334 branch-per-language build duplicates every block per arm. On the latter,335 duplicate each gate into every arm with a fresh flow-element ID — one gate336 does not cover all arms.337- **Capture-before-gate order is load-bearing.** The node that writes a field338 must precede every gate that reads it. Get the order wrong and the gate339 fails *safe* — no error, no fire, just silently dead — which is far more340 dangerous than a routing bug that throws.341- When the exit URL is carried in an embedded-data field consumed by the342 end-of-survey redirect, the node that SETS the field must precede the343 terminating element inside the gate — the termination ends flow evaluation,344 so anything ordered after it never executes.345- On paired-language (twin) instruments, never express a failure condition as346 "the correct option was not selected" — an unanswered field in the347 respondent's *other* language arm also satisfies "not selected" and routes348 out the wrong arm entirely. Express failure as positive selection of a wrong349 option instead.350- Guard any geolocation-based termination with an explicit "value present and351 not equal to the excluded value," not just "not equal to." An unresolved352 lookup (proxy, privacy relay, corporate VPN) must not silently satisfy a353 bare not-equal check and terminate a legitimate respondent.354- Reserve *live* termination for signals that cannot belong to a real,355 eligible respondent: ineligibility, duplicate device/session, a hard machine356 signature. Anything that is scored or graded on a continuum — a bot-risk357 score, a fraud score, an attention-check failure, response-speed outliers —358 belongs in analysis-side exclusion criteria, not a live termination branch,359 because a live gate can't be revisited once it has turned away a respondent.360361### 6. Live text edits vs. source specs362363When a survey is built from versioned source specs (YAML, a survey-builder364config, etc.), a live typo or wording fix still often needs to go directly365against the live question via a targeted patch — matching exact surrounding366text and replacing only the intended span — rather than a full spec rebuild367and repush, because a rebuild will clobber any manual formatting or ordering368that was applied directly in the live tool since the last build. When syncing369the fix back into the source spec afterward, match whitespace-insensitively:370prose in structured source formats commonly soft-wraps, so a byte-for-byte371diff against live text produces false mismatches.372373Keep an explicit list of anything the build pipeline does **not** emit (e.g.374quality-routing branches, vendor-specific disclosures added live) — a rebuild375silently drops these, so they must be reapplied by hand after every rebuild376and repush.377378### 7. Panel-vendor integration basics379380Redirect logic for panel-vendor traffic follows a stable pattern regardless of381vendor: a pre-consent screen-out value, a terminal complete value, and an382end-of-survey redirect to whichever URL parameter carries the vendor's383completion redirect — typically piped from an embedded-data field the vendor's384entry link populated. Bracket any quota-bearing block with a quota-full385redirect variant so respondents who close out a quota mid-survey get routed to386the vendor's quota-full endpoint rather than falling through to a generic387completion or termination redirect.388389**URL query parameters resolve into piped references at session start390regardless of where (or whether) the embedded-data declaration sits in the391flow** — live-verified against redirect pipes. Declare the field anyway:392declaration is what makes the value reliably typed, saved, and exported, and393downstream logic easier to read.394395(Panel vendors vary — a generic panel-vendor redirect endpoint is the concept396that matters here, not any particular vendor's API shape.)397398**Before enabling ANY quota's hard-terminate action (`EndCurrentSurvey`) on a399survey running through a panel vendor, check prior vendor correspondence for400an explicit statement of which real-time termination paths are in use.**401Vendors are sometimes told directly — in an email, not just implied by402default config — that a given exit status (a quota-full redirect code,403say) is deliberately *not* used, with real-time termination limited to a404named, narrower set of conditions (duplicate device, geo-ineligibility,405automation detection). Flipping a quota to hard-terminate is, from inside406Qualtrics, a purely internal config change with a green checkmark and no407warning — but it silently starts exercising a redirect path the vendor's408system was told to expect never to see. This is a compliance question, not409a technical one, and the fix isn't a Qualtrics setting: read the actual410correspondence (search for the vendor's redirect status codes by name, not411just "quota") before assuming a hard quota is safe to activate on a live412vendor-sourced field.413414### 8. Security options415416Treat the survey's security/options block as read-modify-write: fetch the417full current object, change only the target keys, and write the whole object418back — then assert every key you did not intend to touch is byte-identical to419the pre-change value. Options endpoints are as prone to full-replace semantics420as the quota-group endpoint above, and a security setting silently reset to a421default (e.g. a fraud-detection threshold, a ballot-box-stuffing prevention422flag) is the kind of regression that goes unnoticed until an incident, not at423write time.424425**After any live toggling of quota actions or repeated publishes on a426fielding survey, verify no real respondent was actually affected — don't427just reason about it.** Pull the response export, filter to sessions with428`StartDate` inside the affected window, and check `Progress`/`Finished`429across all of them. A clean 100/`Finished=True` for every session is430checkable proof nobody was cut off mid-survey by a config change in flight;431don't rely on "the targets never should have hit zero" reasoning alone when432the actual data is one export call away.433434## Audit mode (audit)435436Run this mode when the task is to *assess* a survey rather than change it,437invoked as `$qualtrics-ops audit`. It reads438the live instrument the way it will actually run, not the way its build files439say it should, and it produces findings, never fixes.440441**Audit mode is read-only on the survey definition.** `GET` everything;442`PUT`/`POST` nothing. If the platform offers a no-op write check for token443scope, that is the only write. Every repair the audit surfaces goes back444through the operations sections above as a separate, explicitly authorized445change — with its own backup, read-back, and publish-with-proof — rather than446being folded into the audit. The single exception is the optional browser walk447(Phase H), which generates test *responses*: data-plane writes with their own448cleanup obligations, and possibly test hits on a vendor dashboard, so it is449opt-in and announced, never silent.450451The API mechanics the audit reads through are documented above and not452repeated here: what proves a change is actually live (§3), quota object shapes,453logic dialect, and list pagination (§4), flow anchoring and capture-before-gate454order (§5), the vendor redirect pattern and query-parameter resolution (§7),455and options read semantics (§8). The audit checks the live objects against456those standards; it does not restate how to write them.457458### When to run an audit459460Immediately before a soft launch or full launch; after any live patch to a461fielding instrument; when a vendor reports a broken redirect or "different462content"; when handed an unfamiliar survey to take over. Inputs: API463credentials and the survey id; ideally also the pre-registration or PAP (for464the report-only-vs-terminating posture), the vendor's integration sheet465(redirect URLs, ID parameter name), and the quota targets. A browser466automation tool enables Phase H; without it, run A–G and say so in the report.467468Fielding now happens in an environment where AI agents complete surveys at469scale and pass conventional attention checks (documented since 2025 in470peer-reviewed and platform validations), panel vendors bill on redirect471passbacks, and platforms silently stage rather than publish edits. Each of472those failure classes is invisible in a casual preview and cheap to catch here.473474### Audit posture475476- Evidence or it didn't happen: every PASS cites the object read back (flow477 element, option key, quota logic), never the absence of an error.478- The registered design wins. Where a PAP declares an item report-only, a live479 branch that terminates on it is a **blocking** finding even if well-built.480481### Phase A — identity and publish state482483- Confirm the survey id, name, and active/inactive state match intent. An484 inactive instrument scheduled for launch is fine; an active one nobody meant485 to open is a finding.486- Apply the §3 standard: the working definition and the published version must487 match, proven by the version list rather than an `in_sync` flag.488 Staged-but-unpublished edits to a fielding survey are a **blocking** finding.489- Response settings that shape the data: partial-response window, multiple-490 submission prevention, anonymization/IP recording, link type, expiration —491 and whether in-progress respondents stay pinned to the version they started.492493### Phase B — consent before anything494495- The first substantive screen a respondent reaches is consent (or a language496 selector whose every arm leads first to consent).497- Nothing evaluates or acts before affirmative consent: no terminating gates,498 no quality branches, no telemetry collectors on or before the consent page.499 (If the approved protocol places a minimal eligibility screener before500 consent, audit that instead for authorization, minimization, and whether501 pre-consent data are retained.)502 Location/device capture nodes may *write* earlier (platforms populate them at503 session start), but every branch that *reads* them must sit after consent.504- Decline path: declining consent must route to the vendor's screen-out (or the505 study's stated exit), not dead-end or count as a complete.506- Consent text ↔ configuration consistency, both directions: if invisible507 scoring or fingerprinting is enabled (reCAPTCHA, device checks), the text508 discloses it; if the text promises skippable questions, optional questions509 actually exist. A consent page describing a survey that isn't this one is a510 finding whichever direction the drift runs.511512### Phase C — question integrity513514- Force-response completeness: enumerate every question; classify descriptive515 (no answer possible), forced, requested, and unvalidated. The check is516 consistency, not a universal forced-by-default norm (optional is often the517 right call for sensitive items): every unvalidated answerable item must be518 one the design *names* optional, and if any exist, Phase B's519 consent-consistency check must see them.520- Attention and manipulation checks: present where the design says, and their521 *consequence* (terminate vs record-only) matches the registration. In the522 current environment, terminating on an attention check screens out humans523 while catching almost no agents — flag it as a design smell even when it524 matches the PAP.525- Multilingual instruments: first identify which architecture you have (§5).526 Under the native translation layer, audit the translations for coverage;527 under a branch-per-language build, every item, choice set, validation528 setting, and embedded JS must exist symmetrically in each arm. A check529 present in one arm only, or logic testing "correct option NOT selected" on a530 twin build, is a **blocking** finding.531532### Phase D — flow structure533534Walk the full flow tree, at every nesting depth:535536- Block order matches the intended instrument; randomizers present with the537 intended settings (even presentation, subset size).538- Capture-before-gate holds for every embedded-data field (§5). A guarded539 condition on a never-yet-written field is silently dead — it fails safe,540 which is exactly why nobody notices.541- Terminating branches: condition logic decodes to the intended trigger; inner542 flow sets the exit redirect *before* the End-of-Survey element; unique flow543 IDs throughout; the terminal "completion" redirect node is the last element.544- On branched (language/arm) instruments, structural checks run per arm, not545 once globally.546547### Phase E — vendor integration548549- Redirect pattern (§7): a pre-consent default carrying the screen-out URL, a550 terminal overwrite carrying the complete URL, end-of-survey set to redirect551 to the piped field. Early leavers must exit as screen-outs, completers as552 completes, quota-fulls (if hard quotas exist) as quota-fulls — each URL553 byte-exact against the vendor's sheet.554- The vendor's respondent-ID parameter is captured as embedded data and echoed555 back on every exit path, including declines. Because query parameters resolve556 regardless of declaration (§7), treat a missing declaration as a minor557 finding and a wrong parameter name as fatal.558- Enumerate which vendor endpoints can receive traffic and which are dead by559 design, and check that against what the vendor was told in writing (§7). A560 quality or quota-full endpoint the vendor expects to fire, wired to nothing,561 is a relationship problem waiting for fieldwork.562563### Phase F — quotas564565- Decode every quota's logic against the live question's choices and audit it566 against the RATIFIED grid, not an assumed one: marginal-family designs567 should partition each frame exactly once with per-family targets summing to568 the commissioned N; interlocked or deliberately overlapping designs have569 their own intended structure (check the multiple-match setting, and identify570 `Simple` vs `Cross` before reading `Logic` at all — §4). Screening571 categories ("I don't live here") belong to no quota either way.572- Hard vs soft actions match the ratified design; group labels say which is573 which truthfully. For any quota whose action is `EndCurrentSurvey`, read574 `ActionInfo` and confirm it is populated with the nested firing shape (§4) —575 an empty stub counts every match and blocks nobody, and no other symptom576 ever surfaces.577- Verify each quota's condition dialect against §4 (`Conjuction`,578 `ChoiceTextEntryValue`, bare field names for `EmbeddedField` operands, both579 the evaluation operand and the UI `ChoiceLocator`). A quota reading `count: 0`580 on a fielding survey is a dialect finding until proven otherwise, and581 pagination must be followed or the audit silently covers page one only.582- All counts are zero before fielding (test responses leave phantom counts even583 after deletion-with-decrement — read the actual counters). On an instrument584 that has already been fielding, `count` cannot tell you composition at all;585 recompute fill from the response export (§4).586587### Phase G — anti-automation layer588589- Platform toggles (bot-detection scoring, device fingerprinting, geo capture)590 are on if the design says so — and disclosed per Phase B.591- Behavioral instrumentation (interaction paradata, honeypots, page timers) is592 present on the pages the design instruments, in every language arm.593- The live-terminating set is restricted to signals that cannot plausibly be a594 real person: ineligibility, duplicate device, machine signature. Anything595 scored or graded (bot-score thresholds, fraud scores, speed cutoffs,596 attention items) belongs to analysis, not to a live gate (§5) — a scored live597 gate is a finding.598599### Phase H — browser walk (optional, needs browser automation)600601- Use the LIVE distribution link, never the preview (preview banners change602 rendering and skip embedded-data population). Append a test value for the603 vendor ID parameter.604- Walk at minimum: one decline (assert the screen-out redirect fires with the605 ID echoed), one complete per language arm (assert the complete redirect), one606 mobile-viewport pass (conjoint tables and stacked layouts render; nothing607 clips). Where feasible add: the quota-full path, one pass per experimental608 arm, a missing-vendor-ID entry, and validation/back-button behavior on one609 forced item.610- Confirm no screen precedes consent, and that the consent page renders in the611 right language for each arm.612- Clean up: delete the test responses with quota decrement, then re-read quota613 counts (Phase F) — and note that in-progress partials usually cannot be614 deleted via API and must expire or be cleared in the UI.615616### Audit report617618Rank findings **blocking / major / minor**, each with the evidence read back619and the phase that produced it. State explicitly: live version vs working620version; which phases ran (and that H was skipped, if it was); which findings621the registered design forces you to leave alone. End with the test-response622cleanup confirmation if Phase H ran.623624## Quality Checks625626- [ ] Pre-change backup saved (survey definition + flow + options as separate627 JSON) before any write628- [ ] Publish issued after every quota/question/flow write, with activation629 (`allow_activation`/equivalent) triggered only at deliberate launch, never630 as a side effect631- [ ] Change verified live via `list_versions` showing a version with your own632 description — not via publish-state or the builder UI badge alone633- [ ] Full object read back post-write; every untouched key confirmed634 byte-identical to the backup635- [ ] Quota-group and options writes sent as complete objects (full636 membership / full key set), never partial637- [ ] Any quota with more than one condition is diffed byte-for-byte against638 a real, platform-exported QSF's quota `Logic` shape (`Conjuction`639 spelling, `ChoiceTextEntryValue` locator) before being trusted —640 never hand-authored from the "obviously correct" spelling641- [ ] Any `LogicType: "EmbeddedField"` quota condition uses the BARE field642 name as `LeftOperand` (`"gc"`), never the `e://Field/` piped form that643 is correct everywhere else in Qualtrics644- [ ] Quota type identified before reading or writing `Logic`: `Simple` =645 one cell, dict-shaped logic, absolute `Occurrences`; `Cross` = a grid,646 array-of-logic-sets, `Occurrences` is the total and `Conjuction` holds647 a percentage share, not And/Or648- [ ] Fill verified against a real response export, never against `count`649 alone, on any survey that was already fielding before the quota was650 created or fixed651- [ ] Every hard quota's `ActionInfo` is POPULATED with the nested652 `{"0":{"0":{"ActionType":<same as QuotaAction>,"Type":"Expression",653 "LogicType":"QuotaAction"},"Type":"If"},"Type":"BooleanExpression"}`654 shape -- an empty `{"Type":"BooleanExpression"}` stub yields a quota655 that counts every match and blocks nobo656657…(truncated)
Run npx skillmds@latest add scdenney/qualtrics-ops in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
Operate 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). It is listed under Security on SkillMD.
This skill has not completed SkillMD's automated safety review yet. Capability flags: makes network calls, reads secrets. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free, and the skill stays under its author's original license.
scdenney (@scdenney) published this skill. Their other Agent Skills are listed on their SkillMD profile.