Failures (In-Survey Quality Checks)
Failures are the in-survey quality checks you run inside your Decipher survey — a honeypot, a trap question, or anything else you use to spot a poor-quality respondent. Sent to DQC, they become the signals behind the In-Survey Quality (ISQ) dispositions.
The Decipher XML Generator writes this for you. Turn on Add In-Survey Quality Checks and paste — both checks come pre-filled. Everything below explains what it produced and how to adjust it.
Quality Checks and Failures lists every check DQC accepts, live from our database. Dispositions explains how failures roll up into a final response category.
Automatic and custom failures
| Group | Examples | Who computes it | What you do |
|---|---|---|---|
| Automatic | dqcFraud, dqcDuplicate | DQC, from the device score, data trust score, persona and duplicate signals the Toolbox already sends | Nothing. Optionally act on them with Termination Scripts |
| Custom | honeyPot, trapQuestion, and anything else you run | You | Send them in the failures field |
For every failure you send, give your raw value. DQC normalizes it:
| You send | DQC records |
|---|---|
a number (e.g. 2) | that many failures |
True or any non-empty text | 1 failure |
0, False, null, "" (empty) | no failure |
Any check can be a count. Several honeypots tripped, three traps failed — send the number.
Turn it on in the generator
In the Decipher XML Generator, under Send Transaction Data to DQC:
- Check Add In-Survey Quality Checks (Failures).
- Copy the XML into Decipher.
That is the whole setup. Honeypot and Trap question are both on by default, each pre-filled with a working example (dqc_hp and dqc_trap), so a survey runs two checks without you typing anything. Add more rows, or point a row at a question of your own, whenever you need to.
A label starting with dqc_ is one the generator owns, so it writes that question into your survey — the honeypot as a hidden text field, the trap as a ready attention check.
Any other label is your question, so the check only reads it. That is what stops the generator from emitting a second <radio label="Q4"> for a question you already have.
Where to edit afterwards
The generated script is split into labelled sections. Two are headed YOU CAN EDIT THIS SECTION; the rest are DQC internals you can leave alone.
Your settings sit at the top of the <exec when="init"> block, next to the API key:
# ===========================================================================
# YOU CAN EDIT THIS SECTION - your settings
# ===========================================================================
DQC_API_KEY = "your-key" # must match the key in the toolbox import URL
dqcHoneypotLabels = ["dqc_hp"] # hidden questions; a filled one means a bot
dqcTrapRows = [("dqc_trap", "r3")] # (question label, correct answer row)
Add a label, remove one, correct a renamed question — no other line needs touching. The second editable section, further down, is where you add checks of your own.
The settings block is flush left. The custom-checks section lives inside send_results_dqc(), so its lines are indented one level (4 spaces). Python cares, and a mismatch is a syntax error Decipher will reject.
The label you give a question (dqc_hp, dqc_trap) is exactly how the check finds it. Write each one as a quoted string — ["dqc_hp", "dqc_hp_2"], never with .val. Get one wrong and you lose that label only: the check keeps counting the others and names the one it couldn't read.
🍯 honeyPot
A honeypot is a question a real person never sees, so they leave it blank — but a bot autofills it. Any content means the respondent is almost certainly a bot. Use several and the value is a count.
The generator creates the question, hidden, titled like any ordinary question so a bot fills it without hesitating. It lands on the first question's page, with no <suspend/> of its own:
<radio label="Q1">
...your first question...
</radio>
<text label="dqc_hp" optional="1" size="40" translateable="0">
<title>How old are you?</title>
</text>
<suspend/>
A honeypot is invisible, so a page holding nothing else renders blank — a dead page for the respondent to click through, and an obvious tell. Keep it on a page with a real question. If the generator's Q1 isn't your first question, move the honeypot block next to yours.
The generator also adds the CSS that hides it, inside the respview.client.meta block:
#question_dqc_hp {
position: absolute;
left: -9999px;
height: 0;
overflow: hidden;
}
Off-screen rather than display: none, which the better bots know to skip. Decipher renders a question's container as #question_<label> and its class as .label_<label>, never the raw label.
And the check, which reads dqcHoneypotLabels from the settings block at the top:
def dqcCountHoneypots():
n = 0
for dqcLabel in dqcHoneypotLabels:
if dqcGetVal(dqcLabel):
n += 1
return n
An unreadable label and an empty answer are both falsy, so one test skips both.
where="execute"Decipher's Hidden Questions feature removes the question from the page entirely, so a bot can't fill it and it is useless as a honeypot. Hiding with CSS keeps it present but invisible.
🪤 trapQuestion
A trap question states its own correct answer — "Select 'Somewhat agree' to show you're paying attention." Answer it wrong and you failed it. A long survey can carry several, so this is a count too.
The generator writes a ready one, on its own page — a trap is a visible question, so unlike the honeypot it does not need to share a page:
<radio label="dqc_trap">
<title>To show you're paying attention, please select "Somewhat agree".</title>
<row label="r1">Strongly disagree</row>
<row label="r2">Disagree</row>
<row label="r3">Somewhat agree</row>
<row label="r4">Agree</row>
</radio>
The generator drops it near the top, because that is where the scaffold ends — not because that is where it belongs. Leave it there and it works against you twice: an attention check in the first few screens is the one place respondents are still reading carefully, and it is the easiest spot for a repeat taker to recognise.
Put it somewhere in the middle, ideally after a long or repetitive block. That is where inattention actually shows up, which is the whole point of the check.
The honeypot is the opposite case: it is invisible and a bot fills it immediately, so the first page is a fine place for it.
The correct answer is the row label (r3), not its text — that is what you pair with the label in dqcTrapRows, up in the settings block. The check itself is:
def dqcTrapFailed(dqcLabel, dqcExpected):
# True only when the respondent answered AND picked another row.
# None when the question or the expected row cannot be read.
try:
dqcQ = globals().get(dqcLabel)
if dqcQ is None:
raise ValueError("no question with this label in the survey")
if getattr(dqcQ, dqcExpected, None) is None:
# A row that is not on the question is a setup mistake, so it is
# named and skipped: a typo must never fail a real respondent.
raise ValueError("no row %s on this question" % dqcExpected)
# Single-select: .val is only the row's INDEX, so compare the label
# of .selected - the row object the respondent picked. Empty when
# they never answered, which is not a failure.
dqcPicked = ("%s" % (getattr(getattr(dqcQ, "selected", None), "label", "") or "",)).strip()
if dqcPicked:
return dqcPicked != dqcExpected
# Multi-select and grids have no single .selected, so read the row
# itself: in Decipher "Q1.r1" is true when that row is chosen.
return bool(getattr(dqcQ, "val", "")) and not getattr(dqcQ, dqcExpected)
except Exception as dqcErr:
dqcNoteFailureError(dqcLabel, dqcErr)
return None
def dqcCountTrapFails():
n = 0
for dqcLabel, dqcExpected in dqcTrapRows:
if dqcTrapFailed(dqcLabel, dqcExpected):
n += 1
return n
.val on a radio is the row's index, not its labelThis is the one thing to know if you write your own trap. Per Decipher's Python Expressions reference, a single-select question's .val gives the position of the chosen row, while .selected.label gives the label — "r3". Comparing .val against "r3" never matches, so every respondent who answers would be counted as failing the trap.
That is why the check reads .selected.label, and why it falls back to the row flag (Q1.r1, true when that row is chosen) for multi-selects and grids, which have no single selected row.
Only answered-and-wrong counts, so abandons are never penalized. A label that isn't in the survey, or an expected row that isn't on the question, is named in the fail row and skipped — a setup mistake never costs a real respondent.
A radio is the case this is built for, and the one the generator writes. A checkbox or a grid row also works through the row fallback, with one caveat: it asks only "is the expected row ticked?", so ticking the right row plus others reads as correct.
Two things a trap cannot read: a grid cell (r1.c2 — the expected answer has to be a row on the question, not a path into it) and a free-text answer (there is no row to test). Both are named in the fail row rather than failing quietly.
Other checks, and your own
DQC accepts more than these two. The full list is on Quality Checks and Failures, rendered live from our database rather than written down here, so it is never out of date.
The generator leaves you a section for them, inside send_results_dqc():
# ===========================================================================
# YOU CAN EDIT THIS SECTION - your own quality checks
# ===========================================================================
def dqcCountMyCheck():
return 2 # your logic; a count, or 1 / 0
dqcAddFailure("other", lambda: dqcCountMyCheck())
Whatever your function returns is the value. A constant needs no function at all — dqcAddFailure("other", 1) does the same thing.
lambda:dqcAddFailure("other", lambda: dqcCountMyCheck()) resolves the name inside the guard, so a typo or a crash costs that one check. Passing the bare name — dqcAddFailure("other", dqcCountMyCheck) — resolves it outside, and a misspelling there takes down every check at once.
This section sits immediately before the payload is sent, so anything you add here is always included. Use other for a check that has no name yet, and contact the DQC team if you run a specific, recurring one that deserves its own.
An open-end has no built-in right answer, and DQC does not score open-ends for you. Whether an answer is low quality is a judgement, so the rule has to be yours: length and emptiness, gibberish or repetition checks, keyword lists, or a call out to your own AI/scoring service.
Decipher <exec> blocks are real Python and can call external APIs, so a scoring service works — at a cost. Every submission waits on that call, and your survey now depends on that service staying up. Wrap it in try/except and default to not flagging on error, so an outage never invents failures.
Send the count of answers your rule rejected under openEndQuestion.
What happens when a check breaks
Every level is guarded, so a mistake stays contained:
| What goes wrong | What still happens |
|---|---|
One label is wrong — renamed, deleted, a typo, a stray .val | That question is skipped and named; every other label in the same check still counts |
| A trap expects a row the question does not have | The trap is skipped and named — a setup mistake never counts against a respondent |
| A whole check raises — a bad rule, an edited function, a service down | Only that key is dropped; every other check is still in the payload |
| Every check fails | failures is still sent, as an empty object, instead of vanishing |
A dropped key is left out, never sent as 0: DQC reads 0 as "the respondent passed this check", which would hide the breakage behind a clean result.
Outside all of it, addDQCField wraps every field it reads in its own try/except. A total collapse of your checks costs you the field and nothing else — never the respondent's session, never the rest of the payload.
An empty answer is not a breakage. A respondent terminated before reaching a question isn't penalized; that is different from a question that isn't in the survey.
Seeing what broke
Problems are named in the dqc_debug holder's fail row, once each, under Responses → View/Edit Responses:
FAILCHK dqc_hp_2: no question with this label in the survey
FAILCHK dqc_trap: no row r9 on this question
The first says a honeypot label points at a question that isn't in the survey; the other honeypots still counted. The second says a trap expects a row that question does not have — a setup mistake, so it was skipped rather than charged to the respondent.
A note keyed on a check name rather than a label (FAILCHK trapQuestion: ...) means that whole check raised, so only its key is missing from the payload.
Surveys from the Decipher XML Generator already have that row. If yours was set up before it existed, add it to your dqc_debug holder:
<row label="fail">failed checks</row>
Nothing breaks without it and no check behaves differently — the notes just have nowhere to go, so a broken check leaves no trace.
To see the value in Responses → View/Edit Responses as well as in DQC, store repr(dqcFailures) in a text field. dqcFailures is built inside send_results_dqc(), so that line has to go there too — after the checks, before the send. The source of truth is what was sent to DQC.
Custom failures are specific to each survey and client. If you're unsure how to detect a check or which name to use, contact the DQC team — we'll help you set it up.
✅ Summary
- Failures are the in-survey checks you run — they power the ISQ dispositions.
dqcFraudanddqcDuplicateare computed by DQC; you don't send them.- The Decipher XML Generator writes
honeyPotandtrapQuestionfor you, including the honeypot questions themselves. Adjust the labels in the YOU CAN EDIT settings block at the top of the init exec. - Value rule: number = that many ·
True/text = 1 ·0/empty = none. - Checks are isolated: a missing question skips only that question, a broken check drops only its own key, and both appear as
FAILCHKentries in thedqc_debugholder'sfailrow. - Everything else DQC accepts is on Quality Checks and Failures — add it with one
dqcAddFailureline.