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

# Supplemental cycle data

> Push per-employee data points into a review cycle so they appear in calibration—define fields, load values, and read them back.

Attaches your own per-employee data points to a performance review cycle—job level, tenure, sales attainment, external metrics—so they surface in [calibration](/features/performance-reviews/calibration) grids, tables, packets, and pre-read reports. See [Supplemental data](/features/performance-reviews/supplemental-data) for how the data appears in the product.

```
PUT /api/v2/c/{companySlug}/cycles/{cycleId}/supplemental-data/fields
GET /api/v2/c/{companySlug}/cycles/{cycleId}/supplemental-data/fields
PUT /api/v2/c/{companySlug}/cycles/{cycleId}/supplemental-data/values
GET /api/v2/c/{companySlug}/cycles/{cycleId}/supplemental-data/values
```

The model is deliberately simple: **fields** are the cycle's column definitions, **values** are per-employee data for those fields. Both PUTs are idempotent full replaces—send the complete desired state and the API reconciles it. Each PUT runs in a single transaction: a request either fully applies or fully fails.

## Authorization

These endpoints enforce two rules beyond a valid [API key](/api-reference/overview#authentication):

<Warning>
  **Cycle admin only.** The member who created the API key must be an admin of the cycle. Cycle admin is granted per cycle; it is not a company-wide role. A valid key without cycle-admin access returns `403 Forbidden` on the `PUT` endpoints.
</Warning>

<Warning>
  **Reads are filtered, not rejected.** The `GET` endpoints never return `403`—a valid key without cycle-admin access on the cycle gets empty results instead. An unknown or cross-company `cycleId` returns `404 Not Found`.
</Warning>

Supplemental values are treated as sensitive: request bodies sent to the values `PUT` are redacted from Windmill's operation logs.

## Path parameters

All four endpoints share the same path parameters.

<ParamField path="companySlug" type="string" required>
  Your company's slug. Find it in your Dashboard URL, right after `/s/`:

  `https://app.gowindmill.com/s/{companySlug}/home`
</ParamField>

<ParamField path="cycleId" type="string" required>
  The id of the review cycle. Open the cycle in the Dashboard and take the last segment of the URL:

  `https://app.gowindmill.com/s/{companySlug}/performance-reviews/{cycleId}`
</ParamField>

## Set supplemental fields

```
PUT /api/v2/c/{companySlug}/cycles/{cycleId}/supplemental-data/fields
```

Replaces the cycle's supplemental field definitions as a full set:

* A field **with an `id`** is updated in place, keeping its id and its existing values. Label-only edits never touch data.
* A field **without an `id`** is created.
* An existing field **omitted from the request** is removed, along with its values.
* A field whose **type changes** keeps its id, but its old values are deleted since they no longer fit the new type.

Field display order follows the request array. The list must be non-empty—to remove the last field, delete the cycle's supplemental data rather than sending an empty list.

### Body parameters

<ParamField body="fields" type="object[]" required>
  The complete ordered set of supplemental fields (at least one). Every field shares a core shape plus type-specific configuration.

  <Expandable title="core properties (all types)">
    <ParamField body="fields[].type" type="string" required>The field type: `TEXT`, `SINGLE_SELECT`, `MULTI_SELECT`, `BOOLEAN`, `SCALE`, `NUMERIC`, `NPS`, or `RATING`. Determines which type-specific properties apply and the shape of matching values. `TEXT` and `BOOLEAN` take no type-specific configuration.</ParamField>
    <ParamField body="fields[].label" type="string" required>Human-readable field label. Leading/trailing whitespace is trimmed.</ParamField>
    <ParamField body="fields[].id" type="string">Existing field id to update in place. Omit to create a new field. An id that doesn't belong to this cycle returns `400 Bad Request`.</ParamField>
    <ParamField body="fields[].externalKey" type="string | null">Caller-owned idempotency key, unique per cycle—use it to make field syncs repeatable from your side. Omit to leave an existing key untouched; send `null` to clear it. Trimmed.</ParamField>
    <ParamField body="fields[].description" type="string">Optional field description.</ParamField>
  </Expandable>

  <Expandable title="SINGLE_SELECT properties">
    <ParamField body="fields[].options" type="object[]" required>
      The selectable options, in display order. Each option's `value` is the stable identifier stored on answers.

      <Expandable title="option properties">
        <ParamField body="fields[].options[].value" type="string" required>Stable option value stored on the answer.</ParamField>
        <ParamField body="fields[].options[].label" type="string" required>Human-readable option label.</ParamField>
        <ParamField body="fields[].options[].description" type="string | null">Optional longer option description.</ParamField>
      </Expandable>
    </ParamField>
  </Expandable>

  <Expandable title="MULTI_SELECT properties">
    <ParamField body="fields[].choices" type="object[]" required>
      The selectable choices, in display order: `{ "value": string, "label": string }`. Each choice's `value` is the stable identifier stored on answers.
    </ParamField>
  </Expandable>

  <Expandable title="SCALE properties">
    <ParamField body="fields[].min" type="number">Minimum allowed scale value.</ParamField>
    <ParamField body="fields[].max" type="number">Maximum allowed scale value. Must be ≥ `min` when both are set.</ParamField>
    <ParamField body="fields[].step" type="number">Step between adjacent scale values.</ParamField>
    <ParamField body="fields[].integer" type="boolean" default="true">Whether answers must be integers.</ParamField>
    <ParamField body="fields[].labels" type="object">Optional endpoint labels: `{ "low": string | null, "high": string | null }`.</ParamField>
  </Expandable>

  <Expandable title="NUMERIC properties">
    <ParamField body="fields[].min" type="number | null" default="null">Minimum allowed value, when bounded.</ParamField>
    <ParamField body="fields[].max" type="number | null" default="null">Maximum allowed value, when bounded. Must be ≥ `min` when both are set.</ParamField>
    <ParamField body="fields[].integer" type="boolean" default="false">Whether answers must be whole numbers.</ParamField>
    <ParamField body="fields[].unit" type="string | null" default="null">Optional display unit shown alongside values, like `%`, `$`, or `hrs`.</ParamField>
  </Expandable>

  <Expandable title="NPS properties">
    <ParamField body="fields[].labels" type="object">Optional endpoint labels: `{ "low": string | null, "high": string | null }` (detractor and promoter ends).</ParamField>
  </Expandable>

  <Expandable title="RATING properties">
    <ParamField body="fields[].points" type="object[]" required>
      The ordered rating points, at least two: `{ "value": integer, "label": string | null }`.
    </ParamField>
  </Expandable>
</ParamField>

### Response

Returns the resulting field definitions—the same shape as [Get supplemental fields](#get-supplemental-fields)—so you can capture the generated `id` of each new field for the values upload.

## Get supplemental fields

```
GET /api/v2/c/{companySlug}/cycles/{cycleId}/supplemental-data/fields
```

Returns the cycle's current supplemental field definitions, in order. A cycle with no supplemental data returns an empty `fields` array.

### Response

<ResponseField name="fields" type="object[]" required>
  The cycle's supplemental fields, in order.

  <Expandable title="properties">
    <ResponseField name="id" type="string" required>Stable field id, unchanged across in-place edits. Use it as the key when uploading values.</ResponseField>
    <ResponseField name="type" type="string" required>The field type: `TEXT`, `SINGLE_SELECT`, `MULTI_SELECT`, `BOOLEAN`, `SCALE`, `NUMERIC`, `NPS`, or `RATING`.</ResponseField>
    <ResponseField name="externalKey" type="string | null" required>Caller-owned idempotency key, or `null`.</ResponseField>
    <ResponseField name="label" type="string" required>Human-readable field label.</ResponseField>
    <ResponseField name="description" type="string" required>Field description; empty string when none.</ResponseField>
    <ResponseField name="helpText" type="string | null" required>Helper text, or `null`.</ResponseField>
    <ResponseField name="required" type="boolean" required>Whether an answer is required.</ResponseField>
    <ResponseField name="order" type="integer" required>Zero-based position within the set. Follows the array order of the last fields `PUT`.</ResponseField>
  </Expandable>

  Each field also carries the same type-specific properties documented under [Set supplemental fields](#set-supplemental-fields): `options` for `SINGLE_SELECT`, `choices` for `MULTI_SELECT`, `min`/`max`/`step`/`integer`/`labels` for `SCALE`, `min`/`max`/`integer`/`unit` for `NUMERIC`, `labels` for `NPS`, and `points` for `RATING`.
</ResponseField>

## Set supplemental values

```
PUT /api/v2/c/{companySlug}/cycles/{cycleId}/supplemental-data/values
```

Sets supplemental values for one or more employees. Define fields first—writing values before any fields exist returns `400 Bad Request`.

Each listed employee is a **full replace**: fields omitted from that employee's `values` map have their existing values deleted, so always send an employee's complete data. Employees not listed in the request are left completely untouched, which makes incremental batch uploads safe.

### Body parameters

<ParamField body="employees" type="object[]" required>
  The employees to write.

  <Expandable title="properties">
    <ParamField body="employees[].email" type="string">Primary email of the employee. Provide either `email` or `employeeId`—one is required. An unknown email, or the same employee appearing twice in one request, returns `400 Bad Request`.</ParamField>
    <ParamField body="employees[].employeeId" type="string">Windmill employee id, as an alternative to `email`.</ParamField>

    <ParamField body="employees[].values" type="object" required>
      Map of supplemental **field id** to value. Keys must be field ids from the cycle's current fields; an unknown id returns `400 Bad Request`. Each value follows the [value shape](#value-shape) for its field's type.
    </ParamField>
  </Expandable>
</ParamField>

### Response

<ResponseField name="updatedEmployeeCount" type="integer" required>
  Number of employees whose values were written.
</ResponseField>

## Get supplemental values

```
GET /api/v2/c/{companySlug}/cycles/{cycleId}/supplemental-data/values
```

Returns a page of employees with their current supplemental values, ordered by employee id. Employees with no values for this cycle are not included.

### Query parameters

<ParamField query="take" type="integer" default="50">
  Maximum number of employees to return, up to 200.
</ParamField>

<ParamField query="cursor" type="string">
  Opaque pagination cursor from a previous page's `nextCursor`. Omit for the first page.
</ParamField>

### Response

<ResponseField name="employees" type="object[]" required>
  The page of employees with supplemental values.

  <Expandable title="properties">
    <ResponseField name="employeeId" type="string" required>Windmill employee id; the stable key to join this person to other records.</ResponseField>
    <ResponseField name="email" type="string | null" required>The employee's primary email, or `null` if unknown. Useful for matching to an external HRIS.</ResponseField>

    <ResponseField name="values" type="object[]" required>
      The employee's current supplemental values.

      <Expandable title="value entry properties">
        <ResponseField name="fieldId" type="string" required>Supplemental field id. Resolve it against [Get supplemental fields](#get-supplemental-fields).</ResponseField>
        <ResponseField name="value" type="object" required>The current value, in the same [value shape](#value-shape) used by the values `PUT`.</ResponseField>
        <ResponseField name="updatedAt" type="string" required>ISO 8601 timestamp of the last write to this value.</ResponseField>
      </Expandable>
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="nextCursor" type="string | null">
  Cursor for the next page; `null` or absent when there are no more.
</ResponseField>

### Value shape

Every value—written by the values `PUT` and returned by the values `GET`—is an object discriminated by `type`, which must match the field's type:

* **`TEXT`** → `{ "type": "TEXT", "value": string }`
* **`SINGLE_SELECT`** → `{ "type": "SINGLE_SELECT", "value": string, "freeText": string | null }` (`value` matches an option's `value`)
* **`MULTI_SELECT`** → `{ "type": "MULTI_SELECT", "value": string[] }` (at least one choice `value`)
* **`BOOLEAN`** → `{ "type": "BOOLEAN", "value": boolean, "freeText": string | null }`
* **`SCALE`** / **`NUMERIC`** / **`RATING`** → `{ "type": "SCALE" | "NUMERIC" | "RATING", "value": number }` (a `RATING` value must match one of the field's `points`)
* **`NPS`** → `{ "type": "NPS", "value": number }` (an integer from 0–10)

<Note>
  Changing supplemental fields or values marks any generated calibration pre-read report as stale so facilitators know to regenerate it. See [Updating data mid-cycle](/features/performance-reviews/supplemental-data#updating-data-mid-cycle).
</Note>

## Errors

| Status             | When                                                                                                                                                                                                                                                                                                                                                                                                   |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `400 Bad Request`  | Validation failed: empty `fields` list, a rating field with fewer than two points, `max` below `min` on a scale or numeric field, an NPS value outside 0–10, an unknown field id in a values map, an unknown employee email/id, duplicate employees or duplicate field ids/external keys in one request, or writing values before any fields are defined (`Cycle has no supplemental fields defined`). |
| `401 Unauthorized` | The API key is missing, malformed, expired, or revoked.                                                                                                                                                                                                                                                                                                                                                |
| `403 Forbidden`    | A `PUT` from a valid key whose creator isn't an admin of this cycle. `GET`s don't return `403`—without access they return empty results.                                                                                                                                                                                                                                                               |
| `404 Not Found`    | The `cycleId` doesn't exist in this company (reads).                                                                                                                                                                                                                                                                                                                                                   |

The error response body is always shaped like this:

```json theme={null}
{
  "error": {
    "code": "FORBIDDEN",
    "message": "You don't have permission to access this resource",
    "requestId": "019f2986-8ea9-79ff-8ac2-612072b7cca6"
  }
}
```

<RequestExample>
  ```bash Set fields theme={null}
  curl -X PUT \
    'https://api.gowindmill.com/api/v2/c/acme/cycles/cyc_8f2a1c/supplemental-data/fields' \
    -H 'Authorization: Bearer wm_api_xxxxxxxxxxxxxxxxxxxx' \
    -H 'Content-Type: application/json' \
    -d '{
      "fields": [
        {
          "type": "SINGLE_SELECT",
          "externalKey": "job-level",
          "label": "Job level",
          "options": [
            { "value": "l3", "label": "L3" },
            { "value": "l4", "label": "L4" },
            { "value": "l5", "label": "L5" }
          ]
        },
        {
          "type": "NUMERIC",
          "externalKey": "sales-attainment",
          "label": "Sales attainment",
          "min": 0,
          "unit": "%"
        },
        {
          "type": "BOOLEAN",
          "externalKey": "promo-nominated",
          "label": "Nominated for promotion"
        }
      ]
    }'
  ```

  ```bash Get fields theme={null}
  curl 'https://api.gowindmill.com/api/v2/c/acme/cycles/cyc_8f2a1c/supplemental-data/fields' \
    -H 'Authorization: Bearer wm_api_xxxxxxxxxxxxxxxxxxxx'
  ```

  ```bash Set values theme={null}
  curl -X PUT \
    'https://api.gowindmill.com/api/v2/c/acme/cycles/cyc_8f2a1c/supplemental-data/values' \
    -H 'Authorization: Bearer wm_api_xxxxxxxxxxxxxxxxxxxx' \
    -H 'Content-Type: application/json' \
    -d '{
      "employees": [
        {
          "email": "ada@acme.com",
          "values": {
            "fld_level": { "type": "SINGLE_SELECT", "value": "l5" },
            "fld_attainment": { "type": "NUMERIC", "value": 103.5 },
            "fld_promo": { "type": "BOOLEAN", "value": true, "freeText": "Nominated by skip-level" }
          }
        },
        {
          "employeeId": "emp_grace",
          "values": {
            "fld_level": { "type": "SINGLE_SELECT", "value": "l4" },
            "fld_attainment": { "type": "NUMERIC", "value": 98 }
          }
        }
      ]
    }'
  ```

  ```bash Get values theme={null}
  curl 'https://api.gowindmill.com/api/v2/c/acme/cycles/cyc_8f2a1c/supplemental-data/values?take=100' \
    -H 'Authorization: Bearer wm_api_xxxxxxxxxxxxxxxxxxxx'
  ```
</RequestExample>

<ResponseExample>
  ```json 200 Fields theme={null}
  {
    "fields": [
      {
        "id": "fld_level",
        "type": "SINGLE_SELECT",
        "externalKey": "job-level",
        "label": "Job level",
        "description": "",
        "helpText": null,
        "required": false,
        "order": 0,
        "options": [
          { "value": "l3", "label": "L3", "description": null },
          { "value": "l4", "label": "L4", "description": null },
          { "value": "l5", "label": "L5", "description": null }
        ]
      },
      {
        "id": "fld_attainment",
        "type": "NUMERIC",
        "externalKey": "sales-attainment",
        "label": "Sales attainment",
        "description": "",
        "helpText": null,
        "required": false,
        "order": 1,
        "min": 0,
        "max": null,
        "integer": false,
        "unit": "%"
      },
      {
        "id": "fld_promo",
        "type": "BOOLEAN",
        "externalKey": "promo-nominated",
        "label": "Nominated for promotion",
        "description": "",
        "helpText": null,
        "required": false,
        "order": 2
      }
    ]
  }
  ```

  ```json 200 Values write theme={null}
  {
    "updatedEmployeeCount": 2
  }
  ```

  ```json 200 Values page theme={null}
  {
    "employees": [
      {
        "employeeId": "emp_ada",
        "email": "ada@acme.com",
        "values": [
          {
            "fieldId": "fld_level",
            "value": { "type": "SINGLE_SELECT", "value": "l5", "freeText": null },
            "updatedAt": "2026-07-18T16:04:12.000Z"
          },
          {
            "fieldId": "fld_attainment",
            "value": { "type": "NUMERIC", "value": 103.5 },
            "updatedAt": "2026-07-18T16:04:12.000Z"
          }
        ]
      }
    ],
    "nextCursor": null
  }
  ```

  ```json 403 Forbidden theme={null}
  {
    "error": {
      "code": "FORBIDDEN",
      "message": "You don't have permission to access this resource",
      "requestId": "019f2986-8ea9-79ff-8ac2-612072b7cca6"
    }
  }
  ```
</ResponseExample>
