Questionnaire & QML Tools — MCP Tool Reference¶
Questionnaire tools manage the QML lifecycle: saving new versions, validating with Z3 formal analysis, publishing a version to a campaign, moving between projects, and unpublishing. The lifecycle enforces constraints — for example, unpublishing is blocked if active campaigns reference the questionnaire.
How QML is stored¶
A questionnaire's QML lives in Askalot's database as an append-only version chain. Every save adds a new version and moves the questionnaire's head to it; earlier versions are never overwritten or deleted.
qml_name is the questionnaire's stable address, of the form projects/{project_id}/questionnaires/{questionnaire_id}.qml. It looks like a path and reads like one, but it identifies a questionnaire — there is no directory to browse and no file to stage. list_qml_files returns the addresses that exist.
Three rules follow from this, and they explain most of the behaviour below:
- A save names the version it was written from. You compose a save from content you read a moment ago, and someone else may have saved since. So
save_qml_fileandfinalize_uploadtakebase_version_id, theversion_idof the read or save answer your content came from. If the head is still that version, your content becomes the new head. If it moved and the two changes touch different lines, the merge of both becomes the head. If both changed the same lines, nothing is written and the answer isoverlap. - Saving never blocks on validation. A sketch, a pasted block, or half-finished logic must be storable, so a save records the formal-validation verdict on the version that lands and returns any problems as
warnings. Besides an address that resolves to no questionnaire you can write and a call the input schema rejects, a save is refused only for: a base that is not a version of the questionnaire (base_version_unknown), anoverlap, a head that moved before every attempt (head_kept_moving), a questionnaire moved to another project while the save ran (questionnaire_moved), a sealed organization (org_sealed), and inline content over 32 KB. - Publishing does refuse.
publish_qml_filepins a version to a campaign, which is the moment the instrument reaches a respondent — so it is the one door that rejects an unsound design. The pinned version is then immutable for that campaign's whole life.
list_questionnaires¶
List all questionnaires, optionally filtered by name.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string |
No | — | Case-insensitive substring filter |
limit |
integer |
No | 100 |
Maximum results to return |
Returns: { items: [...], count, limit }
get_questionnaire¶
Get a questionnaire by ID.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
questionnaire_id |
string |
Yes | — | Questionnaire UUID |
Returns: Questionnaire object or { error }.
create_questionnaire¶
Create a new questionnaire from QML content, or from a template of the library. The questionnaire and version 1 of its QML are created together, so a questionnaire never exists without content.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string |
With content |
the template's name | Display name |
content |
string |
One of content / template |
— | The QML YAML, stored as version 1 |
template |
string |
One of content / template |
— | A template slug from list_questionnaire_templates; version 1 is that template's QML, byte for byte |
project_id |
string |
Yes | — | Parent project ID |
description |
string |
No | — | Free-text description |
language |
string |
No | "en" |
Language code |
estimated_duration |
integer |
No | — | Expected completion time in minutes |
Give content or template, never both: both, or neither, is refused and nothing is written. An unknown template slug is answered not-found.
Returns: Created questionnaire object, plus version_id and version_number of version 1. version_id is the base_version_id for a first save of that content.
Creation is not gated on formal validation — content may be structurally broken. From content, the verdict is recorded with version 1, not enforced; publishing to a campaign is what requires soundness. From a template, validation runs in the background a few seconds after the create, as after a save: until then logic_validated reads false.
list_questionnaire_templates¶
List the questionnaire template library: ready-made questionnaires (course evaluations, customer feedback, event registration, patient intake and more), grouped in categories. Pass a slug to create_questionnaire as template.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
category |
string |
No | — | A category slug; an unknown one is refused with the list of categories |
query |
string |
No | — | Words that must all occur in a template's name or description, in any case |
limit |
integer |
No | 50 |
Page size, 1 to 100 |
cursor |
string |
No | — | The next_cursor of the previous page |
Returns: { data: [{ slug, name, category, subcategory, description }], categories: [{ slug, name }], total, next_cursor, has_more }. total counts every matching template; follow next_cursor while has_more is true. No QML body is listed: create from a slug, then read the questionnaire with get_qml_content.
delete_questionnaire¶
Soft-delete a questionnaire.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
questionnaire_id |
string |
Yes | — | Questionnaire UUID |
Returns: { success: true, questionnaire_id } or error.
Soft-delete inserts a backup row; restore_questionnaire (below) can bring the questionnaire back for a short window.
rename_questionnaire¶
Rename a questionnaire. Pure metadata update — the questionnaire's address and QML version chain are unchanged, so campaigns referencing it keep working.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
questionnaire_id |
string |
Yes | — | Questionnaire UUID |
name |
string |
Yes | — | New display name (max 255 bytes) |
Returns: Updated questionnaire object.
restore_questionnaire¶
Restore a recently soft-deleted questionnaire and re-link its references. Its QML version chain was never touched by the delete, so the restored questionnaire comes back at exactly the version it had.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
questionnaire_id |
string |
Yes | — | Questionnaire UUID |
Returns: Restored questionnaire object, or an error if the restore window has lapsed.
list_qml_files¶
List the organization's QML questionnaires. No parameters required. Each entry's qml_name can be handed to any other QML tool.
Returns:
{
"items": [
{
"name": "3f9c2b10-....qml",
"qml_name": "projects/abc123/questionnaires/3f9c2b10-....qml",
"tier": "project",
"project_id": "abc123"
}
],
"count": 1
}
inspect_qml_file¶
Get a quick summary of a questionnaire's current version — item count and metadata without the full content.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
qml_name |
string |
Yes | — | Questionnaire address from list_qml_files |
Returns: { success, qml_name, items_count, metadata }
preview_qml_item¶
Show one item of a questionnaire's current version as a respondent sees it. A host that renders MCP Apps (such as Claude) opens a read-only panel with the item's real controls; any other host receives the same item as JSON text.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
qml_name |
string |
Yes | — | Questionnaire address from list_qml_files |
item_id |
string |
Yes | — | The item's id |
Returns: the item's rendered fields (id, kind, title, input, and questions, rows or columns where the item has them), with any markup in its text sanitized. An unknown item or address is a tool error.
get_qml_content¶
Retrieve the raw YAML of a questionnaire's current version.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
qml_name |
string |
Yes | — | Questionnaire address |
Returns: { success, qml_name, content, size_bytes, version_id, version_number } for content up to 32 KB. version_id names the version content is; pass it as base_version_id when you save content written from this answer.
Above 32 KB the content isn't inlined — instead the response is { success, qml_name, download_url, size_bytes, version_id, version_number, note }, where download_url is a short-lived link (reusable until it expires) you fetch directly over HTTPS (no further authentication needed). The link serves the head at the time you fetch it, which is version_id unless a save landed in between. Call get_qml_content again if the link has expired.
save_qml_file¶
Save QML content to an existing questionnaire, written against a version you name. Saving never blocks on broken YAML or schema violations — an in-progress, not-yet-valid draft still saves, with any issues returned as a warnings list rather than rejecting the write.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
qml_name |
string |
Yes | — | Address of an existing questionnaire |
content |
string |
Yes | — | QML YAML content, up to 32 KB |
base_version_id |
string |
Yes | — | The version the content was written from: the version_id of your most recent get_qml_content, create_questionnaire or save answer for this questionnaire |
project_id |
string |
No | — | When given, must match the questionnaire's project; a mismatch is refused rather than ignored |
Read first, then save. Call get_qml_content, apply your change to the content it returns, and save with its version_id as base_version_id. After a save, the next save names the version_id that save returned, never an earlier one.
Returns: { success, qml_name, questionnaire_id, version_id, version_number, created, merged, content_changed, bytes_written, logic_validated, content?, content_note?, warnings? }.
| What happened | In the answer |
|---|---|
| The base was still the head | created: true, merged: false. Your content is the new head |
| Someone saved in between, on different lines | created: true, merged: true, content_changed: true. The head holds both changes, and content carries it: it replaces your copy. Lines in it you did not write are the other author's |
| Your content is already the head | created: false. Nothing is written |
| Your content equals its base and the head moved | created: false, content_changed: true, with the head's content |
content is included only when the stored content is under 32 KB; otherwise content_note says to call get_qml_content. warnings is absent on a clean save, or a list of parse/schema issues on a saved-but-imperfect draft.
A save that writes nothing answers { success: false, error, detail }:
error |
Meaning | What to do |
|---|---|---|
overlap |
Someone saved in between and changed the same lines. Carries head_version_id, head_version_number, conflict_count |
Call get_qml_content, apply your change to what it returns, and save with its version_id. Do not send the same content again under the newer id: that would replace the other save |
base_version_unknown |
base_version_id is blank, not a version id, or not a version of this questionnaire |
Call get_qml_content and save with the version_id it returns |
head_kept_moving |
The questionnaire was saved again before each of three attempts | Read again and retry |
org_sealed |
The organization is sealed | Nothing can be written |
questionnaire_moved |
The questionnaire was moved to another project while the save ran | Call list_qml_files for its current address, read it with get_qml_content and save again |
Formal validation runs in the background, a few seconds after the save, on whatever version is then the head (yours, or the merge), and its verdict is recorded on that version, so a version's soundness stays answerable later even after further edits. Until then the save answer's logic_validated is null and logic_validation_note says so, and get_questionnaire reads logic_validated: false; it answers the verdict once it lands, and validate_qml_file reports it on demand. A regressing save flips the questionnaire to not-validated without blocking the write; publishing it to a campaign is refused until it is fixed.
An address that backs no questionnaire is refused — call create_questionnaire first.
Content over 32 KB is refused with {"error": "content_too_large_for_inline_save"} and nothing is written. Use request_upload_url / finalize_upload (below) instead of trimming content to fit.
request_upload_url / finalize_upload¶
A two-step handle for saving QML content too large for save_qml_file's 32 KB inline limit. The content itself never appears in a tool call — you request a handle, upload the bytes directly over HTTPS, then finalize.
-
request_upload_url(qml_name, project_id?)— mints a short-lived, single-use, size-capped upload handle.Returns:
{ upload_url, artifact_id, max_size_bytes, expires_in } -
Upload the raw QML bytes to
upload_urldirectly (an HTTPPUT, outside the MCP tool call). -
finalize_upload(artifact_id, base_version_id)— saves the uploaded content exactly assave_qml_filewould save it on that base: landed, merged with a newer save, or refused.base_version_idis required and means the same as onsave_qml_file. Safe to retry with the sameartifact_id: after a finalize that saved, or that answeredoverlap, a retry returns the first answer and writes nothing, whatever base it names.Returns: the same envelope as
save_qml_fileAn
overlapspends the handle, because the uploaded bytes were written against the old version: read again, re-apply your change, and upload the result through a fresh handle.base_version_unknownandhead_kept_movingkeep the handle, so a corrected call saves the same upload.
Requires an HTTP-capable client
Uploading needs the ability to make an HTTPS request outside the MCP conversation. An MCP client with no such capability cannot complete an above-threshold save — save_qml_file's content_too_large_for_inline_save error names this limitation explicitly.
validate_qml_file¶
Run Z3 SMT formal validation on a questionnaire's current version. Analyzes reachability, consistency, and postcondition satisfiability for every item.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
qml_name |
string |
Yes | — | Questionnaire address |
Returns:
{
"is_valid": true,
"item_count": 12,
"block_count": 3,
"item_classifications": {
"q_age": {
"precondition": "REACHABLE",
"postcondition": "CONTINGENT"
}
},
"statistics": {
"preconditions": {"REACHABLE": 11, "NEVER": 1},
"postconditions": {"CONTINGENT": 8, "TAUTOLOGICAL": 3, "INFEASIBLE": 1}
},
"issues": [
{
"item_id": "q_unreachable",
"severity": "error",
"type": "unreachable_item",
"message": "Item is unreachable",
"suggestion": "Check preconditions"
}
],
"validation_report": "..."
}
Classification Reference¶
Precondition statuses (can the item be reached?):
| Status | Meaning |
|---|---|
REACHABLE |
Item can be reached in at least one flow path |
NEVER |
Item is unreachable — dead code |
UNKNOWN |
Analysis could not determine reachability |
Postcondition statuses (are the item's conditions satisfiable?):
| Status | Meaning |
|---|---|
TAUTOLOGICAL |
Condition is always true (redundant) |
CONTINGENT |
Condition depends on responses (normal) |
INFEASIBLE |
Condition can never be satisfied |
UNKNOWN |
Analysis could not determine |
Issue Types¶
| Type | Severity | Description |
|---|---|---|
unreachable_item |
error | Precondition is NEVER — dead item |
infeasible_postcondition |
error | Postcondition can never be satisfied |
globally_false_postcondition |
error | Postcondition is always false |
tautological_postcondition |
warning | Postcondition is always true (redundant) |
comment_titled_as_question |
warning | A display-only Comment is titled as a request, so it renders with no field to answer it |
roster_subject_not_answerable |
warning | A Roster's subjectFrom names an item that can never supply a subject (missing, outside the block, or a Comment) |
none_unsafe_reference |
warning | A condition orders or computes on an item that is not asked on every path reaching it — that item's outcome is None and the predicate raises. Equality against None is safe and is not flagged |
inexact_condition |
warning | A condition reads an item that may be unanswered in a place where the validator cannot tell whether reading None fails it (under not, in an or operand before the last, or anywhere in a postcondition). A contradiction is still reported, but the condition is not proven always true. Test for an answer first: q.outcome is not None and (…) in a precondition, q.outcome is None or (…) in a postcondition |
uninitialized_read |
warning | A condition or codeBlock reads a variable that a respondent can reach before any assignment has run — it is assigned later, on one branch only, or by an item its own gate can skip. The read raises: a precondition skips the item, while a postcondition accepts the answer unchecked and a codeBlock stops part-way — both of which exclude the survey from datasets. Assign a default in codeInit, assign on both branches, or gate the reader like the assignment |
self_assignment |
warning | A codeBlock statement assigns a value to itself (q.outcome = q.outcome) — it changes nothing but reads like a derivation |
mandated_answer_postcondition |
warning | A postcondition admits only one answer of a choice control, so a respondent giving any other answer cannot move on and that answer is never recorded. Route on the answer instead |
combinable_none_option |
warning | A Checkbox "None of the above"-style option can be ticked together with the options it denies, and no postcondition refuses the combination |
questionnaire_level_condition |
warning | A root-level precondition/postcondition key. Conditions are evaluated at item and block level only, so this one never runs |
file_not_found |
error | The address backs no questionnaire |
validation_error |
error | Exception during parsing or analysis |
qml_quality_report¶
Compute a design-quality scorecard (dimensions D2–D8) for a questionnaire's current version. Use it after validate_qml_file reports zero errors — validation proves logical soundness; this grades design quality (instrument economy, quality-gate density, verification coverage, structural complexity, order coherence, path diversity, and burden balance). Quality-gate density is measured over the questionnaire's numeric inputs — the answers a respondent types or slides, where a postcondition adds a validity bound the control itself does not. Choice items are excluded from that rate, because their label set already pins the answer; a postcondition tying one choice to another is reported separately as a consistency gate. Deterministic and advisory: grades never affect is_valid or publishing.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
qml_name |
string |
Yes | — | Questionnaire address |
Returns:
{
"dimensions": {
"instrument_economy": {
"id": "D2",
"grade": "strong",
"metrics": { },
"offenders": [{"item_id": "...", "detail": "..."}],
"truncated": false
}
},
"item_count": 12,
"notes": []
}
Each dimension's grade is one of strong, adequate, weak, or not_applicable. An unknown address or an analysis failure returns { error }.
publish_qml_file¶
Publish a questionnaire to a campaign — the fielding door, and the only tool in this reference that refuses an unsound design.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
questionnaire_id |
string |
Yes | — | Questionnaire UUID to field |
campaign_id |
string |
Yes | — | Campaign UUID to pin it to |
The questionnaire's current version is formally validated, the verdict is recorded on that version, and only a clean verdict pins it to the campaign.
Returns: { success, questionnaire_id, campaign_id, qml_version_id, logic_validated }
On refusal: { error, issues } — issues lists the error-severity problems to fix. Nothing is pinned, so the campaign keeps fielding whatever it had.
What the pin guarantees
Every survey the campaign produces is served that exact version for its
whole life. Editing the questionnaire afterwards cannot change the
instrument under a respondent mid-interview. Call publish_qml_file again
after an edit to re-validate and re-pin.
The campaign must already reference this questionnaire; if it fields a different one, rebind it with update_campaign_questionnaire first.
move_qml_to_project¶
Move a questionnaire to another project. The questionnaire ID stays stable and its version chain is untouched, so campaigns referencing it — including ones pinned to an earlier version — continue to work. A move is not an edit.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
questionnaire_id |
string |
Yes | — | Questionnaire UUID |
target_project_id |
string |
Yes | — | Destination project UUID |
Returns: { success, questionnaire_id, qml_name } — qml_name is the new address.
unpublish_qml_file¶
Retract a questionnaire from its project by soft-deleting it. Its QML version chain is retained — a completed campaign may still be pinned to one of those versions, so unpublishing removes the questionnaire from your working set without erasing what was fielded. Blocked if the questionnaire has active campaigns.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
questionnaire_id |
string |
Yes | — | Questionnaire UUID |
Returns: { success, questionnaire_id } or error.
Error Handling¶
QML Lifecycle Constraints¶
| Scenario | Response |
|---|---|
| Unpublish with active campaigns | {"error": "Cannot unpublish: questionnaire has active campaign {campaign_id}"} |
| Publish an unsound version | {"error": "...", "issues": [...]} — nothing is pinned |
| Publish to a campaign fielding a different questionnaire | {"error": "Campaign {id} fields questionnaire {other}, not {id}. Use update_campaign_questionnaire to rebind it first."} |
| Already in target project | {"error": "Questionnaire is already in this project"} |
Scoping¶
Every tool resolves questionnaires within your own organization only. An address belonging to another organization is indistinguishable from one that does not exist, and a project_id that disagrees with the questionnaire's own project is refused rather than silently ignored.
Common Errors¶
| Scenario | Response |
|---|---|
| Questionnaire not found | {"error": "Questionnaire {id} not found"} |
| Organization context not set | {"error": "Organization context not set"} |
| Address backs no questionnaire | {"error": "QML document '{name}' not found for this organization"} |
| Save names no usable base | {"success": false, "error": "base_version_unknown", "detail": "..."} |
| Save overlaps a newer save | {"success": false, "error": "overlap", "head_version_id": "...", "head_version_number": n, "conflict_count": n} |
| QML parse error | {"error": "Failed to parse QML: {details}"} |