Topolo Docs Topolo Platform
Open workspace

Transfer workspace ownership

Transfer the primary workspace owner to an eligible same-organization app member, then separately update owner/member grants.

Outcome

Transfer the primary workspace owner to an eligible same-organization app member, then separately update owner/member grants.

These examples prove local handler behavior with real isolated SQL and explicitly simulated identity/permission dependencies.

Before you start

  • Verify identity and the current credential-scoped action contract before preparing input. Workspace-access operations require a consistent active organization context and target workspace authority; metadata discovery does not prove execution permission.

  • Discover the existing canonical workspace in the exact application/organization and intended recipient. These actions do not provision identities, organization memberships, application seats or entitlement.

  • Review the validated plan and intended effect before confirmation. Capture current owner and member grants before a write. Review new-owner eligibility and previous-owner retention separately before transferring ownership.

Discover the target

topolo whoami --json
topolo apps --json
topolo actions get app_topolo_auth.workspace_ownership.transfer --detail --json

Read app_topolo_auth.workspace_access.list_mine for this organization/app; copy data.resources[].id. For management read workspace_access.members.list. Recipient candidates require current active membership and app access.

Input fields

FieldRequiredMeaning
resourceIdYesCanonical workspace id copied from workspace_access.list_mine data.resources[].id, not a resourceId property. Use a current active workspace in the exact organization/application.
organizationIdYesExact organization owning this workspace. It is checked against both the authenticated context and canonical workspace record, independently of CLI selection.
appIdYesCanonical owning application id. This operation does not assign application entitlement or create a seat.
newOwnerUserIdYesExisting active same-organization application member selected through authorized discovery. Transferring ownership to the current owner is a conflict, not an idempotent success.
retainPreviousOwnerAccessNoDefaults to true: after the owner batch, the previous owner receives an admin member grant. false removes the previous owner grant. Grant cleanup/retention are separate writes after ownership changes, so failures require reading actual state.

Effects

  • Changes the primary workspace owner and ownership projection atomically, then separately removes the new owner member grant and retains/removes previous owner access.

  • Audit failures are nonfatal. A grant cleanup failure can return 500 after ownership has already changed.

Interpret the response

Raw target responses use success/message/data/timestamp. Grant returns HTTP 201; the other successes return 200. CLI/MCP envelopes may wrap or project the target response. Epoch-number record times differ from the ISO envelope timestamp.

list_mine data.resources contains canonical workspace records whose identifier is id. accessMode all and canManageAll true require organization owner/super_admin or valid platform authority; ordinary organization admin remains restricted. Owner and explicit unexpired invited_member grants drive the restricted projection.

members.list returns one owner, explicit member grants and at most 500 directory candidates. This directory has no continuation and is not guaranteed exhaustive. A candidate may still fail active membership/app assignment validation. No returned user data should be exported beyond the authorized workflow.

Member data.id is the grant id after profile and grant objects are combined; data.userId is the recipient identity id. Owner data.id is its user id. Owner profile can contain only id/name/accessRole when it is absent from the directory, and member profile fields can be absent. Profile activeContext/orgId describe that user context and do not replace the workspace organization boundary.

Grant is a natural-key upsert. Repetition changes scope, clears old revocation, resets expiry/metadata through creation defaults and preserves its id/createdAt. It does not create organization membership, application assignment, a seat or ownership. Audit persistence is nonfatal; successful grant/revoke/transfer does not prove an audit entry.

Member revoke returns userId/resourceId and removes explicit access only. Repetition returns NOT_FOUND. Owner removal returns CONFLICT. Organization-wide authority and independent application permissions still require separate verification.

The workspace owner and ownership projection update in one atomic access-database batch. Removing the new owner redundant grant and retaining/removing the previous owner grants occur afterward in separate writes; the full transfer is not atomic. A 500 can leave a new owner and a redundant member grant while previous-owner retention is unfinished.

A blind retry after partial transfer is unsafe: the former owner may be denied and a transfer to the now-current owner returns CONFLICT. Read members/current ownership first. The public revoke action refuses the current owner even if a redundant owner grant remains; this family has no customer cleanup operation for that partial state. Preserve evidence and seek authorized operator repair rather than inventing an endpoint or bypassing ownership.

Worked examples

These are complete raw target-handler responses, not CLI/gateway envelopes or live authorization proof. Replace synthetic identifiers using authorized discovery. Save only the reviewed input as payload.json; inspect validation and plan before executing. --confirm assumes the intended operation is approved. Metadata read-back requires separate recipient-principal and owning-application access tests.

Transfer removes new owner member grant and retains previous owner as admin

Input · payload.json:

{
  "organizationId": "org_example",
  "appId": "app_topolo_campaigns",
  "resourceId": "ws_fixture_one",
  "newOwnerUserId": "usr_new_owner"
}

CLI:

topolo actions validate app_topolo_auth.workspace_ownership.transfer --data @payload.json --json
topolo actions plan app_topolo_auth.workspace_ownership.transfer --data @payload.json --confirm --json
topolo actions call app_topolo_auth.workspace_ownership.transfer --data @payload.json --confirm --json

MCP call after validation and plan review:

{
  "tool": "topolo_call_action",
  "arguments": {
    "actionId": "app_topolo_auth.workspace_ownership.transfer",
    "input": {
      "organizationId": "org_example",
      "appId": "app_topolo_campaigns",
      "resourceId": "ws_fixture_one",
      "newOwnerUserId": "usr_new_owner"
    },
    "confirm": true
  }
}

Captured local handler response · HTTP 200:

{
  "success": true,
  "message": "Workspace ownership transferred",
  "data": {
    "ownership": {
      "organizationId": "org_example",
      "appId": "app_topolo_campaigns",
      "resourceId": "ws_fixture_one",
      "ownerUserId": "usr_new_owner"
    }
  },
  "timestamp": "2026-10-02T18:24:03.964Z"
}

Transfer back with no retained previous owner access

Input · payload.json:

{
  "organizationId": "org_example",
  "appId": "app_topolo_campaigns",
  "resourceId": "ws_fixture_one",
  "newOwnerUserId": "usr_owner",
  "retainPreviousOwnerAccess": false
}

CLI:

topolo actions validate app_topolo_auth.workspace_ownership.transfer --data @payload.json --json
topolo actions plan app_topolo_auth.workspace_ownership.transfer --data @payload.json --confirm --json
topolo actions call app_topolo_auth.workspace_ownership.transfer --data @payload.json --confirm --json

MCP call after validation and plan review:

{
  "tool": "topolo_call_action",
  "arguments": {
    "actionId": "app_topolo_auth.workspace_ownership.transfer",
    "input": {
      "organizationId": "org_example",
      "appId": "app_topolo_campaigns",
      "resourceId": "ws_fixture_one",
      "newOwnerUserId": "usr_owner",
      "retainPreviousOwnerAccess": false
    },
    "confirm": true
  }
}

Captured local handler response · HTTP 200:

{
  "success": true,
  "message": "Workspace ownership transferred",
  "data": {
    "ownership": {
      "organizationId": "org_example",
      "appId": "app_topolo_campaigns",
      "resourceId": "ws_fixture_one",
      "ownerUserId": "usr_owner"
    }
  },
  "timestamp": "2026-10-02T18:24:03.965Z"
}

Verify the result

  • Read members.list and require the new user in data.owner.id and no redundant entry in members[].userId. Check previous owner admin membership or absence according to retainPreviousOwnerAccess.

  • Verify new and previous owner access as their intended principals through the owning application. Ownership metadata does not prove every downstream application permission.

  • After a failure read current owner and remaining grants before any retry; former owner authority may have changed. Preserve the partial state for authorized operator repair when this action family cannot complete cleanup.

Errors and recovery

Ineligible transfer target is refused

Captured local refusal · HTTP 400:

{
  "input": {
    "organizationId": "org_example",
    "appId": "app_topolo_campaigns",
    "resourceId": "ws_fixture_one",
    "newOwnerUserId": "usr_inactive"
  },
  "response": {
    "success": false,
    "error": {
      "message": "New owner must be an active member of this organization with app access",
      "code": "VALIDATION_ERROR",
      "details": {
        "newOwnerUserId": true
      },
      "timestamp": "2026-10-02T18:24:03.964Z"
    }
  }
}

Transfer to current owner is a conflict

Captured local refusal · HTTP 409:

{
  "input": {
    "organizationId": "org_example",
    "appId": "app_topolo_campaigns",
    "resourceId": "ws_fixture_one",
    "newOwnerUserId": "usr_owner"
  },
  "response": {
    "success": false,
    "error": {
      "message": "User already owns this workspace",
      "code": "CONFLICT",
      "details": null,
      "timestamp": "2026-10-02T18:24:03.964Z"
    }
  }
}

Transfer can fail after atomic owner change before separate grant cleanup

Captured local refusal · HTTP 500:

{
  "input": {
    "organizationId": "org_example",
    "appId": "app_topolo_campaigns",
    "resourceId": "ws_fixture_one",
    "newOwnerUserId": "usr_new_owner"
  },
  "response": {
    "success": false,
    "error": {
      "message": "Isolated failure after owner batch",
      "code": "SERVER_ERROR",
      "details": null,
      "timestamp": "2026-10-02T18:24:03.965Z"
    }
  }
}

Blind retry by former owner is denied after authority changed

Captured local refusal · HTTP 403:

{
  "input": {
    "organizationId": "org_example",
    "appId": "app_topolo_campaigns",
    "resourceId": "ws_fixture_one",
    "newOwnerUserId": "usr_new_owner"
  },
  "response": {
    "success": false,
    "error": {
      "message": "Only the workspace owner or organization owner can transfer ownership",
      "code": "FORBIDDEN",
      "details": null,
      "timestamp": "2026-10-02T18:24:03.967Z"
    }
  }
}

Retry to already-current owner conflicts even for organization owner

Captured local refusal · HTTP 409:

{
  "input": {
    "organizationId": "org_example",
    "appId": "app_topolo_campaigns",
    "resourceId": "ws_fixture_one",
    "newOwnerUserId": "usr_new_owner"
  },
  "response": {
    "success": false,
    "error": {
      "message": "User already owns this workspace",
      "code": "CONFLICT",
      "details": null,
      "timestamp": "2026-10-02T18:24:03.967Z"
    }
  }
}

Published recovery guidance

  • state_conflict (HTTP 409): Read the current resource state, reconcile the intended change, and retry only if still valid.

  • VALIDATION_ERROR (HTTP 400): Correct the reported fields and use an existing active same-organization app member. Directory candidates alone do not prove eligibility.

  • AUTH_ERROR (HTTP 401): Verify the intended authenticated identity and refresh an expired token if necessary. Do not change credential context automatically.

  • FORBIDDEN (HTTP 403): Check consistent active organization context and exact workspace management authority. Personal/conflicting context is refused even with stale org metadata or platform roles.

  • NOT_FOUND (HTTP 404): Refresh canonical workspace/member discovery. An already revoked member grant is absent; do not fabricate a stored grant.

  • CONFLICT (HTTP 409): Read current owner and grants before reviewing any new decision. Do not grant/revoke the owner as a member or blindly repeat a completed/partial transfer.

  • SERVER_ERROR (HTTP 500): Read current owner and member grants before retrying. Ownership may already have changed. Preserve a partial owner/grant state for authorized repair when customer actions cannot complete cleanup.

Recovery boundaries

A transfer back is a new reviewed ownership decision, requiring current authority and eligible prior owner; it cannot blindly restore the old grant snapshot. After a partial outcome inspect owner/member grants first. Transfer to the now-current owner conflicts, and owner member revoke is refused. No customer cleanup endpoint is invented for a redundant current-owner grant; seek authorized operator repair and then verify both principals.

Discovery and evidence

This record belongs to app_topolo_auth. Local tests prove real canonical workspace/grant SQL and handler responses with simulated identity, directory lookup, memberships and application assignment. They do not prove recipient sign-in, invitation delivery, owning-application resource enforcement, live permissions, deployment or production completion.