Skip to content

Commit 11c0d3b

Browse files
feat(organizations): sweep a joiner's owned workspaces into the org on join, disclose it at accept, and add external workspace invites (#5918)
* feat(invites): explicit external members * update docs * fix(organizations): atomic admin workspace sweep, removal-impact status in dialog, and preview-unavailable disclosure Review round 1: the v1 admin add-member now commits membership and the workspace sweep in one transaction; the remove-member dialog holds confirm while the credential-impact check loads and shows a caution when it fails; a failed join preview flags joinPreviewUnavailable so the accept screen falls back to a generic migration notice. Also aligns the invite test's react-query mock and repairs two pre-existing docs type errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(organizations): close the concurrent-workspace escape in the join sweep Personal workspace creation now serializes with organization joins on the user's billing-identity lock and re-verifies membership inside its transaction; both join paths (invite acceptance and the v1 admin add) re-read the owned-workspace set under that lock after the member insert and roll the whole join back when it diverged from the advisory-lock plan, so a workspace created mid-join can never land outside the organization. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(organizations): fail stale-grant member joins and re-resolve the creation race client-side A member-role org acceptance whose grants all turned stale now rolls back with workspace-not-found instead of stranding a workspace-less member, and the workspace resolver treats the creation-vs-join 409 as a signal to re-resolve (the user is authenticated with org workspaces) rather than falling into the login path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invitations): mirror the stale-grant gate in the join preview The accept-screen preview now returns no-join for a member-role org invite whose grants all left the stamped organization, matching the acceptance-side rollback so the disclosure never promises a migration that acceptance would refuse. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(workspaces): survive the join race on lazy default creation and gate removal on live impact data The workspace list GET now re-lists (returning the join sweep's workspaces) when lazy default creation loses the race to an organization join instead of failing with a 500, and the remove-member dialog gates its confirm on isFetching so a background refetch can never let an admin confirm against a stale credential-impact list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invitations): surface the accept conflict message and refresh workspace caches post-accept The accept route now carries the human-readable message alongside the machine-readable error kind (the client prefers it for server-error), and a successful accept invalidates workspace queries so the swept workspaces appear immediately instead of after the stale window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(billing): sweep archived workspaces in Pro-to-Team conversion and org creation Every attach call site now passes includeArchived so the archived escape hatch is closed uniformly — join-attach, admin move, subscription-driven org provisioning, and manual org creation all sweep archived personal workspaces into the organization. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invitations): reject acceptance when the sweep set differs from the disclosed set The join preview now carries the workspace ids it disclosed, the accept screen echoes them back as a disclosure token, and acceptance rolls back with disclosure-outdated (409) whenever the set it would sweep no longer matches — a workspace created after the preview rendered can never move without the user seeing the refreshed notice first. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invitations): send the disclosure token for no-join previews too A preview that predicted no join still tells the user nothing moves — the empty disclosed set is now echoed on accept, so a join that becomes possible between preview and accept (left another org, billing turned usable, grants un-staled) conflicts with disclosure-outdated instead of sweeping workspaces without a rendered notice. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invitations): gate all-stale member joins before disclosure and always refetch removal impact The all-stale check for member-role org invites now runs before any mutation and before the disclosure comparison, so an invite whose grants all left the org fails with workspace-not-found instead of trapping owners of personal workspaces in a disclosure-outdated retry loop. The removal-impact query drops its stale window (staleTime 0): every dialog open refetches while the confirm is held on isFetching. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(organizations): keep-external on org recovery and label archived move candidates Org creation/recovery now uses the keep-external collaborator policy (matching Pro-to-Team conversion) so different-org collaborators on archived workspaces cannot abort it with a conflict, and the admin workspace-move search and preflight expose an archived flag so internal tooling can label archived targets. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invitations): guard the reverse disclosure direction A will-join notice whose acceptance downgrades to no-join (stale escalation denial, concurrent other-org membership) now fails with disclosure-outdated instead of silently succeeding as an external grant — the disclosure token binds the outcome in both directions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * address comments * fix * fix(invitations): one invite surface, coalesced grants, coherent seat model Consolidate the two invite modals into a single surface and fix the semantics the split had been hiding. Invite flow: - One InviteModal for all three entry points (workspace header, workspace settings, organization settings), with a workspace multi-select and an explicit Membership choice that states the seat consequence. - Coalesce grants instead of 500ing. A partial unique index allows one pending invitation per (email, organization), so inviting someone to a second workspace raised a raw 23505. New workspaces now merge into the pending invitation (with a retry for the concurrent-insert race) and the invitee gets one link covering everything. - External collaborators require their own paid plan, checked at invite time and re-checked on accept, since the invitation lives for 7 days. Imposed externality is exempt in both places: an invitee already in another organization is forced external regardless of the inviter's choice, so the plan gate must not apply to them. - Every invitation must grant at least one workspace, so accepting always lands somewhere. Enforced at creation for all roles. - Revocation is grant-scoped. Since one invitation can span workspaces, revoking from a workspace's member list withdraws only that grant and cancels the invitation just when the last one goes. Whole-invitation revocation now requires authority over all of it rather than admin on any single granted workspace. - The accept screen names every granted workspace and states whether the invitee joins as a member, an admin, or an external collaborator, and whether that uses a seat. Seats: - One rule for the pending-invitation predicate, seat capacity, and the derived figures; the three counting sites now share it. - Team seats are elastic (subscription.seats tracks the member count), so treating that as a cap reported negative headroom on any outstanding invite. Available seats are clamped and gates branch on whether the plan actually has a fixed cap. - POST /api/v1/admin/organizations/[id]/members could never succeed on Team: it validated N members against N seats. It now skips the cap for elastic plans, matching invitation acceptance, and reconciles seats after a committed add. Also surfaces the External label on the workspace Teammates list, which already received the flag and dropped it, and removes dead code: the grantless organization-invite route and contract, three unreferenced invitation helpers, and the unreachable ensureUserInOrganization/addUserToOrganization/validateMembershipAddition cluster. Co-Authored-By: Claude <noreply@anthropic.com> * fix(invitations): close the two accept-disclosure gaps Bugbot found Membership notice ignored the join preview. `buildMembershipNotice` keyed off the invitation's sent `membershipIntent`, but acceptance resolves an internal invite to external when the invitee already belongs to another organization, or when the granted workspace changed organizations after the invite went out. The screen therefore promised "you'll join as a member, which uses one of their seats" to people who would neither join nor consume a seat. It now keys on the preview's `willJoinOrganization` — the same signal the migration notice already used — and falls back to the sent intent only when no preview could be computed. In-app accept skipped the disclosure entirely. `useAcceptMyInvitation` posted an empty body, so `disclosedWorkspaceIds` was absent and the server's consent guard was skipped, and the pending-invitations modal never showed which owned workspaces would move. Accepting from the workspace switcher (including the desktop path) could silently sweep personal workspaces into the organization, bypassing the consent model this PR adds on /invite. The list endpoint now returns each invitation's join preview, the modal renders the same membership/migration disclosure as /invite, and accept echoes `disclosedWorkspaceIds` so the guard applies on both paths. Both notices moved into lib/invitations/disclosure-copy.ts and are consumed by /invite and the modal, so the two accept surfaces cannot drift into disclosing different outcomes for the same invitation — which is how this gap arose. Co-Authored-By: Claude <noreply@anthropic.com> * fix(invitations): carry the membership outcome in the disclosure token Empty disclosure skipped membership consent. The token was only the workspace-id list, so a no-join preview and a will-join preview for someone who owns nothing both echoed `[]`. Neither guard could tell those apart: the forward check compares sweep sets, and the reverse check required a non-empty disclosed set. An invitee who left their other organization between preview and accept would therefore be silently made a seat-consuming member after being told they would stay external, and the mirror case could silently demote a promised join. The accept body now also carries `disclosedWillJoinOrganization`, compared against the resolved outcome before any write, so consent covers the membership decision and not just the migration. Both accept surfaces send it. This was widened by the previous commit: keying the membership notice on the preview made the screen promise a join outcome the token never verified. In-app accept errors lacked copy. `getInvitationErrorMessage` omitted `external-requires-paid-plan`, `disclosure-outdated`, and `workspace-not-found`, so those failures fell through to the generic "may have expired" fallback. `disclosure-outdated` became newly reachable in-app the moment that path started sending the token, so the gap arrived with the fix for it. Co-Authored-By: Claude <noreply@anthropic.com> * fix(invitations): compare the join disclosure against new-membership creation The membership consent guard compared the disclosed outcome against `shouldJoinOrganization`, which stays true for an invitee who already belongs to the target organization — the invitation's intent is still internal. The join preview reports no-join for exactly that case, because nothing changes for them. Every such acceptance therefore failed `disclosure-outdated`, and the retry re-rendered the same preview, so the invitation became permanently unacceptable. The guard now compares against whether acceptance creates a NEW membership (`shouldJoinOrganization && !alreadyMemberOfTargetOrganization`), which is what the disclosure actually promises and what the preview reports. The already-a-member predicate is hoisted and shared with the join block below so the guard and the billing path cannot disagree about it. Regression test asserts a pre-existing member accepts with a no-join disclosure; it fails with `disclosure-outdated` against the previous comparison. Co-Authored-By: Claude <noreply@anthropic.com> * fix(invitations): tell an existing member their standing is unchanged The join preview reported the same no-join shape for two different outcomes: an external collaborator, and an invitee who already belongs to the organization acceptance lands in. `buildMembershipNotice` rendered both as "you'll join as an external collaborator ... everything you own stays yours", which is wrong for an existing member — they stay an internal member and simply gain the granted workspaces. The preview now reports `alreadyMemberOfOrganization` for that case (a membership in a DIFFERENCE organization is still the external path, since acceptance downgrades), and the notice states that standing is unchanged. This is the same conflation behind the accept loop fixed in 4eb7725, now removed from the shape itself rather than worked around per consumer. Also re-verify the removal-impact disclosure at the moment of confirmation. `isFetching` only holds the confirm button while a request is in flight, so an identity-bound credential the member gained after the fetch settled would break on removal without ever being disclosed. Confirm now refetches and, if the set changed, keeps the dialog open on the refreshed warning instead of proceeding. Co-Authored-By: Claude <noreply@anthropic.com> * fix(invitations): serialize the join-preview reads, revert the removal refetch Two corrections to this branch's own review fixes. The pending-invitations list computed each row's join preview with Promise.all. Every preview issues several queries, and the endpoint is hit whenever the workspace switcher opens, so that held one pooled connection per pending invitation for as long as the slowest one took. The loop is sequential now; the list is a handful of rows, so the latency is not worth the pool pressure. The removal-impact refetch on confirm is reverted. It changed behaviour — a click could silently do nothing — and it did not actually close the window it targeted: the credential set can still change between the refetch and the independent DELETE, because the removal endpoint neither receives nor revalidates the disclosed set. Closing that properly means passing the disclosure to the endpoint and revalidating there, which is a feature rather than a review fix, so the prior behaviour stands until it is done deliberately. Co-Authored-By: Claude <noreply@anthropic.com> * fix(invitations): disclose the seat on a personal-workspace join Both accept surfaces scoped the membership notice on `organizationId` or the preview's `organizationName`. A personal-workspace invite has neither until acceptance runs — it creates the organization by converting the billed owner's Pro to Team — so the seat and membership disclosure was suppressed for exactly the case that creates the membership. They now also scope on the preview's `willJoinOrganization`, which is the authoritative signal. Co-Authored-By: Claude <noreply@anthropic.com> * fix(invitations): make the join preview a discriminated outcome The preview returned one no-join shape for five different results — external intent, already a member, a membership in another organization, a dead-grant rejection, and a billing rejection. Two accumulating booleans could not separate them, and the accept screen rendered the external copy for all of them: it told people whose acceptance would fail with `upgrade-required` or `workspace-not-found` that they were getting workspace access without a seat. It now reports one `outcome`: `will-join` (a seat is taken), `already-member` (only workspace access changes), `external` (never a seat), or `blocked` (acceptance fails, so nothing is promised). `blocked` renders no membership notice — silence is accurate where the external claim was false. The accept button is deliberately left enabled: those cases already fail closed with the correct error, and choosing what to actively tell someone whose organization's payment lapsed is a product decision, not a review fix. This also fixes a live mis-attribution the previous commit's guard introduced. The consent check ran before the gates that produce the real cause, so a blocked invitation returned `disclosure-outdated` — and the retry re-rendered the same preview, leaving the invitee looping with no explanation. The guard now sits after the dead-grant gate, and a disclosed `blocked` skips the comparison so the billing gate below can surface `upgrade-required` instead. The accept body carries `disclosedOutcome` in place of the boolean; the membership comparison is unchanged (`will-join` versus a new membership being created), so no acceptance that previously succeeded now fails. Co-Authored-By: Claude <noreply@anthropic.com> * fix(invitations): let billing-disabled personal invites be accepted The consent guard derived "a membership will be created" from `shouldJoinOrganization`, which is still true at that point — it is only cleared much later, after provisioning fails to yield a target organization. With billing disabled and no organization on the workspace there is nothing to provision and nothing to join, so the preview correctly reports `external` while the guard computed `will-join`, rejecting every personal and grandfathered workspace invite as `disclosure-outdated`. The retry rendered the same preview, so those invites could not be accepted at all on billing-disabled deployments. The predicate now mirrors the preview's own condition, so the two cannot drift. Regression test asserts acceptance succeeds with billing off; it fails with `disclosure-outdated` against the previous predicate. Co-Authored-By: Claude <noreply@anthropic.com> * fix(invitations): allow External with billing off, mark cross-org org invites blocked The External paid-plan requirement is seat economics — an external collaborator takes no seat, so somebody else must be paying for them. With billing disabled there are no seats and no subscription rows at all, so every account resolves as `free` and choosing External failed at send time and would have failed at accept. Member and Admin still worked, so a self-hosted deployment had no way to grant workspace-only access without an organization join and a workspace sweep. Both the invite-time and accept-time gates now short-circuit when billing is off. A route test asserted that rejection without setting `isBillingEnabled`, which the shared mock defaults to false — it passed only because the gate ignored the flag. It now opts in explicitly, since the rule it covers is billing-only. Separately, the preview reported `external` for any invitee already in a different organization, but acceptance only downgrades a workspace-kind invite with live grants; an organization-kind invite hard-fails with `already-in-organization`. Those now report `blocked`, so the screen stops promising external access that acceptance can never grant. Legacy organization-kind rows still exist and coalescing preserves that kind, so this is reachable. Co-Authored-By: Claude <noreply@anthropic.com> * fix(invitations): require org admin to grant org Admin, and two disclosure gaps Privilege escalation. `createWorkspaceInvitation` stamped organization role `admin` whenever the caller passed `membership: 'admin'`, but authorization only checked workspace admin access — and unifying the invite modals exposed the Admin option to any workspace admin, where it had previously been reachable only from organization settings. A workspace-scoped administrator could therefore invite someone who joins as an organization Admin, gaining admin on every workspace the organization owns plus member and billing management. The inviter must now already hold organization owner/admin, checked server-side because the batch endpoint is reachable without the modal, and the modal no longer offers Admin to anyone else. The preview promised external access without mirroring acceptance's `external-requires-paid-plan` gate, so a free invitee — one who cancelled Pro, or left the organization that forced the external invite — was told they had workspace access and then refused. It now mirrors that gate, including its exemptions (billing on, organization-owned workspace, externality not imposed), and reports `blocked`. The modal's Enterprise seat check counted every non-External email as a seat. The server does not: an existing organization member is granted access directly, and an invitee already in another organization is forced external. The hard block refused batches the API would have accepted, so it is advisory now — per-email failures already come back with reasons. Co-Authored-By: Claude <noreply@anthropic.com> * fix(invitations): restore the invite admin gate, hedge an unknown outcome Consolidating the modals dropped a permission check. The old workspace-header modal derived `canInviteMembers` from `userPermissions.canAdmin` internally and disabled its field and button; the shared modal takes `canInvite` as a prop that defaults to true, and no call site passed it. Non-admins therefore saw a fully enabled invite form and only learned otherwise when the server refused the send. All three entry points now supply it: workspace admin for the header, the existing `canManage` for the workspace Teammates page, and organization owner/admin for organization settings. When the join preview cannot be computed the outcome is unknown, and the callers send no disclosure token — so acceptance runs without the consent guards. The membership notice nevertheless asserted a seat-taking join from the sent intent, which acceptance may resolve to external, already-a-member, or a failure. It is conditional now ("If you're added to X as a member, that uses one of their seats"), so the consequence is still disclosed without being claimed as settled. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 8260891 commit 11c0d3b

65 files changed

Lines changed: 5676 additions & 3037 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/docs/app/[lang]/[[...slug]]/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
121121
const urlParts = page.url.split('/').filter(Boolean)
122122
let currentPath = ''
123123

124-
urlParts.forEach((part, index) => {
124+
urlParts.forEach((part: string, index: number) => {
125125
if (index === 0 && SUPPORTED_LANGUAGES.has(part)) {
126126
currentPath = `/${part}`
127127
return
@@ -131,7 +131,7 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
131131

132132
const name = part
133133
.split('-')
134-
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
134+
.map((word: string) => word.charAt(0).toUpperCase() + word.slice(1))
135135
.join(' ')
136136

137137
if (index === urlParts.length - 1) {

apps/docs/app/api/chat/route.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { openai } from '@ai-sdk/openai'
2-
import { convertToModelMessages, stepCountIs, streamText, tool, type UIMessage } from 'ai'
2+
import {
3+
convertToModelMessages,
4+
jsonSchema,
5+
stepCountIs,
6+
streamText,
7+
tool,
8+
type UIMessage,
9+
} from 'ai'
310
import { sql } from 'drizzle-orm'
4-
import { z } from 'zod'
511
import { db, docsEmbeddings } from '@/lib/db'
612
import { generateSearchEmbedding } from '@/lib/embeddings'
713

@@ -332,8 +338,21 @@ export async function POST(req: Request) {
332338
searchDocs: tool({
333339
description:
334340
'Search the Sim documentation for relevant content. Use this before answering any question about Sim.',
335-
inputSchema: z.object({
336-
query: z.string().describe('A focused natural-language search query.'),
341+
/**
342+
* The SDK's own schema helper instead of a zod schema: the `ai`
343+
* package's zod-v4 typings lag the workspace zod version, so a zod
344+
* object here fails the tool() overloads whenever the two drift.
345+
*/
346+
inputSchema: jsonSchema<{ query: string }>({
347+
type: 'object',
348+
properties: {
349+
query: {
350+
type: 'string',
351+
description: 'A focused natural-language search query.',
352+
},
353+
},
354+
required: ['query'],
355+
additionalProperties: false,
337356
}),
338357
execute: async ({ query }) => searchDocs(query, locale),
339358
}),

apps/docs/content/docs/en/platform/costs.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ By default, your usage is capped at the credits included in your plan. To allow
321321
| **Max** | Up to 10 ||
322322
| **Team / Enterprise** || Unlimited (Owners and Admins) |
323323

324-
Team and Enterprise plans unlock shared workspaces that belong to your organization. Every workspace created under a Team or Enterprise plan is organization-owned: Owners and Admins can create unlimited shared workspaces, while organization Members cannot create workspaces (personal workspaces created before joining the organization remain accessible). Internal members invited to a shared workspace join the organization and count toward your seat total — Enterprise invites require an available seat at invite time, while Team plans add a seat automatically when the invitee accepts. Existing Sim users who already belong to another organization can be added as external workspace members; they get workspace access without joining your organization or using one of your seats. When a Team or Enterprise subscription is cancelled or downgraded, existing shared workspaces remain accessible to current members but new invites are disabled until the organization is upgraded again.
324+
Team and Enterprise plans unlock shared workspaces that belong to your organization. Every workspace created under a Team or Enterprise plan is organization-owned: Owners and Admins can create unlimited shared workspaces, while organization Members cannot create workspaces. When someone joins the organization as an internal member, personal workspaces they own move into the organization and their usage is billed to it from then on. Internal members count toward your seat total — Enterprise invites require an available seat at invite time, while Team plans add a seat automatically when the invitee accepts. External workspace members get access to specific workspaces without joining your organization or using one of your seats; they must already be on a paid Sim plan (their own Pro or Max subscription, or another organization that seats them), and invitees who already belong to another organization always join this way. When a Team or Enterprise subscription is cancelled or downgraded, existing shared workspaces remain accessible to current members but new invites are disabled until the organization is upgraded again.
325325

326326
### Rate Limits
327327

apps/docs/content/docs/en/platform/permissions.mdx

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Inherited roles are **automatic and locked**. In member lists they show greyed o
3737
Sim has two kinds of workspaces:
3838

3939
- **Personal workspaces** live under your individual account. The number you can create depends on your plan.
40-
- **Shared (organization) workspaces** live under an organization and are available on Team and Enterprise plans. Any organization Owner or Admin can create them. Internal members invited to a shared workspace join the organization and count toward your seat total. Existing Sim users who already belong to another organization can be added as external workspace members instead, giving them access to the workspace without adding them to your organization roster or using one of your seats.
40+
- **Shared (organization) workspaces** live under an organization and are available on Team and Enterprise plans. Any organization Owner or Admin can create them. When you invite someone to a shared workspace you choose their **Membership**: **Member** or **Admin** makes them an internal member of the organization — they use a seat, and their own workspaces move in with them (see [Joining an organization](#joining-an-organization)) — while **External** gives them access to the selected workspaces only. Invitees who already belong to another organization always become external, because a Sim account belongs to at most one organization.
4141

4242
### Workspace Limits by Plan
4343

@@ -48,7 +48,7 @@ Sim has two kinds of workspaces:
4848
| **Max** | Up to 10 ||
4949
| **Team / Enterprise** || Unlimited (Owners and Admins) |
5050

51-
On Team and Enterprise plans, every workspace you create belongs to the organization. Organization Owners and Admins can create unlimited shared workspaces; organization Members cannot create workspaces. Personal workspaces created before joining the organization remain accessible. Enterprise invites require an available seat at invite time; on Team plans, a seat is added automatically when the invitee accepts.
51+
On Team and Enterprise plans, every workspace you create belongs to the organization. Organization Owners and Admins can create unlimited shared workspaces; organization Members cannot create workspaces. Personal workspaces you owned before joining move into the organization when you accept the invite — see [Joining an organization](#joining-an-organization). Enterprise invites require an available seat at invite time; on Team plans, a seat is added automatically when the invitee accepts.
5252

5353
<Callout type="info">
5454
When a Team or Enterprise subscription is cancelled or downgraded, existing shared workspaces stay accessible to current members. New invitations are blocked until the organization is upgraded again.
@@ -60,6 +60,12 @@ On Team and Enterprise plans, every workspace you create belongs to the organiza
6060
<Video src="invitations.mp4" width={700} height={450} />
6161
</div>
6262

63+
One invite covers everything you give a person at once: enter their emails, select **one or more workspaces**, set the workspace access level, and choose their **Membership**. The same dialog opens from the workspace header and from organization settings.
64+
65+
Invites to the same person add up rather than stacking. If you invite someone to another workspace while their first invite is still pending, the new workspace is added to that invitation — they get one link that grants everything, and accept once.
66+
67+
Because one pending invite can span several workspaces, revoking is scoped to where you do it. Revoking from a workspace's **Teammates** list withdraws that workspace's access only and leaves the rest of the invitation pending; the invitation is cancelled outright when you remove its last workspace. To cancel the whole thing at once, revoke it from organization settings — that needs organization admin, or admin on every workspace the invite covers.
68+
6369
## Workspace Permission Levels
6470

6571
When inviting someone to a workspace, you can assign one of three permission levels:
@@ -75,10 +81,29 @@ When inviting someone to a workspace, you can assign one of three permission lev
7581
Workspace permissions are separate from organization membership:
7682

7783
- **Internal organization members** belong to your organization, appear in the organization roster, and count toward your seat total. Invite new teammates this way when they should be part of your company or team in Sim.
78-
- **External workspace members** have access only to the workspace they are invited to. They keep their own organization membership, do not appear in your organization roster, and do not count toward your organization's seats. Use external access for clients, partners, contractors, or collaborators who already use Sim in another organization.
84+
- **External workspace members** have access only to the workspaces they are invited to. They are not part of your organization: they do not count toward your seats, their own workspaces stay theirs, and they appear in your roster with an **External** label so admins can always see and revoke their access. Use external access for clients, partners, and contractors.
85+
86+
You pick between the two with the **Membership** option when sending an invite. Three rules apply:
87+
88+
- **External is only available for people already on a paid Sim plan** — their own Pro or Max subscription, or membership in another organization that seats them. External collaborators do not use one of your seats, so they have to be paying for Sim somewhere else. Inviting someone on the free plan as External is rejected with a message telling you to invite them as a Member or Admin instead, which adds a seat.
89+
- **Invitees who already belong to another organization are always external**, whatever you pick, because an account belongs to at most one organization.
90+
- **Inviting someone as a Member to the organization itself always includes access to at least one workspace**, so every new member has somewhere to land.
91+
92+
Invitees see what they are agreeing to before they accept. The accept screen names every workspace they are being given, says whether they are joining as a member, an admin, or an external collaborator, says whether that uses one of your seats, and names any of their own workspaces that will move into the organization.
7993

8094
External workspace members still receive a workspace permission level — Read, Write, or Admin — and that permission controls what they can do inside the workspace.
8195

96+
## Joining an organization
97+
98+
Accepting an invite that makes you an **internal member** does more than grant access — it brings your work under the organization:
99+
100+
- **Your workspaces move with you.** Every personal workspace you own, archived ones included, becomes an organization workspace. The accept screen names the workspaces that will move before you accept.
101+
- **You keep Admin on them.** You remain their administrator; organization Owners and Admins also gain Admin access through the normal [role inheritance](#how-roles-inherit), and usage in those workspaces is billed to the organization from then on.
102+
- **Your collaborators are not pulled in.** Anyone you had shared those workspaces with keeps their access as an **external member** — they never join the organization or consume a seat as a side effect of your joining.
103+
- **The workspaces stay if you leave.** If you later leave or are removed from the organization, workspaces you brought in remain with the organization.
104+
105+
Accepting an **external** invite changes none of this: you get access to the invited workspace only, and everything you own stays yours.
106+
82107
## What Each Permission Level Can Do
83108

84109
Here's a detailed breakdown of what users can do with each permission level:
@@ -147,7 +172,7 @@ Any Admin — whether invited directly or an admin by way of their organization
147172
2. **Workspace level**: Give them **Admin** permission so they can manage the team and see everything
148173

149174
### Adding a Stakeholder or Client
150-
1. **Organization level**: If they should not join your organization, add them as an **External workspace member**
175+
1. **Membership**: If they should not join your organization, set Membership to **External** — this needs them to already be on a paid Sim plan, since external collaborators do not use one of your seats
151176
2. **Workspace level**: Give them **Read** permission so they can see progress but not make changes
152177

153178
---
@@ -221,11 +246,13 @@ import { FAQ } from '@/components/ui/faq'
221246

222247
<FAQ items={[
223248
{ question: "What is the difference between organization roles and workspace permissions?", answer: "Organization roles (Owner, Admin, or Member) control who can manage the organization itself, including inviting people, creating shared workspaces, and handling billing. Workspace permissions (Read, Write, Admin) control what a user can do within a specific workspace, such as viewing, editing, or managing workflows. Internal members need both an organization role and a workspace permission to work within a shared workspace. External workspace members do not have an organization role in your org; they only have workspace-level access." },
249+
{ question: "What happens to my personal workspaces when I join an organization?", answer: "They move into the organization when you accept the invite — archived workspaces included. The accept screen names the workspaces that will move before you accept. You stay their Admin, organization Owners and Admins gain Admin access through role inheritance, and usage in them is billed to the organization from then on. People you had shared them with keep access as external members. If you later leave the organization, those workspaces stay with it. Accepting an external invite never moves anything — everything you own stays yours." },
250+
{ question: "What happens when a member is removed from the organization?", answer: "Their workspace access is revoked and workflows they created are reassigned so deployed automations keep running. Credentials backed by their personal identity — OAuth connections and personal environment keys they own — stop working until another member reconnects them; the removal dialog names the affected credentials before you confirm. Workspaces they brought into the organization stay with the organization." },
224251
{ question: "What happens to my shared workspaces if I cancel or downgrade my Team plan?", answer: "Existing shared workspaces remain accessible to current members, but new invitations are disabled until you upgrade back to a Team or Enterprise plan. No workspaces or members are deleted — the organization is simply dormant until billing is re-enabled." },
225252
{ question: "Can I restrict which integrations or model providers a team member can use?", answer: "Yes, on Enterprise-entitled organizations. Any organization owner or admin can create permission groups with fine-grained controls, including restricting allowed integrations and allowed model providers to specific lists. You can also disable access to MCP tools, custom tools, skills, and various platform features like the knowledge base, API keys, or Copilot on a per-group basis. Permission groups are scoped to the organization and can govern either all workspaces or a specific subset — a user can belong to multiple groups but is governed by exactly one group in any given workspace." },
226253
{ question: "What happens when a personal environment variable has the same name as a workspace variable?", answer: "The workspace variable wins. When a workflow runs, the resolver checks workspace variables first and falls back to a personal variable only when no workspace variable shares that name. This keeps shared, team-managed values authoritative in production workflows." },
227254
{ question: "Can an Admin remove the workspace owner?", answer: "On a shared (organization) workspace, yes — any Admin can remove the workspace Owner, and ownership passes to the organization's owner so the workspace always has one. The organization's owner is the single account that can't be removed this way, since they're the final fallback. On your personal workspace you are the Owner and can't remove yourself. The Owner is not a higher permission tier than Admin: every Admin — including those who inherit the role from their organization — can manage members and settings and delete the workspace." },
228255
{ question: "Who can manage a workspace's credentials and secrets?", answer: "Workspace Admins are automatically Credential Admins of the workspace's shared credentials — OAuth connections, service accounts, and workspace environment variables — so they can use, edit, delete, and share them, and run workflows that rely on them. Organization Owners and Admins get this too because they are workspace Admins everywhere. Read and Write members get use-only access to shared credentials unless they are explicitly made a Credential Admin. Personal environment variables are never shared; they stay private to their owner." },
229256
{ question: "What are permission groups and how do they work?", answer: "Permission groups are an Enterprise access control feature that lets organization owners and admins define granular restrictions beyond the standard Read/Write/Admin roles. The organization's default group is org-wide; every other group targets specific workspaces and, by default, governs all members of those workspaces (including external members) — add members to restrict it to specific people. A user is governed by one group per workspace: a group they're an explicit member of takes precedence over an all-members group (one with no members) on that workspace, which takes precedence over the organization's default group. A permission group can hide UI sections (like trace spans, knowledge base, API keys, or deployment options), disable features (MCP tools, custom tools, skills, invitations), and restrict which integrations and model providers its members can access. Only one group per organization can be the default; it ignores members and governs everyone not covered by a workspace group, including external members. Restrictions are enforced based on the organization that owns the workflow's workspace, not on which workspace you're currently viewing." },
230-
{ question: "How should I set up permissions for a new team member?", answer: "Start with the lowest permission level they need. Invite teammates to the organization as Members, then add them to the relevant workspace with Read permission if they only need visibility, Write if they need to create and run workflows, or Admin if they need to manage the workspace and its users. For clients, partners, or users who already belong to another Sim organization, use external workspace access so they can collaborate without joining your organization or consuming a seat." },
257+
{ question: "How should I set up permissions for a new team member?", answer: "Start with the lowest permission level they need. Invite them with Read workspace access if they only need visibility, Write if they need to create and run workflows, or Admin if they need to manage the workspace and its users, and leave Membership on Member. For clients, partners, and contractors, choose External so they collaborate without joining your organization or using a seat — this requires them to already be on a paid Sim plan, either their own Pro or Max subscription or another organization that seats them." },
231258
]} />

0 commit comments

Comments
 (0)