$3e
For dropdown (and multi-dropdown) questionnaire variables that map to global metadata (KeyPointer), the backend requires every stored choice value on the questionnaire side to exist as an option value on the global metadata KeyPointer — not merely the same label.
Product constraint (called out in incidents): In Metadata Manager, editing a dropdown option today can effectively change labels while leaving older values in place, reportedly for backward-compatibility reasons. Workflow Manager then validates the full questionnaire against global metadata and fails when QuestionVariable / FrozenQuestionVariable choices (values) are not a subset of KeyPointer.format_option.options[].value.
Confirmed backend error shape (API / validation):
The following choices are not present in the global metadata with name {label}: [...]
(Also: Global metadata with name {label} does not have any choices present if options are empty.)
Code (source of truth for the rule):
questionnaire_v2/utils/question_variable_and_global_metadata_validations.py — validate_dropdown_options_for_question_variable compares question_variable.choices to the set of option["value"] from global metadata format_option.options.questionnaire_v2/question_variable/domain/use_cases/validate_question_variable_with_global_metadata_use_case.py — wires validation for DROPDOWN / MULTI_DROPDOWN.Not the same as:
Use when:
KeyPointer.format_option JSON shows labels matching new copy while values still hold legacy strings.ContractKeyPointer rows still use the legacy option values:
KeyPointer, ContractKeyPointer, and QuestionVariable / FrozenQuestionVariable, then resync affected contracts.KeyPointer and questionnaire variables to the intended full option set without CKP migration.Do not use when:
Capture identifiers from the ticket or admin:
{workspace_id}, cluster (api.{us|in|eu}.spotdraft.com), workflow / template / questionnaire links if provided.KeyPointer id or search by label / template_field_name (question variable name).Inspect global metadata (KeyPointer) — Django admin (replace cluster):
https://api.{cluster}.spotdraft.com/admin/historic_contracts/keypointer/?created_by_workspace_id={workspace_id}&q={search_term}https://api.{cluster}.spotdraft.com/admin/historic_contracts/keypointer/{key_pointer_id}/change/format_option, confirm each option: for dropdowns, value is what validation uses; compare to the labels the customer believes they “renamed.”Inspect questionnaire variables for the same logical field:
QuestionVariable / FrozenQuestionVariable by workspace + name (template_field_name / variable name). Check choices list vs KeyPointer option values. Mismatch confirms the failure mode.Check contracts using old values before choosing the mitigation path:
ContractKeyPointer rows for that key_pointer_id with value equal to legacy option strings.Optional — BigQuery (Prod US pattern) — inspect key pointer JSON and CKP usage (run one query at a time; adjust dataset for cluster):
SELECT id, label, template_field_name, format_option
FROM `spotdraft-prod.prod_usa_db.public_historic_contracts_keypointer`
WHERE created_by_workspace_id = {workspace_id}
AND id = {key_pointer_id}
SELECT id, contract_id, value
FROM `spotdraft-prod.prod_usa_db.public_contracts_v3_contractkeypointer`
WHERE created_by_workspace_id = {workspace_id}
AND key_pointer_id = {key_pointer_id}
AND value IN ({old_value_1}, {old_value_2}, {old_value_3})
ORDER BY id DESC
LIMIT 100
Use prod_india_db / prod_eu_db / prod_mea_db per region as in other skills.
Incidents of this class have been mitigated with a reviewed Django shell script (dry-run first), not a self-serve product button. There are two supported branches:
Use this when ContractKeyPointer rows still carry the old option values.
VALUE_FIXES map: {old_value: new_value} for options where the value must change to match new labels or canonical strings.KeyPointer: deep-update format_option so each affected option’s value (and usually label) matches the intended pairings.ContractKeyPointer: UPDATE rows where value is still an old token to the new value for the same key_pointer_id / workspace; collect **contract_id**s touched.QuestionVariable and FrozenQuestionVariable: set format_option (full options list) and choices (list of values) so questionnaire definitions match KeyPointer.partially_resync_contracts_task (or equivalent) for affected **contract_id**s and relevant fields (e.g. key_pointer_list) so downstream views stay consistent.Use this when the old values are only present in the metadata / questionnaire definitions and there are no affected ContractKeyPointer rows.
KeyPointer: replace or normalize format_option.options to the full intended list.QuestionVariable: update format_option and choices for the linked variable name.FrozenQuestionVariable: update only if published / frozen questionnaire copies for the same logical field exist and still show stale choices.Operational notes:
dry_run=True first; use a transaction with rollback for dry runs if that is your team’s standard.Same as edit-template-questionnaire-contractdata-split: spotdraft-prod + prod_usa_db / prod_india_db / prod_eu_db / prod_mea_db with public_ table prefix where applicable.
template_field_name and frozen questionnaire IDs.choices) — file/attach logs and link a new ticket; use Groundcover / request logs around the failed save if available.| Item | Link / ref |
|---|---|
| Jira (this pattern) | SPD-42511 — The Global Poverty Project: unable to update the dropdown options in the questionnaire (Mitigated) |
| Jira (same pattern, lighter remediation) | SPD-42680 — Zipline: unable to update the dropdown options in the questionnaire (incident opener linked from Rootly) |
| Slack incident channel | C0AMG6UV4J0 — March 2026; mitigation: backend alignment script across KeyPointer, ContractKeyPointer, QuestionVariable/FrozenQuestionVariable + resync |
| Slack incident channel | C0ANFN57CDA — March 2026; mitigation: no CKP values associated, so script updated KeyPointer.format_option plus QuestionVariable choices to the full option set |
| Similar prior handling | Internal thread referenced in incident: Slack C0AKZ05S19V (same script-style mitigation per engineers) |
Do not treat as live config. From SPD-42511 / incident channel:
sd_fee_installment_schedule.Monthly installments during the Term while values remained e.g. Equal monthly installments during the term — validation failed until values and dependent rows were aligned.ContractKeyPointer rows and resync affected contracts.Do not treat as live config. From SPD-42680 / incident channel:
sd_1st_merchant_location_zipping_point.Yes / No / TBD / Maybe, but the stored options still included mismatched values such as TBD and TBD-11a0eb32-3127-4576-93f0-6c5c97b5ec23.KeyPointer option list and update the linked QuestionVariable choices to the canonical values.historic_contracts_keypointer.ContractKeyPointer model context when bulk-touching metadata per contract.