Files
zeekay 1b367e2b62 chore: rebase the tree onto upstream master
The tree was a debranded snapshot of upstream taken around 2026-02-20, and
upstream has moved about five months since. This replaces it with upstream
master, mechanically re-derived: bin/debrand does the rename, and the
pipeline and squashed migrations are restored from the shipped tree.

Not a merge. The previous import replaced the tree wholesale in a single
commit, so there is no shared content history to merge against. Reproducing
the rename on the new tree is what actually converges.

What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints,
compose files, LLM.md, and insights/migrations. The squashed initial
migration is load-bearing — the live database records it by name, so
upstream's ~918 individual migrations cannot come back.

Nine products are dropped as unreferenced. Twenty-one more that we do not run
are kept anyway because the core app still imports them; unpicking those is a
refactor and does not belong in a rebase.

This base does not build yet, and the reasons are not in the rename:
upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against
our 4.2, and the dependency manifests still name upstream packages that we
republish under our own names. Those are decisions, not renames.
2026-08-01 15:44:40 -07:00

1195 lines
75 KiB
JSON

{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"appearance": {
"anyOf": [
{
"properties": {
"allowGoBack": {
"description": "Whether to show a 'Back' button on web surveys after the first question, letting respondents return to a previously visited question. Defaults to false.",
"type": "boolean"
},
"autoDisappear": {
"type": "boolean"
},
"backButtonText": {
"description": "Optional override for the back button label. Defaults to 'Back'.",
"type": "string"
},
"backgroundColor": {
"type": "string"
},
"borderColor": {
"type": "string"
},
"boxPadding": {
"type": "string"
},
"descriptionTextColor": {
"type": "string"
},
"disabledButtonOpacity": {
"type": "string"
},
"displayThankYouMessage": {
"type": "boolean"
},
"fontFamily": {
"type": "string"
},
"maxWidth": {
"type": "string"
},
"placeholder": {
"type": "string"
},
"ratingButtonActiveColor": {
"type": "string"
},
"ratingButtonColor": {
"type": "string"
},
"ratingButtonHoverColor": {
"type": "string"
},
"shuffleQuestions": {
"type": "boolean"
},
"submitButtonColor": {
"type": "string"
},
"submitButtonText": {
"type": "string"
},
"submitButtonTextColor": {
"type": "string"
},
"surveyPopupDelaySeconds": {
"type": "number"
},
"textColor": {
"type": "string"
},
"thankYouMessageCloseButtonText": {
"type": "string"
},
"thankYouMessageDescription": {
"type": "string"
},
"thankYouMessageDescriptionContentType": {
"description": "* `html` - html\n* `text` - text",
"enum": ["html", "text"],
"type": "string"
},
"thankYouMessageHeader": {
"type": "string"
},
"whiteLabel": {
"type": "boolean"
},
"widgetColor": {
"type": "string"
},
"widgetLabel": {
"type": "string"
},
"widgetSelector": {
"type": "string"
},
"widgetType": {
"description": "* `button` - button\n* `tab` - tab\n* `selector` - selector",
"enum": ["button", "tab", "selector"],
"type": "string"
},
"zIndex": {
"type": "string"
}
},
"type": "object"
},
{
"type": "null"
}
],
"description": "Survey appearance customization."
},
"conditions": {
"anyOf": [
{
"properties": {
"deviceTypes": {
"description": "Device types that should match for this survey to be shown.",
"items": {
"description": "* `Desktop` - Desktop\n* `Mobile` - Mobile\n* `Tablet` - Tablet",
"enum": ["Desktop", "Mobile", "Tablet"],
"type": "string"
},
"type": "array"
},
"deviceTypesMatchType": {
"description": "URL/device matching types: 'regex' (matches regex pattern), 'not_regex' (does not match regex pattern), 'exact' (exact string match), 'is_not' (not exact match), 'icontains' (case-insensitive contains), 'not_icontains' (case-insensitive does not contain).\n\n* `regex` - regex\n* `not_regex` - not_regex\n* `exact` - exact\n* `is_not` - is_not\n* `icontains` - icontains\n* `not_icontains` - not_icontains",
"enum": ["regex", "not_regex", "exact", "is_not", "icontains", "not_icontains"],
"type": "string"
},
"events": {
"properties": {
"repeatedActivation": {
"description": "Whether to show the survey every time one of the events is triggered (true), or just once (false).",
"type": "boolean"
},
"values": {
"description": "Array of event names that trigger the survey.",
"items": {
"properties": {
"name": {
"description": "Event name that triggers the survey.",
"type": "string"
}
},
"required": ["name"],
"type": "object"
},
"type": "array"
}
},
"type": "object"
},
"linkedFlagVariant": {
"description": "The variant of the feature flag linked to this survey.",
"type": "string"
},
"seenSurveyWaitPeriodInDays": {
"description": "Don't show this survey to users who saw any survey in the last x days.",
"minimum": 0,
"type": "number"
},
"selector": {
"type": "string"
},
"url": {
"type": "string"
},
"urlMatchType": {
"description": "URL/device matching types: 'regex' (matches regex pattern), 'not_regex' (does not match regex pattern), 'exact' (exact string match), 'is_not' (not exact match), 'icontains' (case-insensitive contains), 'not_icontains' (case-insensitive does not contain).\n\n* `regex` - regex\n* `not_regex` - not_regex\n* `exact` - exact\n* `is_not` - is_not\n* `icontains` - icontains\n* `not_icontains` - not_icontains",
"enum": ["regex", "not_regex", "exact", "is_not", "icontains", "not_icontains"],
"type": "string"
}
},
"type": "object"
},
{
"type": "null"
}
],
"description": "Display and targeting conditions for in-app surveys, such as URL matching, event triggers, device filters, or linked flag variants. Do not use URL, selector, event, device, or linkedFlagVariant conditions for external_survey forms."
},
"description": {
"description": "Survey description.",
"type": "string"
},
"enable_iframe_embedding": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"description": "Allows an external_survey form to be embedded in an iframe. Use only when the user explicitly asks for iframe embedding."
},
"enable_partial_responses": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"description": "When at least one question is answered, the response is stored (true). The response is stored when all questions are answered (false)."
},
"form_content": {
"description": "Optional hosted-form content configuration for external_survey forms. Only include this when the user explicitly asks to customize the hosted form content beyond the standard question flow."
},
"iteration_count": {
"anyOf": [
{
"maximum": 500,
"minimum": 1,
"type": "number"
},
{
"type": "null"
}
],
"description": "For a recurring schedule, this field specifies the number of times the survey should be shown to the user. Use 1 for 'once every X days', higher numbers for multiple repetitions. Works together with iteration_frequency_days to determine the overall survey schedule."
},
"iteration_frequency_days": {
"anyOf": [
{
"maximum": 365,
"minimum": 1,
"type": "number"
},
{
"type": "null"
}
],
"description": "For a recurring schedule, this field specifies the interval in days between each survey instance shown to the user, used alongside iteration_count for precise scheduling."
},
"linked_flag_id": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"description": "Feature flag ID linked to this survey. Use only when the user explicitly wants the survey linked to a feature flag. Resolve the flag ID first, preferably with SQL in v2."
},
"name": {
"description": "Survey name.",
"maxLength": 400,
"minLength": 1,
"type": "string"
},
"questions": {
"anyOf": [
{
"items": {
"anyOf": [
{
"properties": {
"buttonText": {
"description": "Custom button label.",
"type": "string"
},
"description": {
"description": "Optional helper text.",
"type": "string"
},
"descriptionContentType": {
"description": "Format for the description field.\n\n* `text` - text\n* `html` - html",
"enum": ["html", "text"],
"type": "string"
},
"id": {
"description": "Stable question identifier (UUID). When editing an existing question, send back its current id so its responses (keyed by $survey_response_<id>) stay attached; omit it for new questions and the server generates one.",
"type": "string"
},
"optional": {
"description": "Whether respondents may skip this question.",
"type": "boolean"
},
"question": {
"description": "Question text shown to respondents.",
"type": "string"
},
"type": {
"description": "* `open` - open",
"enum": ["open"],
"type": "string"
}
},
"required": ["type", "question"],
"type": "object"
},
{
"properties": {
"buttonText": {
"description": "Custom button label.",
"type": "string"
},
"description": {
"description": "Optional helper text.",
"type": "string"
},
"descriptionContentType": {
"description": "Format for the description field.\n\n* `text` - text\n* `html` - html",
"enum": ["html", "text"],
"type": "string"
},
"id": {
"description": "Stable question identifier (UUID). When editing an existing question, send back its current id so its responses (keyed by $survey_response_<id>) stay attached; omit it for new questions and the server generates one.",
"type": "string"
},
"link": {
"description": "HTTPS or mailto URL for link questions.",
"type": "string"
},
"optional": {
"description": "Whether respondents may skip this question.",
"type": "boolean"
},
"question": {
"description": "Question text shown to respondents.",
"type": "string"
},
"type": {
"description": "* `link` - link",
"enum": ["link"],
"type": "string"
}
},
"required": ["type", "question", "link"],
"type": "object"
},
{
"properties": {
"branching": {
"anyOf": [
{
"anyOf": [
{
"properties": {
"type": {
"description": "Continue to the next question in sequence.\n\n* `next_question` - next_question",
"enum": ["next_question"],
"type": "string"
}
},
"required": ["type"],
"type": "object"
},
{
"properties": {
"type": {
"description": "End the survey.\n\n* `end` - end",
"enum": ["end"],
"type": "string"
}
},
"required": ["type"],
"type": "object"
},
{
"properties": {
"index": {
"description": "0-based index of the next question.",
"minimum": 0,
"type": "number"
},
"type": {
"description": "Jump to a specific question index.\n\n* `specific_question` - specific_question",
"enum": ["specific_question"],
"type": "string"
}
},
"required": ["type", "index"],
"type": "object"
},
{
"properties": {
"responseValues": {
"additionalProperties": {
"anyOf": [
{
"minimum": 0,
"type": "number"
},
{
"enum": ["end"],
"type": "string"
}
]
},
"description": "Response-based branching map. Values can be a question index or 'end'.",
"propertyNames": {
"type": "string"
},
"type": "object"
},
"type": {
"description": "Branch based on the selected or entered response.\n\n* `response_based` - response_based",
"enum": ["response_based"],
"type": "string"
}
},
"required": ["type", "responseValues"],
"type": "object"
}
]
},
{
"type": "null"
}
]
},
"buttonText": {
"description": "Custom button label.",
"type": "string"
},
"description": {
"description": "Optional helper text.",
"type": "string"
},
"descriptionContentType": {
"description": "Format for the description field.\n\n* `text` - text\n* `html` - html",
"enum": ["html", "text"],
"type": "string"
},
"display": {
"description": "Display format: 'number' shows numeric scale, 'emoji' shows emoji scale.\n\n* `number` - number\n* `emoji` - emoji",
"enum": ["number", "emoji"],
"type": "string"
},
"id": {
"description": "Stable question identifier (UUID). When editing an existing question, send back its current id so its responses (keyed by $survey_response_<id>) stay attached; omit it for new questions and the server generates one.",
"type": "string"
},
"lowerBoundLabel": {
"description": "Label for the lowest rating (e.g., 'Very Poor')",
"type": "string"
},
"optional": {
"description": "Whether respondents may skip this question.",
"type": "boolean"
},
"question": {
"description": "Question text shown to respondents.",
"type": "string"
},
"scale": {
"description": "Rating scale can be one of 3, 5, or 7",
"minimum": 1,
"type": "number"
},
"type": {
"description": "* `rating` - rating",
"enum": ["rating"],
"type": "string"
},
"upperBoundLabel": {
"description": "Label for the highest rating (e.g., 'Excellent')",
"type": "string"
}
},
"required": ["type", "question"],
"type": "object"
},
{
"properties": {
"branching": {
"anyOf": [
{
"anyOf": [
{
"properties": {
"type": {
"description": "Continue to the next question in sequence.\n\n* `next_question` - next_question",
"enum": ["next_question"],
"type": "string"
}
},
"required": ["type"],
"type": "object"
},
{
"properties": {
"type": {
"description": "End the survey.\n\n* `end` - end",
"enum": ["end"],
"type": "string"
}
},
"required": ["type"],
"type": "object"
},
{
"properties": {
"index": {
"description": "0-based index of the next question.",
"minimum": 0,
"type": "number"
},
"type": {
"description": "Jump to a specific question index.\n\n* `specific_question` - specific_question",
"enum": ["specific_question"],
"type": "string"
}
},
"required": ["type", "index"],
"type": "object"
},
{
"properties": {
"responseValues": {
"additionalProperties": {
"anyOf": [
{
"minimum": 0,
"type": "number"
},
{
"enum": ["end"],
"type": "string"
}
]
},
"description": "Response-based branching map. Values can be a question index or 'end'.",
"propertyNames": {
"type": "string"
},
"type": "object"
},
"type": {
"description": "Branch based on the selected or entered response.\n\n* `response_based` - response_based",
"enum": ["response_based"],
"type": "string"
}
},
"required": ["type", "responseValues"],
"type": "object"
}
]
},
{
"type": "null"
}
]
},
"buttonText": {
"description": "Custom button label.",
"type": "string"
},
"choices": {
"description": "Array of choice options. Choice indices (0, 1, 2, ...) are used for branching logic.",
"items": {
"type": "string"
},
"maxItems": 20,
"minItems": 2,
"type": "array"
},
"description": {
"description": "Optional helper text.",
"type": "string"
},
"descriptionContentType": {
"description": "Format for the description field.\n\n* `text` - text\n* `html` - html",
"enum": ["html", "text"],
"type": "string"
},
"hasOpenChoice": {
"description": "Whether the final option should be an open-text choice (for example, 'Other').",
"type": "boolean"
},
"id": {
"description": "Stable question identifier (UUID). When editing an existing question, send back its current id so its responses (keyed by $survey_response_<id>) stay attached; omit it for new questions and the server generates one.",
"type": "string"
},
"optional": {
"description": "Whether respondents may skip this question.",
"type": "boolean"
},
"question": {
"description": "Question text shown to respondents.",
"type": "string"
},
"shuffleOptions": {
"description": "Whether to randomize the order of choices for each respondent.",
"type": "boolean"
},
"type": {
"description": "* `single_choice` - single_choice",
"enum": ["single_choice"],
"type": "string"
}
},
"required": ["type", "question", "choices"],
"type": "object"
},
{
"properties": {
"buttonText": {
"description": "Custom button label.",
"type": "string"
},
"choices": {
"description": "Array of choice options. Multiple selections allowed. No branching logic supported.",
"items": {
"type": "string"
},
"maxItems": 20,
"minItems": 2,
"type": "array"
},
"description": {
"description": "Optional helper text.",
"type": "string"
},
"descriptionContentType": {
"description": "Format for the description field.\n\n* `text` - text\n* `html` - html",
"enum": ["html", "text"],
"type": "string"
},
"hasOpenChoice": {
"description": "Whether the final option should be an open-text choice (for example, 'Other').",
"type": "boolean"
},
"id": {
"description": "Stable question identifier (UUID). When editing an existing question, send back its current id so its responses (keyed by $survey_response_<id>) stay attached; omit it for new questions and the server generates one.",
"type": "string"
},
"optional": {
"description": "Whether respondents may skip this question.",
"type": "boolean"
},
"question": {
"description": "Question text shown to respondents.",
"type": "string"
},
"shuffleOptions": {
"description": "Whether to randomize the order of choices for each respondent.",
"type": "boolean"
},
"type": {
"description": "* `multiple_choice` - multiple_choice",
"enum": ["multiple_choice"],
"type": "string"
}
},
"required": ["type", "question", "choices"],
"type": "object"
}
]
},
"type": "array"
},
{
"type": "null"
}
],
"description": "Complete survey question list. Prefer 1-3 questions unless the user explicitly asks for a longer survey. Use rating questions for NPS/CSAT, open for freeform feedback, and choice questions when the user wants structured answers. Questions can include inline translations on each question."
},
"responses_limit": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"description": "The maximum number of responses before automatically stopping the survey."
},
"schedule": {
"anyOf": [
{
"description": "* `once` - once\n* `recurring` - recurring\n* `always` - always",
"enum": ["once", "recurring", "always"],
"type": "string"
},
{
"type": "null"
}
],
"description": "Survey scheduling behavior. Omit this to use the default once behavior. Use recurring only when the user explicitly asks for a repeated schedule."
},
"start_date": {
"anyOf": [
{
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$",
"type": "string"
},
{
"type": "null"
}
],
"description": "Setting this launches the survey immediately. Leave unset unless the user explicitly asks to launch now."
},
"targeting_flag_filters": {
"anyOf": [
{
"properties": {
"aggregation_group_type_index": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"description": "Group type index for group-based feature flags."
},
"early_exit": {
"default": false,
"description": "When true, condition evaluation stops at the first matching condition set rather than continuing to evaluate subsequent groups.",
"type": "boolean"
},
"feature_enrollment": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"description": "Whether this flag has early access feature enrollment enabled. When true, the flag is evaluated against the person property $feature_enrollment/{flag_key}."
},
"groups": {
"description": "Release condition groups for the feature flag.",
"items": {
"properties": {
"aggregation_group_type_index": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"description": "Group type index for this condition set. None means person-level aggregation."
},
"properties": {
"description": "Property conditions for this release condition group.",
"items": {
"anyOf": [
{
"properties": {
"cohort_name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Resolved cohort name for cohort-type filters."
},
"group_type_index": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"description": "Group type index when using group-based filters."
},
"key": {
"description": "Property key used in this feature flag condition.",
"type": "string"
},
"operator": {
"description": "Operator used to compare the property value.\n\n* `exact` - exact\n* `is_not` - is_not\n* `icontains` - icontains\n* `not_icontains` - not_icontains\n* `starts_with` - starts_with\n* `not_starts_with` - not_starts_with\n* `ends_with` - ends_with\n* `not_ends_with` - not_ends_with\n* `regex` - regex\n* `not_regex` - not_regex\n* `gt` - gt\n* `gte` - gte\n* `lt` - lt\n* `lte` - lte",
"enum": [
"exact",
"is_not",
"icontains",
"not_icontains",
"starts_with",
"not_starts_with",
"ends_with",
"not_ends_with",
"regex",
"not_regex",
"gt",
"gte",
"lt",
"lte"
],
"type": "string"
},
"type": {
"description": "Property filter type. Common values are 'person' and 'cohort'.\n\n* `cohort` - cohort\n* `person` - person\n* `group` - group",
"enum": ["cohort", "person", "group"],
"type": "string"
},
"value": {
"description": "Comparison value for the property filter. Supports strings, numbers, booleans, and arrays."
}
},
"required": ["key", "value", "operator"],
"type": "object"
},
{
"properties": {
"cohort_name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Resolved cohort name for cohort-type filters."
},
"group_type_index": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"description": "Group type index when using group-based filters."
},
"key": {
"description": "Property key used in this feature flag condition.",
"type": "string"
},
"operator": {
"description": "Existence operator.\n\n* `is_set` - is_set\n* `is_not_set` - is_not_set",
"enum": ["is_set", "is_not_set"],
"type": "string"
},
"type": {
"description": "Property filter type. Common values are 'person' and 'cohort'.\n\n* `cohort` - cohort\n* `person` - person\n* `group` - group",
"enum": ["cohort", "person", "group"],
"type": "string"
},
"value": {
"description": "Optional value. Runtime behavior determines whether this is ignored."
}
},
"required": ["key", "operator"],
"type": "object"
},
{
"properties": {
"cohort_name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Resolved cohort name for cohort-type filters."
},
"group_type_index": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"description": "Group type index when using group-based filters."
},
"key": {
"description": "Property key used in this feature flag condition.",
"type": "string"
},
"operator": {
"description": "Date comparison operator.\n\n* `is_date_exact` - is_date_exact\n* `is_date_after` - is_date_after\n* `is_date_before` - is_date_before",
"enum": [
"is_date_exact",
"is_date_before",
"is_date_after"
],
"type": "string"
},
"type": {
"description": "Property filter type. Common values are 'person' and 'cohort'.\n\n* `cohort` - cohort\n* `person` - person\n* `group` - group",
"enum": ["cohort", "person", "group"],
"type": "string"
},
"value": {
"description": "Date value in ISO format or relative date expression.",
"type": "string"
}
},
"required": ["key", "operator", "value"],
"type": "object"
},
{
"properties": {
"cohort_name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Resolved cohort name for cohort-type filters."
},
"group_type_index": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"description": "Group type index when using group-based filters."
},
"key": {
"description": "Property key used in this feature flag condition.",
"type": "string"
},
"operator": {
"description": "Semantic version comparison operator.\n\n* `semver_gt` - semver_gt\n* `semver_gte` - semver_gte\n* `semver_lt` - semver_lt\n* `semver_lte` - semver_lte\n* `semver_eq` - semver_eq\n* `semver_neq` - semver_neq\n* `semver_tilde` - semver_tilde\n* `semver_caret` - semver_caret\n* `semver_wildcard` - semver_wildcard",
"enum": [
"semver_gt",
"semver_gte",
"semver_lt",
"semver_lte",
"semver_eq",
"semver_neq",
"semver_tilde",
"semver_caret",
"semver_wildcard"
],
"type": "string"
},
"type": {
"description": "Property filter type. Common values are 'person' and 'cohort'.\n\n* `cohort` - cohort\n* `person` - person\n* `group` - group",
"enum": ["cohort", "person", "group"],
"type": "string"
},
"value": {
"description": "Semantic version string.",
"type": "string"
}
},
"required": ["key", "operator", "value"],
"type": "object"
},
{
"properties": {
"cohort_name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Resolved cohort name for cohort-type filters."
},
"group_type_index": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"description": "Group type index when using group-based filters."
},
"key": {
"description": "Property key used in this feature flag condition.",
"type": "string"
},
"operator": {
"description": "Multi-contains operator.\n\n* `icontains_multi` - icontains_multi\n* `not_icontains_multi` - not_icontains_multi",
"enum": ["icontains_multi", "not_icontains_multi"],
"type": "string"
},
"type": {
"description": "Property filter type. Common values are 'person' and 'cohort'.\n\n* `cohort` - cohort\n* `person` - person\n* `group` - group",
"enum": ["cohort", "person", "group"],
"type": "string"
},
"value": {
"description": "List of strings to evaluate against.",
"items": {
"type": "string"
},
"type": "array"
}
},
"required": ["key", "operator", "value"],
"type": "object"
},
{
"properties": {
"cohort_name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Resolved cohort name for cohort-type filters."
},
"group_type_index": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"description": "Group type index when using group-based filters."
},
"key": {
"description": "Property key used in this feature flag condition.",
"type": "string"
},
"operator": {
"description": "Membership operator for cohort properties.\n\n* `in` - in\n* `not_in` - not_in",
"enum": ["in", "not_in"],
"type": "string"
},
"type": {
"description": "Cohort property type required for in/not_in operators.\n\n* `cohort` - cohort",
"enum": ["cohort"],
"type": "string"
},
"value": {
"description": "Cohort comparison value (single or list, depending on usage)."
}
},
"required": ["key", "type", "operator", "value"],
"type": "object"
},
{
"properties": {
"cohort_name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Resolved cohort name for cohort-type filters."
},
"group_type_index": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"description": "Group type index when using group-based filters."
},
"key": {
"description": "Property key used in this feature flag condition.",
"type": "string"
},
"operator": {
"description": "Operator for feature flag dependency evaluation.\n\n* `flag_evaluates_to` - flag_evaluates_to",
"enum": ["flag_evaluates_to"],
"type": "string"
},
"type": {
"description": "Flag property type required for flag dependency checks.\n\n* `flag` - flag",
"enum": ["flag"],
"type": "string"
},
"value": {
"description": "Value to compare flag evaluation against."
}
},
"required": ["key", "type", "operator", "value"],
"type": "object"
}
]
},
"type": "array"
},
"rollout_percentage": {
"description": "Rollout percentage for this release condition group.",
"type": "number"
},
"variant": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Variant key override for multivariate flags."
}
},
"type": "object"
},
"type": "array"
},
"multivariate": {
"anyOf": [
{
"properties": {
"variants": {
"description": "Variant definitions for multivariate feature flags.",
"items": {
"properties": {
"key": {
"description": "Unique key for this variant.",
"type": "string"
},
"name": {
"description": "Human-readable name for this variant.",
"type": "string"
},
"rollout_percentage": {
"description": "Variant rollout percentage.",
"type": "number"
}
},
"required": ["key", "rollout_percentage"],
"type": "object"
},
"type": "array"
}
},
"required": ["variants"],
"type": "object"
},
{
"type": "null"
}
],
"description": "Multivariate configuration for variant-based rollouts."
},
"payloads": {
"additionalProperties": {
"type": "string"
},
"description": "Optional payload values keyed by variant key.",
"propertyNames": {
"type": "string"
},
"type": "object"
}
},
"type": "object"
},
{
"type": "null"
}
],
"description": "User targeting rules for in-app surveys. Use only when the user wants the survey shown to a subset of users. Do not use this for external_survey forms."
},
"translations": {
"description": "Optional survey-level translations keyed by language code. Use for translated survey name, description, and thank-you message fields. Question text translations belong inside each question's translations object."
},
"type": {
"description": "Survey type. Use popover for most in-app surveys, widget for always-available feedback entrypoints, external_survey for hosted forms with a shareable public URL, and api only for headless custom implementations.",
"enum": ["popover", "widget", "external_survey", "api"],
"type": "string"
}
},
"required": ["name", "type"],
"type": "object"
}