> ## 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.

# Create a backfill

> Import a historical or externally run performance-review cycle into Windmill in a single call, so past reviews live alongside the ones you run in Windmill.

Imports one completed historical [performance-review](/features/performance-reviews) cycle into Windmill: the cycle itself, its reviewees, its stages with questions and submitted answers, and optionally calibration data and manager-review releases. Use it to bring reviews you ran in a previous tool (or on spreadsheets) into Windmill, so managers and employees see their full review history in one place.

```
POST /api/v2/c/{companySlug}/cycles/backfills
```

The call validates the request schema and its references, then runs the import asynchronously. The asynchronous import can find additional errors. A successful request returns `201` with a `jobId` and the pre-assigned `cycleId` — poll [Get backfill status](/api-reference/performance-reviews/get-backfill-status) with the `jobId` to follow the import through to completion. Once the cycle is imported, you can read it back out with [Export cycle packets](/api-reference/performance-reviews/export-cycle-packets).

## Authorization

This endpoint enforces two rules beyond a valid [API key](/api-reference/overview#authentication):

<Warning>
  **Admins and HR Admins only.** The member who created the API key must hold the `performance_reviews.manage_cycles` capability, which the **Admin** and **HR Admin** roles grant. This is a company-wide role check — unlike the [packets export](/api-reference/performance-reviews/export-cycle-packets), it isn't tied to admin access on any particular cycle. A valid key without the capability returns `403 Forbidden`.
</Warning>

All employee ids in the payload must be existing Windmill employees in your company (and in your primary org). Import your people first — via an HRIS integration or the Dashboard — then backfill their reviews.

## 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>

## Body parameters

The payload describes the entire historical cycle in one document. Stages, questions, and answers are wired together with **refs** — short consumer-local identifiers you choose (like `"self"` or `"q-overall-rating"`). Refs only need to be unique within their scope (stage refs within the request, question refs within their stage) and never leave the payload; Windmill mints its own ids during import.

<ParamField body="idempotencyKey" type="string">
  Optional caller-provided idempotency key for this import. Strongly recommended — see [Idempotency](#idempotency). Without it, every call creates a new job and a new cycle.
</ParamField>

<ParamField body="cycle" type="object" required>
  Metadata for the cycle to import.

  <Expandable title="cycle properties">
    <ParamField body="cycle.name" type="string" required>The cycle's display name.</ParamField>
    <ParamField body="cycle.description" type="string">Optional cycle description.</ParamField>
    <ParamField body="cycle.defaultReviewPeriodStart" type="string" required>Default review period start date, as an ISO calendar date (`YYYY-MM-DD`). Applies to reviewees that don't set their own period.</ParamField>
    <ParamField body="cycle.defaultReviewPeriodEnd" type="string" required>Default review period end date (`YYYY-MM-DD`). Also the fallback `startedAt` for stages that don't set one, so it must not be in the future.</ParamField>
    <ParamField body="cycle.isTest" type="boolean" default="false">Whether to import this as a test cycle.</ParamField>

    <ParamField body="cycle.releaseConfig" type="object">
      Release behavior configuration. One property: `allowManagerSharing` (boolean, default `false`) — whether managers can share review packets.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="admins" type="string[]" default="[]">
  Employee ids granted write access to the imported cycle (cycle admins).
</ParamField>

<ParamField body="reviewees" type="object[]" required>
  The employees enrolled in the cycle as reviewees. At least one is required, each employee may appear only once, and every reviewee must be the target of at least one stage. Manager relationships cannot contain a circular reporting chain.

  <Expandable title="reviewee properties">
    <ParamField body="reviewees[].employeeId" type="string" required>Existing Windmill employee id of the reviewee.</ParamField>
    <ParamField body="reviewees[].managerId" type="string | null" required>The reviewee's manager at the time of the historical cycle; `null` if they had none.</ParamField>
    <ParamField body="reviewees[].reviewPeriodStart" type="string | null" required>Review period start date for this reviewee (`YYYY-MM-DD`); `null` to use the cycle default.</ParamField>
    <ParamField body="reviewees[].reviewPeriodEnd" type="string | null" required>Review period end date for this reviewee (`YYYY-MM-DD`); `null` to use the cycle default.</ParamField>
  </Expandable>
</ParamField>

<ParamField body="stages" type="object[]" required>
  The cycle's stages, in payload order. At least one is required. Each stage is discriminated by `type`: `SELF`, `MANAGER`, `UPWARD`, or `PEER`. A request can include at most one `MANAGER` stage and one `UPWARD` stage; a reviewee can appear in at most one `SELF` stage and one `PEER` stage.

  <Expandable title="stage properties">
    <ParamField body="stages[].type" type="string" required>Stage direction: `SELF`, `MANAGER`, `UPWARD`, or `PEER`.</ParamField>
    <ParamField body="stages[].ref" type="string" required>Consumer-local stage identifier, unique within this request.</ParamField>
    <ParamField body="stages[].name" type="string">Optional display name for this stage.</ParamField>
    <ParamField body="stages[].startedAt" type="string">ISO timestamp for when this stage historically opened. Defaults to the cycle's `defaultReviewPeriodEnd`. Must not be in the future.</ParamField>
    <ParamField body="stages[].questions" type="object[]" required>The questions asked in this stage; at least one. See [Question shape](#question-shape).</ParamField>
    <ParamField body="stages[].participants" type="object[]" required>Who reviewed whom in this stage; at least one pair. See [Participant shape](#participant-shape).</ParamField>
    <ParamField body="stages[].answers" type="object[]" default="[]">The answers submitted in this stage. See [Answer shape](#answer-shape). Every `required` question must have an answered value for every participant pair.</ParamField>
    <ParamField body="stages[].feedbackCollectionStartTime" type="string">**`PEER` stages only, required there.** ISO timestamp for when peer-feedback collection historically opened. It must be on or after `startedAt`.</ParamField>
  </Expandable>
</ParamField>

<ParamField body="calibration" type="object">
  Optional calibration data — overrides applied to manager-stage answers during a historical calibration process.

  <Expandable title="calibration properties">
    <ParamField body="calibration.committee" type="object[]" default="[]">
      Calibration committee members: `{ "employeeId": string, "role": string }`, where `role` is `FACILITATOR`, `COMMITTEE_MEMBER`, or `REVIEWEE`.
    </ParamField>

    <ParamField body="calibration.answers" type="object[]" required>
      Calibration answer overrides; at least one, and at most one per (`targetEmployeeId`, `questionRef`) pair. Each entry must reference a `MANAGER`-stage question that the manager actually answered for that reviewee:

      * `targetEmployeeId` (string, required) — the reviewee receiving the calibrated answer.
      * `questionRef` (string, required) — the manager-stage question ref being calibrated.
      * `answer` (object | null, required) — the calibrated value, in the [answer value shape](#answer-value-shape).
      * `originalAnswer` (object, optional) — the manager's original answer, used as a validation cross-check; if provided, it must match the manager's answer in the `MANAGER` stage exactly.
      * `justification` (string | null, required) — the calibration justification.
      * `submittedAt` (string, optional) — historical calibration submission time.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="releases" type="object[]" default="[]">
  Historical manager-review packet releases — what was shared back with each reviewee. At most one entry per reviewee, and all question refs must point at `MANAGER`-stage questions.

  <Warning>
    **Question visibility is cycle-wide, not per release.** Whether a question is hidden from employees is configured once per question for the entire cycle, so every release that includes a given question must agree on whether it appears in `hiddenFromEmployeeQuestionRefs`. This consistency check spans releases of every `status` — `PENDING` releases count just as much as `ENABLED` and `SHARED` ones. If two releases disagree about a question's visibility, the request fails with `400 Bad Request`.
  </Warning>

  <Expandable title="release properties">
    <ParamField body="releases[].targetEmployeeId" type="string" required>The reviewee whose manager-review packet was released.</ParamField>
    <ParamField body="releases[].status" type="string" required>Historical release status: `PENDING`, `ENABLED`, or `SHARED`.</ParamField>
    <ParamField body="releases[].questionRefs" type="string[]">Manager-stage question refs included in this release. **If omitted, the release includes every question from the `MANAGER` stage** — pass an explicit list to release only a subset. A `SHARED` release must include at least one question.</ParamField>
    <ParamField body="releases[].hiddenFromEmployeeQuestionRefs" type="string[]" default="[]">Included manager-stage question refs hidden from the employee. Because visibility is cycle-wide, hiding a question here requires every other release that includes that question (at any status) to hide it too.</ParamField>
    <ParamField body="releases[].enabledAt" type="string">Historical time the release was enabled.</ParamField>
    <ParamField body="releases[].sharedAt" type="string">Historical time the release was shared. Required when `status` is `SHARED`.</ParamField>
  </Expandable>
</ParamField>

### Question shape

Each entry in a stage's `questions` array is a question definition plus a `ref`:

* **`ref`** (string, required) — consumer-local question identifier, unique within the stage. Stage answers, calibration entries, and releases point back at it via `questionRef`.
* **`type`** (string, required) — one of `TEXT`, `SINGLE_SELECT`, `MULTI_SELECT`, `BOOLEAN`, `SCALE`, `NPS`, `RATING`, or `NUMERIC`. Determines the shape of `options` and of matching answers.
* **`label`** (string, required) — the question prompt shown to the respondent.
* **`description`** (string, required) — supporting context beneath the prompt; pass an empty string when there is none.
* **`helpText`** (string, optional) — tooltip/help text.
* **`required`** (boolean, required) — whether the question had to be answered. If `true`, the backfill must include an answered value for every participant pair in the stage.
* **`formattingInstructions`** (string, optional) — **`TEXT` questions only**; formatting guidance shown to the respondent.
* **`options`** (object) — type-specific configuration. Required for everything except `TEXT`:
  * **`TEXT`** → optional `{ "maxWordCount": number }`.
  * **`SINGLE_SELECT`** → `{ "options": [{ "value": string, "label": string, "description"?: string }], "enableFreeText"?: boolean }` — at least one option.
  * **`MULTI_SELECT`** → `{ "choices": [{ "value": string, "label": string }], "minSelections"?: number, "maxSelections"?: number }`.
  * **`BOOLEAN`** → `{ "defaultValue": boolean | null, "freeTextIfTrue"?: boolean, "freeTextIfFalse"?: boolean }`.
  * **`SCALE`** → `{ "min"?: number, "max"?: number, "step"?: number, "integer"?: boolean, "labels"?: { "low"?: string, "high"?: string } }`.
  * **`NPS`** → `{ "labels"?: { "low"?: string, "high"?: string } }`.
  * **`RATING`** → `{ "points": [{ "value": number, "label"?: string }] }` — at least two points.
  * **`NUMERIC`** → `{ "min"?: number | null, "max"?: number | null, "integer"?: boolean, "unit"?: string | null }`.

### Participant shape

Each entry in a stage's `participants` array declares one actor-to-reviewee assignment:

* **`actingEmployeeId`** (string, required) — the employee who performed the stage work (the self-reviewer, manager, report, or peer).
* **`revieweeEmployeeId`** (string | null, required) — the employee being reviewed. For `SELF` stages, pass `null` (it defaults to the actor) or the actor's own id. For `MANAGER`, `UPWARD`, and `PEER` stages, it's required and must differ from `actingEmployeeId`.

Duplicate (actor, reviewee) pairs aren't allowed within a stage. A `MANAGER` stage allows only one participant per reviewee, and an `UPWARD` stage allows each actor to appear only once. Every reviewee targeted here must also be declared in the top-level `reviewees` array.

### Answer shape

Each entry in a stage's `answers` array is one submitted answer:

* **`actingEmployeeId`** (string, required) — the employee who submitted the answer.
* **`targetEmployeeId`** (string, required) — the reviewee the answer is about. Together with `actingEmployeeId`, this must match one of the stage's participant pairs.
* **`questionRef`** (string, required) — a question `ref` from this same stage.
* **`answer`** (object, required) — the submitted value; see [Answer value shape](#answer-value-shape).
* **`submittedAt`** (string, optional) — historical submission time.

At most one answer per (actor, reviewee, question) triple.

### Answer value shape

The `answer` object's shape depends on the referenced question's `type`, and each value is validated against the question's `options` (select values must match declared options, scale/numeric values must respect bounds, NPS must be 0–10, ratings must match a defined point value):

* **`TEXT`** → `{ "value": string | null }`
* **`SINGLE_SELECT`** → `{ "value": string | null, "freeText": string | null }` (`value` matches an option's `value`)
* **`MULTI_SELECT`** → `{ "value": string[] | null }`
* **`BOOLEAN`** → `{ "value": boolean, "freeText": string | null }`
* **`SCALE`** / **`NPS`** / **`RATING`** / **`NUMERIC`** → `{ "value": number | null }`

## Idempotency

Pass an `idempotencyKey` so retries can't create duplicate cycles. When Windmill sees a key it has processed before (for your company):

* **Same key, same body** — returns `201` with the original `jobId` and `cycleId`. If the earlier attempt never started importing, it's re-queued; if it's already importing, completed, or failed, nothing re-runs.
* **Same key, different body** — returns `409 Conflict`. One key maps to exactly one payload.
* **To re-run a failed import**, send a new request with a **different** `idempotencyKey` — a failed job isn't retried under its original key.

If the request validates and saves but cannot be queued, the endpoint returns `503 Service Unavailable`. If the request has an `idempotencyKey`, retry with the same key. If the request has no key, do not retry it because a retry can create a duplicate cycle. Contact Windmill Support.

## Response

Returns `201 Created` after validation. Windmill queues new and `VALIDATED` jobs. An idempotent replay of an `IMPORTING`, `COMPLETED`, or `FAILED` job returns the original IDs but does not queue the job again.

<ResponseField name="jobId" type="string" required>
  Id of the import job tracking this backfill. Poll it with [Get backfill status](/api-reference/performance-reviews/get-backfill-status).
</ResponseField>

<ResponseField name="cycleId" type="string" required>
  The pre-assigned id of the cycle being imported. The cycle materializes in Windmill as the import runs; treat it as final only once the job reports `COMPLETED`.
</ResponseField>

## Errors

| Status                    | When                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400 Bad Request`         | The payload failed validation: schema mismatches, unknown or cross-company employee ids, duplicate refs or reviewees, a reviewee not targeted by any stage, answers that don't match a participant pair or their question's option constraints, missing answers for `required` questions, a stage `startedAt` in the future, calibration/release entries that don't line up with the `MANAGER` stage, or releases that disagree about a question's employee visibility. Also returned when the API key's creator has no associated member or employee record. The response body lists each issue with its JSON path. |
| `401 Unauthorized`        | The API key is missing, malformed, expired, or revoked.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `403 Forbidden`           | The key is valid but its creator doesn't hold the `performance_reviews.manage_cycles` capability (Admin or HR Admin role).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `409 Conflict`            | The `idempotencyKey` was already used with a different request body.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `503 Service Unavailable` | The backfill was saved but couldn't be queued. Retry with the same `idempotencyKey` to resume the job without creating a duplicate cycle.                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |

Most error responses carry a single `code`, `message`, and `requestId`:

```json theme={null}
{
  "error": {
    "code": "FORBIDDEN",
    "message": "You do not have permission to perform this action",
    "requestId": "019f2986-8ea9-79ff-8ac2-612072b7cca6"
  }
}
```

Validation failures (`400`) use the code `VALIDATION_ERROR` and add a `details` array with one entry per issue. Each entry's `path` locates the offending field in your payload (array indices included):

```json theme={null}
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "requestId": "019f2986-8ea9-79ff-8ac2-612072b7cca6",
    "details": [
      {
        "path": ["admins", 0],
        "message": "Employee \"019f4a10-9b2c-7d33-a1e0-5f8c7b6a4d21\" not found in this company"
      }
    ]
  }
}
```

<RequestExample>
  ```bash Self + manager stages theme={null}
  curl -X POST \
    'https://api.gowindmill.com/api/v2/c/acme/cycles/backfills' \
    -H 'Authorization: Bearer wm_api_xxxxxxxxxxxxxxxxxxxx' \
    -H 'Content-Type: application/json' \
    -d '{
      "idempotencyKey": "h2-2025-import-v1",
      "cycle": {
        "name": "H2 2025 Performance Review (imported)",
        "description": "Imported from our previous review tool.",
        "defaultReviewPeriodStart": "2025-07-01",
        "defaultReviewPeriodEnd": "2025-12-31"
      },
      "admins": ["emp_grace"],
      "reviewees": [
        {
          "employeeId": "emp_ada",
          "managerId": "emp_charles",
          "reviewPeriodStart": null,
          "reviewPeriodEnd": null
        }
      ],
      "stages": [
        {
          "type": "SELF",
          "ref": "self",
          "name": "Self review",
          "startedAt": "2025-12-01T09:00:00Z",
          "questions": [
            {
              "ref": "q-accomplishments",
              "type": "TEXT",
              "label": "What are you most proud of this half?",
              "description": "",
              "required": true
            }
          ],
          "participants": [
            { "actingEmployeeId": "emp_ada", "revieweeEmployeeId": null }
          ],
          "answers": [
            {
              "actingEmployeeId": "emp_ada",
              "targetEmployeeId": "emp_ada",
              "questionRef": "q-accomplishments",
              "answer": { "value": "Shipped the analytics pipeline ahead of schedule." },
              "submittedAt": "2025-12-10T15:00:00Z"
            }
          ]
        },
        {
          "type": "MANAGER",
          "ref": "manager",
          "name": "Manager review",
          "startedAt": "2025-12-15T09:00:00Z",
          "questions": [
            {
              "ref": "q-overall-rating",
              "type": "SINGLE_SELECT",
              "label": "Overall performance rating",
              "description": "",
              "required": true,
              "options": {
                "options": [
                  { "value": "exceeds", "label": "Exceeds expectations" },
                  { "value": "meets", "label": "Meets expectations" },
                  { "value": "below", "label": "Below expectations" }
                ]
              }
            },
            {
              "ref": "q-summary",
              "type": "TEXT",
              "label": "Overall summary for the employee",
              "description": "",
              "required": true
            }
          ],
          "participants": [
            { "actingEmployeeId": "emp_charles", "revieweeEmployeeId": "emp_ada" }
          ],
          "answers": [
            {
              "actingEmployeeId": "emp_charles",
              "targetEmployeeId": "emp_ada",
              "questionRef": "q-overall-rating",
              "answer": { "value": "exceeds" },
              "submittedAt": "2025-12-20T18:30:00Z"
            },
            {
              "actingEmployeeId": "emp_charles",
              "targetEmployeeId": "emp_ada",
              "questionRef": "q-summary",
              "answer": { "value": "A standout half with strong delivery." },
              "submittedAt": "2025-12-20T18:30:00Z"
            }
          ]
        }
      ],
      "releases": [
        {
          "targetEmployeeId": "emp_ada",
          "status": "SHARED",
          "questionRefs": ["q-overall-rating", "q-summary"],
          "sharedAt": "2026-01-05T17:00:00Z"
        }
      ]
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json 201 Created theme={null}
  {
    "jobId": "019f4a10-3c2e-7a91-b6d4-1f0a2c9e8b77",
    "cycleId": "019f4a10-3c2d-7f42-a8c1-9b6e5d3a2f10"
  }
  ```

  ```json 409 Conflict theme={null}
  {
    "error": {
      "code": "CONFLICT",
      "message": "This idempotency key is already associated with a different backfill request.",
      "requestId": "019f2986-8ea9-79ff-8ac2-612072b7cca6"
    }
  }
  ```
</ResponseExample>
