Link two workspaces
Request or activate a consumer/provider workspace link, including existing and revived pair behavior.
Action and scope
app_topolo_developers.workspace_links.link
POST /api/developer-console/workspace-links
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 |
|---|---|---|
consumerAppId | Yes | string; pattern ^app_[a-z0-9]+(?:_[a-z0-9]+)+$ |
consumerWorkspaceId | Yes | string; pattern ^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$ |
providerAppId | Yes | string; pattern ^app_[a-z0-9]+(?:_[a-z0-9]+)+$ |
providerWorkspaceId | Yes | string; pattern ^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$ |
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.
A nonremoved app connection must have stored status active or degraded. The caller must currently be able to use both consumer and provider workspaces; read membership is sufficient for this prerequisite. Workspace-admin access, rather than organization-admin status alone, supplies side approvals.
For a new pair, both admin sides create active status and HTTP201 outcome linked. Otherwise it creates needs_approval and HTTP202 requested, recording each side the caller administers; a caller administering neither side can request this new row when both memberships and connections:write pass.
For a live active pair, connection and both workspace checks still run, then HTTP200 unchanged returns the existing row. For a pending pair, new admin-side approvals can return HTTP200 approved or activated; an admin with no missing own-side approval gets unchanged. A caller administering neither pending side is refused.
The unique workspace pair is reused when relinking a removed row. The store preserves original id and createdAt, replaces requester/request time and current approvals, clears removal fields and updates the timestamp. It does not retain an immutable sequence of all former approval values.
Input contains no idempotency key or expected version. The store leaves a live conflict row unchanged; concurrent state changes and several ordered writes are not an exactly-once or transactional guarantee. After lost/error response, read the exact pair and approvals before deciding whether another separately approved request is still valid.
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 · connection_not_found — Discover the intended live connection; do not create or repair it automatically.
HTTP 409 · connection_not_active — Preserve actual status and ask its owner for a separately authorized connection change.
HTTP 403 · workspace_access_required — Use the caller’s discovered accessible workspace in the intended organization.
HTTP 503 · workspace_access_unavailable — Restore Auth membership availability; unavailability is not empty membership or permission to broaden access.
HTTP 403 · workspace_admin_required — A pending existing link needs a workspace-admin side. Read awaiting sides and obtain the appropriate owner’s approval.
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
A new pair is immediately linked
SOURCE-DERIVED SYNTHETIC UNRUN HTTP 201. Assume active/degraded app connection, current read access to both workspaces, workspace admin access on both sides, no prior row, newLinkId equal to the fictional id shown and now equal to the initial timestamp. The operation is explicitly authorized but UNRUN.
Input:
{
"consumerAppId": "app_docs_consumer",
"consumerWorkspaceId": "ws_consumer_fixture",
"providerAppId": "app_docs_provider",
"providerWorkspaceId": "ws_provider_fixture"
}
Complete response body:
{
"success": true,
"message": "OK",
"data": {
"outcome": "linked",
"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_both_admin",
"requestedAt": "2026-10-23T00:00:00.000Z",
"consumerApprovedBy": "user_fixture_both_admin",
"consumerApprovedAt": "2026-10-23T00:00:00.000Z",
"providerApprovedBy": "user_fixture_both_admin",
"providerApprovedAt": "2026-10-23T00:00:00.000Z",
"removedBy": null,
"removedAt": null,
"createdAt": "2026-10-23T00:00:00.000Z",
"updatedAt": "2026-10-23T00:00:00.000Z"
}
}
}
The selected source would create an active link with both approvals recorded. Verify actual pair/id/status/approval times under the same organization before using it.
Members who administer neither side request a new link
SOURCE-DERIVED SYNTHETIC UNRUN HTTP 202. Assume active app connection, current read membership in both workspaces, connections:write and no existing row. Caller user_fixture_reader administers neither side. The new link requires both approvals. This conditional source example is UNRUN.
Input:
{
"consumerAppId": "app_docs_consumer",
"consumerWorkspaceId": "ws_consumer_fixture",
"providerAppId": "app_docs_provider",
"providerWorkspaceId": "ws_provider_fixture"
}
Complete response body:
{
"success": true,
"message": "OK",
"data": {
"outcome": "requested",
"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": "needs_approval",
"requestedBy": "user_fixture_reader",
"requestedAt": "2026-10-23T00:00:00.000Z",
"consumerApprovedBy": null,
"consumerApprovedAt": null,
"providerApprovedBy": null,
"providerApprovedAt": null,
"removedBy": null,
"removedAt": null,
"createdAt": "2026-10-23T00:00:00.000Z",
"updatedAt": "2026-10-23T00:00:00.000Z"
}
}
}
A202 request is pending, with both approvals null. Creation does not require admin access on a side in this new-row branch; it does not make the pair usable under linked scope.
Linking a pending pair can activate it
SOURCE-DERIVED SYNTHETIC UNRUN HTTP 200. Assume a live needs_approval row already has the consumer approval at the initial timestamp. The caller is provider workspace admin, can use both workspaces, and approves at the later timestamp; the connection remains usable. Both store updates succeed.
Input:
{
"consumerAppId": "app_docs_consumer",
"consumerWorkspaceId": "ws_consumer_fixture",
"providerAppId": "app_docs_provider",
"providerWorkspaceId": "ws_provider_fixture"
}
Complete response body:
{
"success": true,
"message": "OK",
"data": {
"outcome": "activated",
"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:01:00.000Z",
"removedBy": null,
"removedAt": null,
"createdAt": "2026-10-23T00:00:00.000Z",
"updatedAt": "2026-10-23T00:01:00.000Z"
}
}
}
Existing pending rows use the approval path. Its activation returns HTTP200 and outcome activated, rather than the201 linked outcome of a new active row.
Relinking a removed pair revives its original identity
SOURCE-DERIVED SYNTHETIC UNRUN HTTP 201. Assume the old pair row has this same id and createdAt from the initial timestamp, but was soft-removed. At the final timestamp user_fixture_new_admin administers both sides. The unique-pair conflict revives that row; a newly generated candidate id is not substituted. No concurrent writer changes the row.
Input:
{
"consumerAppId": "app_docs_consumer",
"consumerWorkspaceId": "ws_consumer_fixture",
"providerAppId": "app_docs_provider",
"providerWorkspaceId": "ws_provider_fixture"
}
Complete response body:
{
"success": true,
"message": "OK",
"data": {
"outcome": "linked",
"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_new_admin",
"requestedAt": "2026-10-23T00:02:00.000Z",
"consumerApprovedBy": "user_fixture_new_admin",
"consumerApprovedAt": "2026-10-23T00:02:00.000Z",
"providerApprovedBy": "user_fixture_new_admin",
"providerApprovedAt": "2026-10-23T00:02:00.000Z",
"removedBy": null,
"removedAt": null,
"createdAt": "2026-10-23T00:00:00.000Z",
"updatedAt": "2026-10-23T00:02:00.000Z"
}
}
}
The original id and createdAt remain; requestedBy/requestedAt, approvals and updatedAt are replaced and removal fields cleared. This is a conditional source path, not an immutable history or exactly-once guarantee.
Linking an active pair is unchanged
SOURCE-DERIVED SYNTHETIC UNRUN HTTP 200. Assume usable connection and current access to both workspaces. The row is already active; caller has connections:write but need not administer a side for this selected early return.
Input:
{
"consumerAppId": "app_docs_consumer",
"consumerWorkspaceId": "ws_consumer_fixture",
"providerAppId": "app_docs_provider",
"providerWorkspaceId": "ws_provider_fixture"
}
Complete response body:
{
"success": true,
"message": "OK",
"data": {
"outcome": "unchanged",
"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": null,
"removedAt": null,
"createdAt": "2026-10-23T00:00:00.000Z",
"updatedAt": "2026-10-23T00:00:00.000Z"
}
}
}
The existing record is returned without a new link or approval write. Current connection/access checks occurred; unchanged is not evidence that this request originally created the link.
A pending existing pair requires an approving workspace admin
SOURCE-DERIVED SYNTHETIC UNRUN HTTP 403. Assume current access to both workspaces and the existing consumer-approved pending row. Caller administers neither side. The app connection is usable; no approval write is reached.
Input:
{
"consumerAppId": "app_docs_consumer",
"consumerWorkspaceId": "ws_consumer_fixture",
"providerAppId": "app_docs_provider",
"providerWorkspaceId": "ws_provider_fixture"
}
Complete response body:
{
"success": false,
"error": {
"message": "Only an admin of a linked workspace can approve this link",
"code": "workspace_admin_required",
"details": {
"awaiting": [
"app_docs_provider/ws_provider_fixture"
]
}
}
}
A member may request a new row, but cannot add approvals to this existing pending row without the required workspace-admin access.
Verify the result
Retain the original organization, exact pair/link id, pre-write record, approved effects and actual request/response identity. Read workspace_links.list with the exact four pair filters and compare id,status,approvals and timestamps.
A successful outcome can be linked,requested,approved,activated or unchanged depending on the action and current state. Confirm the intended final state; do not equate HTTP200 with new approval, active usability or attribution to this particular request.
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 the exact pair after any lost/error response. If an intentional removal of a verified link/request is desired, workspace_links.unlink is a separate consequential operation that may mark placements access_removed. It is not an automatic inverse or a silent retry; require current authority and explicit approval.
Related actions
app_topolo_developers.workspace_links.approveapp_topolo_developers.workspace_links.unlinkapp_topolo_developers.references.workspaces.list
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.link with exact state/access boundaries, complete synthetic unrun responses, verification and recovery limits. No application behavior change is asserted.