Every answer to “Fork list”
The UI branches on HTTP status + code + reason only — never on
message. Success closes the dialog and navigates; every refusal keeps the dialog open with the answer in
place of the options, and never loses where the user came from.
201 Created — a new fork
Lands on the fork's detail, with a toast
Route: /settings/selection-lists/{fork.id} from list.id in the
201 body. The detail shows the editable 4.0.0 value editor plus the provenance card in its
“comparison unavailable” state (frame 7): under 4.1.0 the
201 carries only forked_from.revision and the source exposes no current
revision, so a new fork is never shown as “up to date”. The list index is refreshed: the common
Priority leaves the Common group and the fork appears under Your organization.
Private draft variant
200 OK — your organization already had a fork
Existing fork, active
Shown on arrival at the existing fork's detail (the dialog closes and navigates, as for 201). Not an error: no red, no retry.
Existing fork, archived (it was the “revert to common”)
“Restore” is PATCH {"status":"active"} and renders only when the fork's
editable is true. Its answers are below — a 403 is branched on
code, never read as “no permission” by status alone.
Restoring — PATCH {"status":"active"} in flight
Native disabled while in flight, so a second activation cannot send a second request.
Restore refused — 403 QUOTA_EXCEEDED · scope org_lists | user_lists
- user_lists
- “The person who created this fork already has the most active lists they may create (25 of 25). The fork stays archived.”
- other scope
- “A limit was reached. The fork stays archived.”
Un-archiving rechecks org_lists and, for a list a user created, that creator's
user_lists — the same scope-driven copy as the fork refusal (quota), with numbers from
limit/current. user_lists counts the fork's
creator, who may not be the person restoring it, so it never says “you”.
Restore refused — 403 FORBIDDEN
Only code: FORBIDDEN reaches this message; QUOTA_EXCEEDED shares the
403 status and renders the quota copy instead.
Refusals — shown inside the dialog
409 CONFLICT · reason fork_exists
The 409 carries no fork id (you can't read it), so there is nothing to link to and the UI does not invent
one. Not retryable. No escalation to an organization admin: list permissions are instance-only, so a tenant
admin holds no manage_access on the fork either — only its owner can act.
409 CONFLICT · reason fork_not_applicable
Defensive only: the CTA is never offered on an org/private list
(frame 3). Reaching this means stale data; the detail reloads on close.
409 CONFLICT · no reason — the key is taken by another list
The plain 409 proves only the key collision. The colliding list may be archived,
private or otherwise unreadable to you, so the copy never says forms use it, never names or links it, and the UI looks nothing up.
“Not a fork of this common list” is derivable: a fork of this source would have answered 200 or
fork_exists.
403 QUOTA_EXCEEDED · scope org_lists | user_lists | list_items
- user_lists
- “You've reached the number of lists you can own (25 of 25).”
- list_items
- “This common list has more values (312) than a list in your organization may hold (250).”
Copy is chosen by scope, numbers come from limit/current
— the approved 4.0.0 QuotaWarning wording. An unknown scope falls back to “A limit was reached”.
400 VALIDATION_ERROR — source over 5000 values
Server-decided only — the dialog does not pre-check the size. An organization may already hold a fork
of this source the page can't see (archived, so the key lookup skips it); that request answers 200
with the existing fork and copies nothing (existing, archived → Restore), and a client-side cap would have
blocked it. Confirm stays enabled; this message renders only from the service's 400.
403 FORBIDDEN — you can't create lists
After this the detail page replaces its Fork CTA with this explanation for the rest of the session.
404 NOT_FOUND — the source is gone or unreadable
Test hooks: [data-frame='06-fork-results'] · [data-result='created'] · [data-toast='forked'] · [data-result='created-private'] · [data-result='existing'] · [data-result='existing-archived'] · [data-action='restore-fork'] · [data-state='restoring'] [data-action='restore-fork'][disabled] · [data-case='restore-quota'][data-error='QUOTA_EXCEEDED'][data-quota-scope='org_lists'] · [data-case='restore-quota'] [data-quota-scope='user_lists'] · [data-case='restore-forbidden'][data-error='FORBIDDEN'] · [data-reason='fork_exists'] · [data-reason='fork_not_applicable'] · [data-result='key-conflict'] · [data-error='QUOTA_EXCEEDED'][data-quota-scope='org_lists'] · [data-quota-scope='user_lists'] · [data-quota-scope='list_items'] · [data-case='fork-size-cap'] · [data-case='fork-no-create'] · [data-result='not-found'] · [data-error='network'] · [data-action='retry'] · [data-action='close']
Contract: POST /v1/selection-lists/{listId}/fork → 201 | 200 | 400 | 403 (FORBIDDEN / QUOTA_EXCEEDED with scope, limit, current) | 404 | 409 (reason: fork_exists | fork_not_applicable | absent). An unknown reason renders as the plain-conflict copy. PATCH /v1/selection-lists/{listId} {status: active} (restore).
Tokens only — no raw hex, spacing or type outside tokens.css / frame.css.