Unlink two workspaces
Soft-remove a workspace link and reconcile its conditional placement-access cascade.
Action and scope
app_topolo_developers.workspace_links.unlink
POST /api/developer-console/workspace-links/{linkId}/unlink
Mutation; explicit confirmation is required.
Before you start
Use the intended environment, authenticated identity and active organization. Discover actual link/pair identities; every worked value is fictional and every example is SYNTHETIC / UNRUN.
Use the current credential-scoped contract and any required transport resource binding separately from action input. No input field overrides the active organization.
Source and declared-action revision are 7b89bd3b39092cdc29f87540847bf99a243f0a40. The separate Docs payload-format schema revision9530cc90f2762ce1c59d63175ff37fb23a1b24ba is not the Developers source revision.
Declared permission: app_topolo_developers.connections:write. Read the actual link/pair and relevant access first.
This action can change workspace connectivity or placement access. Review the actual current state and affected pair, obtain explicit approval for those effects, then provide the published CLI/MCP confirmation only for that intended mutation. No worked example executes or authorizes it.
A link row, organization role or successful response is not a new provider permission grant. Caller and membership/approval boundaries below still apply.
Input fields
| Field | Required | Type and constraints |
|---|---|---|
linkId | Yes | string; pattern ^wsl_[A-Za-z0-9-]{1,80}$ |
The action input is closed. Path linkId and any supplied representation must agree; no organization override, idempotency key or expected version is accepted.
Behavior and response
Success uses {success:true,message:"OK",data:...}. A list has organizationId,links,count. A mutation has outcome and the complete link record; unlink also has accessRemoved. The declared data schema is open, and the complete source-qualified examples below retain every link-record field.
Finds a live same-organization link. An organization actor classified as admin may unlink without member lookups; otherwise at least one workspace must currently grant accessLevel admin and both membership lookups must be available. The route derives actor admin from its existing organization/platform policy; this guide grants no role.
Soft removal writes removedBy,removedAt,updatedAt first. Stored status and approval fields are retained; outcome unlinked is separate from the active/needs_approval status vocabulary. The row is no longer returned by the live list.
It then reads the app connection. A missing/removed connection or scope linked cascades to matching live placements for this organization, consumer/provider apps and exact workspace pair. Rows not already access_removed become access_removed with reason workspace_unlinked and an updated timestamp. accessRemoved is the store’s changed-row count.
A current nonremoved connection scoped all skips that placement update and returns accessRemoved:0, regardless of connection status. This operation’s zero count is not proof that placements or provider access are usable.
Link removal precedes the separate cascade and final read; no transaction or request idempotency/expected-version guard is shown. A failure or lost response can leave the link removed while cascade outcome is unresolved. A subsequent unlink can return404 because only live links are accepted. Reconcile before retrying; never infer no effects from an error.
Errors and recovery
HTTP 403 · organization_required — Select the intended active organization through supported context controls.
HTTP 422 · invalid_input — Use only the exact declared fields and canonical id syntax; preserve current path/body agreement.
HTTP 404 · link_not_found — Reconcile retained pair and placements after unknown outcomes; absence of a live id alone does not prove cascade completion.
HTTP 503 · workspace_access_unavailable — A non-organization-admin path needs available membership lookups; do not substitute a larger role.
HTTP 403 · workspace_admin_required — Use an authorized workspace admin or organization admin for the actual intended removal.
Shared authentication, declared permission or framework failures may occur before these selected branches. Uncaught storage/concurrency failures can have a different carrier; preserve actual status/body and do not infer no effects from a failed response.
Worked examples — synthetic and unrun
Linked-scope removal cascades to this pair’s placements
SOURCE-DERIVED SYNTHETIC UNRUN HTTP 200. Assume a same-organization live active row, caller administers the consumer workspace, membership lookups succeed, and current connection scope is linked. Soft removal succeeds, then the selected placement update reports exactly two changed rows at the later timestamp. The mutation is explicitly authorized but UNRUN.
Input:
{
"linkId": "wsl_00000000-0000-4000-8000-000000000001"
}
Complete response body:
{
"success": true,
"message": "OK",
"data": {
"outcome": "unlinked",
"link": {
"id": "wsl_00000000-0000-4000-8000-000000000001",
"organizationId": "org_docs_fixture",
"consumerAppId": "app_docs_consumer",
"consumerWorkspaceId": "ws_consumer_fixture",
"providerAppId": "app_docs_provider",
"providerWorkspaceId": "ws_provider_fixture",
"status": "active",
"requestedBy": "user_fixture_requester",
"requestedAt": "2026-10-23T00:00:00.000Z",
"consumerApprovedBy": "user_fixture_consumer_admin",
"consumerApprovedAt": "2026-10-23T00:00:00.000Z",
"providerApprovedBy": "user_fixture_provider_admin",
"providerApprovedAt": "2026-10-23T00:00:00.000Z",
"removedBy": "user_fixture_consumer_admin",
"removedAt": "2026-10-23T00:01:00.000Z",
"createdAt": "2026-10-23T00:00:00.000Z",
"updatedAt": "2026-10-23T00:01:00.000Z"
},
"accessRemoved": 2
}
}
Outcome is unlinked, but link.status remains active and removedAt/removedBy identify the soft deletion. Matching live placements receive status access_removed and reason workspace_unlinked; other pairs are outside this cascade.
All-workspace scope leaves placements unchanged by this operation
SOURCE-DERIVED SYNTHETIC UNRUN HTTP 200. Assume caller is organization owner (actor.isAdmin:true), the link is live, and a current nonremoved app connection has workspaceScope all. Its status may be paused; this cascade decision does not test status. Workspace membership lookups are skipped for this actor.
Input:
{
"linkId": "wsl_00000000-0000-4000-8000-000000000001"
}
Complete response body:
{
"success": true,
"message": "OK",
"data": {
"outcome": "unlinked",
"link": {
"id": "wsl_00000000-0000-4000-8000-000000000001",
"organizationId": "org_docs_fixture",
"consumerAppId": "app_docs_consumer",
"consumerWorkspaceId": "ws_consumer_fixture",
"providerAppId": "app_docs_provider",
"providerWorkspaceId": "ws_provider_fixture",
"status": "active",
"requestedBy": "user_fixture_requester",
"requestedAt": "2026-10-23T00:00:00.000Z",
"consumerApprovedBy": "user_fixture_consumer_admin",
"consumerApprovedAt": "2026-10-23T00:00:00.000Z",
"providerApprovedBy": "user_fixture_provider_admin",
"providerApprovedAt": "2026-10-23T00:00:00.000Z",
"removedBy": "user_fixture_org_owner",
"removedAt": "2026-10-23T00:01:00.000Z",
"createdAt": "2026-10-23T00:00:00.000Z",
"updatedAt": "2026-10-23T00:01:00.000Z"
},
"accessRemoved": 0
}
}
The link is soft-removed and this operation changes no placements. This does not certify that the existing connection or placements are otherwise usable.
Caller has no unlink authority
SOURCE-DERIVED SYNTHETIC UNRUN HTTP 403. Assume caller is not an organization admin and both successful membership lookups grant no workspace-admin side. The live row has not been changed.
Input:
{
"linkId": "wsl_00000000-0000-4000-8000-000000000001"
}
Complete response body:
{
"success": false,
"error": {
"message": "Only an admin of a linked workspace or an organization admin can unlink",
"code": "workspace_admin_required"
}
}
Use the responsible authorized owner. Do not expand the caller’s role or treat connections:write alone as sufficient unlink authority.
A removed link is not a live link id
SOURCE-DERIVED SYNTHETIC UNRUN HTTP 404. Assume no nonremoved row with this id exists in the active organization. This can describe an already removed row, an incorrect id or a different organization. It is not an instruction to replay an earlier uncertain unlink.
Input:
{
"linkId": "wsl_00000000-0000-4000-8000-000000000001"
}
Complete response body:
{
"success": false,
"error": {
"message": "No workspace link wsl_00000000-0000-4000-8000-000000000001 in this organization",
"code": "link_not_found"
}
}
Reconcile the exact retained pair with workspace_links.list and relevant placements before any further authorized mutation. The404 alone neither attributes a prior removal nor proves a cascade completed.
Verify the result
On a returned successful response, verify outcome unlinked and the exact returned removed-link id, organization, pair, stored status, approvals, removedBy, removedAt and updatedAt against the retained preimage and approved caller/context. Use the exact four pair filters in the bounded live list only to check whether a live pair remains; removed-row fields come from the returned removed link, not a live-list entry.
Compare accessRemoved with separately authorized reads of the relevant placement state for this exact pair. Live-pair absence or a zero count alone proves neither cascade completion nor attribution to a prior request, especially after unknown or concurrent outcomes. Preserve the qualified cascade conditions and actual access_removed/workspace_unlinked placement evidence.
After an unknown outcome, reconcile current link state before any further request. For unlink, also read the relevant placements under separate authorized read scope; a removed link alone does not prove a successful cascade.
Recovery boundary
First reconcile live-link absence and placement state after response loss or failure. An intended restoration requires separately authorized workspace_links.link for the same pair, which can revive the original id with fresh approvals and may remain pending. That operation does not automatically restore placements; each intended placements.refresh has separate provider/access/effect approval and verification. No atomic inverse or automatic retry is implied.
Related actions
app_topolo_developers.workspace_links.listapp_topolo_developers.placements.listapp_topolo_developers.workspace_links.linkapp_topolo_developers.placements.refresh
Source provenance and limits
Reviewed immutable Developers 7b89bd3b39092cdc29f87540847bf99a243f0a40 (tree b3817ca104370c067b373f1f9899b57f8283d086), selected placement route/service/store, caller context, connection lookup, actor policy and HTTP envelope. The saved Developers descriptors also declare 7b89bd3b39092cdc29f87540847bf99a243f0a40. Docs native-format validation separately uses9530cc90f2762ce1c59d63175ff37fb23a1b24ba. Source/declaration agreement does not certify serving adoption, transactionality, a runtime business result or current provider authorization. All examples are source-derived synthetic and unrun; automatic Knowledge invocation during builds remains UNKNOWN.
Change log
- 2026-10-23: Initial documentation of workspace_links.unlink with exact state/access boundaries, complete synthetic unrun responses, verification and recovery limits. No application behavior change is asserted.