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

# Assessments

> Create, manage, and configure hiring assessments

# Assessments

Assessments are the core building block of Gradient. Each assessment defines a task, time limits, available data sources, and a scoring rubric.

## List Assessments

<CodeGroup>
  ```bash cURL theme={null}
  curl https://app.trygradient.ai/api/assessments \
    -H "Authorization: Bearer gai_your_key"
  ```

  ```javascript Node.js theme={null}
  const res = await fetch('https://app.trygradient.ai/api/assessments', {
    headers: { Authorization: `Bearer ${apiKey}` }
  });
  const { assessments, total } = await res.json();
  ```
</CodeGroup>

```
GET /api/assessments
```

Returns all assessments in your organization.

<ParamField query="limit" type="integer" default="20">
  Results per page (1-100)
</ParamField>

<ParamField query="offset" type="integer" default="0">
  Offset for pagination
</ParamField>

<ParamField query="isActive" type="boolean">
  Filter by active status
</ParamField>

<Accordion title="Response">
  ```json theme={null}
  {
    "assessments": [
      {
        "id": "uuid",
        "orgId": "uuid",
        "name": "Senior PM Assessment",
        "role": "Senior Product Manager",
        "taskPrompt": "Create a product strategy deck...",
        "deliverableType": "pptx",
        "taskTimeMinutes": 45,
        "bonusPhaseEnabled": false,
        "bonusPhaseMinutes": 0,
        "connectorConfig": [],
        "seededData": [],
        "availableSkills": [],
        "scoringRubric": { "..." : "..." },
        "candidatePermissions": {
          "can_browse_connector_documents": true,
          "can_add_custom_connectors": false,
          "can_add_skills": true,
          "can_edit_agents_md": true,
          "can_add_memory": true
        },
        "sendFeedback": false,
        "feedbackDelayHours": 24,
        "dueDate": null,
        "isActive": true,
        "createdAt": "2025-01-15T10:00:00Z",
        "updatedAt": "2025-01-15T10:00:00Z"
      }
    ],
    "total": 5,
    "limit": 20,
    "offset": 0
  }
  ```
</Accordion>

***

## Get Assessment

```
GET /api/assessments/:id
```

Returns a single assessment by ID, plus the org's branding (for candidate-facing display) and whether session-replay capture is enabled for the org.

<Accordion title="Response">
  ```json theme={null}
  {
    "assessment": {
      "id": "uuid",
      "name": "Senior PM Assessment",
      "role": "Senior Product Manager",
      "taskPrompt": "Create a product strategy deck...",
      "deliverableType": "pptx",
      "..."
    },
    "orgBranding": {
      "logoUrl": "https://.../logo.png",
      "wordmarkUrl": null,
      "orgName": "Acme Inc"
    },
    "captureReplaysEnabled": false
  }
  ```

  `orgBranding` is `null` when the org row can't be loaded.
</Accordion>

***

## Create Assessment

```
POST /api/assessments/create
```

<Info>Requires admin role (owner or admin).</Info>

<ParamField body="name" type="string" required>
  Display name for the assessment
</ParamField>

<ParamField body="role" type="string" required>
  Target role (e.g. "Senior PM")
</ParamField>

<ParamField body="taskPrompt" type="string" required>
  The task brief shown to candidates. For multi-phase assessments this is synthesized from the phases and is not required — send `phases` instead.
</ParamField>

<ParamField body="deliverableType" type="string" default="pptx">
  Top-level output format: `pptx`, `docx`, `xlsx`, `email`, or `workflow`. For multi-phase assessments this is the spine/default; the **per-phase** `deliverableType` inside `phases` governs each phase's editor and the AI's tools.
</ParamField>

<ParamField body="phases" type="array">
  Ordered list of phases for a multi-phase assessment. Each phase has its own `label`, `deliverableType`, `taskPrompt`, `timeMinutes`, and optional `inboxConfig` / `workflowConfig`. When provided, the top-level `taskPrompt` is optional and `taskTimeMinutes` should be the sum of phase times.
</ParamField>

<ParamField body="connectorConfig" type="array">
  Connectors (data sources) the candidate's AI can search. Configured in the wizard's Connectors & Data step; seed documents are attached on the assessment detail page after creation.
</ParamField>

<ParamField body="candidatePermissions" type="object">
  What candidates may do during the assessment. See [Candidate Permissions](#candidate-permissions) for the keys and defaults.
</ParamField>

<ParamField body="adminGuide" type="object">
  Reviewer-facing notes: `overview`, `goodAnswers`, `pitfalls`, `buriedInfo` (array), and optional per-phase guidance.
</ParamField>

<ParamField body="taskTimeMinutes" type="integer" default="45">
  Total time limit for the assessment (minutes). For multi-phase assessments, the sum of the phase times.
</ParamField>

<ParamField body="setupTimeMinutes" type="integer">
  Time allotted for the (legacy) setup phase, in minutes. New assessments have no separate setup phase; leave unset.
</ParamField>

<ParamField body="jdLink" type="string">
  URL of the source job description this assessment was built from, if any.
</ParamField>

<ParamField body="dueDate" type="string">
  ISO 8601 deadline for the assessment
</ParamField>

<ParamField body="sendFeedback" type="boolean" default="false" deprecated>
  Deprecated. Automated candidate feedback is not currently delivered, and the toggle has been removed from the UI. The field is still accepted (defaults to `false`) but has no effect. `feedbackDelayHours` is likewise inert.
</ParamField>

<ParamField body="feedbackDelayHours" type="integer" deprecated>
  Deprecated companion to `sendFeedback`. Accepted but inert — automated candidate feedback is not currently delivered.
</ParamField>

<Accordion title="Response - 201 Created">
  ```json theme={null}
  {
    "assessment": {
      "id": "uuid",
      "name": "Senior PM Assessment",
      "..."
    }
  }
  ```
</Accordion>

***

## Update Assessment

```
PATCH /api/assessments/:id
```

Send only the fields you want to update.

**Allowed fields:** `name`, `role`, `roleId`, `task_prompt`, `system_prompt`, `is_active`, `jd_link`, `due_date`, `scoring_rubric`, `bonus_phase_enabled`, `bonus_phase_minutes`, `setup_time_minutes`, `task_time_minutes`, `timer_mode`, `connector_config`, `candidate_permissions`, `phases`, `admin_guide`, `best_practices`

### Role

`roleId` is the id of an existing `roles` row to link (Plan 54's Settings-tab role picker). When provided, the server resolves the role's `title` and writes it to the denormalized `role` string, then reconciles the `assessment_roles` join table (unlinking whatever was previously linked, linking the new pick) — mirroring the sequence assessment creation already uses (insert with `role.title`, then a separate link call). `role` remains independently settable as a raw string for callers that don't use the picker; sending both `role` and `roleId` in the same request lets `roleId`'s resolved title win. An unknown or soft-deleted `roleId` returns a `not_found` error rather than silently no-op-ing.

### Best Practices

The `best_practices` field is a JSON array describing "what great looks like" for this task. It anchors the tactical suggestions candidates receive, and is also mined from top-scoring sessions (see [Mine Best Practices](#mine-best-practices)). Each item:

| Key        | Type                      | Description                                           |
| ---------- | ------------------------- | ----------------------------------------------------- |
| `id`       | string                    | Stable identifier (generated if omitted)              |
| `text`     | string                    | The practice, phrased as a transferable habit         |
| `source`   | `"authored"` \| `"mined"` | Authored by an admin, or distilled from top sessions  |
| `evidence` | string (optional)         | Short generalized note on why high performers do this |

Blank entries are dropped and a missing `source` defaults to `"authored"` on write.

<Note>`send_feedback` and `feedback_delay_hours` are still accepted for backward compatibility but are inert — automated candidate feedback is not currently delivered.</Note>

### Candidate Permissions

The `candidate_permissions` field is a JSON object controlling what candidates can do during the assessment:

| Key                              | Type    | Default | Description                                               |
| -------------------------------- | ------- | ------- | --------------------------------------------------------- |
| `can_browse_connector_documents` | boolean | `true`  | Candidates can see individual documents within connectors |
| `can_add_custom_connectors`      | boolean | `false` | Candidates can connect their own MCP servers              |
| `can_add_skills`                 | boolean | `true`  | Candidates can add/edit/toggle skills                     |
| `can_edit_agents_md`             | boolean | `true`  | Candidates can edit the AI system prompt                  |
| `can_add_memory`                 | boolean | `true`  | Candidates can add persistent memory entries              |

<Accordion title="Response">
  ```json theme={null}
  {
    "assessment": { "..." }
  }
  ```
</Accordion>

***

## Mine Best Practices

```
POST /api/assessments/:id/best-practices/mine
```

Refresh the assessment's **mined** best practices by distilling the recurring moves of its top-scoring candidates. Takes the top quartile by `final_score`, reads their prompts and strongest cited evidence, and generalizes a deduped list (no verbatim peer quotes). Authored practices are always preserved and take precedence over mined duplicates.

Idempotent: re-running replaces the previously mined items. Requires at least 8 scored sessions to generalize reliably; below that it skips.

<Accordion title="Response">
  Cohort large enough and practices distilled:

  ```json theme={null}
  {
    "status": "ok",
    "cohortSize": 14,
    "topQuartileSize": 4,
    "minedCount": 5,
    "bestPractices": [
      { "id": "uuid", "text": "Quantify the recommendation with a specific figure.", "source": "mined", "evidence": "top performers cited hard numbers" }
    ]
  }
  ```

  Cohort too small (nothing mined):

  ```json theme={null}
  {
    "status": "skipped_cohort_too_small",
    "cohortSize": 3,
    "minRequired": 8
  }
  ```
</Accordion>

***

## Delete Assessment

```
DELETE /api/assessments/:id
```

Permanently deletes the assessment and all associated sessions, scores, events, connector data, and context documents.

<Warning>This action is irreversible. All candidate data for this assessment will be permanently deleted.</Warning>

<Accordion title="Response">
  ```json theme={null}
  {
    "ok": true
  }
  ```
</Accordion>

***

## Duplicate Assessment

```
POST /api/assessments/:id/duplicate
```

<Info>Requires admin role (owner or admin).</Info>

Clones the assessment into a brand-new, fully editable draft: `is_active: false`, a new id, and the name suffixed with " (copy)". Copies the scoring rubric (with `calibrationHistory` dropped — that audit trail belongs to the original's scored sessions), phases, connector config, seeded data, available skills, admin guide, best practices, candidate permissions, and the linked role (if any). Does **not** copy candidates, sessions, scores, connector data, or context documents — the duplicate starts with zero candidates.

Scoring and weights are frozen on an assessment once it's published (`is_active`) or already has candidates, even if later un-published — this is the only way to change scoring at that point. The original is left completely untouched.

<Accordion title="Response - 201 Created">
  ```json theme={null}
  {
    "assessment": {
      "id": "uuid",
      "name": "PM Case Study (copy)",
      "isActive": false,
      "...": "the rest of the Assessment object, matching GET /api/assessments/:id"
    }
  }
  ```
</Accordion>

***

## Invite Candidate

```
POST /api/assessments/:id/invite
```

Creates a candidate (or finds an existing one by email) and generates an assessment session with a unique invite link. Sends an invitation email automatically unless `sendEmail` is `false`.

<ParamField body="email" type="string" required>
  Candidate's email address
</ParamField>

<ParamField body="name" type="string" required>
  Candidate's display name
</ParamField>

<ParamField body="dueDate" type="string">
  ISO 8601 per-candidate deadline (overrides assessment-level deadline)
</ParamField>

<ParamField body="sendEmail" type="boolean" default="true">
  When `false`, Gradient does not send its default invitation email — the caller is expected to deliver a custom email using the returned `inviteUrl`. Only affects newly-created sessions; idempotent hits on an existing session never re-send (use `POST /api/assessments/:id/invite/resend` instead).
</ParamField>

<Accordion title="Response - 201 Created (new session)">
  ```json theme={null}
  {
    "session": {
      "id": "uuid",
      "token": "abc123"
    },
    "inviteUrl": "/assess/abc123"
  }
  ```
</Accordion>

If the candidate already has an active session for this assessment, returns `200 OK`:

<Accordion title="Response - 200 OK (existing session)">
  ```json theme={null}
  {
    "session": { "id": "uuid", "token": "abc123" },
    "message": "Candidate already has an active session",
    "inviteUrl": "/assess/abc123"
  }
  ```
</Accordion>

***

## Batch Invite Candidates

```
POST /api/assessments/:id/invite/batch
```

Bulk version of `POST /api/assessments/:id/invite`: creates/re-uses a candidate and session for each `{name, email}` pair in one request, instead of one HTTP round trip per candidate. Each row is processed independently — one row failing (e.g. a bad email) doesn't stop the rest — and the response reports a per-row outcome in the same order as the request.

<ParamField body="candidates" type="array" required>
  Array of `{ name: string, email: string, dueDate?: string }`. Max 500 per request; larger pastes should be split client-side.
</ParamField>

<ParamField body="sendEmail" type="boolean" default="false">
  Same semantics as the single-invite endpoint, applied to every row.
</ParamField>

<Accordion title="Response - 200 OK">
  ```json theme={null}
  {
    "results": [
      { "name": "Jane Smith", "email": "jane@example.com", "status": "created", "session": { "id": "uuid", "token": "abc123" }, "inviteUrl": "/assess/abc123" },
      { "name": "Alex Rivera", "email": "alex@example.com", "status": "existing", "session": { "id": "uuid", "token": "def456" }, "inviteUrl": "/assess/def456" },
      { "name": "Bad Row", "email": "not-an-email", "status": "error", "message": "Invalid email address" }
    ]
  }
  ```
</Accordion>

***

## Resend Invite Email

```
POST /api/assessments/:id/invite/resend
```

Resends the invitation email for an existing session.

<ParamField body="sessionId" type="string" required>
  The session ID to resend the invite for
</ParamField>

<Accordion title="Response">
  ```json theme={null}
  {
    "success": true
  }
  ```
</Accordion>

***

## Export Invited Candidates (CSV)

```
GET /api/assessments/:id/sessions/export
```

Downloads the assessment's invited candidates as a CSV file (`Content-Type: text/csv`, `Content-Disposition: attachment`). Columns: `name, email, link, status, invited_at, submitted_at, due_date`, where `link` is the candidate's absolute `/assess/:token` URL. Org-scoped; admin or API-key auth.

<Accordion title="Response - 200 OK (text/csv)">
  ```
  name,email,link,status,invited_at,submitted_at,due_date
  Jane Smith,jane@example.com,https://app.trygradient.ai/assess/abc123,invited,2026-07-15T13:00:00Z,,
  "Sousa, Alex",alex@example.com,https://app.trygradient.ai/assess/def456,submitted,2026-07-15T13:00:00Z,2026-07-15T14:10:00Z,
  ```
</Accordion>

***

## AI Draft (Assessment Wizard)

```
POST /api/assessments/ai-draft
```

<Info>Requires admin role (owner or admin).</Info>

Generates a suggested assessment configuration from a free-text description. Used by the "Start with AI" flow in the create-assessment wizard to pre-fill fields. Nothing is persisted — the response is a draft the user edits before calling `POST /api/assessments/create`.

<ParamField body="description" type="string" required>
  Free-text description of what the assessment should measure (min 10 chars).
</ParamField>

<ParamField body="role" type="string">
  Optional target role hint; the model biases its draft toward this title.
</ParamField>

<ParamField body="deliverableType" type="string">
  Optional hint (`pptx`, `docx`, `email`, `workflow`). When omitted, the model picks one.
</ParamField>

<ParamField body="isMultiPhase" type="boolean" default="false">
  Hint that the assessment should be multi-phase. Populates `adminGuide.phases`.
</ParamField>

<ParamField body="guideOnly" type="boolean" default="false">
  Draft only the admin guide (used by the "Draft from task prompt" button on the admin-guide wizard step).
</ParamField>

<ParamField body="existingTaskPrompt" type="string">
  When `guideOnly` is true, the existing task prompt the guide should describe.
</ParamField>

<ParamField body="existingRole" type="string">
  When `guideOnly` is true, the existing role the guide should describe.
</ParamField>

<Accordion title="Response">
  ```json theme={null}
  {
    "draft": {
      "name": "Ops Lead Inbox Triage",
      "role": "Operations Lead",
      "taskPrompt": "## Background\nYou are...",
      "deliverableType": "email",
      "adminGuide": {
        "overview": "...",
        "goodAnswers": "...",
        "pitfalls": "...",
        "buriedInfo": ["..."]
      }
    }
  }
  ```
</Accordion>

## Refine assessment with AI

```
POST /api/assessments/chat-edit
```

Applies a natural-language instruction to an assessment's task brief (used by the
build wizard's "Refine with AI" panel) and returns the whole updated draft plus a
short reply. Echoes the input unchanged when the model is unavailable.

<ParamField body="taskPrompt" type="string" required>
  The current task brief to refine.
</ParamField>

<ParamField body="userMessage" type="string" required>
  The instruction, e.g. "make the task harder" or "add a contradiction in the data".
</ParamField>

<ParamField body="name" type="string">
  The current assessment name (may be updated).
</ParamField>

<ParamField body="messages" type="array">
  Prior conversation turns for context (`{ role: "user" | "assistant", content }`). Only the most recent few are replayed to the model.
</ParamField>

<ParamField body="assessmentId" type="string">
  When present, the refine exchange is logged as product feedback for that assessment. It is never written into the brief itself.
</ParamField>

<Accordion title="Response">
  ```json theme={null}
  {
    "reply": "I made the task harder by adding a conflicting data point.",
    "draft": {
      "name": "Ops Lead Inbox Triage",
      "taskPrompt": "## Background\nYou are..."
    }
  }
  ```
</Accordion>
