---
title: "Transition a knowledge gap"
method: POST
path: "/knowledge-gaps/{gap_id}/transitions"
tags: ["knowledge-gaps", "knowledge-gaps"]
---

# Transition a knowledge gap

`POST /knowledge-gaps/{gap_id}/transitions`

Drive the gap state machine (open → routed → ingested → resolved, plus dismissed/duplicate/expired/reopened). `expected_from` is a compare-and-set guard: if the gap already moved, the write loses with 409. Illegal transitions return 422.

## Path parameters

- `gap_id` string, uuid, required

## Request body

- GapTransitionRequest — Body for ``POST /knowledge-gaps/{gap_id}/transitions``. ``expected_from`` makes the transition a compare-and-set: the integrator states which state it believes the gap is in, and the write only applies if that still holds (otherwise 409). This is how concurrent actors are detected.
  - `expected_from` 'open' | 'routed' | 'ingested' | 'resolved' | 'expired' | 'dismissed' | 'duplicate' | 'reopened', required — State the caller believes the gap is in (compare-and-set guard).
  - `to_state` 'open' | 'routed' | 'ingested' | 'resolved' | 'expired' | 'dismissed' | 'duplicate' | 'reopened', required — Target state.
  - `actor` string, required — Who performed the transition.
  - `note` string, nullable
  - `evidence_ref` string, nullable — Optional link to the ingestion job / contribution that drove this transition.
  - `assigned_to` string, nullable — Opaque expert/DRI id the integrator routed to (set when moving to 'routed').

## Response `200`

Successful Response

- KnowledgeGap — Persisted gap item — repository row shape and API response body.
  - `gap_id` string, uuid — UUIDv7 — chronologically sortable.
  - `workspace_id` string, uuid, required — Owning workspace; multi-tenant isolation key.
  - `pot_id` string, uuid, required — POT the question was asked against.
  - `query_text` string, required
  - `requester_id` string, required
  - `domain` string, required
  - `trigger` 'low_confidence' | 'no_coverage' | 'manual'
  - `trigger_score` number, nullable
  - `state` 'open' | 'routed' | 'ingested' | 'resolved' | 'expired' | 'dismissed' | 'duplicate' | 'reopened'
  - `assigned_to` string, nullable — Opaque id of the expert/DRI the integrator routed to. Core stores, never decides, this value.
  - `resolved_cluster_id` string, uuid, nullable — Forward-compat hook: the Knowledge Cluster (nebula) a filled gap will seed in Phase 2. Always null in the pilot; the Cluster table is not built yet.
  - `capsule_id` string, uuid, nullable — Capsule whose coverage evaluation created this gap (Capsules PR1b). Null for gaps born on the /query path or created manually. Marks the ORIGIN only — the trigger vocabulary is unchanged (no GapTrigger extension), so existing integrator payloads keep validating.
  - `idempotency_key` string, nullable
  - `transitions` GapTransition[] — Append-only transition history (actor + timestamp per state change).
    - `from_state` 'open' | 'routed' | 'ingested' | 'resolved' | 'expired' | 'dismissed' | 'duplicate' | 'reopened', required
    - `to_state` 'open' | 'routed' | 'ingested' | 'resolved' | 'expired' | 'dismissed' | 'duplicate' | 'reopened', required
    - `actor` string, required — Who/what performed the transition (opaque integrator id).
    - `at` string, date-time
    - `note` string, nullable
    - `evidence_ref` string, nullable — Optional link to the ingestion job / contribution that drove this transition.
  - `timeout_at` string, date-time, nullable — Optional SLA deadline. When elapsed, the integrator transitions the gap to 'expired' (core does not auto-expire — no scheduler in scope).
  - `created_at` string, date-time
  - `updated_at` string, date-time
  - `resolved_at` string, date-time, nullable

## Other responses

- `422` — Validation Error

---

[API](https://skmtc.dev/scipot/apis/scipot-core-api.md) · [All operations](https://skmtc.dev/scipot/apis/scipot-core-api/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/scipot/scipot-core-api/revisions/e8712e9ffeae/schema)
