Skip to content

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_file and finalize_upload take base_version_id, the version_id of 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 is overlap.
  • 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), an overlap, 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_file pins 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.

  1. 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 }

  2. Upload the raw QML bytes to upload_url directly (an HTTP PUT, outside the MCP tool call).

  3. finalize_upload(artifact_id, base_version_id) — saves the uploaded content exactly as save_qml_file would save it on that base: landed, merged with a newer save, or refused. base_version_id is required and means the same as on save_qml_file. Safe to retry with the same artifact_id: after a finalize that saved, or that answered overlap, a retry returns the first answer and writes nothing, whatever base it names.

    Returns: the same envelope as save_qml_file

    An overlap spends 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_unknown and head_kept_moving keep 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}"}