> ## Documentation Index
> Fetch the complete documentation index at: https://docs.expectedparrot.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Humanize schema

> Reference for the humanize schema: a dict that configures how survey questions are presented in web surveys with human respondents.

The **humanize schema** is a Python dictionary of settings for the participant experience: which questions are optional, how answer controls look, and whether to offer comments, timers, or voice interviews. Your Survey still defines question wording, answer choices, and routing. Include only the settings you want to change; the rest use their defaults.

## Make a question optional and offer a comment

This example keeps the rating required, adds a place to explain it, and makes the final free-text question optional. Settings under `questions` use the question names from the Survey:

```python theme={null}
from edsl import QuestionFreeText, QuestionMultipleChoice, Survey

survey = Survey([
    QuestionMultipleChoice(
        question_name="rating", question_text="How was your experience?",
        question_options=["Good", "OK", "Bad"],
    ),
    QuestionFreeText(question_name="feedback", question_text="What should improve?"),
])
humanize_schema = {
    "questions": {
        "rating": {"comment": {"label": "What led to your rating?"}},
        "feedback": {"optional": True},
    }
}
preview_url = survey.preview(humanize_schema=humanize_schema)
preview_url
```

Previewing requires [Expected Parrot access](https://docs.expectedparrot.com/en/latest/api_keys.md). Pass the same `humanize_schema` to `survey.humanize(...)` when you publish. The rating and optional feedback remain answer columns; the accompanying explanation is recorded as `comment.rating_comment`. Use the field reference below for additional settings, or [Custom CSS](https://docs.expectedparrot.com/en/latest/custom_css.md) for colors and spacing.

## Fields

**questions** `dict`

Map of question name (string) -> `HumanizeQuestionSchema`.

**survey** `dict`

Optional survey-level options (e.g. custom CSS).

**properties**

**survey.presentation** `string` (default: question)

`"question"` requests question-by-question presentation (the default); `"group"` requests one question group per page. Define groups on the survey with `add_question_group()` and include every question you want displayed. See [Grouped surveys](https://docs.expectedparrot.com/en/latest/humanize_groups.md) for setup and branching between pages. Question time limits (`time_limit`) are ignored under `"group"`.

**survey.custom\_css** `string`

Optional custom CSS for the survey. Can be `null` or omitted. See [Custom CSS](https://docs.expectedparrot.com/en/latest/custom_css.md) for the `edsl-*` CSS classes and HTML structure per question type.

**survey.login\_required** `boolean` (default: false)

Require respondents to sign in to an Expected Parrot account before taking the survey. Logged-out visitors are sent to sign in and returned to the survey afterward. When enabled, completed-response results include the account username as `agent.expectedparrot_username`. Use this only when account identification is appropriate for the study, and tell respondents how their username will be used. Prolific participants must sign in too. A survey that requires sign-in can't be taken by an AI agent through its agent link, and its preview links also need sign-in: any signed-in Expected Parrot account can open them.

**survey.progress** `dict`

How the respondent is shown their position in the survey. Omit to keep the progress bar, which is what surveys render by default.

**properties**

**type** `string` (default: bar)

Which progress indicator to render.

* `"bar"` — a filled bar, optionally labelled (default).
* `"hidden"` — no progress indicator anywhere in the survey.
* `"steps"` — markers joined by a connector, one per step, with the current step highlighted.

**label** `dict`

**`bar` only.** The label beneath the bar. Set to `null` to keep the bar and drop the label.

**properties**

**type** `string` (default: percent)

Must be `"percent"` — the share of the survey completed, e.g. `"33%"`.

**marker** `string` (default: number)

**`steps` only.** Whether each marker carries its step number (`"number"`) or is a bare dot (`"dot"`).

**steps** `array` *required*

**`steps` only.** The steps, in order. At least two are required.

A step is a *boundary*, not a bucket: it covers every survey item from the end of the previous step through its `complete_after`. Questions can be added or removed inside a step without touching this config, and items a respondent skips past inside a step don't move the marker.

Each step is an object with:

* `label` (string, optional) — the word under the marker, 1–60 characters. Omit or set to `null` to render the marker alone.
* `complete_after` (string, optional) — the name of the survey item (question or instruction) after which this step is complete. Only the final step may omit it, which runs that step to the end of the survey; every earlier step must name an item. The same item must not be named by two steps.

**survey.branding** `dict`

The author's brand on the respondent page. Omit or set to `null` for no banner, which is how a survey without this field renders.

**properties**

**logo** `dict`

The logo drawn in the survey banner, above the questions. Set to `null` to remove it.

Upload the image first, with `Coop().upload_human_survey_asset("lab_logo.png")` or `ep humanize assets upload lab_logo.png`, and reference the uuid it returns. A schema never carries image bytes.

There is no size field: the banner picks a default height. Style `.edsl-logo` in `custom_css` to change it. See [Custom CSS](https://docs.expectedparrot.com/en/latest/custom_css.md).

**properties**

**source** `dict` *required*

Where the image comes from. Must be `{"type": "asset", "asset_uuid": "<uuid>"}`, naming an image in your asset library.

**alt** `string` *required*

Alt text for screen readers, up to 200 characters. Required, so that leaving it out is a decision rather than an accident. Use an empty string to mark the logo decorative, which renders it with `alt=""`.

**position** `string` (default: left)

Where the logo sits in the banner: `"left"`, `"center"`, or `"right"`.

**survey.assistant** `dict`

An AI chat beside the survey that explains questions and fills in answers from what the respondent tells it. Omit for no assistant. See [Survey assistant](https://docs.expectedparrot.com/en/latest/humanize_assistant.md) for its fields and behavior.

## HumanizeQuestionSchema (per question)

Each `questions[question_name]` value is a `HumanizeQuestionSchema` object.

**optional** `boolean` (default: false)

Whether the question is optional. Default is `false` when omitted.

**Supported question types:** `free_text`, `budget`, `checkbox`, `checkbox_with_other`, `dropdown`, `edit`, `email`, `file_upload`, `interview`, `likert_five`, `linear_scale`, `list`, `matrix`, `multiple_choice`, `multiple_choice_with_other`, `numerical`, `rank`, `top_k`, `url`, `yes_no`.

Note: `optional` currently has no effect for `interview` and `rank`.

In Humanize, `QuestionDropdown` is a searchable combo box: respondents enter terms, choose from up to `top_k` ranked matches (5 by default), and the choice fills the input. Respondents must choose a listed option, and the answer is that option's text.

In Humanize, `QuestionDemand` shows a demand curve: for each price, respondents set a quantity with a slider or by typing a number, and the curve redraws as they go. Quantities can't be negative, and the answer is one quantity per price, in price order. `demand` doesn't support `optional`.

In Humanize, `QuestionDict` shows one field per key in `answer_keys`, labelled with the key and its value type, with its entry from `value_descriptions` underneath. `int` and `float` keys take a number, `bool` keys take Yes or No, list keys (`list`, `list[str]`, `list[int]`, `list[float]`, `list[bool]`) take one item per line, with `list[bool]` items written as `true` or `false`, and any other key takes text. The answer has exactly the question's keys. `dict` doesn't support `optional`.

In Humanize, `QuestionURL` is a one-line text box. Respondents can leave off the scheme: `example.com` is accepted and recorded as `https://example.com`, so the answer is always an absolute URL, as EDSL requires.

In Humanize, `QuestionEmail` is a one-line email box. Spaces around the address are dropped, and the answer must be a valid email address, as EDSL requires.

In Humanize, `QuestionEdit` shows `text_to_edit` in an editable text box, with a tracked-changes preview underneath: deleted text is struck through in red and inserted text is green. The answer is the full revised text.

**time\_limit** `dict`

A limit on how long the respondent may spend answering the question. Omit or set to `null` for no limit.

While the limit runs, a countdown is shown above the question. When it reaches zero the answer locks: the respondent can no longer change it and clicks **Next** to continue. They are not moved on automatically. Whatever they had entered is submitted as their answer, and a question they had not answered is recorded as unanswered, even if it is not `optional`.

A time limit applies only while the question is alone on its page. When `survey.presentation` is `"group"`, every question's `time_limit` is ignored, including a question that is the only one in its group. The countdown runs in the respondent's browser, so reloading the page starts it again.

**Supported question types:** `free_text`, `budget`, `checkbox`, `checkbox_with_other`, `demand`, `dict`, `dropdown`, `edit`, `email`, `file_upload`, `likert_five`, `linear_scale`, `list`, `matrix`, `multiple_choice`, `multiple_choice_with_other`, `numerical`, `rank`, `top_k`, `url`, `yes_no`.

**properties**

**duration** `dict` *required*

How long the respondent has. Must be `{"type": "fixed", "seconds": <seconds>}`, the same number of seconds for every respondent. `seconds` must be between 30 and 7200 (two hours). `type` is required, even though `"fixed"` is its only value.

**format** `dict`

Display format. Varies by question type:

* **MC-style** (`likert_five`, `linear_scale`, `multiple_choice`, `yes_no`): `"radio"` (list of radio buttons) or `"dropdown"` (single `<select>`). Default is `"radio"`. Use `{"type": "radio"}` or `{"type": "dropdown"}`.
* **Numerical** (`numerical`): `"input"` (number input field) or `"slider"` (range slider). Default is `"input"`. Use `{"type": "input"}` or `{"type": "slider", "min": 0, "max": 100, "step": 1}`. For slider, `min` must be less than `max`, `step` must be positive and not exceed `(max - min)`.
* **Matrix** (`matrix`): `"table"` (the whole grid at once) or `"carousel"` (one item at a time). Default is `"table"`. Use `{"type": "table"}` or `{"type": "carousel"}`.

`dropdown` questions always use their searchable option picker and do not support `format`. Other question types do not support `format`.

**matrix carousel properties**

**advance\_on\_select** `boolean` (default: true)

**`carousel` only.** Whether answering an item moves to the next one on its own, rather than waiting for the respondent to click Next.

Every answer advances, corrections included — the same rule respondents will have met in Qualtrics. Set to `false` when respondents are expected to revise as they go, since under `true` a correction costs the way back as well.

**preselection** `dict`

**Matrix only** (`matrix`). Cells checked before the respondent touches the grid. Omit or set to `null` for an empty grid, which is what matrices render by default.

Use preselection when respondents should review and change a supplied starting answer. A row they leave untouched is recorded as the preselected option, so the resulting answer alone does not distinguish accepting that value from leaving the row unexamined. Omit preselection when each row should require a fresh choice.

**properties**

**rules** `array` *required*

The rules, in order. At least one is required.

Rules are tried in order and **the first match wins**, so the list reads like a routing table: the specific rows first, the catch-all last. A rule that matches every item must therefore be last — anything behind it could never be reached, and a list in the wrong order raises a validation error rather than silently dropping the rules it shadows.

Each rule is an object with:

* `match` (dict, required) — which rows the rule fills. Must be `{"type": "all"}`, every row of the grid.
* `option` (dict, required) — which column those rows are set to. Must be `{"type": "text", "value": "<option text>"}`, naming an option by its exact text in the question's `question_options`. Matching is literal, so surrounding whitespace is not trimmed for you.

**If the option you name is not in the list as served, the rule fills nothing.** It does not raise, and the survey still runs. A matrix's columns can be piped from an earlier answer, so the text you name may not exist until the question reaches the respondent — and a preselection must not be the thing that breaks a survey in front of them. Where the columns are a literal list, a name that matches none of them is caught when the survey is saved, while you can still fix it.

Those rows are then left empty; they do not fall through to the next rule. A row belongs to the first rule that matches it whether or not that rule finds its option, so a row singled out by an early rule cannot quietly pick up the catch-all that rule was written to override.

**custom\_validation** `dict`

**Multiple choice only** (`multiple_choice`). Optional custom validation rules.

**properties**

**select\_exact\_answer** `string`

If set, the human survey will require the respondent to select this exact option. Use for attention checks or forcing a particular choice. Omit or set to `null` to allow any selection.

**select\_all** `dict`

**Checkbox only** (`checkbox`; not `checkbox_with_other`, which shows no such box). The Select all box beneath the options: ticking it selects every option, unticking it clears them. Omit it to keep the box, which every checkbox question shows by default, or set it to `null` to remove it.

Exclusive options are left out of "all", since checking one clears every other selection.

**properties**

**label** `string`

The box's wording. `"Select all"` is the only accepted value, so setting it changes nothing.

**exclusive\_options** `array`

**Checkbox only** (`checkbox`, `checkbox_with_other`). Options that stand alone: checking one clears every other selection — including any other exclusive option, and for `checkbox_with_other` the respondent's "other" entries — and selecting anything else clears it. Use for options like `"None of the above"` or `"Prefer not to say"`.

Each entry must be the exact text of an option in the question's `question_options` — matching is literal, so surrounding whitespace is not trimmed for you. Entries must be non-empty and unique. Because an exclusive option is an ordinary member of the option list, the submitted answer looks like any other selection. Defaults to an empty list, meaning no option is exclusive.

**initial\_distribution** `dict`

**Distribution only** (`distribution`). What the chart shows before the respondent touches it. Respondents answer a distribution question by painting probability across its outcomes or bins; the percentages always add up to 100%.

In the live survey, respondents can also choose **Reset to spiked** to put all probability on the first outcome, or **Reset to uniform** to spread it equally across outcomes. **Reset to start** returns to the configured `initial_distribution`. These are respondent controls, not additional schema settings.

**properties**

**type** `string` (default: uniform)

Which starting state to show. Required, even though `"uniform"` is the default when `initial_distribution` is omitted.

* `"uniform"` — equal probability for every outcome or bin (default). This is equal probability *per bin*, not per unit of width, so bins of unequal width still start equal. It is the answer's starting value: a respondent who agrees with it can click **Next** without painting.
* `"empty"` — nothing allocated. The respondent must paint at least one bar before they can continue.

**distribution\_summary** `dict`

**Distribution only** (`distribution`). Statistics implied by the respondent's distribution, shown beneath the chart and updated as they paint. Omit or set to `null` for no summary, which is the default.

The statistics assume probability is spread evenly within each bin, and the chart says so. They appear only for finite numeric bins: categories have no values to average, and an open-ended bin such as `[10,Inf)` has no midpoint. They are a display aid and do not change the submitted answer.

**properties**

**statistics** `array`

Which statistics to show, in the order to show them. Each entry is an object with a `type`:

* `{"type": "mean"}` — the implied mean: each bin's midpoint weighted by its probability.
* `{"type": "variance"}` — the implied variance, including each bin's own spread (width² / 12), in squared units of the bins.

Must be non-empty (omit `distribution_summary` instead of passing an empty list) and must not repeat a type. Defaults to mean, then variance.

**interview\_mode** `string` (default: text)

**Interview only** (`interview`). Controls the input mode offered to respondents.

* `"text"` — text-based interview (default).
* `"voice"` — voice-based interview.
* `"both"` — respondent can choose between text and voice.

**intro\_screen** `dict`

**Interview only** (`interview`). The intro screen shown to the respondent before the interview begins. The intro screen is always shown; this field customizes its text. Omit or set to `null` to use the default text (`"This will be a conversation with an AI agent."`).

**properties**

**type** `string` (default: default)

Must be `"default"`.

**instruction** `string` *required*

The intro screen body text. Must be between 1 and 2500 characters.

**voice\_interview\_config** `dict`

**Interview only** (`interview`). Optional configuration for voice-mode interviews. Ignored when `interview_mode` is `"text"`.

**properties**

**language** `string` (default: english)

The spoken language for the voice interview. A lowercase language id from the supported set below. Case and surrounding whitespace are normalized (e.g. `"  French "` becomes `"french"`); omit or set to `null` to use the default (`"english"`). An unsupported value raises a validation error.

**Supported languages:** `english`, `french`, `german`, `spanish`, `portuguese`, `chinese`, `japanese`, `hindi`, `italian`, `korean`, `dutch`, `polish`, `russian`, `swedish`, `turkish`, `tagalog`, `bulgarian`, `romanian`, `arabic`, `czech`, `greek`, `finnish`, `croatian`, `malay`, `slovak`, `danish`, `tamil`, `ukrainian`, `hungarian`, `norwegian`, `vietnamese`, `bangla`, `thai`, `hebrew`, `georgian`, `indonesian`, `telugu`, `gujarati`, `kannada`, `malayalam`, `marathi`, `punjabi`, `multilingual`.

**turn\_taking** `dict`

How the interviewer decides the respondent has finished speaking. Omit to use the default (`{"initial_speed": "default"}`).

**properties**

**initial\_speed** `string` (default: default)

How long the interviewer waits after the respondent pauses before it speaks, at the start of the call. The respondent can change it from the interview screen.

* `"fastest"` — jumps in almost as soon as the respondent pauses.
* `"faster"`
* `"default"` — a natural pause (default).
* `"slower"`
* `"slowest"` — waits a long time, for respondents who pause mid-thought.

**text\_interview\_config** `dict`

**Interview only** (`interview`). Optional configuration for text-mode interviews. Ignored when `interview_mode` is `"voice"`.

**properties**

**language** `string` (default: english)

The language for the text interview. A lowercase language id from the supported set below. Case and surrounding whitespace are normalized (e.g. `"  French "` becomes `"french"`); omit or set to `null` to use the default (`"english"`). An unsupported value raises a validation error.

**Supported languages:** `english`, `french`, `german`, `spanish`, `portuguese`, `chinese`, `japanese`, `hindi`, `italian`, `korean`, `dutch`, `polish`, `russian`, `swedish`, `turkish`, `tagalog`, `bulgarian`, `romanian`, `arabic`, `czech`, `greek`, `finnish`, `croatian`, `malay`, `slovak`, `danish`, `tamil`, `ukrainian`, `hungarian`, `norwegian`, `vietnamese`, `bangla`, `thai`, `hebrew`, `georgian`, `indonesian`, `telugu`, `gujarati`, `kannada`, `malayalam`, `marathi`, `punjabi`, `multilingual`.

**interviewer\_name** `string`

Label shown above the interviewer's messages. Must be between 1 and 200 characters. Defaults to `"AI Interviewer"`. Omit or set to `null` to use the default.

**end\_interview\_message** `string`

Message shown to respondents when the interview ends. Must be between 1 and 2500 characters. Defaults to `"Thank you for your time. This interview has been ended. Your responses have been recorded and will be valuable for our research."` Omit or set to `null` to use the default.

**checklist** `dict`

A checklist of items the interviewer works through during the interview. Omit or set to `null` to run without a checklist.

**properties**

**initial** `dict`

The starting set of checklist items, written by the survey author. Defaults to an empty list.

**properties**

**type** `string` (default: manual)

Must be `"manual"`.

**items** `array` (default: \[])

The list of checklist items. Each item is an object with:

* `id` (string, required) — a unique identifier, 1–64 characters, restricted to letters, digits, underscores, and hyphens (regex pattern `^[A-Za-z0-9_\-]+$`). Item ids must be unique within the checklist.
* `label` (string, required) — participant-facing text shown in the checklist UI, 1–200 characters.
* `instructions` (string, required) — model-facing text describing the condition under which the model should check the item off, 1–2000 characters.

**participant\_visibility** `string` (default: visible)

Whether the participant sees the checklist. The model always sees it (it's in the system prompt) regardless of this setting.

* `"visible"` — the floating checklist panel is shown to the participant during the interview (default).
* `"hidden"` — the participant never sees the checklist; it acts as a pure interviewer instrument (status is still tracked internally).

**structured\_questions** `dict`

Lets the interviewer ask multiple-choice and numerical questions mid-interview, rendered as input widgets rather than prose. Omit or set to `null` to disable structured questions.

**properties**

**allowed\_types** `array`

Which question types the model may use. Each entry must be `"multiple_choice"` or `"numerical"`. Defaults to both. Narrowing this restricts the model to the listed types. Must be non-empty (omit `structured_questions` instead of passing an empty list) and must not contain duplicates.

**end\_policy** `dict`

Controls how the interview is allowed to end. Omit to use the default (`{"control": "respondent"}`).

**properties**

**control** `string` (default: respondent)

Who decides when the interview is over.

* `"respondent"` — the participant can end at any time; the End Interview button is always available (default). No other fields apply.
* `"interviewer_gated"` — the End Interview button is hidden until the model has signalled that its goals are met; only then can the participant end. This mode also accepts `interview_marked_complete_message` and `participant_chat_after_complete`.

**interview\_marked\_complete\_message** `dict`

**`interviewer_gated` only.** A message to surface on the turn the interviewer first marks the interview complete. If omitted or set to `null`, the LLM will generate text for that turn instead.

**properties**

**message** `string` *required*

The message text. Must be between 1 and 2500 characters.

**method** `string` (default: replace)

How `message` relates to the model's text on the marking turn.

* `"replace"` — show this `message` for that one turn instead of using a model to generate text.

**participant\_chat\_after\_complete** `string` (default: open)

**`interviewer_gated` only.** Whether the participant can keep sending messages after the interviewer has marked the interview complete.

* `"open"` — the message composer stays live; marking the interview complete only unlocks the End Interview button (default).
* `"locked"` — the composer closes as well, so the marking turn is the last thing the participant reads and ending the interview is their only remaining action.

**participant\_chat\_during\_reply** `string` (default: open)

Whether the participant can type while the interviewer's reply is still being generated.

* `"open"` — the message composer stays live, so the participant can compose ahead of a reply they have not read yet (default).
* `"locked"` — the composer closes until the reply arrives, so each reply is read before anything is written against it.

Unlike `end_policy.participant_chat_after_complete`, this lock is per-turn and reopens; it applies to every turn under either end policy.

**comment** `dict`

Optional comment input shown with the question.
Submitted comment text appears in survey results under `comment.{question_name}_comment`.

**Supported question types:** `free_text`, `budget`, `checkbox`, `checkbox_with_other`, `demand`, `dict`, `dropdown`, `edit`, `email`, `likert_five`, `linear_scale`, `list`, `matrix`, `multiple_choice`, `multiple_choice_with_other`, `numerical`, `rank`, `top_k`, `url`, `yes_no`.

**properties**

**label** `string` *required*

Label text displayed for the comment field.

**submitting\_indicator** `dict`

Optional indicator shown after the respondent clicks **Next**, while the following question runs. Omit or set to `null` for no indicator.

**Supported question types:** `free_text`, `budget`, `checkbox`, `checkbox_with_other`, `demand`, `dict`, `dropdown`, `edit`, `email`, `file_upload`, `likert_five`, `linear_scale`, `list`, `matrix`, `multiple_choice`, `multiple_choice_with_other`, `numerical`, `rank`, `top_k`, `url`, `yes_no`.

Note: not supported for `compute` or `image_generation` (both run in the background and auto-advance, so neither is ever the question a respondent submits), or for `interview`.

**properties**

**type** `string` (default: callout)

How the indicator is rendered. Must be `"callout"` — a callout box shown while the next question runs, with the subtitle `"We'll continue automatically when it's ready."`

**title** `string`

Optional title for the callout, 1–200 characters. Omit or set to `null` to use the frontend's default label for the indicator type.

**javascript** `dict`

Optional JavaScript that runs on the respondent's page for this question. Omit or set to `null` to run no script. See [Custom JavaScript](https://docs.expectedparrot.com/en/latest/custom_javascript.md) for how to write a hook, what a script may do, and how to retrieve the events it logs.

**Supported question types:** `free_text`, `budget`, `checkbox`, `checkbox_with_other`, `demand`, `dict`, `distribution`, `dropdown`, `edit`, `email`, `file_upload`, `interview`, `likert_five`, `linear_scale`, `list`, `matrix`, `multiple_choice`, `multiple_choice_with_other`, `numerical`, `rank`, `top_k`, `url`, `yes_no`, `survey_message`.

Note: not supported for `compute` or `image_generation`. Both run in the background and are dropped from the page, so there is no rendered question for a script to attach to.

This feature is available on approved accounts only. Write to [info@expectedparrot.com](mailto:info@expectedparrot.com) to request approval; until your account is approved, a schema carrying a hook is rejected when you push it.

**properties**

**hooks** `dict`

Maps a lifecycle event to the JavaScript source that runs on it. An unrecognized event name raises a validation error rather than being stored and silently never run.

Supported events:

* `"question.ready"` — fires once the question has been rendered and its inputs are in the DOM.

Each hook's source must be at most 64,000 characters, and all hooks on one question at most 256,000 characters in total.

## Example

The fuller example below combines a rating with comments, a probability distribution over waiting times, optional feedback, and a lab logo. The logo upload requires a local image and account access; use the simpler example above if you only need question settings.

```python theme={null}
from edsl import (
    Coop,
    Survey,
    QuestionDistribution,
    QuestionFreeText,
    QuestionMultipleChoice,
)

# Upload the logo once; the schema references it by uuid
logo = Coop().upload_human_survey_asset("lab_logo.png")

survey = Survey([
    QuestionMultipleChoice(
        question_name="rating",
        question_text="Rate your experience.",
        question_options=["Good", "OK", "Bad"],
    ),
    QuestionDistribution(
        question_name="wait",
        question_text="How many minutes do you expect to wait next time?",
        min_value=0, max_value=30, bucket_size=5,
    ),
    QuestionFreeText(
        question_name="feedback",
        question_text="Any additional feedback you'd like to share?",
    ),
])

humanize_schema = {
    "questions": {
        "rating": {
            "optional": False,
            "format": {"type": "dropdown"},
            "comment": {"label": "Why did you choose this rating?"},
            "submitting_indicator": {"type": "callout", "title": "Analyzing your answer…"},
        },
        "wait": {
            "initial_distribution": {"type": "empty"},
            "distribution_summary": {"statistics": [{"type": "mean"}]},
        },
        "feedback": {
            "optional": True,
            "time_limit": {"duration": {"type": "fixed", "seconds": 300}},
        },
    },
    "survey": {
        "custom_css": None,
        "progress": {"type": "bar", "label": {"type": "percent"}},
        "branding": {
            "logo": {
                "source": {"type": "asset", "asset_uuid": logo.uuid},
                "alt": "Acme Research logo",
                "position": "center",
            }
        },
    },
}

# Validation runs before creating the human survey
survey.humanize(humanize_schema=humanize_schema)
```

## Retrieving a deployed schema

To read back the schema stored on a survey you already deployed:

```python theme={null}
from edsl import Coop

schema = Coop().get_human_survey_humanize_schema("your-human-survey-uuid")
```

It returns the schema as stored, or `None` if the survey was created without one.

In Python, use `Coop().patch_human_survey_humanize_schema(human_survey_uuid, partial_schema=changes)` to update an existing survey. Nested dictionaries are merged by key; lists, scalars, and explicit `None` replace the existing value. The complete merged schema is validated. Review the changed fields before updating a live instrument.

From the CLI, `ep humanize schema get <uuid>` prints it, and `--out schema.json`
writes it to a file you can edit and send back with `ep humanize schema patch`.
Give `--out` a `.json.gz` suffix to write the schema gzipped; any other suffix
writes plain JSON. Both are readable by the commands that take `--schema`.

## Validation

If the schema is invalid, Expected Parrot raises `HumanizeSchemaValidationError`. Common causes:

* A key in `questions` is not a question name in the survey, or is an instruction.
* A question's type is not supported for humanize schema (e.g. `extract`).
* A question's entry has the wrong shape for its type (e.g. wrong field types or extra fields that aren’t allowed).
* A `time_limit` has `seconds` outside 30–7200, or a `duration` without its `"type": "fixed"`.
* Top-level structure is invalid (e.g. `questions` not a dict, or an entry not a dict).

You can also pass the schema to `survey.preview(humanize_schema=...)` to get a preview URL.

Ensure your humanize schema matches the parameters above for each question type in your survey.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.