An open, JSON-based format for defining surveys and forms. Used by [YourOpinion.is](https://youropinion.is) and free for anyone to implement.

**Current version:** `1.0`

## Simplified Markdown Version

Alternatively, there is a simplified Markdown-based import protocol designed mostly for AI agents. This protocol uses a "Rich Markdown" dialect to define questions, options, and page breaks using standard text formatting. Instead of constructing a deep JSON object, an agent generates a structured Markdown string and encodes it into a single clickable URL. Clicking the link reconstructs the survey instantly in the browser.

- **[Simplified Markdown Version](./survey-import-link)**

## Download Specification

- **[JSON Schema](https://youropinion.is/json-schema/1.0)** - Machine-readable format definition
- **[Markdown](./survey-format.md)** - Human-readable documentation (optimized for LLMs)

## Introduction

The Open Survey Format provides a standardized way to define surveys, questionnaires, and forms as JSON objects. This format enables:

- **Portability** - Move surveys between different platforms and tools
- **Version control** - Track survey changes using standard diff tools
- **Programmatic generation** - Create surveys dynamically from code
- **LLM compatibility** - AI assistants can easily create and modify surveys

## Core Concepts

### Collections (Pages)

Surveys are organized into **collections**, which function as pages or sections. Each survey must contain at least one collection.

### Elements

Individual components within a collection, such as:

- Questions (text input, multiple choice, ratings, etc.)
- Content blocks (markdown text, images)
- Control flow elements (conditional logic, page jumps)

### Assets

Reusable components that can be referenced by multiple questions:

- Option lists (for select questions)
- Rating scales
- Shared resources

## Document Structure

A survey is represented as a JSON object with this structure:

```json
{
    "$schema": "https://youropinion.is/json-schema/1.0",
    "$readme": "https://youropinion.is/docs/survey-format.md",
    "collections": {
        "collection-id-1": {
            "name": "Collection Name",
            "elements": {
                "element-id-1": {
                    "type": "Markdown",
                    "data": {
                        /* Element-specific data */
                    }
                }
            },
            "displayOrder": ["element-id-1"]
        }
    },
    "displayOrder": ["collection-id-1"],
    "assets": {
        "asset-id-1": {
            "type": "options",
            "data": {
                /* Asset-specific data */
            }
        }
    }
}
```

### Top-Level Properties

#### `$schema` (optional)

URL to the JSON Schema definition for validation:

```json
"$schema": "https://youropinion.is/json-schema/1.0"
```

#### `$readme` (optional)

URL to human-readable documentation:

```json
"$readme": "https://youropinion.is/docs/survey-format.md"
```

#### `collections` (required)

Object containing survey pages/sections. Each key is a unique collection ID:

```json
"collections": {
    "welcome": {
        "name": "Welcome Page",
        "elements": { /* ... */ },
        "displayOrder": ["intro", "consent"]
    },
    "questions": {
        "name": "Main Questions",
        "elements": { /* ... */ },
        "displayOrder": ["q1", "q2", "q3"]
    }
}
```

**Collection Properties:**

- **`name`** (string, optional) - Human-readable collection name (not displayed to users, used for reference)
- **`condition`** (object, optional) - Conditional logic to determine if this collection should be shown (see [Conditional Logic](#conditional-logic) section)
- **`elements`** (object) - Contains the elements for this collection. Each key is a unique element ID
- **`displayOrder`** (array) - Array of element IDs defining display order

**Important:** The `displayOrder` array acts as both a filter and sequencer:

- Only elements listed in `displayOrder` will be shown
- Elements not in `displayOrder` remain in the definition but are hidden
- Including a non-existent element ID will cause an error

#### `displayOrder` (required)

Array specifying the order of collections:

```json
"displayOrder": ["welcome", "questions", "thank-you"]
```

Same filtering rules apply: only collections listed here will be shown.

#### `assets` (optional)

Reusable components referenced by multiple elements. Assets reduce duplication and make surveys easier to maintain.

**Asset Types:**

1. **`options`** - Reusable option lists for SelectOne/SelectMany questions
2. **`ordinal-scale`** - Reusable labeled scales for OrdinalScale questions
3. **`interval-scale`** - Reusable numeric scales for IntervalScale questions

**Example - Options asset:**

```json
"assets": {
    "color-options": {
        "type": "options",
        "name": "Color Options",
        "data": {
            "options": {
                "red": { "label": "Red" },
                "blue": { "label": "Blue" },
                "green": { "label": "Green" },
                "yellow": { "label": "Yellow" }
            },
            "displayOrder": ["red", "blue", "green", "yellow"]
        }
    },
    "satisfaction-scale": {
        "type": "ordinal-scale",
        "name": "Standard Satisfaction Scale",
        "data": {
            "labels": {
                "1": "Very Dissatisfied",
                "2": "Dissatisfied",
                "3": "Neutral",
                "4": "Satisfied",
                "5": "Very Satisfied"
            }
        }
    },
    "nps-scale": {
        "type": "interval-scale",
        "name": "Net Promoter Score Scale",
        "data": {
            "start": 0,
            "end": 10,
            "labels": {
                "start": "Not likely",
                "end": "Very likely"
            }
        }
    }
}
```

**All asset types require:**

- **`type`** - Asset type: `"options"`, `"ordinal-scale"`, or `"interval-scale"`
- **`name`** - Human-readable name for documentation purposes
- **`data`** - Type-specific data structure

Assets can be referenced using JSON references (see JSON References section).

---

## Element Types

Elements are the building blocks of your survey. Each element has a `type` and `data` property. All elements can optionally include an `extensions` object for custom metadata (not used by the standard renderer).

**Available element types:**

| Type            | Category | Description                                                      |
| --------------- | -------- | ---------------------------------------------------------------- |
| `Markdown`      | Content  | Display formatted text and content                               |
| `FlowControl`   | Control  | Conditional logic and navigation                                 |
| `String`        | Question | Text input (single or multi-line)                                |
| `OpenList`      | Question | List of text entries added by user                               |
| `Number`        | Question | Numeric input                                                    |
| `Date`          | Question | Date picker                                                      |
| `Boolean`       | Question | Yes/No checkbox                                                  |
| `SelectOne`     | Question | Single choice from options (radio buttons)                       |
| `SelectMany`    | Question | Multiple choices from options (checkboxes)                       |
| `Ranking`       | Question | Rank multiple options by importance or preference                |
| `IntervalScale` | Question | Numeric rating scale (e.g., 0-10 for NPS, 1-5 for satisfaction)  |
| `OrdinalScale`  | Question | Labeled rating scale (e.g., Strongly Disagree to Strongly Agree) |
| `Payment`       | Question | Payment processing element                                       |

### Content Elements

#### Markdown

Display formatted text, headings, lists, and other content using Markdown syntax.

```json
{
    "type": "Markdown",
    "data": {
        "markdown": "# Welcome!\n\nThank you for taking our survey.\n\n- Please answer honestly\n- All responses are anonymous"
    }
}
```

**Supported Markdown:**

- Headings (`#`, `##`, `###`)
- Lists (ordered and unordered)
- Bold (`**text**`) and italic (`*text*`)
- Links (`[text](url)`)
- Code blocks

#### FlowControl

Control survey flow with conditional logic and navigation.

```json
{
    "type": "FlowControl",
    "data": {
        "condition": {
            /* Optional - See Conditional Logic section */
        },
        "action": {
            "type": "survey-finish" // Options: "survey-finish" or "page-finish"
        }
    }
}
```

**Action Types:**

- `survey-finish` - Complete the survey immediately
- `page-finish` - End current page and move to next

For detailed information on condition structure and examples, see the [Conditional Logic](#conditional-logic) section below.

---

### Question Types

All questions share these common properties:

```json
{
    "type": "QuestionType",
    "data": {
        "label": "Your question text here",
        "required": "yes", // Optional: "yes", "no", or "suggested" (default is "no")
        "markdown": "Help text" // Optional: additional context in Markdown
    }
}
```

**Common properties:**

- `label` (string, required) - The question text shown to users
- `required` (string, optional) - Whether answer is required: `"yes"`, `"no"`, or `"suggested"` (shows as optional but encouraged)
- `markdown` (string, optional) - Additional help text or description in Markdown format
- `defaultValue` (any, optional) - Pre-filled value for the question

#### String

Single or multi-line text input.

```json
{
    "type": "String",
    "data": {
        "label": "What is your name?",
        "placeholder": "Enter your full name", // Optional
        "multiline": false, // Optional: true for textarea
        "required": "yes"
    }
}
```

#### OpenList

Multiple text entries as a list. Users can add items one by one.

```json
{
    "type": "OpenList",
    "data": {
        "label": "List your top priorities",
        "multiline": false, // Optional: true for paragraph entries
        "minItems": 1, // Optional: minimum number of items
        "maxItems": 5, // Optional: maximum number of items
        "required": "yes"
    }
}
```

**Properties:**

- `multiline` (boolean, optional) - If true, each item is a paragraph (textarea). If false (default), each item is a single line
- `minItems` (number, optional) - Minimum number of items required
- `maxItems` (number, optional) - Maximum number of items allowed

#### Number

Numeric input with optional validation.

```json
{
    "type": "Number",
    "data": {
        "label": "How many employees?",
        "min": 1, // Optional: minimum value
        "max": 1000, // Optional: maximum value
        "step": 1, // Optional: increment size (default: 1)
        "required": "yes"
    }
}
```

**Properties:**

- `min` (number, optional) - Minimum allowed value
- `max` (number, optional) - Maximum allowed value
- `step` (number, optional) - Increment size for input validation (default: 1)

#### Date

Date picker input with optional accuracy level and min/max constraints.

```json
{
    "type": "Date",
    "data": {
        "label": "When did you join?",
        "required": "suggested",
        "accuracy": "day", // Optional: "day", "month", or "year" (default: "day")
        "min": "2020-01-01", // Optional: earliest selectable date
        "max": "2025-12-31" // Optional: latest selectable date
    }
}
```

**Properties:**

- `accuracy` (string, optional) - Date precision level:
    - `"day"` - Full date picker with calendar (default)
    - `"month"` - Month and year dropdown selection
    - `"year"` - Year-only dropdown selection
- `min` (string, optional) - Earliest selectable date. Can be:
    - ISO date string (e.g., `"2020-01-01"`)
    - Relative time string (e.g., `"+ 3 months"`, `"now"`, `"- 1 year"`)
- `max` (string, optional) - Latest selectable date. Same format options as `min`

#### Boolean

Yes/No checkbox. Default value is `false` (unchecked).

```json
{
    "type": "Boolean",
    "data": {
        "label": "Terms and Conditions",
        "description": "I agree to the terms and conditions" // Shown next to checkbox
    }
}
```

#### SelectOne

Multiple choice question (radio buttons) - users select one option.

```json
{
    "type": "SelectOne",
    "data": {
        "label": "What is your favorite color?",
        "required": "yes",
        "options": {
            "options": {
                "red": { "label": "Red" },
                "blue": { "label": "Blue" },
                "green": { "label": "Green" }
            },
            "displayOrder": ["red", "blue", "green"]
        }
    }
}
```

**Using asset references:**

```json
{
    "type": "SelectOne",
    "data": {
        "label": "Pick a color",
        "options": { "$ref": "#/assets/color-options/data" }
    }
}
```

#### SelectMany

Multiple choice question (checkboxes) - users can select multiple options.

```json
{
    "type": "SelectMany",
    "data": {
        "label": "Which operating systems do you use?",
        "required": "yes",
        "options": {
            "options": {
                "windows": { "label": "Windows" },
                "macos": { "label": "macOS" },
                "linux": { "label": "Linux" }
            },
            "displayOrder": ["windows", "macos", "linux"]
        },
        "other": true, // Optional: add "Other" option with text input
        "minSelections": 1, // Optional: minimum number of selections required
        "maxSelections": 3 // Optional: maximum number of selections allowed
    }
}
```

#### Ranking

Rank multiple options by importance, preference, or priority. Users assign scores to each option, typically through comparative methods like MaxDiff, pairwise comparison, drag-and-drop sorting, or budget allocation.

```json
{
    "type": "Ranking",
    "data": {
        "label": "Rank these features by importance",
        "required": "yes",
        "options": {
            "options": {
                "performance": { "label": "Fast performance" },
                "design": { "label": "Clean design" },
                "price": { "label": "Affordable price" },
                "support": { "label": "24/7 customer support" }
            },
            "displayOrder": ["performance", "design", "price", "support"]
        }
    }
}
```

#### IntervalScale

Numeric rating scale with a defined range (e.g., 0-10 for NPS, 1-5 for satisfaction). Commonly used for:

- Net Promoter Score (NPS): 0-10 scale
- Satisfaction ratings: 1-5 or 1-7 scale
- Likelihood questions: 0-10 scale

```json
{
    "type": "IntervalScale",
    "data": {
        "label": "How likely are you to recommend us to a friend?",
        "required": "yes",
        "scale": {
            "start": 0, // Starting number of the scale
            "end": 10, // Ending number of the scale
            "labels": {
                // Labels for start and end points (required)
                "start": "Not at all likely",
                "end": "Extremely likely"
            }
        }
    }
}
```

**Common use cases:**

```json
// Net Promoter Score (NPS)
{
    "type": "IntervalScale",
    "data": {
        "label": "How likely are you to recommend us?",
        "scale": {
            "start": 0,
            "end": 10,
            "labels": {
                "start": "Not likely",
                "end": "Very likely"
            }
        }
    }
}

// 5-point satisfaction scale
{
    "type": "IntervalScale",
    "data": {
        "label": "How satisfied are you with our service?",
        "scale": {
            "start": 1,
            "end": 5,
            "labels": {
                "start": "Very dissatisfied",
                "end": "Very satisfied"
            }
        }
    }
}
```

**Display:** Shows a horizontal scale with clickable numbers. Labels appear below the start and end points.

#### OrdinalScale

Labeled rating scale where each point has a custom label. Ideal for:

- Likert scales with specific wording for each point
- Agreement scales (Strongly Disagree → Strongly Agree)
- Frequency scales (Never → Always)
- Custom rating scales with meaningful labels

```json
{
    "type": "OrdinalScale",
    "data": {
        "label": "How satisfied are you with your position?",
        "required": "yes",
        "scale": {
            "labels": {
                "1": "Very Dissatisfied",
                "2": "Dissatisfied",
                "3": "Neutral",
                "4": "Satisfied",
                "5": "Very Satisfied"
            }
        }
    }
}
```

**Common use cases:**

```json
// Likert agreement scale
{
    "type": "OrdinalScale",
    "data": {
        "label": "The product met my expectations",
        "scale": {
            "labels": {
                "1": "Strongly Disagree",
                "2": "Disagree",
                "3": "Neither Agree nor Disagree",
                "4": "Agree",
                "5": "Strongly Agree"
            }
        }
    }
}

// Frequency scale
{
    "type": "OrdinalScale",
    "data": {
        "label": "How often do you use our product?",
        "scale": {
            "labels": {
                "1": "Never",
                "2": "Rarely",
                "3": "Sometimes",
                "4": "Often",
                "5": "Always"
            }
        }
    }
}
```

**Display:** Shows labeled buttons or options. Each label is displayed fully, making the scale self-explanatory.

**IntervalScale vs OrdinalScale:**

- Use **IntervalScale** when the numbers themselves have meaning (0-10, 1-5)
- Use **OrdinalScale** when you need custom labels for each point
- IntervalScale is more compact; OrdinalScale is more descriptive

#### Payment

Collect payment information and process transactions. Integrates with payment providers configured in your survey settings.

```json
{
    "type": "Payment",
    "data": {
        "label": "Payment",
        "required": "yes",
        "amount": {
            "value": 29.99,
            "currency": "USD"
        },
        "captureMethod": "automatic" // Optional: "immediate", "manual", or "automatic"
    }
}
```

**Properties:**

- `amount` (object, required) - Payment amount with `value` (number) and `currency` (3-letter code, e.g., "USD", "EUR", "GBP")
- `captureMethod` (string, optional) - When to capture payment:
    - `"immediate"` - Capture payment immediately when survey is submitted
    - `"manual"` - Require manual capture through your payment dashboard
    - `"automatic"` - Automatically capture when survey is completed (default)

**Note:** Payment processing requires a payment provider to be configured in your channel settings. Supported currencies and payment methods depend on your provider configuration.

---

## JSON References

Reduce duplication by referencing reusable components. References use this format:

```json
{ "$ref": "[<location>]#<path>" }
```

- **`location`** - URL to source document (empty = current document)
- **`path`** - Path from document root using `/` separators

**Example - Inline duplication:**

```json
{
    "question-1": {
        "type": "SelectOne",
        "data": {
            "label": "Favorite color?",
            "options": {
                "options": {
                    "red": { "label": "Red" },
                    "blue": { "label": "Blue" }
                },
                "displayOrder": ["red", "blue"]
            }
        }
    },
    "question-2": {
        "type": "SelectOne",
        "data": {
            "label": "Least favorite color?",
            "options": {
                "options": {
                    "red": { "label": "Red" },
                    "blue": { "label": "Blue" }
                },
                "displayOrder": ["red", "blue"]
            }
        }
    }
}
```

**Better - Using assets:**

```json
{
    "collections": {
        "main": {
            "elements": {
                "question-1": {
                    "type": "SelectOne",
                    "data": {
                        "label": "Favorite color?",
                        "options": { "$ref": "#/assets/colors/data" }
                    }
                },
                "question-2": {
                    "type": "SelectOne",
                    "data": {
                        "label": "Least favorite color?",
                        "options": { "$ref": "#/assets/colors/data" }
                    }
                }
            },
            "displayOrder": ["question-1", "question-2"]
        }
    },
    "assets": {
        "colors": {
            "type": "options",
            "data": {
                "options": {
                    "red": { "label": "Red" },
                    "blue": { "label": "Blue" }
                },
                "displayOrder": ["red", "blue"]
            }
        }
    }
}
```

**Benefits:**

- Single source of truth
- Easier maintenance
- Better version control diffs
- Smaller file size

---

## Conditional Logic

Conditional logic allows you to create dynamic surveys that adapt based on user responses. Conditions can be used in two ways:

### 1. Collection-Level Conditions

Show or hide entire pages based on conditions. If a collection's condition evaluates to `false`, the entire page is skipped and execution moves to the next collection.

```json
{
    "collections": {
        "follow-up": {
            "name": "Follow-up Questions",
            "condition": {
                "type": "condition",
                "fact": "questions/nps-score",
                "operator": "lt",
                "compare": { "value": 7 }
            }
            "elements": {
                /* ... */
            },
            "displayOrder": ["question-1"]
        }
    }
}
```

In this example, the "follow-up" page only shows if the NPS score is less than 7.

### 2. FlowControl Elements

Control survey flow within a page using `FlowControl` elements. When the condition evaluates to `true`, the specified action is executed.

**Available actions:**

- **`survey-finish`** - Complete the survey immediately (skip all remaining pages)
- **`page-finish`** - End the current page and move to the next (skip remaining elements on current page)

```json
{
    "type": "FlowControl",
    "data": {
        "condition": {
            "type": "condition",
            "fact": "satisfaction",
            "operator": "eq",
            "compare": { "value": "very-satisfied" }
        },
        "action": {
            "type": "page-finish"
        }
    }
}
```

**Note:** If no `condition` is provided, the action always executes when the element is reached.

### Condition Structure

Conditions are defined as a binary tree with two node types:

#### 1. Comparison Conditions

Compare a fact (survey response) against a value or another fact.

```json
{
    "type": "condition",
    "fact": "questions/age",
    "operator": "gt",
    "compare": { "value": 18 }
}
```

**Properties:**

- **`type`** - Always `"condition"` for comparisons
- **`fact`** (string) - The element reference in the format 'collection-id/element-id' to read the value from
- **`operator`** (string) - Comparison operator (see operators below)
- **`compare`** (object, optional) - What to compare against:
    - `{ "value": <any> }` - Compare to a literal value
    - `{ "fact": "<collection-id/element-id>" }` - Compare to another element's value
    - Not required for operators like `"exists"` or `"true"`
- **`not`** (boolean, optional) - Invert the result (default: `false`)

**Supported operators by element type:**

**Number, IntervalScale, OrdinalScale, Date:**

- `"eq"` - Equal to
- `"gt"` - Greater than
- `"gte"` - Greater than or equal to
- `"lt"` - Less than
- `"lte"` - Less than or equal to
- `"exists"` - Has a value (no `compare` needed)

**String:**

- `"eq"` - Equal to
- `"contains"` - String contains value
- `"exists"` - Has a value (no `compare` needed)

**Boolean:**

- `"true"` - Value is true (no `compare` needed)
- `"exists"` - Has a value (no `compare` needed)

**SelectOne:**

- `"eq"` - Equal to specific option
- `"in"` - Is one of multiple options (compare value should be SelectMany format)
- `"exists"` - Has a value (no `compare` needed)

**SelectMany:**

- `"eq"` - Exactly matches set of options
- `"exists"` - Has at least one value (no `compare` needed)

#### 2. Chain Conditions

Combine multiple conditions using logical operators.

```json
{
    "type": "all",
    "items": [
        {
            "type": "condition",
            "fact": "questions/age",
            "operator": "gt",
            "compare": { "value": 18 }
        },
        {
            "type": "condition",
            "fact": "questions/country",
            "operator": "eq",
            "compare": { "value": "US" }
        }
    ]
}
```

**Properties:**

- **`type`** - Logical operator:
    - `"all"` - All conditions must be true (AND logic)
    - `"any"` - At least one condition must be true (OR logic)
- **`items`** (array) - Array of conditions (can be comparisons or nested chains)
- **`not`** (boolean, optional) - Invert the entire chain result
- **`name`** (string, optional) - Human-readable label for documentation

### Examples

#### Skip survey for satisfied customers

```json
{
    "type": "FlowControl",
    "data": {
        "condition": {
            "type": "condition",
            "fact": "questions/nps",
            "operator": "gte",
            "compare": { "value": 9 }
        },
        "action": {
            "type": "survey-finish"
        }
    }
}
```

#### Show page only for specific age range

```json
{
    "collections": {
        "teen-questions": {
            "name": "Questions for Teens",
            "condition": {
                "type": "all",
                "items": [
                    {
                        "type": "condition",
                        "fact": "questions/age",
                        "operator": "gte",
                        "compare": { "value": 13 }
                    },
                    {
                        "type": "condition",
                        "fact": "questions/age",
                        "operator": "lt",
                        "compare": { "value": 20 }
                    }
                ]
            },
            "elements": {
                /* ... */
            }
        }
    }
}
```

#### Complex nested conditions

```json
{
    "type": "any",
    "name": "Premium users or high spenders",
    "items": [
        {
            "type": "condition",
            "fact": "questions/membership",
            "operator": "eq",
            "compare": { "value": "premium" }
        },
        {
            "type": "all",
            "items": [
                {
                    "type": "condition",
                    "fact": "questions/total-spent",
                    "operator": "gt",
                    "compare": { "value": 1000 }
                },
                {
                    "type": "condition",
                    "fact": "questions/active-months",
                    "operator": "gte",
                    "compare": { "value": 6 }
                }
            ]
        }
    ]
}
```

#### Comparing two facts

```json
{
    "type": "condition",
    "fact": "questions/current-salary",
    "operator": "gt",
    "compare": { "fact": "questions/desired-salary" }
}
```

#### Using the `not` property

```json
{
    "type": "condition",
    "fact": "questions/email-consent",
    "operator": "true",
    "not": true // Inverts result: true when email-consent is NOT true
}
```

## Version History

| Version | Date       | Changes                |
| ------- | ---------- | ---------------------- |
| 1.0     | 2025-11-15 | Payment element        |
| 0.5     | 2025-01-24 | Initial public release |