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.
Two rules follow from this, and they explain most of the behaviour below:
- Saving never refuses. A sketch, a pasted block, or half-finished logic must be storable, so
save_qml_filerecords the formal-validation verdict on the version it just wrote and returns any problems aswarnings. - 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. 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 |
Yes | — | Display name |
content |
string |
Yes | — | The QML YAML, stored as version 1 |
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 |
allow_adhoc |
boolean |
No | true |
Whether ad-hoc (campaign-less) surveys may use it |
Returns: Created questionnaire object.
Creation is not gated on formal validation — content may be structurally broken. The verdict is recorded, not enforced; publishing to a campaign is what requires soundness.
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 }
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 } for content up to 32 KB.
Above 32 KB the content isn't inlined — instead the response is { success, qml_name, download_url, size_bytes, note }, where download_url is a short-lived link (reusable until it expires) you fetch directly over HTTPS (no further authentication needed). Call get_qml_content again if the link has expired.
save_qml_file¶
Save QML content as an existing questionnaire's new version, and move its head to that version. 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 |
project_id |
string |
No | — | When given, must match the questionnaire's project; a mismatch is refused rather than ignored |
content |
string |
Yes | — | QML YAML content, up to 32 KB |
Returns: { success, qml_name, questionnaire_id, version_id, version_number, bytes_written, logic_validated, warnings? } — warnings is absent on a clean save, or a list of parse/schema issues on a saved-but-imperfect draft.
Formal validation is re-run against the new content on every save and its verdict is recorded on the version that lands, so a version's soundness stays answerable later even after further edits. 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)— saves the uploaded content as the questionnaire's new version, with exactly the same validation and never-blocks behaviour assave_qml_file. Safe to retry with the sameartifact_id.Returns: the same envelope as
save_qml_file
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) |
vacuous_postcondition |
warning | Postcondition is vacuously true (unreachable item) |
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). 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"} |
| QML parse error | {"error": "Failed to parse QML: {details}"} |