> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sublay.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Remove From Subtree

> Offboarding convenience — remove a user from this workspace and the descendants you can reach

Removes the target user's **direct** memberships on this workspace and every descendant **you can reach**, in one action. An **owner** (or ancestor owner) reaches the whole subtree via the god-path; a **non-owner** reaches a descendant when they hold `remove-member` there — which they may hold either by reaching in through an unbroken open inherit chain, or by holding a direct member row on that node. So sealing (`inheritsFromParent = false`) stops authority reaching in **from above**; it does not stop authority held **locally**. A non-owner who is a direct member of a sealed child with `remove-member` there does sweep it. **The credential does not widen this** — `actingUserId` is *required* here, so a service/master key always sweeps as the user it names, with that user's reach; act as the owner for a full sweep. Reach-based access auto-revokes with the relevant ancestor membership. Each membership-row deletion fires `workspace.member.removed`. Rank rules apply **per node** — the sweep is refused (`403 workspace/insufficient-rank`) if you are outranked by the target at any node where you both hold a direct member row.

**Owned-descendant handling — block + report.** Because owners have no member row, this sweep does **not** cover descendant workspaces the user *owns*. Instead it **blocks and reports** them (`409 workspace/owns-descendants`) rather than silently orphaning ownership. An admin or any ancestor owner then reassigns each via [transfer-ownership](/v8/api-reference/workspaces/transfer-ownership) or deletes it. **No silent ownership change.**

<Warning>
  **A non-owner's sweep may be partial — check `skipped` before you call someone offboarded.** If the target still holds a membership in a descendant you cannot reach, the `200` reports it in `skippedCount` / `skipped`. `removedCount` alone does **not** mean the user is gone from the subtree: treat any `skippedCount > 0` as "still a member somewhere" and escalate to an owner or ancestor owner of that node. Only an **owner or ancestor owner** is guaranteed `skippedCount: 0`. A service/master key is **not** — it must name an `actingUserId` on this route, and inherits that user's reach, so a key acting as a mid-level admin can absolutely come back with `skippedCount > 0`. See [Acting on behalf of a user](/v8/sdk/workspaces/overview#acting-on-behalf-of-a-user).
</Warning>

## Path Parameters

<ParamField path="id" type="string" required>
  The workspace UUID (subtree root).
</ParamField>

<ParamField path="userId" type="string" required>
  The user to offboard.
</ParamField>

## Body Parameters

<ParamField body="actingUserId" type="string" required>
  Service/master keys only — the **acting** user (must hold `remove-member`, rank-bounded per node). **Required** for a key: there is no unbounded path on this route, so the sweep's reach is always that user's — a key that names nobody is refused with `400 workspace/missing-user-id`. Name the **owner** for a full-subtree sweep.

  A plain user token omits it — the actor is the token's own user. The field is **not** ignored for such a token: sending your *own* user id is a harmless no-op, but sending **anyone else's** is a `403 workspace/unauthorized`. See [Acting on behalf of a user](/v8/sdk/workspaces/overview#acting-on-behalf-of-a-user).
</ParamField>

## Response

```json theme={null}
{
  "removedCount": 3,
  "removed": [
    { "workspaceId": "ws_a", "userId": "u_pat" },
    { "workspaceId": "ws_b", "userId": "u_pat" },
    { "workspaceId": "ws_c", "userId": "u_pat" }
  ],
  "skippedCount": 2,
  "skipped": [
    { "id": "ws_d", "name": "Finance", "reason": "out-of-reach" },
    { "id": null, "name": null, "reason": "out-of-reach" }
  ]
}
```

<ResponseField name="removedCount" type="number">
  How many membership rows were torn down.
</ResponseField>

<ResponseField name="removed" type="object[]">
  One `{ workspaceId, userId }` entry per removed membership.
</ResponseField>

<ResponseField name="skippedCount" type="number">
  How many memberships the target **retained** because the sweep could not reach them. `0` for an owner or ancestor owner. A service/master key gets whatever its **acting user** gets — the credential grants no extra reach here. Equals `skipped.length`.
</ResponseField>

<ResponseField name="skipped" type="object[]">
  One entry per retained membership — the descendants where the target is *still* a member after this call.

  * `id` / `name` — the workspace, mirroring the `ownedWorkspaces` shape on the 409. **Both are `null` when you have no standing on that workspace**: the sweep tells you a membership survived, but does not disclose the existence or name of a sealed sub-workspace you have no authority over. This is the same sealing fence the [roster read](/v8/api-reference/workspaces/members/list-members) applies to `include=descendants`. Get an owner or ancestor owner of that branch to finish the offboarding.
  * `reason` — currently always `"out-of-reach"`: your authority does not extend to removing members there.

  Only descendants where the target **actually still holds a direct membership** appear. Unreachable descendants the target was never a member of are never listed — the report is scoped to what actually survived the offboarding, not to the shape of the tree.
</ResponseField>

<Note>
  `skipped` covers surviving **memberships** only. Descendants the target *owns* are handled by the separate `409 workspace/owns-descendants` block, and an owned descendant outside a non-owner's reach is neither swept nor reported here — an owner re-running the sweep will surface it as the 409.
</Note>

## Error Responses

<AccordionGroup>
  <Accordion title="Owns Descendants — 409">
    ```json theme={null}
    {
      "error": "The user owns workspaces in this subtree. Transfer or delete them first.",
      "code": "workspace/owns-descendants",
      "ownedWorkspaces": [{ "id": "ws_d", "name": "Client X" }]
    }
    ```
  </Accordion>

  <Accordion title="Insufficient Rank — 403">
    ```json theme={null}
    { "error": "You may only remove members ranked strictly below you.", "code": "workspace/insufficient-rank" }
    ```
  </Accordion>

  <Accordion title="Missing Acting User — 400">
    ```json theme={null}
    { "error": "Missing user ID", "code": "workspace/missing-user-id" }
    ```

    A service/master key called this route without naming an `actingUserId`. The sweep's reach is defined by the acting user's standing per node, so it must be performed *as* somebody.
  </Accordion>

  <Accordion title="Acted as Another User — 403">
    ```json theme={null}
    { "error": "Unauthorized", "code": "workspace/unauthorized" }
    ```

    A plain user token sent an `actingUserId` naming a **different** user. Only a service/master key may act as someone else.
  </Accordion>

  <Accordion title="Invalid Path Parameter — 400">
    ```json theme={null}
    { "error": "Invalid workspace id: expected a UUID.", "code": "workspace/invalid-params" }
    ```

    Every path id on the workspaces bundle is checked for UUID shape before the route runs, so a malformed one is a plain `400` rather than a `500` from the database.
  </Accordion>
</AccordionGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.