Skip to content

Authoring response processing

Response processing is what turns a candidate’s answer into a score. It’s declarative: you write a small tree of rule and expression elements inside <qti-response-processing>, and the library walks that tree and executes it whenever something calls processResponse() on the item.

The fast path: built-in templates

Most items don’t need custom rules at all. QTI defines a handful of standard response-processing templates, and <qti-response-processing> recognizes them by URL — this is what the Quick Start’s example item actually uses:

<qti-response-processing template="https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/match_correct.xml" />
TemplateScoring
match_correct1 point if the response exactly matches the declared correct response, 0 otherwise.
map_responseLooks up the response in the response variable’s qti-mapping (per-choice point values).
map_response_pointSame idea, for point-based interactions (e.g. Select Point), via qti-area-mapping.

No network fetch happens for these — the library recognizes the template by its filename and expands it locally.

What match_correct actually expands to

Seeing the real markup is the fastest way to understand the rule model, so here’s exactly what that one-line shortcut expands to internally:

<qti-response-processing>
<qti-response-condition>
<qti-response-if>
<qti-match>
<qti-variable identifier="RESPONSE"></qti-variable>
<qti-correct identifier="RESPONSE"></qti-correct>
</qti-match>
<qti-set-outcome-value identifier="SCORE">
<qti-base-value base-type="float">1</qti-base-value>
</qti-set-outcome-value>
</qti-response-if>
<qti-response-else>
<qti-set-outcome-value identifier="SCORE">
<qti-base-value base-type="float">0</qti-base-value>
</qti-set-outcome-value>
</qti-response-else>
</qti-response-condition>
</qti-response-processing>

Reading it as an execution model rather than markup:

  1. <qti-response-processing> runs each of its child rules in order — normally just one <qti-response-condition>.
  2. <qti-response-condition> evaluates its <qti-response-if>/<qti-response-else-if>/<qti-response-else> children in order, and runs the sub-rules of the first one whose own expression evaluates to true.
  3. <qti-match> is a boolean expression — it compares its two child expressions for equality. Here that’s <qti-variable identifier="RESPONSE"> (the candidate’s answer) against <qti-correct identifier="RESPONSE"> (the answer declared in <qti-response-declaration>’s <qti-correct-response>).
  4. <qti-set-outcome-value identifier="SCORE"> assigns the outcome variable named SCORE to whatever its child expression evaluates to — here, a literal <qti-base-value base-type="float">.

Writing this out by hand instead of using the template shortcut is exactly how you’d handle anything the standard templates don’t cover — partial credit, multiple conditions, combining several interactions’ responses into one score, and so on.

Live example

This item uses the exact rule tree above, written out explicitly rather than via the template shortcut. Pick an answer and press Submit to actually run it:

Common operators

Response processing has a full expression vocabulary beyond qti-match — around 35 operators in @qti-components/processing, covering logic, comparison, math, and container manipulation. Rather than a page per operator, here’s the full list at a glance; each links conceptually to the QTI 3.0 operators spec for exact semantics:

Logic — qti-and, qti-or, qti-not, qti-any-n (true if between N and M sub-expressions are true)

Comparison — qti-match, qti-equal, qti-equal-rounded, qti-gt/qti-gte, qti-lt/qti-lte, qti-duration-lt/qti-duration-gte, qti-pattern-match, qti-substring, qti-is-null, qti-null

Math — qti-sum, qti-product, qti-subtract, qti-divide, qti-integer-divide, qti-integer-modulus, qti-power, qti-round, qti-round-to, qti-truncate, qti-gcd, qti-min/qti-max, qti-integer-to-float, qti-math-operator, qti-math-constant (pi or e), qti-stats-operator

Containers & lookups — qti-index, qti-field-value, qti-container-size, qti-delete, qti-repeat, qti-random-integer, qti-map-response, qti-map-response-point, qti-correct, qti-default

Variables — qti-variable (read any declared variable by identifier), qti-base-value (a literal value)

Lookup tables

qti-lookup-outcome-value reads the table on the outcome declaration it writes to.

A qti-interpolation-table maps a number onto a range. Each entry’s source-value is the lower bound of that range, not a value to match exactly, and include-boundary (default true) says whether the bound itself is in range. Entries are tested in document order and the first one the value clears wins, so tables are authored from the highest bound down:

<qti-interpolation-table>
<qti-interpolation-table-entry source-value="10" target-value="2" />
<qti-interpolation-table-entry source-value="5" target-value="1" />
<qti-interpolation-table-entry source-value="0" target-value="0" />
</qti-interpolation-table>

A qti-match-table matches its source-value exactly instead, and its targets need not be numeric — a match table may map onto identifiers.

When nothing matches, the outcome is left at the default its declaration gave it rather than overwritten with a score the table never specified.

Custom operators

A custom operator is outside the QTI vocabulary, so what it does is supplied by the delivery engine rather than by the item. <qti-custom-operator> offers two ways to supply one.

definition names an operator, and the operator is handed the calculated values of the element’s child expressions:

<qti-custom-operator definition="depcp:Trim">
<qti-variable identifier="RESPONSE" />
</qti-custom-operator>

Three are built in — Trim, ToAscii (replaces diacritics) and ParseCommaDecimal (a decimal comma becomes a dot) — each registered under the depcp:, questify: and qade: prefixes. Register your own, or override a built-in of the same name, from @qti-components/base:

import { registerCustomOperator } from '@qti-components/base';
registerCustomOperator('acme:Shout', values => String(values[0]).toUpperCase());

A definition nothing provides is NULL rather than an error, so qti-is-null can test for it.

class="js.org" instead runs inline JavaScript from a CDATA section, receiving the item context. The two mechanisms do not interact; definition is checked first.

Flow rules

Beyond qti-response-condition, two rules shape how a run proceeds:

  • qti-exit-response ends the attempt’s response processing wherever it sits — inside a condition branch or a fragment included. No rule after it runs, and the outcomes already set are kept. qti-exit-test is its counterpart in a test’s outcome processing.
  • qti-response-processing-fragment groups rules for reuse. It is transparent: its rules run in place, in document order, exactly as if written where the fragment sits. A fragment held in a separate file is not pulled in, because qti-include is not resolved.

See also