diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9c110d101e7..92b44041af3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -231,7 +231,7 @@ jobs:
github.event_name == 'push' &&
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging' || github.ref == 'refs/heads/dev')
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
- timeout-minutes: 15
+ timeout-minutes: 30
outputs:
version: ${{ steps.deploy.outputs.deploymentVersion }}
environment: ${{ steps.target.outputs.environment }}
@@ -289,7 +289,7 @@ jobs:
if [ -n "$TRIGGER_BRANCH" ]; then
TARGET_ARGS+=(--branch "$TRIGGER_BRANCH")
fi
- bunx trigger.dev@4.5.12 deploy "${TARGET_ARGS[@]}" --skip-promotion
+ bunx trigger.dev@4.5.16 deploy "${TARGET_ARGS[@]}" --skip-promotion
- name: Validate deployment version output
env:
@@ -643,7 +643,7 @@ jobs:
if [ -n "$TRIGGER_BRANCH" ]; then
TARGET_ARGS+=(--branch "$TRIGGER_BRANCH")
fi
- bunx trigger.dev@4.5.12 promote "$VERSION" "${TARGET_ARGS[@]}"
+ bunx trigger.dev@4.5.16 promote "$VERSION" "${TARGET_ARGS[@]}"
# Build ARM64 images for GHCR (main branch only, runs in parallel with
# tests). Pushes only the immutable sha tag — latest-arm64/version-arm64
diff --git a/apps/docs/content/docs/api-reference/meta.json b/apps/docs/content/docs/api-reference/meta.json
index d6d47878d24..bd9a0920315 100644
--- a/apps/docs/content/docs/api-reference/meta.json
+++ b/apps/docs/content/docs/api-reference/meta.json
@@ -17,6 +17,8 @@
"(generated)/files",
"(generated)/knowledge-bases",
"(generated)/workspaces",
+ "(generated)/organizations",
+ "(generated)/permission-groups",
"(generated)/workspace-sync",
"(generated)/mcp-servers",
"(generated)/skills",
diff --git a/apps/docs/content/docs/cli/commands.mdx b/apps/docs/content/docs/cli/commands.mdx
index b1c1e6f400a..e63345e7a0f 100644
--- a/apps/docs/content/docs/cli/commands.mdx
+++ b/apps/docs/content/docs/cli/commands.mdx
@@ -44,6 +44,8 @@ These apply to every command, and may be written before or after it.
| [`sim logs`](/cli/logs) | Manage logs |
| [`sim mcp-servers`](/cli/mcp-servers) | Manage mcp servers |
| [`sim meta`](/cli/meta) | Manage meta |
+| [`sim organizations`](/cli/organizations) | Manage organizations |
+| [`sim permission-groups`](/cli/permission-groups) | Manage permission groups |
| [`sim sandboxes`](/cli/sandboxes) | Manage sandboxes |
| [`sim secrets`](/cli/secrets) | Manage secrets |
| [`sim selectors`](/cli/selectors) | Manage selectors |
diff --git a/apps/docs/content/docs/cli/meta.json b/apps/docs/content/docs/cli/meta.json
index de9eac6e055..0ba739e7a34 100644
--- a/apps/docs/content/docs/cli/meta.json
+++ b/apps/docs/content/docs/cli/meta.json
@@ -27,6 +27,8 @@
"logs",
"mcp-servers",
"meta",
+ "organizations",
+ "permission-groups",
"sandboxes",
"secrets",
"selectors",
diff --git a/apps/docs/content/docs/cli/organizations.mdx b/apps/docs/content/docs/cli/organizations.mdx
new file mode 100644
index 00000000000..07b9f42b668
--- /dev/null
+++ b/apps/docs/content/docs/cli/organizations.mdx
@@ -0,0 +1,278 @@
+---
+title: Organizations
+description: Manage organizations — every subcommand, argument, and flag
+---
+
+import { CommandTable } from '@/components/ui/command-table'
+
+Every command below also accepts the [global options](/cli/commands#global-options).
+
+## Create organization invitation
+
+```bash
+sim organizations invitations create [options]
+```
+
+Create Organization Invitation (OAuth login or personal API key required)
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+| `--email ` | Yes | Email address of the person to invite. |
+| `--role ` | No | Organization role to offer. Defaults to member; grants no workspace-specific permissions. Accepted values: `member`, `admin`. |
+
+
+
+## Get organization invitation
+
+```bash
+sim organizations invitations get [options]
+```
+
+Get Organization Invitation (OAuth login or personal API key required)
+
+**Arguments**
+
+
+
+| Argument | Required | Description |
+| --- | --- | --- |
+| `invitationId` | Yes | Invitation identifier. |
+
+
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+
+
+
+## List organization invitations
+
+```bash
+sim organizations invitations list [options]
+```
+
+List Organization Invitations (OAuth login or personal API key required)
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+| `--search ` | No | Case-insensitive substring match against the invitee email. |
+| `--status ` | No | Filter by current invitation status. Omit to include all statuses. Accepted values: `pending`, `accepted`, `rejected`, `cancelled`, `expired`. |
+| `--sort-by ` | No | Field used to sort the result. Accepted values: `email`, `createdAt`. |
+| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
+| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `100`. |
+| `--cursor ` | No | Continue from nextCursor returned by a previous result. |
+
+
+
+## Resend organization invitation
+
+```bash
+sim organizations invitations resend [options]
+```
+
+Resend Organization Invitation (OAuth login or personal API key required)
+
+**Arguments**
+
+
+
+| Argument | Required | Description |
+| --- | --- | --- |
+| `invitationId` | Yes | Invitation identifier. |
+
+
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+
+
+
+## Revoke organization invitation
+
+```bash
+sim organizations invitations revoke [options]
+```
+
+Revoke Organization Invitation (OAuth login or personal API key required)
+
+**Arguments**
+
+
+
+| Argument | Required | Description |
+| --- | --- | --- |
+| `invitationId` | Yes | Invitation identifier. |
+
+
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+| `-y, --yes` | Yes | Confirm this operation. |
+
+
+
+## Get organization
+
+```bash
+sim organizations get
+```
+
+Get Organization (OAuth login or personal API key required)
+
+**Arguments**
+
+
+
+| Argument | Required | Description |
+| --- | --- | --- |
+| `organizationId` | Yes | Organization identifier. |
+
+
+
+## List organization members
+
+```bash
+sim organizations members list [options]
+```
+
+List Organization Members (OAuth login or personal API key required)
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+| `--search ` | No | Case-insensitive substring match against member name or email. |
+| `--sort-by ` | No | Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order. Accepted values: `name`, `email`, `joinedAt`. |
+| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
+| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `0`. |
+
+
+
+## Remove organization member
+
+```bash
+sim organizations members remove [options]
+```
+
+Remove Organization Member (OAuth login or personal API key required)
+
+**Arguments**
+
+
+
+| Argument | Required | Description |
+| --- | --- | --- |
+| `userId` | Yes | User identifier of the organization member. |
+
+
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+| `-y, --yes` | Yes | Confirm this operation. |
+
+
+
+## Update organization member
+
+```bash
+sim organizations members update [options]
+```
+
+Update Organization Member (OAuth login or personal API key required)
+
+**Arguments**
+
+
+
+| Argument | Required | Description |
+| --- | --- | --- |
+| `userId` | Yes | User identifier of the organization member. |
+
+
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+| `--role ` | Yes | New organization role. Ownership transfers use a separate operation. Accepted values: `member`, `admin`. |
+
+
+
+## List organizations
+
+```bash
+sim organizations list [options]
+```
+
+List Organizations (OAuth login or personal API key required)
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--search ` | No | Case-insensitive substring match against the organization name. |
+| `--sort-by ` | No | Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order. Accepted values: `name`, `createdAt`. |
+| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
+| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `0`. |
+
+
+
+## List organization workspaces
+
+```bash
+sim organizations workspaces [options]
+```
+
+List Organization Workspaces (OAuth login or personal API key required)
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+| `--search ` | No | Case-insensitive substring match against the workspace name. |
+| `--sort-by ` | No | Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order. Accepted values: `name`, `id`. |
+| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
+| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `0`. |
+
+
diff --git a/apps/docs/content/docs/cli/permission-groups.mdx b/apps/docs/content/docs/cli/permission-groups.mdx
new file mode 100644
index 00000000000..95a5cecb700
--- /dev/null
+++ b/apps/docs/content/docs/cli/permission-groups.mdx
@@ -0,0 +1,240 @@
+---
+title: Permission Groups
+description: Manage permission groups — every subcommand, argument, and flag
+---
+
+import { CommandTable } from '@/components/ui/command-table'
+
+`sim permission-groups` is also spelled `sim permission-group`.
+
+Every command below also accepts the [global options](/cli/commands#global-options).
+
+## Add permission group member
+
+```bash
+sim permission-groups members add [options]
+```
+
+Add Permission Group Member (OAuth login or personal API key required)
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+| `--group ` | Yes | Permission group identifier. |
+| `--user ` | Yes | Existing organization member to add. |
+
+
+
+## Bulk add permission group members
+
+```bash
+sim permission-groups members batch-add [options]
+```
+
+Bulk Add Permission Group Members (OAuth login or personal API key required)
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+| `--group ` | Yes | Permission group identifier. |
+| `--user ` | No | User IDs to add; cannot be combined with --all-members (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
+| `--all-members` | No | Add every current organization member; cannot be combined with --user. |
+
+
+
+## List permission group members
+
+```bash
+sim permission-groups members list [options]
+```
+
+List Permission Group Members (OAuth login or personal API key required)
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+| `--group ` | Yes | Permission group identifier. |
+| `--sort-by ` | No | Field used to sort the result. Accepted values: `assignedAt`, `userId`. |
+| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
+| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `0`. |
+
+
+
+## Remove permission group member
+
+```bash
+sim permission-groups members remove [options]
+```
+
+Remove Permission Group Member (OAuth login or personal API key required)
+
+**Arguments**
+
+
+
+| Argument | Required | Description |
+| --- | --- | --- |
+| `userId` | Yes | User identifier of the member to remove. |
+
+
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+| `--group ` | Yes | Permission group identifier. |
+| `-y, --yes` | Yes | Confirm this operation. |
+
+
+
+## Create permission group
+
+```bash
+sim permission-groups create [options]
+```
+
+Create Permission Group (OAuth login or personal API key required)
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+| `--name ` | Yes | Group name, unique within the organization. |
+| `--description ` | No | Optional group description. |
+| `--config ` | No | Permission restrictions to set. Omitted keys use the default permission configuration. (JSON, or @path / @- to read a file or stdin). |
+| `--default` | No | Whether the group is the organization default. Only one group can be the default. |
+| `--no-default` | No | Send --default as false. |
+| `--workspace-ids ` | No | Workspace IDs targeted by a non-default group. Required when creating a non-default group; omit for a default group. (JSON, or @path / @- to read a file or stdin). |
+
+
+
+## Delete permission group
+
+```bash
+sim permission-groups delete [options]
+```
+
+Delete Permission Group (OAuth login or personal API key required)
+
+**Arguments**
+
+
+
+| Argument | Required | Description |
+| --- | --- | --- |
+| `groupId` | Yes | Permission group identifier. |
+
+
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+| `-y, --yes` | Yes | Confirm this operation. |
+
+
+
+## Get permission group
+
+```bash
+sim permission-groups get [options]
+```
+
+Get Permission Group (OAuth login or personal API key required)
+
+**Arguments**
+
+
+
+| Argument | Required | Description |
+| --- | --- | --- |
+| `groupId` | Yes | Permission group identifier. |
+
+
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+
+
+
+## List permission groups
+
+```bash
+sim permission-groups list [options]
+```
+
+List Permission Groups (OAuth login or personal API key required)
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+| `--search ` | No | Case-insensitive substring match against the group name. |
+| `--sort-by ` | No | Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order. Accepted values: `name`, `createdAt`, `updatedAt`. |
+| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
+| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `0`. |
+
+
+
+## Update permission group
+
+```bash
+sim permission-groups update [options]
+```
+
+Update Permission Group (OAuth login or personal API key required)
+
+**Arguments**
+
+
+
+| Argument | Required | Description |
+| --- | --- | --- |
+| `groupId` | Yes | Permission group identifier. |
+
+
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+| `--name ` | No | Group name, unique within the organization. |
+| `--description ` | No | Group description. Null or an empty string clears it; omission leaves it unchanged. (--description null sends the word, not JSON null). |
+| `--config ` | No | Patch of permission restrictions. Omitted keys remain unchanged; each supplied array replaces that entire list. (JSON, or @path / @- to read a file or stdin). |
+| `--default` | No | Whether the group is the organization default. Only one group can be the default. |
+| `--no-default` | No | Send --default as false. |
+| `--workspace-ids ` | No | Workspace identifiers for a non-default group. Required on creation; an empty update makes the group inactive. (JSON, or @path / @- to read a file or stdin). |
+
+
diff --git a/apps/docs/content/docs/cli/reference.mdx b/apps/docs/content/docs/cli/reference.mdx
index 483d1677144..7ece46c6d2d 100644
--- a/apps/docs/content/docs/cli/reference.mdx
+++ b/apps/docs/content/docs/cli/reference.mdx
@@ -2996,6 +2996,512 @@ Show what this API supports and which limits apply
sim meta status
```
+## sim organizations
+
+### sim organizations invitations create
+
+Create Organization Invitation (OAuth login or personal API key required)
+
+```bash
+sim organizations invitations create [options]
+```
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+| `--email ` | Yes | Email address of the person to invite. |
+| `--role ` | No | Organization role to offer. Defaults to member; grants no workspace-specific permissions. Accepted values: `member`, `admin`. |
+
+
+
+### sim organizations invitations get
+
+Get Organization Invitation (OAuth login or personal API key required)
+
+```bash
+sim organizations invitations get [options]
+```
+
+**Arguments**
+
+
+
+| Argument | Required | Description |
+| --- | --- | --- |
+| `invitationId` | Yes | Invitation identifier. |
+
+
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+
+
+
+### sim organizations invitations list
+
+List Organization Invitations (OAuth login or personal API key required)
+
+```bash
+sim organizations invitations list [options]
+```
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+| `--search ` | No | Case-insensitive substring match against the invitee email. |
+| `--status ` | No | Filter by current invitation status. Omit to include all statuses. Accepted values: `pending`, `accepted`, `rejected`, `cancelled`, `expired`. |
+| `--sort-by ` | No | Field used to sort the result. Accepted values: `email`, `createdAt`. |
+| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
+| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `100`. |
+| `--cursor ` | No | Continue from nextCursor returned by a previous result. |
+
+
+
+### sim organizations invitations resend
+
+Resend Organization Invitation (OAuth login or personal API key required)
+
+```bash
+sim organizations invitations resend [options]
+```
+
+**Arguments**
+
+
+
+| Argument | Required | Description |
+| --- | --- | --- |
+| `invitationId` | Yes | Invitation identifier. |
+
+
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+
+
+
+### sim organizations invitations revoke
+
+Revoke Organization Invitation (OAuth login or personal API key required)
+
+```bash
+sim organizations invitations revoke [options]
+```
+
+**Arguments**
+
+
+
+| Argument | Required | Description |
+| --- | --- | --- |
+| `invitationId` | Yes | Invitation identifier. |
+
+
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+| `-y, --yes` | Yes | Confirm this operation. |
+
+
+
+### sim organizations get
+
+Get Organization (OAuth login or personal API key required)
+
+```bash
+sim organizations get
+```
+
+**Arguments**
+
+
+
+| Argument | Required | Description |
+| --- | --- | --- |
+| `organizationId` | Yes | Organization identifier. |
+
+
+
+### sim organizations members list
+
+List Organization Members (OAuth login or personal API key required)
+
+```bash
+sim organizations members list [options]
+```
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+| `--search ` | No | Case-insensitive substring match against member name or email. |
+| `--sort-by ` | No | Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order. Accepted values: `name`, `email`, `joinedAt`. |
+| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
+| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `0`. |
+
+
+
+### sim organizations members remove
+
+Remove Organization Member (OAuth login or personal API key required)
+
+```bash
+sim organizations members remove [options]
+```
+
+**Arguments**
+
+
+
+| Argument | Required | Description |
+| --- | --- | --- |
+| `userId` | Yes | User identifier of the organization member. |
+
+
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+| `-y, --yes` | Yes | Confirm this operation. |
+
+
+
+### sim organizations members update
+
+Update Organization Member (OAuth login or personal API key required)
+
+```bash
+sim organizations members update [options]
+```
+
+**Arguments**
+
+
+
+| Argument | Required | Description |
+| --- | --- | --- |
+| `userId` | Yes | User identifier of the organization member. |
+
+
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+| `--role ` | Yes | New organization role. Ownership transfers use a separate operation. Accepted values: `member`, `admin`. |
+
+
+
+### sim organizations list
+
+List Organizations (OAuth login or personal API key required)
+
+```bash
+sim organizations list [options]
+```
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--search ` | No | Case-insensitive substring match against the organization name. |
+| `--sort-by ` | No | Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order. Accepted values: `name`, `createdAt`. |
+| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
+| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `0`. |
+
+
+
+### sim organizations workspaces
+
+List Organization Workspaces (OAuth login or personal API key required)
+
+```bash
+sim organizations workspaces [options]
+```
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+| `--search ` | No | Case-insensitive substring match against the workspace name. |
+| `--sort-by ` | No | Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order. Accepted values: `name`, `id`. |
+| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
+| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `0`. |
+
+
+
+## sim permission-groups
+
+Also spelled `sim permission-group`.
+
+### sim permission-groups members add
+
+Add Permission Group Member (OAuth login or personal API key required)
+
+```bash
+sim permission-groups members add [options]
+```
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+| `--group ` | Yes | Permission group identifier. |
+| `--user ` | Yes | Existing organization member to add. |
+
+
+
+### sim permission-groups members batch-add
+
+Bulk Add Permission Group Members (OAuth login or personal API key required)
+
+```bash
+sim permission-groups members batch-add [options]
+```
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+| `--group ` | Yes | Permission group identifier. |
+| `--user ` | No | User IDs to add; cannot be combined with --all-members (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
+| `--all-members` | No | Add every current organization member; cannot be combined with --user. |
+
+
+
+### sim permission-groups members list
+
+List Permission Group Members (OAuth login or personal API key required)
+
+```bash
+sim permission-groups members list [options]
+```
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+| `--group ` | Yes | Permission group identifier. |
+| `--sort-by ` | No | Field used to sort the result. Accepted values: `assignedAt`, `userId`. |
+| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
+| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `0`. |
+
+
+
+### sim permission-groups members remove
+
+Remove Permission Group Member (OAuth login or personal API key required)
+
+```bash
+sim permission-groups members remove [options]
+```
+
+**Arguments**
+
+
+
+| Argument | Required | Description |
+| --- | --- | --- |
+| `userId` | Yes | User identifier of the member to remove. |
+
+
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+| `--group ` | Yes | Permission group identifier. |
+| `-y, --yes` | Yes | Confirm this operation. |
+
+
+
+### sim permission-groups create
+
+Create Permission Group (OAuth login or personal API key required)
+
+```bash
+sim permission-groups create [options]
+```
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+| `--name ` | Yes | Group name, unique within the organization. |
+| `--description ` | No | Optional group description. |
+| `--config ` | No | Permission restrictions to set. Omitted keys use the default permission configuration. (JSON, or @path / @- to read a file or stdin). |
+| `--default` | No | Whether the group is the organization default. Only one group can be the default. |
+| `--no-default` | No | Send --default as false. |
+| `--workspace-ids ` | No | Workspace IDs targeted by a non-default group. Required when creating a non-default group; omit for a default group. (JSON, or @path / @- to read a file or stdin). |
+
+
+
+### sim permission-groups delete
+
+Delete Permission Group (OAuth login or personal API key required)
+
+```bash
+sim permission-groups delete [options]
+```
+
+**Arguments**
+
+
+
+| Argument | Required | Description |
+| --- | --- | --- |
+| `groupId` | Yes | Permission group identifier. |
+
+
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+| `-y, --yes` | Yes | Confirm this operation. |
+
+
+
+### sim permission-groups get
+
+Get Permission Group (OAuth login or personal API key required)
+
+```bash
+sim permission-groups get [options]
+```
+
+**Arguments**
+
+
+
+| Argument | Required | Description |
+| --- | --- | --- |
+| `groupId` | Yes | Permission group identifier. |
+
+
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+
+
+
+### sim permission-groups list
+
+List Permission Groups (OAuth login or personal API key required)
+
+```bash
+sim permission-groups list [options]
+```
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+| `--search ` | No | Case-insensitive substring match against the group name. |
+| `--sort-by ` | No | Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order. Accepted values: `name`, `createdAt`, `updatedAt`. |
+| `--sort-order ` | No | Sort direction. Accepted values: `asc`, `desc`. |
+| `--limit ` | No | Maximum items to return (0 for everything). Defaults to `0`. |
+
+
+
+### sim permission-groups update
+
+Update Permission Group (OAuth login or personal API key required)
+
+```bash
+sim permission-groups update [options]
+```
+
+**Arguments**
+
+
+
+| Argument | Required | Description |
+| --- | --- | --- |
+| `groupId` | Yes | Permission group identifier. |
+
+
+
+**Options**
+
+
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `--organization ` | Yes | Organization identifier. |
+| `--name ` | No | Group name, unique within the organization. |
+| `--description ` | No | Group description. Null or an empty string clears it; omission leaves it unchanged. (--description null sends the word, not JSON null). |
+| `--config ` | No | Patch of permission restrictions. Omitted keys remain unchanged; each supplied array replaces that entire list. (JSON, or @path / @- to read a file or stdin). |
+| `--default` | No | Whether the group is the organization default. Only one group can be the default. |
+| `--no-default` | No | Send --default as false. |
+| `--workspace-ids ` | No | Workspace identifiers for a non-default group. Required on creation; an empty update makes the group inactive. (JSON, or @path / @- to read a file or stdin). |
+
+
+
## sim sandboxes
Also spelled `sim sandbox`.
@@ -4551,8 +5057,8 @@ sim tables views update [options]
| `--name ` | No | Replacement saved-view display name. |
| `--config ` | No | Complete replacement saved-view configuration. (JSON, or @path / @- to read a file or stdin). |
| `--config-patch ` | No | Saved-view configuration fields to shallow-merge. (JSON, or @path / @- to read a file or stdin). |
-| `--is-default` | No | Whether to promote this view to the table default. |
-| `--no-is-default` | No | Send --is-default as false. |
+| `--default` | No | Whether to promote this view to the table default. |
+| `--no-default` | No | Send --default as false. |
diff --git a/apps/docs/content/docs/cli/tables.mdx b/apps/docs/content/docs/cli/tables.mdx
index b1290e4af50..7cf0b265b3f 100644
--- a/apps/docs/content/docs/cli/tables.mdx
+++ b/apps/docs/content/docs/cli/tables.mdx
@@ -1012,8 +1012,8 @@ sim tables views update [options]
| `--name ` | No | Replacement saved-view display name. |
| `--config ` | No | Complete replacement saved-view configuration. (JSON, or @path / @- to read a file or stdin). |
| `--config-patch ` | No | Saved-view configuration fields to shallow-merge. (JSON, or @path / @- to read a file or stdin). |
-| `--is-default` | No | Whether to promote this view to the table default. |
-| `--no-is-default` | No | Send --is-default as false. |
+| `--default` | No | Whether to promote this view to the table default. |
+| `--no-default` | No | Send --default as false. |
diff --git a/apps/docs/lib/openapi-download.test.ts b/apps/docs/lib/openapi-download.test.ts
index 5fccfe6ce59..6cc51ca68d5 100644
--- a/apps/docs/lib/openapi-download.test.ts
+++ b/apps/docs/lib/openapi-download.test.ts
@@ -33,7 +33,7 @@ describe('OpenAPI download', () => {
const tags = document.tags as Array<{ name: string }>
expect(document.openapi).toBe('3.1.0')
- expect(Object.keys(paths)).toHaveLength(157)
+ expect(Object.keys(paths)).toHaveLength(170)
expect(tags.map((tag) => tag.name)).toEqual([
'Workspace Sync',
'Workflows',
@@ -44,6 +44,8 @@ describe('OpenAPI download', () => {
'Tables',
'Knowledge Bases',
'Billing',
+ 'Organizations',
+ 'Permission Groups',
'Meta',
'Workspaces',
'MCP Servers',
diff --git a/apps/docs/openapi-v2-resources.json b/apps/docs/openapi-v2-resources.json
index 533dcf0f5f3..de991e52ce0 100644
--- a/apps/docs/openapi-v2-resources.json
+++ b/apps/docs/openapi-v2-resources.json
@@ -1,8 +1,8 @@
{
"openapi": "3.1.0",
"info": {
- "title": "Sim API v2 — Workspace Resources",
- "description": "Version 2 of the Sim REST API for workspace metadata, members, MCP servers, skills, custom tools, sandboxes, credentials, write-only secrets, and the block, tool, and connector-type catalogs.",
+ "title": "Sim API v2 — Resources",
+ "description": "Version 2 of the Sim REST API for workspace metadata, members, MCP servers, skills, custom tools, sandboxes, credentials, write-only secrets, organization permission groups, and the block, tool, and connector-type catalogs.",
"version": "2.0.0",
"contact": {
"name": "Sim Support",
@@ -21,6 +21,14 @@
}
],
"tags": [
+ {
+ "name": "Organizations",
+ "description": "Discover organizations and manage their members and invitations."
+ },
+ {
+ "name": "Permission Groups",
+ "description": "Manage organization permission groups, their restrictions, and membership."
+ },
{
"name": "Meta",
"description": "Discover what the calling API credential can reach."
@@ -4739,2767 +4747,5979 @@
}
}
}
- }
- },
- "components": {
- "securitySchemes": {
- "apiKey": {
- "type": "apiKey",
- "in": "header",
- "name": "X-API-Key",
- "description": "Your Sim API key, personal or workspace-scoped. Generate one under Settings, then API Keys. Operations that reject workspace keys say so in their own description."
- },
- "oauthBearer": {
- "type": "http",
- "scheme": "bearer",
- "bearerFormat": "OAuth 2.0 access token",
- "description": "A Sim OAuth access token obtained by a registered client through the authorization-code flow. Each operation declares its required scope: api:read permits reads and searches; api:write also permits changes and execution and implies api:read. Scope requirements follow the application operation, independent of HTTP method or workspace role."
- }
- },
- "headers": {
- "X-RateLimit-Limit": {
- "description": "Maximum requests allowed in the current window.",
- "schema": {
- "type": "integer",
- "minimum": 0,
- "maximum": 9007199254740991,
- "title": "Rate limit",
- "description": "Maximum requests allowed in the current window."
- }
- },
- "X-RateLimit-Remaining": {
- "description": "Requests remaining in the current window.",
- "schema": {
- "type": "integer",
- "minimum": 0,
- "maximum": 9007199254740991,
- "title": "Rate limit remaining",
- "description": "Requests remaining in the current window."
- }
- },
- "X-RateLimit-Reset": {
- "description": "ISO 8601 timestamp when the current rate-limit window resets.",
- "schema": {
- "type": "string",
- "format": "date-time",
- "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
- "title": "Rate limit reset",
- "description": "ISO 8601 timestamp when the current rate-limit window resets."
- }
- },
- "Retry-After": {
- "description": "Seconds to wait before retrying, sent on `429` and `503`. Add jitter rather than retrying at exactly this offset.",
- "schema": {
- "type": "integer",
- "minimum": 0,
- "maximum": 9007199254740991,
- "title": "Retry after",
- "description": "Seconds to wait before retrying, sent on `429` and `503`. Add jitter rather than retrying at exactly this offset."
- }
- },
- "X-Run-Id": {
- "description": "Identifier assigned to the workflow run.",
- "schema": {
- "type": "string",
- "minLength": 1,
- "title": "Run identifier",
- "description": "Identifier assigned to the workflow run."
- }
- }
},
- "responses": {
- "BadRequest": {
- "description": "The request is invalid. This includes a query parameter sent with no value (`?limit=`, `?search=`), which is rejected rather than read as zero, empty, or the parameter default — omit the parameter instead.",
- "content": {
- "application/json": {
+ "/api/v2/organizations/{organizationId}/permission-groups": {
+ "get": {
+ "operationId": "listPermissionGroups",
+ "summary": "List Permission Groups",
+ "description": "List permission groups in an organization with cursor pagination. Requires organization admin or owner access and active Access Control. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:read`.",
+ "x-sim-operation": "permission_groups.list",
+ "x-oauth-scope": "api:read",
+ "tags": ["Permission Groups"],
+ "parameters": [
+ {
+ "name": "organizationId",
+ "in": "path",
+ "required": true,
+ "description": "Organization that owns the permission groups.",
"schema": {
- "$ref": "#/components/schemas/V2Error"
- },
- "example": {
- "error": {
- "code": "BAD_REQUEST",
- "message": "Invalid request"
- }
+ "type": "string",
+ "minLength": 1,
+ "description": "Organization that owns the permission groups."
}
- }
- }
- },
- "Unauthorized": {
- "description": "The API credential is missing or invalid.",
- "content": {
- "application/json": {
+ },
+ {
+ "name": "search",
+ "in": "query",
+ "required": false,
+ "description": "Case-insensitive substring match against the group name.",
"schema": {
- "$ref": "#/components/schemas/V2Error"
- },
- "example": {
- "error": {
- "code": "UNAUTHORIZED",
- "message": "Authentication required"
- }
+ "description": "Case-insensitive substring match against the group name.",
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200
}
- }
- }
- },
- "Forbidden": {
- "description": "The caller lacks the rights this operation requires. When the cause is one a caller can act on, `error.details.code` names it. A resource in a workspace the caller cannot reach at all answers `404` instead, so absence and denial are indistinguishable.",
- "content": {
- "application/json": {
+ },
+ {
+ "name": "sortBy",
+ "in": "query",
+ "required": false,
+ "description": "Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order.",
"schema": {
- "$ref": "#/components/schemas/V2Error"
- },
- "example": {
- "error": {
- "code": "FORBIDDEN",
- "message": "Insufficient workspace permissions",
- "details": {
- "code": "INSUFFICIENT_WORKSPACE_ROLE"
- }
- }
+ "default": "createdAt",
+ "description": "Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order.",
+ "type": "string",
+ "enum": ["name", "createdAt", "updatedAt"]
}
- }
- }
- },
- "NotFound": {
- "description": "The requested resource was not found.",
- "content": {
- "application/json": {
+ },
+ {
+ "name": "sortOrder",
+ "in": "query",
+ "required": false,
+ "description": "Sort direction.",
"schema": {
- "$ref": "#/components/schemas/V2Error"
- },
- "example": {
- "error": {
- "code": "NOT_FOUND",
- "message": "Not found"
- }
+ "default": "desc",
+ "description": "Sort direction.",
+ "type": "string",
+ "enum": ["asc", "desc"]
}
- }
- }
- },
- "Conflict": {
- "description": "The request conflicts with current resource state.",
- "content": {
- "application/json": {
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "description": "Maximum permission groups to return per page. Must be a whole number from 1 to 100. Defaults to 50.",
"schema": {
- "$ref": "#/components/schemas/V2Error"
+ "default": 50,
+ "description": "Maximum permission groups to return per page. Must be a whole number from 1 to 100. Defaults to 50.",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100
+ }
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "description": "Opaque cursor from the previous page. Send it back with the same sort and filters; only `limit` may change. Change anything else and pagination must restart without a cursor.",
+ "schema": {
+ "description": "Opaque cursor from the previous page. Send it back with the same sort and filters; only `limit` may change. Change anything else and pagination must restart without a cursor.",
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "List Permission Groups result.",
+ "headers": {
+ "X-RateLimit-Limit": {
+ "$ref": "#/components/headers/X-RateLimit-Limit"
+ },
+ "X-RateLimit-Remaining": {
+ "$ref": "#/components/headers/X-RateLimit-Remaining"
+ },
+ "X-RateLimit-Reset": {
+ "$ref": "#/components/headers/X-RateLimit-Reset"
+ }
},
- "example": {
- "error": {
- "code": "CONFLICT",
- "message": "The request conflicts with the current state of the resource"
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ListPermissionGroupsResponse"
+ }
}
}
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "429": {
+ "$ref": "#/components/responses/RateLimited"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalError"
+ },
+ "503": {
+ "$ref": "#/components/responses/ServiceUnavailable"
}
}
},
- "PayloadTooLarge": {
- "description": "The request, or a resource collection it must materialize, exceeds the allowed size: an oversized request body, a generated artifact past the download ceiling, or a workspace folder tree too large to load in full.",
- "content": {
- "application/json": {
+ "post": {
+ "operationId": "createPermissionGroup",
+ "summary": "Create Permission Group",
+ "description": "Create a permission group. A non-default group requires workspaces and initially governs everyone in them. Creating a default group demotes the previous default to an inactive group until it is assigned workspaces. Overlapping all-member scopes conflict. Requires organization admin or owner access and active Access Control. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:write`.",
+ "x-sim-operation": "permission_groups.create",
+ "x-oauth-scope": "api:write",
+ "tags": ["Permission Groups"],
+ "parameters": [
+ {
+ "name": "organizationId",
+ "in": "path",
+ "required": true,
+ "description": "Organization that owns the permission groups.",
"schema": {
- "$ref": "#/components/schemas/V2Error"
- },
- "example": {
- "error": {
- "code": "PAYLOAD_TOO_LARGE",
- "message": "Request body is too large"
- }
+ "type": "string",
+ "minLength": 1,
+ "description": "Organization that owns the permission groups."
}
}
- }
- },
- "UnsupportedMediaType": {
- "description": "The request uses an unsupported media type.",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/V2Error"
- },
- "example": {
- "error": {
- "code": "UNSUPPORTED_MEDIA_TYPE",
- "message": "Request body must be sent as application/json"
+ ],
+ "requestBody": {
+ "required": true,
+ "description": "Create Permission Group inputs.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/CreatePermissionGroupRequest"
}
}
}
- }
- },
- "RateLimited": {
- "description": "The caller exceeded the request rate limit.",
- "headers": {
- "Retry-After": {
- "$ref": "#/components/headers/Retry-After"
- }
},
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/V2Error"
+ "responses": {
+ "201": {
+ "description": "Create Permission Group result.",
+ "headers": {
+ "X-RateLimit-Limit": {
+ "$ref": "#/components/headers/X-RateLimit-Limit"
+ },
+ "X-RateLimit-Remaining": {
+ "$ref": "#/components/headers/X-RateLimit-Remaining"
+ },
+ "X-RateLimit-Reset": {
+ "$ref": "#/components/headers/X-RateLimit-Reset"
+ }
},
- "example": {
- "error": {
- "code": "RATE_LIMITED",
- "message": "API rate limit exceeded",
- "details": {
- "retryAfter": "2026-01-01T00:00:30.000Z"
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/CreatePermissionGroupResponse"
}
}
}
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "413": {
+ "$ref": "#/components/responses/PayloadTooLarge"
+ },
+ "415": {
+ "$ref": "#/components/responses/UnsupportedMediaType"
+ },
+ "429": {
+ "$ref": "#/components/responses/RateLimited"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalError"
+ },
+ "503": {
+ "$ref": "#/components/responses/ServiceUnavailable"
}
}
- },
- "InternalError": {
- "description": "An unexpected server error occurred.",
- "content": {
- "application/json": {
+ }
+ },
+ "/api/v2/organizations/{organizationId}/permission-groups/{groupId}": {
+ "get": {
+ "operationId": "getPermissionGroup",
+ "summary": "Get Permission Group",
+ "description": "Get a permission group and its resolved restrictions. Requires organization admin or owner access and active Access Control. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:read`.",
+ "x-sim-operation": "permission_groups.read",
+ "x-oauth-scope": "api:read",
+ "tags": ["Permission Groups"],
+ "parameters": [
+ {
+ "name": "organizationId",
+ "in": "path",
+ "required": true,
+ "description": "Organization that owns the permission groups.",
"schema": {
- "$ref": "#/components/schemas/V2Error"
+ "type": "string",
+ "minLength": 1,
+ "description": "Organization that owns the permission groups."
+ }
+ },
+ {
+ "name": "groupId",
+ "in": "path",
+ "required": true,
+ "description": "Permission group identifier.",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Permission group identifier."
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Get Permission Group result.",
+ "headers": {
+ "X-RateLimit-Limit": {
+ "$ref": "#/components/headers/X-RateLimit-Limit"
+ },
+ "X-RateLimit-Remaining": {
+ "$ref": "#/components/headers/X-RateLimit-Remaining"
+ },
+ "X-RateLimit-Reset": {
+ "$ref": "#/components/headers/X-RateLimit-Reset"
+ }
},
- "example": {
- "error": {
- "code": "INTERNAL_ERROR",
- "message": "Internal server error"
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/GetPermissionGroupResponse"
+ }
}
}
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "429": {
+ "$ref": "#/components/responses/RateLimited"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalError"
+ },
+ "503": {
+ "$ref": "#/components/responses/ServiceUnavailable"
}
}
},
- "ServiceUnavailable": {
- "description": "A required service is temporarily unavailable. `Retry-After` carries the seconds to wait; treat it as a floor and add jitter. The header is omitted when `error.details.code` is `ASYNC_ENQUEUE_AMBIGUOUS`, because the run may already have started — reconcile against the returned run id instead of retrying.",
- "headers": {
- "Retry-After": {
- "$ref": "#/components/headers/Retry-After"
+ "patch": {
+ "operationId": "updatePermissionGroup",
+ "summary": "Update Permission Group",
+ "description": "Update a permission group. Omitted fields remain unchanged; config keys are patched and supplied arrays replace their lists. Promoting a group to default demotes the previous default; demoting without workspaceIds leaves it inactive. Overlapping member or all-member scopes conflict. Requires organization admin or owner access and active Access Control. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:write`.",
+ "x-sim-operation": "permission_groups.update",
+ "x-oauth-scope": "api:write",
+ "tags": ["Permission Groups"],
+ "parameters": [
+ {
+ "name": "organizationId",
+ "in": "path",
+ "required": true,
+ "description": "Organization that owns the permission groups.",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Organization that owns the permission groups."
+ }
+ },
+ {
+ "name": "groupId",
+ "in": "path",
+ "required": true,
+ "description": "Permission group identifier.",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Permission group identifier."
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "description": "Update Permission Group inputs.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UpdatePermissionGroupRequest"
+ }
+ }
}
},
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/V2Error"
+ "responses": {
+ "200": {
+ "description": "Update Permission Group result.",
+ "headers": {
+ "X-RateLimit-Limit": {
+ "$ref": "#/components/headers/X-RateLimit-Limit"
+ },
+ "X-RateLimit-Remaining": {
+ "$ref": "#/components/headers/X-RateLimit-Remaining"
+ },
+ "X-RateLimit-Reset": {
+ "$ref": "#/components/headers/X-RateLimit-Reset"
+ }
},
- "example": {
- "error": {
- "code": "SERVICE_UNAVAILABLE",
- "message": "Service temporarily unavailable"
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UpdatePermissionGroupResponse"
+ }
}
}
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "413": {
+ "$ref": "#/components/responses/PayloadTooLarge"
+ },
+ "415": {
+ "$ref": "#/components/responses/UnsupportedMediaType"
+ },
+ "429": {
+ "$ref": "#/components/responses/RateLimited"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalError"
+ },
+ "503": {
+ "$ref": "#/components/responses/ServiceUnavailable"
}
}
- }
- },
- "schemas": {
- "V2ActionableForbiddenDetails": {
- "type": "object",
- "properties": {
- "code": {
- "$ref": "#/components/schemas/V2ForbiddenDetailCode"
- }
- },
- "required": ["code"],
- "additionalProperties": {
- "description": "Additional context for this refusal."
- },
- "title": "Actionable forbidden details",
- "description": "Machine-readable cause and optional context for an actionable `403` response."
},
- "V2ForbiddenDetailCode": {
- "type": "string",
- "enum": [
- "INSUFFICIENT_WORKSPACE_ROLE",
- "PERSONAL_API_KEYS_DISABLED",
- "WORKSPACE_KEY_OPERATION_NOT_PERMITTED",
- "PRINCIPAL_KIND_NOT_PERMITTED",
- "ORGANIZATION_MEMBERSHIP_REQUIRED",
- "ORGANIZATION_ADMIN_REQUIRED",
- "ENTERPRISE_PLAN_REQUIRED",
- "ORGANIZATION_PLAN_REQUIRED",
- "AUDIT_LOGS_DISABLED",
- "SKILL_EDITOR_ACCESS_REQUIRED",
- "SECRET_ADMIN_ACCESS_REQUIRED",
- "WORKSPACE_RESOURCE_LIMIT_REACHED",
- "PUBLIC_SHARING_NOT_ALLOWED",
- "CREDENTIAL_ADMIN_ACCESS_REQUIRED",
- "MCP_SERVER_URL_NOT_ALLOWED",
- "WORKSPACE_PLAN_CAPABILITY_REQUIRED",
- "CHAT_AUTH_MODE_NOT_PERMITTED",
- "CONNECTOR_MANAGED_RESOURCE_READ_ONLY",
- "PERMISSION_GROUP_CAPABILITY_BLOCKED",
- "INTEGRATION_NOT_ALLOWED",
- "INSUFFICIENT_SCOPE",
- "SCIM_MANAGED_MEMBERSHIP"
+ "delete": {
+ "operationId": "deletePermissionGroup",
+ "summary": "Delete Permission Group",
+ "description": "Permanently delete a permission group and its membership assignments. Members then inherit any other applicable restrictions. Requires organization admin or owner access and active Access Control. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:write`.",
+ "x-sim-operation": "permission_groups.delete",
+ "x-oauth-scope": "api:write",
+ "tags": ["Permission Groups"],
+ "parameters": [
+ {
+ "name": "organizationId",
+ "in": "path",
+ "required": true,
+ "description": "Organization that owns the permission groups.",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Organization that owns the permission groups."
+ }
+ },
+ {
+ "name": "groupId",
+ "in": "path",
+ "required": true,
+ "description": "Permission group identifier.",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Permission group identifier."
+ }
+ }
],
- "title": "Forbidden detail code",
- "description": "Stable cause code for an actionable `403` response."
- },
- "V2Error": {
- "type": "object",
- "properties": {
- "error": {
- "type": "object",
- "properties": {
- "code": {
- "type": "string",
- "description": "Stable machine-readable error code."
+ "responses": {
+ "200": {
+ "description": "Delete Permission Group result.",
+ "headers": {
+ "X-RateLimit-Limit": {
+ "$ref": "#/components/headers/X-RateLimit-Limit"
},
- "message": {
- "type": "string",
- "description": "Human-readable explanation of the error."
+ "X-RateLimit-Remaining": {
+ "$ref": "#/components/headers/X-RateLimit-Remaining"
},
- "details": {
- "description": "Structured error context whose keys depend on the error. Actionable `403` responses use the `V2ActionableForbiddenDetails` shape; validation failures may return issue arrays instead.",
- "anyOf": [
- {
- "$ref": "#/components/schemas/V2ActionableForbiddenDetails"
- },
- {
- "description": "Other structured context defined by the specific error."
- }
- ]
+ "X-RateLimit-Reset": {
+ "$ref": "#/components/headers/X-RateLimit-Reset"
}
},
- "required": ["code", "message"],
- "additionalProperties": false,
- "description": "Canonical error details."
- }
- },
- "required": ["error"],
- "additionalProperties": false,
- "title": "v2 error response",
- "description": "Canonical error envelope returned by the public v2 API.",
- "examples": [
- {
- "error": {
- "code": "BAD_REQUEST",
- "message": "The request is invalid."
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/DeletePermissionGroupResponse"
+ }
+ }
}
- }
- ]
- },
- "V2Workspace": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string",
- "minLength": 1,
- "maxLength": 128,
- "description": "Unique workspace identifier."
},
- "name": {
- "type": "string",
- "description": "Workspace display name."
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
},
- "color": {
- "type": "string",
- "description": "Workspace color as a hexadecimal color value."
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
},
- "logoUrl": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Workspace logo URL, or null when none is configured."
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
},
- "memberCount": {
- "type": "integer",
- "minimum": 0,
- "maximum": 9007199254740991,
- "description": "Number of effective members, including inherited organization administrators."
+ "404": {
+ "$ref": "#/components/responses/NotFound"
},
- "createdAt": {
- "type": "string",
- "format": "date-time",
- "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
- "description": "ISO 8601 timestamp when the workspace was created."
+ "429": {
+ "$ref": "#/components/responses/RateLimited"
},
- "updatedAt": {
- "type": "string",
- "format": "date-time",
- "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
- "description": "ISO 8601 timestamp when the workspace was last updated."
- }
- },
- "required": ["id", "name", "color", "logoUrl", "memberCount", "createdAt", "updatedAt"],
- "additionalProperties": false,
- "title": "Workspace",
- "description": "Public metadata for an accessible workspace."
- },
- "ListWorkspacesResponse": {
- "type": "object",
- "properties": {
- "data": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/V2Workspace"
- },
- "description": "Items in the current page."
+ "500": {
+ "$ref": "#/components/responses/InternalError"
},
- "nextCursor": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Opaque cursor for the next page. Send it back as `cursor`; `null` means there is nothing further to fetch. Never construct one yourself."
- }
- },
- "required": ["data", "nextCursor"],
- "additionalProperties": false,
- "title": "List workspaces response",
- "description": "Public metadata for workspaces available to the credential.",
- "examples": [
- {
- "data": [
- {
- "id": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
- "name": "Engineering",
- "color": "#33C482",
- "logoUrl": null,
- "memberCount": 14,
- "createdAt": "2026-01-15T10:30:00.000Z",
- "updatedAt": "2026-06-20T14:02:11.000Z"
- }
- ],
- "nextCursor": null
- }
- ]
- },
- "GetWorkspaceResponse": {
- "type": "object",
- "properties": {
- "data": {
- "description": "Response data.",
- "$ref": "#/components/schemas/V2Workspace"
+ "503": {
+ "$ref": "#/components/responses/ServiceUnavailable"
}
- },
- "required": ["data"],
- "additionalProperties": false,
- "title": "Get workspace response",
- "description": "Public metadata for one workspace.",
- "examples": [
+ }
+ }
+ },
+ "/api/v2/organizations/{organizationId}/permission-groups/{groupId}/members": {
+ "get": {
+ "operationId": "listPermissionGroupMembers",
+ "summary": "List Permission Group Members",
+ "description": "List explicit membership assignments in a permission group with cursor pagination. An empty inherit group applies to everyone in its workspaces. Requires organization admin or owner access and active Access Control. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:read`.",
+ "x-sim-operation": "permission_groups.members.list",
+ "x-oauth-scope": "api:read",
+ "tags": ["Permission Groups"],
+ "parameters": [
{
- "data": {
- "id": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
- "name": "Engineering",
- "color": "#33C482",
- "logoUrl": null,
- "memberCount": 14,
- "createdAt": "2026-01-15T10:30:00.000Z",
- "updatedAt": "2026-06-20T14:02:11.000Z"
+ "name": "organizationId",
+ "in": "path",
+ "required": true,
+ "description": "Organization that owns the permission groups.",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Organization that owns the permission groups."
}
- }
- ]
- },
- "V2WorkspaceMember": {
- "type": "object",
- "properties": {
- "email": {
- "type": "string",
- "format": "email",
- "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
- "description": "Member email address and public member identifier."
},
- "name": {
- "type": "string",
- "description": "Member display name."
+ {
+ "name": "groupId",
+ "in": "path",
+ "required": true,
+ "description": "Permission group identifier.",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Permission group identifier."
+ }
},
- "image": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Member profile image URL, or null when absent."
+ {
+ "name": "sortBy",
+ "in": "query",
+ "required": false,
+ "description": "Field used to sort the result.",
+ "schema": {
+ "default": "assignedAt",
+ "description": "Field used to sort the result.",
+ "type": "string",
+ "enum": ["assignedAt", "userId"]
+ }
},
- "role": {
- "type": "string",
- "enum": ["admin", "write", "read"],
- "description": "Effective role in the workspace."
+ {
+ "name": "sortOrder",
+ "in": "query",
+ "required": false,
+ "description": "Sort direction.",
+ "schema": {
+ "default": "asc",
+ "description": "Sort direction.",
+ "type": "string",
+ "enum": ["asc", "desc"]
+ }
},
- "isExternal": {
- "type": "boolean",
- "description": "Whether the member belongs to a different organization than the workspace. True only for an explicitly granted member whose own organization differs; inherited organization-administrator access is always reported as false, so this does not detect every outside caller."
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "description": "Maximum group members to return per page. Must be a whole number from 1 to 100. Defaults to 50.",
+ "schema": {
+ "default": 50,
+ "description": "Maximum group members to return per page. Must be a whole number from 1 to 100. Defaults to 50.",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100
+ }
},
- "joinedAt": {
- "type": "string",
- "format": "date-time",
- "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
- "description": "ISO 8601 timestamp when access was granted."
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "description": "Opaque cursor from the previous page. Send it back with the same sort and filters; only `limit` may change. Change anything else and pagination must restart without a cursor.",
+ "schema": {
+ "description": "Opaque cursor from the previous page. Send it back with the same sort and filters; only `limit` may change. Change anything else and pagination must restart without a cursor.",
+ "type": "string",
+ "minLength": 1
+ }
}
- },
- "required": ["email", "name", "image", "role", "isExternal", "joinedAt"],
- "additionalProperties": false,
- "title": "Workspace member",
- "description": "An effective workspace member and their public access role."
- },
- "ListWorkspaceMembersResponse": {
- "type": "object",
- "properties": {
- "data": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/V2WorkspaceMember"
- },
- "description": "Items in the current page."
- },
- "nextCursor": {
- "anyOf": [
- {
- "type": "string"
+ ],
+ "responses": {
+ "200": {
+ "description": "List Permission Group Members result.",
+ "headers": {
+ "X-RateLimit-Limit": {
+ "$ref": "#/components/headers/X-RateLimit-Limit"
},
- {
- "type": "null"
+ "X-RateLimit-Remaining": {
+ "$ref": "#/components/headers/X-RateLimit-Remaining"
+ },
+ "X-RateLimit-Reset": {
+ "$ref": "#/components/headers/X-RateLimit-Reset"
}
- ],
- "description": "Opaque cursor for the next page. Send it back as `cursor`; `null` means there is nothing further to fetch. Never construct one yourself."
- }
- },
- "required": ["data", "nextCursor"],
- "additionalProperties": false,
- "title": "List workspace members response",
- "description": "A cursor-paginated page of effective workspace members.",
- "examples": [
- {
- "data": [
- {
- "email": "jane@example.com",
- "name": "Jane Smith",
- "image": null,
- "role": "admin",
- "isExternal": false,
- "joinedAt": "2026-01-15T10:30:00.000Z"
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ListPermissionGroupMembersResponse"
+ }
}
- ],
- "nextCursor": null
- }
- ]
- },
- "V2McpServer": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string",
- "description": "Unique server identifier derived from the workspace and endpoint URL."
- },
- "name": {
- "type": "string",
- "description": "Server display name."
- },
- "description": {
- "description": "Optional server description.",
- "type": "string"
+ }
},
- "transport": {
- "default": "streamable-http",
- "description": "Transport used to communicate with the server.",
- "type": "string",
- "enum": ["streamable-http"]
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
},
- "authType": {
- "description": "Authentication method used by the server.",
- "type": "string",
- "enum": ["none", "headers", "oauth"]
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
},
- "url": {
- "description": "Server endpoint URL.",
- "type": "string"
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
},
- "timeout": {
- "description": "Per-request timeout in milliseconds.",
- "type": "number"
+ "404": {
+ "$ref": "#/components/responses/NotFound"
},
- "retries": {
- "description": "Number of retries attempted per request.",
- "type": "number"
+ "429": {
+ "$ref": "#/components/responses/RateLimited"
},
- "enabled": {
- "type": "boolean",
- "description": "Whether the server tools are available to workflows."
+ "500": {
+ "$ref": "#/components/responses/InternalError"
},
- "connectionStatus": {
- "description": "Result of the most recent connection attempt. Registration and re-registration establish no connection — the auth-type probe they may send does not count as one — so a server begins, and returns to, `disconnected` until a tool discovery runs.",
- "type": "string",
- "enum": ["connected", "disconnected", "error"]
+ "503": {
+ "$ref": "#/components/responses/ServiceUnavailable"
+ }
+ }
+ },
+ "post": {
+ "operationId": "addPermissionGroupMember",
+ "summary": "Add Permission Group Member",
+ "description": "Assign an organization member to a permission group. An existing assignment or membership in another group targeting the same workspace returns a conflict. Requires organization admin or owner access and active Access Control. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:write`.",
+ "x-sim-operation": "permission_groups.members.add",
+ "x-oauth-scope": "api:write",
+ "tags": ["Permission Groups"],
+ "parameters": [
+ {
+ "name": "organizationId",
+ "in": "path",
+ "required": true,
+ "description": "Organization that owns the permission groups.",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Organization that owns the permission groups."
+ }
},
- "lastError": {
- "description": "Message from the most recent failed connection, or null when absent. A re-registration clears it, since the configuration it described no longer applies.",
- "anyOf": [
- {
- "type": "string"
+ {
+ "name": "groupId",
+ "in": "path",
+ "required": true,
+ "description": "Permission group identifier.",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Permission group identifier."
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "description": "Add Permission Group Member inputs.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/AddPermissionGroupMemberRequest"
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Add Permission Group Member result.",
+ "headers": {
+ "X-RateLimit-Limit": {
+ "$ref": "#/components/headers/X-RateLimit-Limit"
},
- {
- "type": "null"
+ "X-RateLimit-Remaining": {
+ "$ref": "#/components/headers/X-RateLimit-Remaining"
+ },
+ "X-RateLimit-Reset": {
+ "$ref": "#/components/headers/X-RateLimit-Reset"
}
- ]
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/AddPermissionGroupMemberResponse"
+ }
+ }
+ }
},
- "toolCount": {
- "description": "Number of tools discovered on the server.",
- "type": "number"
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
},
- "lastToolsRefresh": {
- "description": "ISO 8601 timestamp of the most recent tool-list refresh.",
- "type": "string",
- "format": "date-time",
- "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
},
- "lastConnected": {
- "description": "ISO 8601 timestamp of the most recent successful connection. Absent until the server completes one; registering a server does not set it.",
- "type": "string",
- "format": "date-time",
- "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
},
- "createdAt": {
- "description": "ISO 8601 timestamp when the server was registered.",
- "type": "string",
- "format": "date-time",
- "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
+ "404": {
+ "$ref": "#/components/responses/NotFound"
},
- "updatedAt": {
- "description": "ISO 8601 timestamp when the server was last updated.",
- "type": "string",
- "format": "date-time",
- "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
+ "409": {
+ "$ref": "#/components/responses/Conflict"
},
- "oauthClientId": {
- "description": "Pre-registered OAuth client identifier, when configured.",
- "type": "string"
+ "413": {
+ "$ref": "#/components/responses/PayloadTooLarge"
},
- "hasHeaders": {
- "type": "boolean",
- "description": "Whether any request headers are configured."
+ "415": {
+ "$ref": "#/components/responses/UnsupportedMediaType"
},
- "headerNames": {
- "type": "array",
- "items": {
+ "429": {
+ "$ref": "#/components/responses/RateLimited"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalError"
+ },
+ "503": {
+ "$ref": "#/components/responses/ServiceUnavailable"
+ }
+ }
+ }
+ },
+ "/api/v2/organizations/{organizationId}/permission-groups/{groupId}/members/{userId}": {
+ "delete": {
+ "operationId": "removePermissionGroupMember",
+ "summary": "Remove Permission Group Member",
+ "description": "Remove a member by user identifier. Removing the last member from an inherit group makes it govern everyone in its workspaces; a conflicting all-member group prevents the removal. Requires organization admin or owner access and active Access Control. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:write`.",
+ "x-sim-operation": "permission_groups.members.remove",
+ "x-oauth-scope": "api:write",
+ "tags": ["Permission Groups"],
+ "parameters": [
+ {
+ "name": "organizationId",
+ "in": "path",
+ "required": true,
+ "description": "Organization that owns the permission groups.",
+ "schema": {
"type": "string",
- "description": "Configured header name."
- },
- "description": "Names of configured request headers. Header values are never returned."
+ "minLength": 1,
+ "description": "Organization that owns the permission groups."
+ }
},
- "hasOauthClientSecret": {
- "type": "boolean",
- "description": "Whether an OAuth client secret is stored. The value is never returned."
+ {
+ "name": "groupId",
+ "in": "path",
+ "required": true,
+ "description": "Permission group identifier.",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Permission group identifier."
+ }
+ },
+ {
+ "name": "userId",
+ "in": "path",
+ "required": true,
+ "description": "User identifier of the member to remove.",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "description": "User identifier of the member to remove."
+ }
}
- },
- "required": [
- "id",
- "name",
- "transport",
- "enabled",
- "createdAt",
- "updatedAt",
- "hasHeaders",
- "headerNames",
- "hasOauthClientSecret"
],
- "additionalProperties": false,
- "title": "MCP server",
- "description": "Public MCP server configuration without write-only credential values."
- },
- "ListMcpServersResponse": {
- "type": "object",
- "properties": {
- "data": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/V2McpServer"
- },
- "description": "Items in the current page."
- },
- "nextCursor": {
- "anyOf": [
- {
- "type": "string"
+ "responses": {
+ "200": {
+ "description": "Remove Permission Group Member result.",
+ "headers": {
+ "X-RateLimit-Limit": {
+ "$ref": "#/components/headers/X-RateLimit-Limit"
},
- {
- "type": "null"
+ "X-RateLimit-Remaining": {
+ "$ref": "#/components/headers/X-RateLimit-Remaining"
+ },
+ "X-RateLimit-Reset": {
+ "$ref": "#/components/headers/X-RateLimit-Reset"
}
- ],
- "description": "Opaque cursor for the next page. Send it back as `cursor`; `null` means there is nothing further to fetch. Never construct one yourself."
- }
- },
- "required": ["data", "nextCursor"],
- "additionalProperties": false,
- "title": "List MCP servers response",
- "description": "MCP servers registered in the workspace.",
- "examples": [
- {
- "data": [
- {
- "id": "mcp-3f7a9c21",
- "name": "Docs server",
- "description": "Internal documentation tools",
- "transport": "streamable-http",
- "authType": "headers",
- "url": "https://mcp.example.com/sse",
- "timeout": 30000,
- "retries": 3,
- "enabled": true,
- "connectionStatus": "connected",
- "lastError": null,
- "toolCount": 7,
- "lastToolsRefresh": "2026-06-20T14:02:11.000Z",
- "lastConnected": "2026-06-20T14:02:11.000Z",
- "createdAt": "2026-06-01T09:14:00.000Z",
- "updatedAt": "2026-06-20T14:02:11.000Z",
- "hasHeaders": true,
- "headerNames": ["Authorization"],
- "hasOauthClientSecret": false
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/RemovePermissionGroupMemberResponse"
+ }
}
- ],
- "nextCursor": null
- }
- ]
- },
- "CreateMcpServerResponse": {
- "type": "object",
- "properties": {
- "data": {
- "description": "Response data.",
- "$ref": "#/components/schemas/V2McpServer"
- }
- },
- "required": ["data"],
- "additionalProperties": false,
- "title": "Create MCP server response",
- "description": "The registered MCP server without write-only credentials.",
- "examples": [
- {
- "data": {
- "id": "mcp-3f7a9c21",
- "name": "Docs server",
- "description": "Internal documentation tools",
- "transport": "streamable-http",
- "authType": "headers",
- "url": "https://mcp.example.com/sse",
- "timeout": 30000,
- "retries": 3,
- "enabled": true,
- "connectionStatus": "disconnected",
- "lastError": null,
- "toolCount": 0,
- "createdAt": "2026-06-01T09:14:00.000Z",
- "updatedAt": "2026-06-20T14:02:11.000Z",
- "hasHeaders": true,
- "headerNames": ["Authorization"],
- "hasOauthClientSecret": false
}
- }
- ]
- },
- "CreateMcpServerRequest": {
- "type": "object",
- "properties": {
- "workspaceId": {
- "type": "string",
- "minLength": 1,
- "maxLength": 128,
- "description": "Workspace in which to register the server."
},
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 255,
- "description": "Server display name."
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
},
- "description": {
- "description": "Optional server description.",
- "type": "string",
- "maxLength": 2000
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
},
- "transport": {
- "description": "Transport protocol. Defaults to `streamable-http` on creation.",
- "default": "streamable-http",
- "type": "string",
- "enum": ["streamable-http"]
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
},
- "url": {
- "type": "string",
- "minLength": 1,
- "maxLength": 2048,
- "description": "Absolute HTTP or HTTPS endpoint URL without `{{ENV_VAR}}` references. It determines server identity and is immutable: delete and recreate the server to change endpoints."
+ "404": {
+ "$ref": "#/components/responses/NotFound"
},
- "authType": {
- "description": "Authentication method. When omitted, and no `headers` are sent, registration probes the endpoint once to classify it, falling back to `headers` when the probe fails or the server does not advertise OAuth. A server publishing RFC 9728 metadata is therefore stored as `oauth`, and headers configured afterwards will not authenticate — send this field explicitly to pin the method.",
- "type": "string",
- "enum": ["none", "headers", "oauth"]
+ "409": {
+ "$ref": "#/components/responses/Conflict"
},
- "headers": {
- "description": "Write-only request headers sent to the server. Replaced wholesale rather than merged on update: sending this field drops every stored header it does not repeat.",
- "writeOnly": true,
- "type": "object",
- "propertyNames": {
- "type": "string",
- "minLength": 1
- },
- "additionalProperties": {
+ "429": {
+ "$ref": "#/components/responses/RateLimited"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalError"
+ },
+ "503": {
+ "$ref": "#/components/responses/ServiceUnavailable"
+ }
+ }
+ }
+ },
+ "/api/v2/organizations/{organizationId}/permission-groups/{groupId}/members/bulk": {
+ "post": {
+ "operationId": "bulkAddPermissionGroupMembers",
+ "summary": "Bulk Add Permission Group Members",
+ "description": "Assign up to 1000 selected organization members, or the entire organization roster, atomically. Existing assignments are skipped and users outside the organization are ignored. Any overlapping membership conflict rejects the entire batch. Requires organization admin or owner access and active Access Control. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:write`.",
+ "x-sim-operation": "permission_groups.members.bulk_add",
+ "x-oauth-scope": "api:write",
+ "tags": ["Permission Groups"],
+ "parameters": [
+ {
+ "name": "organizationId",
+ "in": "path",
+ "required": true,
+ "description": "Organization that owns the permission groups.",
+ "schema": {
"type": "string",
- "description": "Header value sent to the MCP server."
+ "minLength": 1,
+ "description": "Organization that owns the permission groups."
}
},
- "timeout": {
- "description": "Per-request timeout in milliseconds. Defaults to 30000 on creation.",
- "default": 30000,
- "type": "integer",
- "minimum": 1000,
- "maximum": 300000
- },
- "retries": {
- "description": "Number of retries per request. Defaults to 3 on creation.",
- "default": 3,
- "type": "integer",
- "minimum": 0,
- "maximum": 10
- },
- "enabled": {
- "description": "Whether workflows can use the server's tools. Defaults to true on creation.",
- "default": true,
- "type": "boolean"
- },
- "oauthClientId": {
- "description": "Pre-registered OAuth client identifier. Changing it on update revokes the stored OAuth grant and forces reauthorization.",
- "anyOf": [
- {
- "type": "string",
- "maxLength": 512
- },
- {
- "type": "null"
- }
- ]
- },
- "oauthClientSecret": {
- "description": "Write-only pre-registered OAuth client secret. Sending it on update as null or a new value revokes the stored OAuth grant and forces reauthorization, as does switching away from OAuth authentication.",
- "writeOnly": true,
- "anyOf": [
- {
- "type": "string",
- "maxLength": 2048
- },
- {
- "type": "null"
- }
- ]
- }
- },
- "required": ["workspaceId", "name", "url"],
- "additionalProperties": false,
- "title": "Create MCP server request",
- "description": "Configuration for a new MCP server.",
- "examples": [
{
- "workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
- "name": "Docs server",
- "url": "https://mcp.example.com/sse",
- "authType": "headers",
- "headers": {
- "Authorization": "Bearer YOUR_TOKEN"
+ "name": "groupId",
+ "in": "path",
+ "required": true,
+ "description": "Permission group identifier.",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Permission group identifier."
}
}
- ]
- },
- "GetMcpServerResponse": {
- "type": "object",
- "properties": {
- "data": {
- "description": "Response data.",
- "$ref": "#/components/schemas/V2McpServer"
- }
- },
- "required": ["data"],
- "additionalProperties": false,
- "title": "Get MCP server response",
- "description": "One MCP server without write-only credentials.",
- "examples": [
- {
- "data": {
- "id": "mcp-3f7a9c21",
- "name": "Docs server",
- "description": "Internal documentation tools",
- "transport": "streamable-http",
- "authType": "headers",
- "url": "https://mcp.example.com/sse",
- "timeout": 30000,
- "retries": 3,
- "enabled": true,
- "connectionStatus": "connected",
- "lastError": null,
- "toolCount": 7,
- "lastToolsRefresh": "2026-06-20T14:02:11.000Z",
- "lastConnected": "2026-06-20T14:02:11.000Z",
- "createdAt": "2026-06-01T09:14:00.000Z",
- "updatedAt": "2026-06-20T14:02:11.000Z",
- "hasHeaders": true,
- "headerNames": ["Authorization"],
- "hasOauthClientSecret": false
+ ],
+ "requestBody": {
+ "required": true,
+ "description": "Bulk Add Permission Group Members inputs.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BulkAddPermissionGroupMembersRequest"
+ }
}
}
- ]
- },
- "UpdateMcpServerResponse": {
- "type": "object",
- "properties": {
- "data": {
- "description": "Response data.",
- "$ref": "#/components/schemas/V2McpServer"
- }
},
- "required": ["data"],
- "additionalProperties": false,
- "title": "Update MCP server response",
- "description": "The updated MCP server.",
- "examples": [
- {
- "data": {
- "id": "mcp-3f7a9c21",
- "name": "Docs server",
- "description": "Internal documentation tools",
- "transport": "streamable-http",
- "authType": "headers",
- "url": "https://mcp.example.com/sse",
- "timeout": 30000,
- "retries": 3,
- "enabled": false,
- "connectionStatus": "connected",
- "lastError": null,
- "toolCount": 7,
- "lastToolsRefresh": "2026-06-20T14:02:11.000Z",
- "lastConnected": "2026-06-20T14:02:11.000Z",
- "createdAt": "2026-06-01T09:14:00.000Z",
- "updatedAt": "2026-06-20T14:02:11.000Z",
- "hasHeaders": true,
- "headerNames": ["Authorization"],
- "hasOauthClientSecret": false
+ "responses": {
+ "200": {
+ "description": "Bulk Add Permission Group Members result.",
+ "headers": {
+ "X-RateLimit-Limit": {
+ "$ref": "#/components/headers/X-RateLimit-Limit"
+ },
+ "X-RateLimit-Remaining": {
+ "$ref": "#/components/headers/X-RateLimit-Remaining"
+ },
+ "X-RateLimit-Reset": {
+ "$ref": "#/components/headers/X-RateLimit-Reset"
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BulkAddPermissionGroupMembersResponse"
+ }
+ }
}
- }
- ]
- },
- "UpdateMcpServerRequest": {
- "type": "object",
- "properties": {
- "workspaceId": {
- "type": "string",
- "minLength": 1,
- "maxLength": 128,
- "description": "Workspace that owns the MCP server."
- },
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 255,
- "description": "Server display name."
},
- "description": {
- "description": "Optional server description.",
- "type": "string",
- "maxLength": 2000
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
},
- "transport": {
- "description": "Transport protocol. Defaults to `streamable-http` on creation.",
- "default": "streamable-http",
- "type": "string",
- "enum": ["streamable-http"]
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
},
- "url": {
- "description": "Immutable server URL. When provided, it must equal the current URL; use delete and create to change endpoints.",
- "type": "string",
- "minLength": 1,
- "maxLength": 2048
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
},
- "authType": {
- "description": "Authentication method. When omitted, and no `headers` are sent, registration probes the endpoint once to classify it, falling back to `headers` when the probe fails or the server does not advertise OAuth. A server publishing RFC 9728 metadata is therefore stored as `oauth`, and headers configured afterwards will not authenticate — send this field explicitly to pin the method.",
- "type": "string",
- "enum": ["none", "headers", "oauth"]
+ "404": {
+ "$ref": "#/components/responses/NotFound"
},
- "headers": {
- "description": "Write-only request headers sent to the server. Replaced wholesale rather than merged on update: sending this field drops every stored header it does not repeat.",
- "writeOnly": true,
- "type": "object",
- "propertyNames": {
- "type": "string",
- "minLength": 1
- },
- "additionalProperties": {
- "type": "string",
- "description": "Header value sent to the MCP server."
- }
+ "409": {
+ "$ref": "#/components/responses/Conflict"
},
- "timeout": {
- "description": "Per-request timeout in milliseconds. Defaults to 30000 on creation.",
- "default": 30000,
- "type": "integer",
- "minimum": 1000,
- "maximum": 300000
+ "413": {
+ "$ref": "#/components/responses/PayloadTooLarge"
},
- "retries": {
- "description": "Number of retries per request. Defaults to 3 on creation.",
- "default": 3,
- "type": "integer",
- "minimum": 0,
- "maximum": 10
+ "415": {
+ "$ref": "#/components/responses/UnsupportedMediaType"
},
- "enabled": {
- "description": "Whether workflows can use the server's tools. Defaults to true on creation.",
- "default": true,
- "type": "boolean"
+ "429": {
+ "$ref": "#/components/responses/RateLimited"
},
- "oauthClientId": {
- "description": "Pre-registered OAuth client identifier. Changing it on update revokes the stored OAuth grant and forces reauthorization.",
- "anyOf": [
- {
- "type": "string",
- "maxLength": 512
- },
- {
- "type": "null"
- }
- ]
+ "500": {
+ "$ref": "#/components/responses/InternalError"
},
- "oauthClientSecret": {
- "description": "Write-only pre-registered OAuth client secret. Sending it on update as null or a new value revokes the stored OAuth grant and forces reauthorization, as does switching away from OAuth authentication.",
- "writeOnly": true,
- "anyOf": [
- {
- "type": "string",
- "maxLength": 2048
- },
- {
- "type": "null"
- }
- ]
+ "503": {
+ "$ref": "#/components/responses/ServiceUnavailable"
}
- },
- "required": ["workspaceId"],
- "additionalProperties": false,
- "title": "Update MCP server request",
- "description": "MCP server fields to change; omitted fields retain their stored values.",
- "examples": [
+ }
+ }
+ },
+ "/api/v2/organizations": {
+ "get": {
+ "operationId": "listOrganizations",
+ "summary": "List Organizations",
+ "description": "List organizations the acting user belongs to. Organizations that disallow the calling credential are omitted. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:read`.",
+ "x-sim-operation": "organizations.list",
+ "x-oauth-scope": "api:read",
+ "tags": ["Organizations"],
+ "parameters": [
{
- "workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
- "enabled": false
- }
- ]
- },
- "V2McpServerDeleteData": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string",
- "description": "Identifier of the deleted MCP server."
+ "name": "search",
+ "in": "query",
+ "required": false,
+ "description": "Case-insensitive substring match against the organization name.",
+ "schema": {
+ "description": "Case-insensitive substring match against the organization name.",
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200
+ }
},
- "deleted": {
- "type": "boolean",
- "const": true,
- "description": "Whether the server was deleted."
- }
- },
- "required": ["id", "deleted"],
- "additionalProperties": false,
- "title": "Delete MCP server data",
- "description": "MCP server deletion acknowledgement."
- },
- "DeleteMcpServerResponse": {
- "type": "object",
- "properties": {
- "data": {
- "description": "Response data.",
- "$ref": "#/components/schemas/V2McpServerDeleteData"
- }
- },
- "required": ["data"],
- "additionalProperties": false,
- "title": "Delete MCP server response",
- "description": "Acknowledgement that the MCP server was deleted.",
- "examples": [
{
- "data": {
- "id": "mcp-3f7a9c21",
- "deleted": true
+ "name": "sortBy",
+ "in": "query",
+ "required": false,
+ "description": "Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order.",
+ "schema": {
+ "default": "name",
+ "description": "Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order.",
+ "type": "string",
+ "enum": ["name", "createdAt"]
}
- }
- ]
- },
- "V2McpTool": {
- "type": "object",
- "properties": {
- "name": {
- "type": "string",
- "description": "Tool name, as the MCP server reports it."
},
- "description": {
- "description": "Tool description reported by the server.",
- "type": "string"
+ {
+ "name": "sortOrder",
+ "in": "query",
+ "required": false,
+ "description": "Sort direction.",
+ "schema": {
+ "default": "asc",
+ "description": "Sort direction.",
+ "type": "string",
+ "enum": ["asc", "desc"]
+ }
},
- "inputSchema": {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "const": "object",
- "description": "JSON Schema type of the argument object. MCP requires `object`."
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "description": "Maximum organizations to return per page. Must be a whole number from 1 to 100. Defaults to 50.",
+ "schema": {
+ "default": 50,
+ "description": "Maximum organizations to return per page. Must be a whole number from 1 to 100. Defaults to 50.",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100
+ }
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "description": "Opaque cursor from the previous page. Send it back with the same sort and filters; only `limit` may change. Change anything else and pagination must restart without a cursor.",
+ "schema": {
+ "description": "Opaque cursor from the previous page. Send it back with the same sort and filters; only `limit` may change. Change anything else and pagination must restart without a cursor.",
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "List Organizations result.",
+ "headers": {
+ "X-RateLimit-Limit": {
+ "$ref": "#/components/headers/X-RateLimit-Limit"
},
- "properties": {
- "description": "Argument schemas keyed by argument name.",
- "type": "object",
- "propertyNames": {
- "type": "string"
- },
- "additionalProperties": {
- "description": "Server-defined JSON Schema for one tool argument."
- }
+ "X-RateLimit-Remaining": {
+ "$ref": "#/components/headers/X-RateLimit-Remaining"
},
- "required": {
- "description": "Names of the arguments the tool requires.",
- "type": "array",
- "items": {
- "type": "string",
- "description": "Name of a required argument."
- }
+ "X-RateLimit-Reset": {
+ "$ref": "#/components/headers/X-RateLimit-Reset"
}
},
- "required": ["type"],
- "additionalProperties": {
- "description": "Additional JSON Schema keyword reported by the server."
- },
- "description": "JSON Schema for the tool's arguments, as reported by the server."
- },
- "serverId": {
- "type": "string",
- "description": "Identifier of the MCP server exposing the tool."
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ListOrganizationsResponse"
+ }
+ }
+ }
},
- "serverName": {
- "type": "string",
- "description": "Display name of the MCP server exposing the tool."
- }
- },
- "required": ["name", "inputSchema", "serverId", "serverName"],
- "additionalProperties": false,
- "title": "MCP tool",
- "description": "A tool exposed by a registered MCP server."
- },
- "ListMcpServerToolsResponse": {
- "type": "object",
- "properties": {
- "data": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/V2McpTool"
- },
- "description": "Items in the current page."
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
},
- "nextCursor": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Always `null` — this list has no `cursor` or `limit` param and returns its whole bounded set in one page. Present so the list can gain pages later without a shape change."
- }
- },
- "required": ["data", "nextCursor"],
- "additionalProperties": false,
- "title": "List MCP server tools response",
- "description": "Tools exposed by the MCP server.",
- "examples": [
- {
- "data": [
- {
- "name": "search_docs",
- "description": "Search the internal documentation",
- "inputSchema": {
- "type": "object",
- "properties": {
- "query": {
- "type": "string",
- "description": "Search terms"
- }
- },
- "required": ["query"]
- },
- "serverId": "mcp-3f7a9c21",
- "serverName": "Docs server"
- }
- ],
- "nextCursor": null
- }
- ]
- },
- "V2SkillSummary": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string",
- "description": "Unique skill identifier. A built-in skill is `builtin-` followed by its name, for example `builtin-research`."
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
},
- "name": {
- "type": "string",
- "description": "Kebab-case name that agents use to reference the skill."
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
},
- "description": {
- "type": "string",
- "description": "One-line summary of when the skill applies."
+ "404": {
+ "$ref": "#/components/responses/NotFound"
},
- "readOnly": {
- "type": "boolean",
- "description": "Whether this is a built-in skill that cannot be modified or deleted."
+ "429": {
+ "$ref": "#/components/responses/RateLimited"
},
- "createdAt": {
- "type": "string",
- "format": "date-time",
- "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
- "description": "ISO 8601 timestamp when the skill was created. Built-in skills report the Unix epoch."
+ "500": {
+ "$ref": "#/components/responses/InternalError"
},
- "updatedAt": {
- "type": "string",
- "format": "date-time",
- "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
- "description": "ISO 8601 timestamp when the skill was last updated. Built-in skills report the Unix epoch."
+ "503": {
+ "$ref": "#/components/responses/ServiceUnavailable"
}
- },
- "required": ["id", "name", "description", "readOnly", "createdAt", "updatedAt"],
- "additionalProperties": false,
- "title": "Skill summary",
- "description": "Public summary metadata for a workspace or built-in skill."
- },
- "ListSkillsResponse": {
- "type": "object",
- "properties": {
- "data": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/V2SkillSummary"
- },
- "description": "Items in the current page."
- },
- "nextCursor": {
- "anyOf": [
- {
- "type": "string"
+ }
+ }
+ },
+ "/api/v2/organizations/{organizationId}": {
+ "get": {
+ "operationId": "getOrganization",
+ "summary": "Get Organization",
+ "description": "Get organization metadata and the acting user’s organization role. Requires organization membership. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:read`.",
+ "x-sim-operation": "organizations.read",
+ "x-oauth-scope": "api:read",
+ "tags": ["Organizations"],
+ "parameters": [
+ {
+ "name": "organizationId",
+ "in": "path",
+ "required": true,
+ "description": "Organization identifier.",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Organization identifier."
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Get Organization result.",
+ "headers": {
+ "X-RateLimit-Limit": {
+ "$ref": "#/components/headers/X-RateLimit-Limit"
},
- {
- "type": "null"
+ "X-RateLimit-Remaining": {
+ "$ref": "#/components/headers/X-RateLimit-Remaining"
+ },
+ "X-RateLimit-Reset": {
+ "$ref": "#/components/headers/X-RateLimit-Reset"
}
- ],
- "description": "Opaque cursor for the next page. Send it back as `cursor`; `null` means there is nothing further to fetch. Never construct one yourself."
- }
- },
- "required": ["data", "nextCursor"],
- "additionalProperties": false,
- "title": "List skills response",
- "description": "Skill summaries available in the workspace.",
- "examples": [
- {
- "data": [
- {
- "id": "V1StGXR8Z5jdHi6BmyT",
- "name": "refund-policy",
- "description": "How support should handle refund requests",
- "readOnly": false,
- "createdAt": "2026-06-01T09:14:00.000Z",
- "updatedAt": "2026-06-20T14:02:11.000Z"
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/GetOrganizationResponse"
+ }
}
- ],
- "nextCursor": null
- }
- ]
- },
- "V2Skill": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string",
- "description": "Unique skill identifier. A built-in skill is `builtin-` followed by its name, for example `builtin-research`."
+ }
},
- "name": {
- "type": "string",
- "description": "Kebab-case name that agents use to reference the skill."
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
},
- "description": {
- "type": "string",
- "description": "One-line summary of when the skill applies."
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
},
- "readOnly": {
- "type": "boolean",
- "description": "Whether this is a built-in skill that cannot be modified or deleted."
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
},
- "createdAt": {
- "type": "string",
- "format": "date-time",
- "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
- "description": "ISO 8601 timestamp when the skill was created. Built-in skills report the Unix epoch."
+ "404": {
+ "$ref": "#/components/responses/NotFound"
},
- "updatedAt": {
- "type": "string",
- "format": "date-time",
- "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
- "description": "ISO 8601 timestamp when the skill was last updated. Built-in skills report the Unix epoch."
+ "429": {
+ "$ref": "#/components/responses/RateLimited"
},
- "content": {
- "type": "string",
- "description": "Skill body containing the instructions given to the agent."
- }
- },
- "required": ["id", "name", "description", "readOnly", "createdAt", "updatedAt", "content"],
- "additionalProperties": false,
- "title": "Skill",
- "description": "A workspace or built-in skill including its instruction body."
- },
- "CreateSkillResponse": {
- "type": "object",
- "properties": {
- "data": {
- "description": "Response data.",
- "$ref": "#/components/schemas/V2Skill"
+ "500": {
+ "$ref": "#/components/responses/InternalError"
+ },
+ "503": {
+ "$ref": "#/components/responses/ServiceUnavailable"
}
- },
- "required": ["data"],
- "additionalProperties": false,
- "title": "Create skill response",
- "description": "The created skill including its content.",
- "examples": [
+ }
+ }
+ },
+ "/api/v2/organizations/{organizationId}/workspaces": {
+ "get": {
+ "operationId": "listOrganizationWorkspaces",
+ "summary": "List Organization Workspaces",
+ "description": "List active workspaces owned by the organization. Requires organization administrator access; does not require Access Control. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:read`.",
+ "x-sim-operation": "organizations.workspaces.list",
+ "x-oauth-scope": "api:read",
+ "tags": ["Organizations"],
+ "parameters": [
{
- "data": {
- "id": "V1StGXR8Z5jdHi6BmyT",
- "name": "refund-policy",
- "description": "How support should handle refund requests",
- "readOnly": false,
- "createdAt": "2026-06-01T09:14:00.000Z",
- "updatedAt": "2026-06-20T14:02:11.000Z",
- "content": "# Refund policy\n\nAlways check the order date first."
+ "name": "organizationId",
+ "in": "path",
+ "required": true,
+ "description": "Organization identifier.",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Organization identifier."
}
- }
- ]
- },
- "CreateSkillRequest": {
- "type": "object",
- "properties": {
- "workspaceId": {
- "type": "string",
- "minLength": 1,
- "maxLength": 128,
- "description": "Workspace in which to create the skill."
- },
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 64,
- "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
- "description": "Kebab-case name, unique within the workspace and not reserved by a built-in skill."
- },
- "description": {
- "type": "string",
- "minLength": 1,
- "maxLength": 1024,
- "description": "One-line summary of when the skill applies."
},
- "content": {
- "type": "string",
- "minLength": 1,
- "maxLength": 50000,
- "description": "Skill body containing the instructions given to the agent."
- }
- },
- "required": ["workspaceId", "name", "description", "content"],
- "additionalProperties": false,
- "title": "Create skill request",
- "description": "Definition of a new skill.",
- "examples": [
{
- "workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
- "name": "refund-policy",
- "description": "How support should handle refund requests",
- "content": "# Refund policy\n\nAlways check the order date first."
- }
- ]
- },
- "GetSkillResponse": {
- "type": "object",
- "properties": {
- "data": {
- "description": "Response data.",
- "$ref": "#/components/schemas/V2Skill"
- }
- },
- "required": ["data"],
- "additionalProperties": false,
- "title": "Get skill response",
- "description": "One skill including its full content.",
- "examples": [
+ "name": "search",
+ "in": "query",
+ "required": false,
+ "description": "Case-insensitive substring match against the workspace name.",
+ "schema": {
+ "description": "Case-insensitive substring match against the workspace name.",
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200
+ }
+ },
{
- "data": {
- "id": "V1StGXR8Z5jdHi6BmyT",
- "name": "refund-policy",
- "description": "How support should handle refund requests",
- "readOnly": false,
- "createdAt": "2026-06-01T09:14:00.000Z",
- "updatedAt": "2026-06-20T14:02:11.000Z",
- "content": "# Refund policy\n\nAlways check the order date first."
+ "name": "sortBy",
+ "in": "query",
+ "required": false,
+ "description": "Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order.",
+ "schema": {
+ "default": "name",
+ "description": "Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order.",
+ "type": "string",
+ "enum": ["name", "id"]
}
- }
- ]
- },
- "UpdateSkillResponse": {
- "type": "object",
- "properties": {
- "data": {
- "description": "Response data.",
- "$ref": "#/components/schemas/V2Skill"
- }
- },
- "required": ["data"],
- "additionalProperties": false,
- "title": "Update skill response",
- "description": "The updated skill including its full content.",
- "examples": [
+ },
{
- "data": {
- "id": "V1StGXR8Z5jdHi6BmyT",
- "name": "refund-policy",
- "description": "Updated refund guidance",
- "readOnly": false,
- "createdAt": "2026-06-01T09:14:00.000Z",
- "updatedAt": "2026-06-20T14:02:11.000Z",
- "content": "# Refund policy\n\nAlways check the order date first."
+ "name": "sortOrder",
+ "in": "query",
+ "required": false,
+ "description": "Sort direction.",
+ "schema": {
+ "default": "asc",
+ "description": "Sort direction.",
+ "type": "string",
+ "enum": ["asc", "desc"]
}
- }
- ]
- },
- "UpdateSkillRequest": {
- "type": "object",
- "properties": {
- "workspaceId": {
- "type": "string",
- "minLength": 1,
- "maxLength": 128,
- "description": "Workspace that owns the skill."
- },
- "name": {
- "description": "New kebab-case skill name.",
- "type": "string",
- "minLength": 1,
- "maxLength": 64,
- "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
},
- "description": {
- "description": "New one-line summary of when the skill applies.",
- "type": "string",
- "minLength": 1,
- "maxLength": 1024
- },
- "content": {
- "description": "Replacement skill body.",
- "type": "string",
- "minLength": 1,
- "maxLength": 50000
- }
- },
- "required": ["workspaceId"],
- "additionalProperties": false,
- "title": "Update skill request",
- "description": "Skill fields to change; at least one editable field is required.",
- "examples": [
{
- "workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
- "description": "Updated refund guidance"
- }
- ]
- },
- "V2SkillDeleteData": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string",
- "description": "Identifier of the deleted skill."
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "description": "Maximum workspaces to return per page. Must be a whole number from 1 to 100. Defaults to 50.",
+ "schema": {
+ "default": 50,
+ "description": "Maximum workspaces to return per page. Must be a whole number from 1 to 100. Defaults to 50.",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100
+ }
},
- "deleted": {
- "type": "boolean",
- "const": true,
- "description": "Whether the skill was deleted."
- }
- },
- "required": ["id", "deleted"],
- "additionalProperties": false,
- "title": "Delete skill data",
- "description": "Skill deletion acknowledgement."
- },
- "DeleteSkillResponse": {
- "type": "object",
- "properties": {
- "data": {
- "description": "Response data.",
- "$ref": "#/components/schemas/V2SkillDeleteData"
- }
- },
- "required": ["data"],
- "additionalProperties": false,
- "title": "Delete skill response",
- "description": "Acknowledgement that the skill was deleted.",
- "examples": [
{
- "data": {
- "id": "V1StGXR8Z5jdHi6BmyT",
- "deleted": true
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "description": "Opaque cursor from the previous page. Send it back with the same sort and filters; only `limit` may change. Change anything else and pagination must restart without a cursor.",
+ "schema": {
+ "description": "Opaque cursor from the previous page. Send it back with the same sort and filters; only `limit` may change. Change anything else and pagination must restart without a cursor.",
+ "type": "string",
+ "minLength": 1
}
}
- ]
- },
- "V2SkillEditor": {
- "type": "object",
- "properties": {
- "email": {
- "type": "string",
- "format": "email",
- "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
- "description": "Email address of the skill editor."
- },
- "name": {
- "anyOf": [
- {
- "type": "string"
+ ],
+ "responses": {
+ "200": {
+ "description": "List Organization Workspaces result.",
+ "headers": {
+ "X-RateLimit-Limit": {
+ "$ref": "#/components/headers/X-RateLimit-Limit"
},
- {
- "type": "null"
- }
- ],
- "description": "Display name of the skill editor."
- },
- "image": {
- "anyOf": [
- {
- "type": "string"
+ "X-RateLimit-Remaining": {
+ "$ref": "#/components/headers/X-RateLimit-Remaining"
},
- {
- "type": "null"
+ "X-RateLimit-Reset": {
+ "$ref": "#/components/headers/X-RateLimit-Reset"
}
- ],
- "description": "Profile image URL of the skill editor."
- },
- "isWorkspaceAdmin": {
- "type": "boolean",
- "description": "Whether editor access is derived from workspace administration."
- }
- },
- "required": ["email", "name", "image", "isWorkspaceAdmin"],
- "additionalProperties": false,
- "title": "Skill editor",
- "description": "Public identity fields for a user who can edit a skill."
- },
- "ListSkillEditorsResponse": {
- "type": "object",
- "properties": {
- "data": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/V2SkillEditor"
},
- "description": "Items in the current page."
- },
- "nextCursor": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Opaque cursor for the next page. Send it back as `cursor`; `null` means there is nothing further to fetch. Never construct one yourself."
- }
- },
- "required": ["data", "nextCursor"],
- "additionalProperties": false,
- "title": "List skill editors response",
- "description": "Public identity fields for users who can edit the skill.",
- "examples": [
- {
- "data": [
- {
- "email": "jane@example.com",
- "name": "Jane Smith",
- "image": null,
- "isWorkspaceAdmin": false
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ListOrganizationWorkspacesResponse"
+ }
}
- ],
- "nextCursor": null
- }
- ]
- },
- "GrantSkillEditorResponse": {
- "type": "object",
- "properties": {
- "data": {
- "description": "Response data.",
- "$ref": "#/components/schemas/V2SkillEditor"
- }
- },
- "required": ["data"],
- "additionalProperties": false,
- "title": "Grant skill editor response",
- "description": "Public identity fields for the editor.",
- "examples": [
- {
- "data": {
- "email": "jane@example.com",
- "name": "Jane Smith",
- "image": null,
- "isWorkspaceAdmin": false
}
- }
- ]
- },
- "GrantSkillEditorRequest": {
- "type": "object",
- "properties": {
- "workspaceId": {
- "type": "string",
- "minLength": 1,
- "maxLength": 128,
- "description": "Workspace that owns the skill."
},
- "email": {
- "type": "string",
- "format": "email",
- "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
- "description": "Email address of a current workspace member."
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "429": {
+ "$ref": "#/components/responses/RateLimited"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalError"
+ },
+ "503": {
+ "$ref": "#/components/responses/ServiceUnavailable"
}
- },
- "required": ["workspaceId", "email"],
- "additionalProperties": false,
- "title": "Grant skill editor request",
- "description": "Workspace scope and email of the member to grant.",
- "examples": [
+ }
+ }
+ },
+ "/api/v2/organizations/{organizationId}/members": {
+ "get": {
+ "operationId": "listOrganizationMembers",
+ "summary": "List Organization Members",
+ "description": "List organization members by name or email. Ordinary members must have access to the member directory; organization administrators retain access. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:read`.",
+ "x-sim-operation": "organizations.members.list",
+ "x-oauth-scope": "api:read",
+ "tags": ["Organizations"],
+ "parameters": [
{
- "workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
- "email": "jane@example.com"
- }
- ]
- },
- "V2SkillEditorDeleteData": {
- "type": "object",
- "properties": {
- "email": {
- "type": "string",
- "format": "email",
- "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
- "description": "Email address whose explicit editor grant was revoked."
+ "name": "organizationId",
+ "in": "path",
+ "required": true,
+ "description": "Organization identifier.",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Organization identifier."
+ }
},
- "revoked": {
- "type": "boolean",
- "const": true,
- "description": "Whether the explicit editor grant was revoked."
- }
- },
- "required": ["email", "revoked"],
- "additionalProperties": false,
- "title": "Revoke skill editor data",
- "description": "Skill editor revocation acknowledgement."
- },
- "RevokeSkillEditorResponse": {
- "type": "object",
- "properties": {
- "data": {
- "description": "Response data.",
- "$ref": "#/components/schemas/V2SkillEditorDeleteData"
- }
- },
- "required": ["data"],
- "additionalProperties": false,
- "title": "Revoke skill editor response",
- "description": "Acknowledgement that the explicit editor grant was revoked.",
- "examples": [
{
- "data": {
- "email": "jane@example.com",
- "revoked": true
+ "name": "search",
+ "in": "query",
+ "required": false,
+ "description": "Case-insensitive substring match against member name or email.",
+ "schema": {
+ "description": "Case-insensitive substring match against member name or email.",
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200
}
- }
- ]
- },
- "V2CustomTool": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string",
- "description": "Unique custom tool identifier."
},
- "title": {
- "type": "string",
- "description": "Display title, unique within the workspace."
+ {
+ "name": "sortBy",
+ "in": "query",
+ "required": false,
+ "description": "Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order.",
+ "schema": {
+ "default": "name",
+ "description": "Field used to sort the result. Sorting by `name` is case-sensitive and follows the storage collation, so do not rely on a case-insensitive order.",
+ "type": "string",
+ "enum": ["name", "email", "joinedAt"]
+ }
},
- "schema": {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "const": "function",
- "description": "Function declaration discriminator."
+ {
+ "name": "sortOrder",
+ "in": "query",
+ "required": false,
+ "description": "Sort direction.",
+ "schema": {
+ "default": "asc",
+ "description": "Sort direction.",
+ "type": "string",
+ "enum": ["asc", "desc"]
+ }
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "description": "Maximum members to return per page. Must be a whole number from 1 to 100. Defaults to 50.",
+ "schema": {
+ "default": 50,
+ "description": "Maximum members to return per page. Must be a whole number from 1 to 100. Defaults to 50.",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100
+ }
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "description": "Opaque cursor from the previous page. Send it back with the same sort and filters; only `limit` may change. Change anything else and pagination must restart without a cursor.",
+ "schema": {
+ "description": "Opaque cursor from the previous page. Send it back with the same sort and filters; only `limit` may change. Change anything else and pagination must restart without a cursor.",
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "List Organization Members result.",
+ "headers": {
+ "X-RateLimit-Limit": {
+ "$ref": "#/components/headers/X-RateLimit-Limit"
},
- "function": {
- "type": "object",
- "properties": {
- "name": {
- "type": "string",
- "minLength": 1,
- "description": "Function name presented to the model."
- },
- "description": {
- "description": "Optional explanation of what the function does.",
- "type": "string"
- },
- "parameters": {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "description": "JSON Schema type for the arguments, usually `object`."
- },
- "properties": {
- "type": "object",
- "propertyNames": {
- "type": "string"
- },
- "additionalProperties": {
- "description": "Caller-defined JSON Schema for one tool argument."
- },
- "description": "Caller-defined argument schemas keyed by argument name."
- },
- "required": {
- "description": "Names of required arguments.",
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- },
- "required": ["type", "properties"],
- "additionalProperties": {
- "description": "Caller-defined extension value preserved by the public API."
- },
- "description": "JSON Schema describing the arguments accepted by the tool."
- }
- },
- "required": ["name", "parameters"],
- "additionalProperties": {
- "description": "Caller-defined extension value preserved by the public API."
- },
- "description": "OpenAI-style function definition."
+ "X-RateLimit-Remaining": {
+ "$ref": "#/components/headers/X-RateLimit-Remaining"
+ },
+ "X-RateLimit-Reset": {
+ "$ref": "#/components/headers/X-RateLimit-Reset"
}
},
- "required": ["type", "function"],
- "additionalProperties": {
- "description": "Caller-defined extension value preserved by the public API."
- },
- "description": "OpenAI-style function declaration describing the callable tool surface."
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ListOrganizationMembersResponse"
+ }
+ }
+ }
},
- "code": {
- "type": "string",
- "description": "Tool implementation executed in the sandboxed function runtime."
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
},
- "createdAt": {
- "type": "string",
- "format": "date-time",
- "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
- "description": "ISO 8601 timestamp when the tool was created."
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
},
- "updatedAt": {
- "type": "string",
- "format": "date-time",
- "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
- "description": "ISO 8601 timestamp when the tool was last updated."
- }
- },
- "required": ["id", "title", "schema", "code", "createdAt", "updatedAt"],
- "additionalProperties": false,
- "title": "Custom tool",
- "description": "A workspace custom tool and its callable function declaration."
- },
- "ListCustomToolsResponse": {
- "type": "object",
- "properties": {
- "data": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/V2CustomTool"
- },
- "description": "Items in the current page."
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
},
- "nextCursor": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Opaque cursor for the next page. Send it back as `cursor`; `null` means there is nothing further to fetch. Never construct one yourself."
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "429": {
+ "$ref": "#/components/responses/RateLimited"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalError"
+ },
+ "503": {
+ "$ref": "#/components/responses/ServiceUnavailable"
}
- },
- "required": ["data", "nextCursor"],
- "additionalProperties": false,
- "title": "List custom tools response",
- "description": "Custom tools defined in the workspace.",
- "examples": [
+ }
+ }
+ },
+ "/api/v2/organizations/{organizationId}/members/{userId}": {
+ "patch": {
+ "operationId": "updateOrganizationMember",
+ "summary": "Update Organization Member",
+ "description": "Change a member’s organization role. Requires organization administrator access. The owner’s role and memberships managed by an identity provider cannot be changed here. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:write`.",
+ "x-sim-operation": "organizations.members.update",
+ "x-oauth-scope": "api:write",
+ "tags": ["Organizations"],
+ "parameters": [
{
- "data": [
- {
- "id": "V1StGXR8Z5jdHi6BmyT",
- "title": "lookup_order",
- "schema": {
- "type": "function",
- "function": {
- "name": "lookup_order",
- "description": "Look up an order by id",
- "parameters": {
- "type": "object",
- "properties": {
- "orderId": {
- "type": "string"
- }
- },
- "required": ["orderId"]
- }
- }
- },
- "code": "return { ok: true }",
- "createdAt": "2026-06-01T09:14:00.000Z",
- "updatedAt": "2026-06-20T14:02:11.000Z"
- }
- ],
- "nextCursor": null
+ "name": "organizationId",
+ "in": "path",
+ "required": true,
+ "description": "Organization identifier.",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Organization identifier."
+ }
+ },
+ {
+ "name": "userId",
+ "in": "path",
+ "required": true,
+ "description": "User identifier of the organization member.",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "description": "User identifier of the organization member."
+ }
}
- ]
- },
- "CreateCustomToolResponse": {
- "type": "object",
- "properties": {
- "data": {
- "description": "Response data.",
- "$ref": "#/components/schemas/V2CustomTool"
+ ],
+ "requestBody": {
+ "required": true,
+ "description": "Update Organization Member input.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UpdateOrganizationMemberBody"
+ }
+ }
}
},
- "required": ["data"],
- "additionalProperties": false,
- "title": "Create custom tool response",
- "description": "The created custom tool.",
- "examples": [
- {
- "data": {
- "id": "V1StGXR8Z5jdHi6BmyT",
- "title": "lookup_order",
- "schema": {
- "type": "function",
- "function": {
- "name": "lookup_order",
- "description": "Look up an order by id",
- "parameters": {
- "type": "object",
- "properties": {
- "orderId": {
- "type": "string"
- }
- },
- "required": ["orderId"]
- }
- }
+ "responses": {
+ "200": {
+ "description": "Update Organization Member result.",
+ "headers": {
+ "X-RateLimit-Limit": {
+ "$ref": "#/components/headers/X-RateLimit-Limit"
},
- "code": "return { ok: true }",
- "createdAt": "2026-06-01T09:14:00.000Z",
- "updatedAt": "2026-06-20T14:02:11.000Z"
+ "X-RateLimit-Remaining": {
+ "$ref": "#/components/headers/X-RateLimit-Remaining"
+ },
+ "X-RateLimit-Reset": {
+ "$ref": "#/components/headers/X-RateLimit-Reset"
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UpdateOrganizationMemberResponse"
+ }
+ }
}
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "413": {
+ "$ref": "#/components/responses/PayloadTooLarge"
+ },
+ "415": {
+ "$ref": "#/components/responses/UnsupportedMediaType"
+ },
+ "429": {
+ "$ref": "#/components/responses/RateLimited"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalError"
+ },
+ "503": {
+ "$ref": "#/components/responses/ServiceUnavailable"
}
- ]
+ }
},
- "CreateCustomToolRequest": {
- "type": "object",
- "properties": {
- "workspaceId": {
- "type": "string",
- "minLength": 1,
- "maxLength": 128,
- "description": "Workspace in which to create the custom tool."
- },
- "title": {
- "type": "string",
- "minLength": 1,
- "maxLength": 200,
- "description": "Display title, unique within the workspace."
+ "delete": {
+ "operationId": "removeOrganizationMember",
+ "summary": "Remove Organization Member",
+ "description": "Remove a member and revoke their access to organization workspaces. Administrators may remove members; members may remove themselves. The organization owner cannot be removed. Owned organization resources are reassigned and the departing member’s sessions end. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:write`.",
+ "x-sim-operation": "organizations.members.remove",
+ "x-oauth-scope": "api:write",
+ "tags": ["Organizations"],
+ "parameters": [
+ {
+ "name": "organizationId",
+ "in": "path",
+ "required": true,
+ "description": "Organization identifier.",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Organization identifier."
+ }
},
- "schema": {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "const": "function",
- "description": "Function declaration discriminator."
+ {
+ "name": "userId",
+ "in": "path",
+ "required": true,
+ "description": "User identifier of the organization member.",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "description": "User identifier of the organization member."
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Remove Organization Member result.",
+ "headers": {
+ "X-RateLimit-Limit": {
+ "$ref": "#/components/headers/X-RateLimit-Limit"
},
- "function": {
- "type": "object",
- "properties": {
- "name": {
- "type": "string",
- "minLength": 1,
- "description": "Function name presented to the model."
- },
- "description": {
- "description": "Optional explanation of what the function does.",
- "type": "string"
- },
- "parameters": {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "description": "JSON Schema type for the arguments, usually `object`."
- },
- "properties": {
- "type": "object",
- "propertyNames": {
- "type": "string"
- },
- "additionalProperties": {
- "description": "Caller-defined JSON Schema for one tool argument."
- },
- "description": "Caller-defined argument schemas keyed by argument name."
- },
- "required": {
- "description": "Names of required arguments.",
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- },
- "required": ["type", "properties"],
- "additionalProperties": {
- "description": "Caller-defined extension value preserved by the public API."
- },
- "description": "JSON Schema describing the arguments accepted by the tool."
- }
- },
- "required": ["name", "parameters"],
- "additionalProperties": {
- "description": "Caller-defined extension value preserved by the public API."
- },
- "description": "OpenAI-style function definition."
+ "X-RateLimit-Remaining": {
+ "$ref": "#/components/headers/X-RateLimit-Remaining"
+ },
+ "X-RateLimit-Reset": {
+ "$ref": "#/components/headers/X-RateLimit-Reset"
}
},
- "required": ["type", "function"],
- "additionalProperties": {
- "description": "Caller-defined extension value preserved by the public API."
- },
- "description": "OpenAI-style function declaration describing the callable tool surface."
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/RemoveOrganizationMemberResponse"
+ }
+ }
+ }
},
- "code": {
- "type": "string",
- "maxLength": 100000,
- "description": "Tool implementation executed in the sandboxed function runtime."
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "429": {
+ "$ref": "#/components/responses/RateLimited"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalError"
+ },
+ "503": {
+ "$ref": "#/components/responses/ServiceUnavailable"
}
- },
- "required": ["workspaceId", "title", "schema", "code"],
- "additionalProperties": false,
- "title": "Create custom tool request",
- "description": "Definition and implementation of a new custom tool.",
- "examples": [
+ }
+ }
+ },
+ "/api/v2/organizations/{organizationId}/invitations": {
+ "get": {
+ "operationId": "listOrganizationInvitations",
+ "summary": "List Organization Invitations",
+ "description": "List invitations owned by the organization, including invitations with workspace grants. Requires organization administrator access. Expired invitations are reported without modifying them. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:read`.",
+ "x-sim-operation": "organizations.invitations.list",
+ "x-oauth-scope": "api:read",
+ "tags": ["Organizations"],
+ "parameters": [
{
- "workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
- "title": "lookup_order",
+ "name": "organizationId",
+ "in": "path",
+ "required": true,
+ "description": "Organization identifier.",
"schema": {
- "type": "function",
- "function": {
- "name": "lookup_order",
- "description": "Look up an order by id",
- "parameters": {
- "type": "object",
- "properties": {
- "orderId": {
- "type": "string"
- }
- },
- "required": ["orderId"]
- }
+ "type": "string",
+ "minLength": 1,
+ "description": "Organization identifier."
+ }
+ },
+ {
+ "name": "search",
+ "in": "query",
+ "required": false,
+ "description": "Case-insensitive substring match against the invitee email.",
+ "schema": {
+ "description": "Case-insensitive substring match against the invitee email.",
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200
+ }
+ },
+ {
+ "name": "status",
+ "in": "query",
+ "required": false,
+ "description": "Filter by current invitation status. Omit to include all statuses.",
+ "schema": {
+ "description": "Filter by current invitation status. Omit to include all statuses.",
+ "type": "string",
+ "enum": ["pending", "accepted", "rejected", "cancelled", "expired"]
+ }
+ },
+ {
+ "name": "sortBy",
+ "in": "query",
+ "required": false,
+ "description": "Field used to sort the result.",
+ "schema": {
+ "default": "createdAt",
+ "description": "Field used to sort the result.",
+ "type": "string",
+ "enum": ["email", "createdAt"]
+ }
+ },
+ {
+ "name": "sortOrder",
+ "in": "query",
+ "required": false,
+ "description": "Sort direction.",
+ "schema": {
+ "default": "desc",
+ "description": "Sort direction.",
+ "type": "string",
+ "enum": ["asc", "desc"]
+ }
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "description": "Maximum invitations to return per page. Must be a whole number from 1 to 100. Defaults to 50.",
+ "schema": {
+ "default": 50,
+ "description": "Maximum invitations to return per page. Must be a whole number from 1 to 100. Defaults to 50.",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 100
+ }
+ },
+ {
+ "name": "cursor",
+ "in": "query",
+ "required": false,
+ "description": "Opaque cursor from the previous page. Send it back with the same sort and filters; only `limit` may change. Change anything else and pagination must restart without a cursor.",
+ "schema": {
+ "description": "Opaque cursor from the previous page. Send it back with the same sort and filters; only `limit` may change. Change anything else and pagination must restart without a cursor.",
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "List Organization Invitations result.",
+ "headers": {
+ "X-RateLimit-Limit": {
+ "$ref": "#/components/headers/X-RateLimit-Limit"
+ },
+ "X-RateLimit-Remaining": {
+ "$ref": "#/components/headers/X-RateLimit-Remaining"
+ },
+ "X-RateLimit-Reset": {
+ "$ref": "#/components/headers/X-RateLimit-Reset"
}
},
- "code": "return { ok: true }"
- }
- ]
- },
- "GetCustomToolResponse": {
- "type": "object",
- "properties": {
- "data": {
- "description": "Response data.",
- "$ref": "#/components/schemas/V2CustomTool"
- }
- },
- "required": ["data"],
- "additionalProperties": false,
- "title": "Get custom tool response",
- "description": "One custom tool.",
- "examples": [
- {
- "data": {
- "id": "V1StGXR8Z5jdHi6BmyT",
- "title": "lookup_order",
- "schema": {
- "type": "function",
- "function": {
- "name": "lookup_order",
- "description": "Look up an order by id",
- "parameters": {
- "type": "object",
- "properties": {
- "orderId": {
- "type": "string"
- }
- },
- "required": ["orderId"]
- }
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ListOrganizationInvitationsResponse"
}
- },
- "code": "return { ok: true }",
- "createdAt": "2026-06-01T09:14:00.000Z",
- "updatedAt": "2026-06-20T14:02:11.000Z"
+ }
}
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "429": {
+ "$ref": "#/components/responses/RateLimited"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalError"
+ },
+ "503": {
+ "$ref": "#/components/responses/ServiceUnavailable"
}
- ]
+ }
},
- "UpdateCustomToolResponse": {
- "type": "object",
- "properties": {
- "data": {
- "description": "Response data.",
- "$ref": "#/components/schemas/V2CustomTool"
- }
- },
- "required": ["data"],
- "additionalProperties": false,
- "title": "Update custom tool response",
- "description": "The updated custom tool.",
- "examples": [
+ "post": {
+ "operationId": "createOrganizationInvitation",
+ "summary": "Create Organization Invitation",
+ "description": "Email an invitation to join the organization as a member or administrator. Requires organization administrator access, invitations enabled, and an available seat on an eligible plan. This grants no workspace-specific permissions. An unexpired pending invitation for the email conflicts; use Resend Organization Invitation to send it again. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:write`.",
+ "x-sim-operation": "organizations.invitations.create",
+ "x-oauth-scope": "api:write",
+ "tags": ["Organizations"],
+ "parameters": [
{
- "data": {
- "id": "V1StGXR8Z5jdHi6BmyT",
- "title": "lookup_order",
+ "name": "organizationId",
+ "in": "path",
+ "required": true,
+ "description": "Organization identifier.",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Organization identifier."
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "description": "Create Organization Invitation input.",
+ "content": {
+ "application/json": {
"schema": {
- "type": "function",
- "function": {
- "name": "lookup_order",
- "description": "Look up an order by id",
- "parameters": {
- "type": "object",
- "properties": {
- "orderId": {
- "type": "string"
- }
- },
- "required": ["orderId"]
- }
- }
- },
- "code": "return { ok: false }",
- "createdAt": "2026-06-01T09:14:00.000Z",
- "updatedAt": "2026-06-20T14:02:11.000Z"
+ "$ref": "#/components/schemas/CreateOrganizationInvitationBody"
+ }
}
}
- ]
- },
- "UpdateCustomToolRequest": {
- "type": "object",
- "properties": {
- "workspaceId": {
- "type": "string",
- "minLength": 1,
- "maxLength": 128,
- "description": "Workspace that owns the custom tool."
- },
- "title": {
- "description": "New display title for the tool.",
- "type": "string",
- "minLength": 1,
- "maxLength": 200
- },
- "schema": {
- "description": "Replacement function declaration.",
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "const": "function",
- "description": "Function declaration discriminator."
+ },
+ "responses": {
+ "201": {
+ "description": "Create Organization Invitation result.",
+ "headers": {
+ "X-RateLimit-Limit": {
+ "$ref": "#/components/headers/X-RateLimit-Limit"
},
- "function": {
- "type": "object",
- "properties": {
- "name": {
- "type": "string",
- "minLength": 1,
- "description": "Function name presented to the model."
- },
- "description": {
- "description": "Optional explanation of what the function does.",
- "type": "string"
- },
- "parameters": {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "description": "JSON Schema type for the arguments, usually `object`."
- },
- "properties": {
- "type": "object",
- "propertyNames": {
- "type": "string"
- },
- "additionalProperties": {
- "description": "Caller-defined JSON Schema for one tool argument."
- },
- "description": "Caller-defined argument schemas keyed by argument name."
- },
- "required": {
- "description": "Names of required arguments.",
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- },
- "required": ["type", "properties"],
- "additionalProperties": {
- "description": "Caller-defined extension value preserved by the public API."
- },
- "description": "JSON Schema describing the arguments accepted by the tool."
- }
- },
- "required": ["name", "parameters"],
- "additionalProperties": {
- "description": "Caller-defined extension value preserved by the public API."
- },
- "description": "OpenAI-style function definition."
+ "X-RateLimit-Remaining": {
+ "$ref": "#/components/headers/X-RateLimit-Remaining"
+ },
+ "X-RateLimit-Reset": {
+ "$ref": "#/components/headers/X-RateLimit-Reset"
}
},
- "required": ["type", "function"],
- "additionalProperties": {
- "description": "Caller-defined extension value preserved by the public API."
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/CreateOrganizationInvitationResponse"
+ }
+ }
}
},
- "code": {
- "description": "Replacement tool implementation.",
- "type": "string",
- "maxLength": 100000
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "413": {
+ "$ref": "#/components/responses/PayloadTooLarge"
+ },
+ "415": {
+ "$ref": "#/components/responses/UnsupportedMediaType"
+ },
+ "429": {
+ "$ref": "#/components/responses/RateLimited"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalError"
+ },
+ "503": {
+ "$ref": "#/components/responses/ServiceUnavailable"
}
- },
- "required": ["workspaceId"],
- "additionalProperties": false,
- "title": "Update custom tool request",
- "description": "Custom tool fields to change; at least one editable field is required.",
- "examples": [
+ }
+ }
+ },
+ "/api/v2/organizations/{organizationId}/invitations/{invitationId}": {
+ "get": {
+ "operationId": "getOrganizationInvitation",
+ "summary": "Get Organization Invitation",
+ "description": "Get an invitation owned by the organization. Requires organization administrator access. The response excludes the acceptance token. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:read`.",
+ "x-sim-operation": "organizations.invitations.read",
+ "x-oauth-scope": "api:read",
+ "tags": ["Organizations"],
+ "parameters": [
{
- "workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
- "code": "return { ok: false }"
- }
- ]
- },
- "V2CustomToolDeleteData": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string",
- "description": "Identifier of the deleted custom tool."
+ "name": "organizationId",
+ "in": "path",
+ "required": true,
+ "description": "Organization identifier.",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Organization identifier."
+ }
},
- "deleted": {
- "type": "boolean",
- "const": true,
- "description": "Whether the custom tool was deleted."
- }
- },
- "required": ["id", "deleted"],
- "additionalProperties": false,
- "title": "Delete custom tool data",
- "description": "Custom tool deletion acknowledgement."
- },
- "DeleteCustomToolResponse": {
- "type": "object",
- "properties": {
- "data": {
- "description": "Response data.",
- "$ref": "#/components/schemas/V2CustomToolDeleteData"
- }
- },
- "required": ["data"],
- "additionalProperties": false,
- "title": "Delete custom tool response",
- "description": "Acknowledgement that the custom tool was deleted.",
- "examples": [
{
- "data": {
- "id": "V1StGXR8Z5jdHi6BmyT",
- "deleted": true
+ "name": "invitationId",
+ "in": "path",
+ "required": true,
+ "description": "Invitation identifier.",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Invitation identifier."
}
}
- ]
- },
- "V2Sandbox": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string",
- "description": "Unique sandbox identifier."
+ ],
+ "responses": {
+ "200": {
+ "description": "Get Organization Invitation result.",
+ "headers": {
+ "X-RateLimit-Limit": {
+ "$ref": "#/components/headers/X-RateLimit-Limit"
+ },
+ "X-RateLimit-Remaining": {
+ "$ref": "#/components/headers/X-RateLimit-Remaining"
+ },
+ "X-RateLimit-Reset": {
+ "$ref": "#/components/headers/X-RateLimit-Reset"
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/GetOrganizationInvitationResponse"
+ }
+ }
+ }
},
- "name": {
- "type": "string",
- "description": "Display name, unique within the workspace."
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
},
- "language": {
- "type": "string",
- "enum": ["javascript", "python"],
- "description": "Dependency ecosystem: `javascript` installs from npm, `python` from PyPI."
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
},
- "dependencies": {
- "type": "array",
- "items": {
- "type": "string"
- },
- "description": "Package specifiers installed into the sandbox, one per entry."
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
},
- "cliTools": {
- "type": "array",
- "items": {
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "429": {
+ "$ref": "#/components/responses/RateLimited"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalError"
+ },
+ "503": {
+ "$ref": "#/components/responses/ServiceUnavailable"
+ }
+ }
+ },
+ "delete": {
+ "operationId": "revokeOrganizationInvitation",
+ "summary": "Revoke Organization Invitation",
+ "description": "Cancel an unexpired pending invitation and all its workspace grants so it can no longer be accepted. Requires organization administrator access. This does not remove a person who already accepted; use Remove Organization Member for that. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:write`.",
+ "x-sim-operation": "invitations.revoke",
+ "x-oauth-scope": "api:write",
+ "tags": ["Organizations"],
+ "parameters": [
+ {
+ "name": "organizationId",
+ "in": "path",
+ "required": true,
+ "description": "Organization identifier.",
+ "schema": {
"type": "string",
- "enum": [
- "google-cloud-cli@577.0.0-r1",
- "aws-cli@2.36.15-r1",
- "azure-cli@2.89.0-r1",
- "doctl@1.166.0-r1",
- "github-cli@2.97.0-r1",
- "gitlab-cli@1.111.0-r1",
- "kubectl@1.36.3-r1",
- "helm@4.2.3-r1",
- "kustomize@5.8.1-r1",
- "argocd@3.4.6-r1",
- "terraform@1.15.8-r1",
- "pulumi@3.255.0-r1",
- "supabase-cli@2.111.0-r1",
- "firebase-cli@15.25.1-r1",
- "flyctl@0.4.78-r1",
- "railway-cli@5.30.4-r1",
- "stripe-cli@1.45.0-r1",
- "duckdb@1.5.5-r1",
- "rclone@1.75.0-r1",
- "restic@0.19.1-r1",
- "minio-mc@RELEASE.2025-08-13T08-35-41Z-r1",
- "mongosh@2.9.2-r1",
- "sops@3.13.3-r1",
- "age@1.3.1-r1"
- ]
- },
- "description": "Pinned managed CLI ids installed into the sandbox, at most 10, no duplicates."
- },
- "systemPackages": {
- "type": "array",
- "items": {
- "type": "string"
- },
- "description": "Debian packages installed into the sandbox, one per entry."
+ "minLength": 1,
+ "description": "Organization identifier."
+ }
},
- "buildStatus": {
- "anyOf": [
- {
- "type": "string",
- "enum": ["pending", "building", "ready", "failed"]
+ {
+ "name": "invitationId",
+ "in": "path",
+ "required": true,
+ "description": "Invitation identifier.",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Invitation identifier."
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Revoke Organization Invitation result.",
+ "headers": {
+ "X-RateLimit-Limit": {
+ "$ref": "#/components/headers/X-RateLimit-Limit"
},
- {
- "type": "null"
- }
- ],
- "description": "Image build state. `null` when the deployment installs dependencies at run time and has nothing to build."
- },
- "errorCode": {
- "anyOf": [
- {
- "type": "string"
+ "X-RateLimit-Remaining": {
+ "$ref": "#/components/headers/X-RateLimit-Remaining"
},
- {
- "type": "null"
+ "X-RateLimit-Reset": {
+ "$ref": "#/components/headers/X-RateLimit-Reset"
}
- ],
- "description": "Classified build failure code, or `null`."
- },
- "errorMessage": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/RevokeOrganizationInvitationResponse"
+ }
}
- ],
- "description": "Human-readable build failure summary, or `null`."
+ }
},
- "errorDetail": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Tail of the installer log for a failed build, or `null`."
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
},
- "builtAt": {
- "anyOf": [
- {
- "type": "string",
- "format": "date-time",
- "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
- },
- {
- "type": "null"
- }
- ],
- "description": "ISO 8601 timestamp when the current image finished building, or `null`."
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
},
- "createdAt": {
- "type": "string",
- "format": "date-time",
- "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
- "description": "ISO 8601 timestamp when the sandbox was created."
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
},
- "updatedAt": {
- "type": "string",
- "format": "date-time",
- "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
- "description": "ISO 8601 timestamp when the sandbox was last updated."
- }
- },
- "required": [
- "id",
- "name",
- "language",
- "dependencies",
- "cliTools",
- "systemPackages",
- "buildStatus",
- "errorCode",
- "errorMessage",
- "errorDetail",
- "builtAt",
- "createdAt",
- "updatedAt"
- ],
- "additionalProperties": false,
- "title": "Sandbox",
- "description": "A workspace sandbox: a reusable dependency set that Function blocks execute against."
- },
- "ListSandboxesResponse": {
- "type": "object",
- "properties": {
- "data": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/V2Sandbox"
- },
- "description": "Items in the current page."
+ "404": {
+ "$ref": "#/components/responses/NotFound"
},
- "nextCursor": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Opaque cursor for the next page. Send it back as `cursor`; `null` means there is nothing further to fetch. Never construct one yourself."
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "429": {
+ "$ref": "#/components/responses/RateLimited"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalError"
+ },
+ "503": {
+ "$ref": "#/components/responses/ServiceUnavailable"
}
- },
- "required": ["data", "nextCursor"],
- "additionalProperties": false,
- "title": "List sandboxes response",
- "description": "Sandboxes defined in the workspace.",
- "examples": [
+ }
+ }
+ },
+ "/api/v2/organizations/{organizationId}/invitations/{invitationId}/resend": {
+ "post": {
+ "operationId": "resendOrganizationInvitation",
+ "summary": "Resend Organization Invitation",
+ "description": "Email an unexpired pending invitation again, renew its expiry, and replace its previous acceptance link. Requires organization administrator access and current invitation eligibility. Retrying sends another email; inspect the invitation after a delivery failure before retrying. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:write`.",
+ "x-sim-operation": "invitations.resend",
+ "x-oauth-scope": "api:write",
+ "tags": ["Organizations"],
+ "parameters": [
{
- "data": [
- {
- "id": "V1StGXR8Z5jdHi6BmyT",
- "name": "data-tools",
- "language": "python",
- "dependencies": ["pandas==2.2.2", "requests"],
- "cliTools": [],
- "systemPackages": ["graphviz"],
- "buildStatus": "ready",
- "errorCode": null,
- "errorMessage": null,
- "errorDetail": null,
- "builtAt": "2026-06-20T14:05:40.000Z",
- "createdAt": "2026-06-01T09:14:00.000Z",
- "updatedAt": "2026-06-20T14:02:11.000Z"
- }
- ],
- "nextCursor": null
- }
- ]
- },
- "CreateSandboxResponse": {
- "type": "object",
- "properties": {
- "data": {
- "description": "Response data.",
- "$ref": "#/components/schemas/V2Sandbox"
- }
- },
- "required": ["data"],
- "additionalProperties": false,
- "title": "Create sandbox response",
- "description": "The created sandbox. `buildStatus` is `pending` while an image builds and `null` where nothing is built.",
- "examples": [
+ "name": "organizationId",
+ "in": "path",
+ "required": true,
+ "description": "Organization identifier.",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Organization identifier."
+ }
+ },
{
- "data": {
- "id": "V1StGXR8Z5jdHi6BmyT",
- "name": "data-tools",
- "language": "python",
- "dependencies": ["pandas==2.2.2", "requests"],
- "cliTools": [],
- "systemPackages": ["graphviz"],
- "buildStatus": "pending",
- "errorCode": null,
- "errorMessage": null,
- "errorDetail": null,
- "builtAt": null,
- "createdAt": "2026-06-01T09:14:00.000Z",
- "updatedAt": "2026-06-20T14:02:11.000Z"
+ "name": "invitationId",
+ "in": "path",
+ "required": true,
+ "description": "Invitation identifier.",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Invitation identifier."
}
}
- ]
- },
- "CreateSandboxRequest": {
- "type": "object",
- "properties": {
- "workspaceId": {
- "type": "string",
- "minLength": 1,
- "maxLength": 128,
- "description": "Workspace in which to create the sandbox."
+ ],
+ "requestBody": {
+ "required": false,
+ "description": "Resend Organization Invitation input.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ResendOrganizationInvitationBody"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Resend Organization Invitation result.",
+ "headers": {
+ "X-RateLimit-Limit": {
+ "$ref": "#/components/headers/X-RateLimit-Limit"
+ },
+ "X-RateLimit-Remaining": {
+ "$ref": "#/components/headers/X-RateLimit-Remaining"
+ },
+ "X-RateLimit-Reset": {
+ "$ref": "#/components/headers/X-RateLimit-Reset"
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ResendOrganizationInvitationResponse"
+ }
+ }
+ }
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "413": {
+ "$ref": "#/components/responses/PayloadTooLarge"
+ },
+ "415": {
+ "$ref": "#/components/responses/UnsupportedMediaType"
+ },
+ "429": {
+ "$ref": "#/components/responses/RateLimited"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalError"
+ },
+ "503": {
+ "$ref": "#/components/responses/ServiceUnavailable"
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "securitySchemes": {
+ "apiKey": {
+ "type": "apiKey",
+ "in": "header",
+ "name": "X-API-Key",
+ "description": "Your Sim API key, personal or workspace-scoped. Generate one under Settings, then API Keys. Operations that reject workspace keys say so in their own description."
+ },
+ "oauthBearer": {
+ "type": "http",
+ "scheme": "bearer",
+ "bearerFormat": "OAuth 2.0 access token",
+ "description": "A Sim OAuth access token obtained by a registered client through the authorization-code flow. Each operation declares its required scope: api:read permits reads and searches; api:write also permits changes and execution and implies api:read. Scope requirements follow the application operation, independent of HTTP method or workspace role."
+ }
+ },
+ "headers": {
+ "X-RateLimit-Limit": {
+ "description": "Maximum requests allowed in the current window.",
+ "schema": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 9007199254740991,
+ "title": "Rate limit",
+ "description": "Maximum requests allowed in the current window."
+ }
+ },
+ "X-RateLimit-Remaining": {
+ "description": "Requests remaining in the current window.",
+ "schema": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 9007199254740991,
+ "title": "Rate limit remaining",
+ "description": "Requests remaining in the current window."
+ }
+ },
+ "X-RateLimit-Reset": {
+ "description": "ISO 8601 timestamp when the current rate-limit window resets.",
+ "schema": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
+ "title": "Rate limit reset",
+ "description": "ISO 8601 timestamp when the current rate-limit window resets."
+ }
+ },
+ "Retry-After": {
+ "description": "Seconds to wait before retrying, sent on `429` and `503`. Add jitter rather than retrying at exactly this offset.",
+ "schema": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 9007199254740991,
+ "title": "Retry after",
+ "description": "Seconds to wait before retrying, sent on `429` and `503`. Add jitter rather than retrying at exactly this offset."
+ }
+ },
+ "X-Run-Id": {
+ "description": "Identifier assigned to the workflow run.",
+ "schema": {
+ "type": "string",
+ "minLength": 1,
+ "title": "Run identifier",
+ "description": "Identifier assigned to the workflow run."
+ }
+ }
+ },
+ "responses": {
+ "BadRequest": {
+ "description": "The request is invalid. This includes a query parameter sent with no value (`?limit=`, `?search=`), which is rejected rather than read as zero, empty, or the parameter default — omit the parameter instead.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2Error"
+ },
+ "example": {
+ "error": {
+ "code": "BAD_REQUEST",
+ "message": "Invalid request"
+ }
+ }
+ }
+ }
+ },
+ "Unauthorized": {
+ "description": "The API credential is missing or invalid.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2Error"
+ },
+ "example": {
+ "error": {
+ "code": "UNAUTHORIZED",
+ "message": "Authentication required"
+ }
+ }
+ }
+ }
+ },
+ "Forbidden": {
+ "description": "The caller lacks the rights this operation requires. When the cause is one a caller can act on, `error.details.code` names it. A resource in a workspace the caller cannot reach at all answers `404` instead, so absence and denial are indistinguishable.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2Error"
+ },
+ "example": {
+ "error": {
+ "code": "FORBIDDEN",
+ "message": "Insufficient workspace permissions",
+ "details": {
+ "code": "INSUFFICIENT_WORKSPACE_ROLE"
+ }
+ }
+ }
+ }
+ }
+ },
+ "NotFound": {
+ "description": "The requested resource was not found.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2Error"
+ },
+ "example": {
+ "error": {
+ "code": "NOT_FOUND",
+ "message": "Not found"
+ }
+ }
+ }
+ }
+ },
+ "Conflict": {
+ "description": "The request conflicts with current resource state.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2Error"
+ },
+ "example": {
+ "error": {
+ "code": "CONFLICT",
+ "message": "The request conflicts with the current state of the resource"
+ }
+ }
+ }
+ }
+ },
+ "PayloadTooLarge": {
+ "description": "The request, or a resource collection it must materialize, exceeds the allowed size: an oversized request body, a generated artifact past the download ceiling, or a workspace folder tree too large to load in full.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2Error"
+ },
+ "example": {
+ "error": {
+ "code": "PAYLOAD_TOO_LARGE",
+ "message": "Request body is too large"
+ }
+ }
+ }
+ }
+ },
+ "UnsupportedMediaType": {
+ "description": "The request uses an unsupported media type.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2Error"
+ },
+ "example": {
+ "error": {
+ "code": "UNSUPPORTED_MEDIA_TYPE",
+ "message": "Request body must be sent as application/json"
+ }
+ }
+ }
+ }
+ },
+ "RateLimited": {
+ "description": "The caller exceeded the request rate limit.",
+ "headers": {
+ "Retry-After": {
+ "$ref": "#/components/headers/Retry-After"
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2Error"
+ },
+ "example": {
+ "error": {
+ "code": "RATE_LIMITED",
+ "message": "API rate limit exceeded",
+ "details": {
+ "retryAfter": "2026-01-01T00:00:30.000Z"
+ }
+ }
+ }
+ }
+ }
+ },
+ "InternalError": {
+ "description": "An unexpected server error occurred.",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2Error"
+ },
+ "example": {
+ "error": {
+ "code": "INTERNAL_ERROR",
+ "message": "Internal server error"
+ }
+ }
+ }
+ }
+ },
+ "ServiceUnavailable": {
+ "description": "A required service is temporarily unavailable. `Retry-After` carries the seconds to wait; treat it as a floor and add jitter. The header is omitted when `error.details.code` is `ASYNC_ENQUEUE_AMBIGUOUS`, because the run may already have started — reconcile against the returned run id instead of retrying.",
+ "headers": {
+ "Retry-After": {
+ "$ref": "#/components/headers/Retry-After"
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2Error"
+ },
+ "example": {
+ "error": {
+ "code": "SERVICE_UNAVAILABLE",
+ "message": "Service temporarily unavailable"
+ }
+ }
+ }
+ }
+ }
+ },
+ "schemas": {
+ "V2ActionableForbiddenDetails": {
+ "type": "object",
+ "properties": {
+ "code": {
+ "$ref": "#/components/schemas/V2ForbiddenDetailCode"
+ }
+ },
+ "required": ["code"],
+ "additionalProperties": {
+ "description": "Additional context for this refusal."
+ },
+ "title": "Actionable forbidden details",
+ "description": "Machine-readable cause and optional context for an actionable `403` response."
+ },
+ "V2ForbiddenDetailCode": {
+ "type": "string",
+ "enum": [
+ "INSUFFICIENT_WORKSPACE_ROLE",
+ "PERSONAL_API_KEYS_DISABLED",
+ "WORKSPACE_KEY_OPERATION_NOT_PERMITTED",
+ "PRINCIPAL_KIND_NOT_PERMITTED",
+ "ORGANIZATION_MEMBERSHIP_REQUIRED",
+ "ORGANIZATION_ADMIN_REQUIRED",
+ "ENTERPRISE_PLAN_REQUIRED",
+ "ORGANIZATION_PLAN_REQUIRED",
+ "AUDIT_LOGS_DISABLED",
+ "SKILL_EDITOR_ACCESS_REQUIRED",
+ "SECRET_ADMIN_ACCESS_REQUIRED",
+ "WORKSPACE_RESOURCE_LIMIT_REACHED",
+ "PUBLIC_SHARING_NOT_ALLOWED",
+ "CREDENTIAL_ADMIN_ACCESS_REQUIRED",
+ "MCP_SERVER_URL_NOT_ALLOWED",
+ "WORKSPACE_PLAN_CAPABILITY_REQUIRED",
+ "CHAT_AUTH_MODE_NOT_PERMITTED",
+ "CONNECTOR_MANAGED_RESOURCE_READ_ONLY",
+ "PERMISSION_GROUP_CAPABILITY_BLOCKED",
+ "INTEGRATION_NOT_ALLOWED",
+ "INSUFFICIENT_SCOPE",
+ "SCIM_MANAGED_MEMBERSHIP"
+ ],
+ "title": "Forbidden detail code",
+ "description": "Stable cause code for an actionable `403` response."
+ },
+ "V2Error": {
+ "type": "object",
+ "properties": {
+ "error": {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "string",
+ "description": "Stable machine-readable error code."
+ },
+ "message": {
+ "type": "string",
+ "description": "Human-readable explanation of the error."
+ },
+ "details": {
+ "description": "Structured error context whose keys depend on the error. Actionable `403` responses use the `V2ActionableForbiddenDetails` shape; validation failures may return issue arrays instead.",
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/V2ActionableForbiddenDetails"
+ },
+ {
+ "description": "Other structured context defined by the specific error."
+ }
+ ]
+ }
+ },
+ "required": ["code", "message"],
+ "additionalProperties": false,
+ "description": "Canonical error details."
+ }
+ },
+ "required": ["error"],
+ "additionalProperties": false,
+ "title": "v2 error response",
+ "description": "Canonical error envelope returned by the public v2 API.",
+ "examples": [
+ {
+ "error": {
+ "code": "BAD_REQUEST",
+ "message": "The request is invalid."
+ }
+ }
+ ]
+ },
+ "V2Workspace": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 128,
+ "description": "Unique workspace identifier."
+ },
+ "name": {
+ "type": "string",
+ "description": "Workspace display name."
+ },
+ "color": {
+ "type": "string",
+ "description": "Workspace color as a hexadecimal color value."
+ },
+ "logoUrl": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Workspace logo URL, or null when none is configured."
+ },
+ "memberCount": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 9007199254740991,
+ "description": "Number of effective members, including inherited organization administrators."
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
+ "description": "ISO 8601 timestamp when the workspace was created."
+ },
+ "updatedAt": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
+ "description": "ISO 8601 timestamp when the workspace was last updated."
+ }
+ },
+ "required": ["id", "name", "color", "logoUrl", "memberCount", "createdAt", "updatedAt"],
+ "additionalProperties": false,
+ "title": "Workspace",
+ "description": "Public metadata for an accessible workspace."
+ },
+ "ListWorkspacesResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/V2Workspace"
+ },
+ "description": "Items in the current page."
+ },
+ "nextCursor": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Opaque cursor for the next page. Send it back as `cursor`; `null` means there is nothing further to fetch. Never construct one yourself."
+ }
+ },
+ "required": ["data", "nextCursor"],
+ "additionalProperties": false,
+ "title": "List workspaces response",
+ "description": "Public metadata for workspaces available to the credential.",
+ "examples": [
+ {
+ "data": [
+ {
+ "id": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
+ "name": "Engineering",
+ "color": "#33C482",
+ "logoUrl": null,
+ "memberCount": 14,
+ "createdAt": "2026-01-15T10:30:00.000Z",
+ "updatedAt": "2026-06-20T14:02:11.000Z"
+ }
+ ],
+ "nextCursor": null
+ }
+ ]
+ },
+ "GetWorkspaceResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2Workspace"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Get workspace response",
+ "description": "Public metadata for one workspace.",
+ "examples": [
+ {
+ "data": {
+ "id": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
+ "name": "Engineering",
+ "color": "#33C482",
+ "logoUrl": null,
+ "memberCount": 14,
+ "createdAt": "2026-01-15T10:30:00.000Z",
+ "updatedAt": "2026-06-20T14:02:11.000Z"
+ }
+ }
+ ]
+ },
+ "V2WorkspaceMember": {
+ "type": "object",
+ "properties": {
+ "email": {
+ "type": "string",
+ "format": "email",
+ "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
+ "description": "Member email address and public member identifier."
+ },
+ "name": {
+ "type": "string",
+ "description": "Member display name."
+ },
+ "image": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Member profile image URL, or null when absent."
+ },
+ "role": {
+ "type": "string",
+ "enum": ["admin", "write", "read"],
+ "description": "Effective role in the workspace."
+ },
+ "isExternal": {
+ "type": "boolean",
+ "description": "Whether the member belongs to a different organization than the workspace. True only for an explicitly granted member whose own organization differs; inherited organization-administrator access is always reported as false, so this does not detect every outside caller."
+ },
+ "joinedAt": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
+ "description": "ISO 8601 timestamp when access was granted."
+ }
+ },
+ "required": ["email", "name", "image", "role", "isExternal", "joinedAt"],
+ "additionalProperties": false,
+ "title": "Workspace member",
+ "description": "An effective workspace member and their public access role."
+ },
+ "ListWorkspaceMembersResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/V2WorkspaceMember"
+ },
+ "description": "Items in the current page."
+ },
+ "nextCursor": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Opaque cursor for the next page. Send it back as `cursor`; `null` means there is nothing further to fetch. Never construct one yourself."
+ }
+ },
+ "required": ["data", "nextCursor"],
+ "additionalProperties": false,
+ "title": "List workspace members response",
+ "description": "A cursor-paginated page of effective workspace members.",
+ "examples": [
+ {
+ "data": [
+ {
+ "email": "jane@example.com",
+ "name": "Jane Smith",
+ "image": null,
+ "role": "admin",
+ "isExternal": false,
+ "joinedAt": "2026-01-15T10:30:00.000Z"
+ }
+ ],
+ "nextCursor": null
+ }
+ ]
+ },
+ "V2McpServer": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique server identifier derived from the workspace and endpoint URL."
+ },
+ "name": {
+ "type": "string",
+ "description": "Server display name."
+ },
+ "description": {
+ "description": "Optional server description.",
+ "type": "string"
+ },
+ "transport": {
+ "default": "streamable-http",
+ "description": "Transport used to communicate with the server.",
+ "type": "string",
+ "enum": ["streamable-http"]
+ },
+ "authType": {
+ "description": "Authentication method used by the server.",
+ "type": "string",
+ "enum": ["none", "headers", "oauth"]
+ },
+ "url": {
+ "description": "Server endpoint URL.",
+ "type": "string"
+ },
+ "timeout": {
+ "description": "Per-request timeout in milliseconds.",
+ "type": "number"
+ },
+ "retries": {
+ "description": "Number of retries attempted per request.",
+ "type": "number"
+ },
+ "enabled": {
+ "type": "boolean",
+ "description": "Whether the server tools are available to workflows."
+ },
+ "connectionStatus": {
+ "description": "Result of the most recent connection attempt. Registration and re-registration establish no connection — the auth-type probe they may send does not count as one — so a server begins, and returns to, `disconnected` until a tool discovery runs.",
+ "type": "string",
+ "enum": ["connected", "disconnected", "error"]
+ },
+ "lastError": {
+ "description": "Message from the most recent failed connection, or null when absent. A re-registration clears it, since the configuration it described no longer applies.",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "toolCount": {
+ "description": "Number of tools discovered on the server.",
+ "type": "number"
+ },
+ "lastToolsRefresh": {
+ "description": "ISO 8601 timestamp of the most recent tool-list refresh.",
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
+ },
+ "lastConnected": {
+ "description": "ISO 8601 timestamp of the most recent successful connection. Absent until the server completes one; registering a server does not set it.",
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
+ },
+ "createdAt": {
+ "description": "ISO 8601 timestamp when the server was registered.",
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
+ },
+ "updatedAt": {
+ "description": "ISO 8601 timestamp when the server was last updated.",
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
+ },
+ "oauthClientId": {
+ "description": "Pre-registered OAuth client identifier, when configured.",
+ "type": "string"
+ },
+ "hasHeaders": {
+ "type": "boolean",
+ "description": "Whether any request headers are configured."
+ },
+ "headerNames": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "description": "Configured header name."
+ },
+ "description": "Names of configured request headers. Header values are never returned."
+ },
+ "hasOauthClientSecret": {
+ "type": "boolean",
+ "description": "Whether an OAuth client secret is stored. The value is never returned."
+ }
+ },
+ "required": [
+ "id",
+ "name",
+ "transport",
+ "enabled",
+ "createdAt",
+ "updatedAt",
+ "hasHeaders",
+ "headerNames",
+ "hasOauthClientSecret"
+ ],
+ "additionalProperties": false,
+ "title": "MCP server",
+ "description": "Public MCP server configuration without write-only credential values."
+ },
+ "ListMcpServersResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/V2McpServer"
+ },
+ "description": "Items in the current page."
+ },
+ "nextCursor": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Opaque cursor for the next page. Send it back as `cursor`; `null` means there is nothing further to fetch. Never construct one yourself."
+ }
+ },
+ "required": ["data", "nextCursor"],
+ "additionalProperties": false,
+ "title": "List MCP servers response",
+ "description": "MCP servers registered in the workspace.",
+ "examples": [
+ {
+ "data": [
+ {
+ "id": "mcp-3f7a9c21",
+ "name": "Docs server",
+ "description": "Internal documentation tools",
+ "transport": "streamable-http",
+ "authType": "headers",
+ "url": "https://mcp.example.com/sse",
+ "timeout": 30000,
+ "retries": 3,
+ "enabled": true,
+ "connectionStatus": "connected",
+ "lastError": null,
+ "toolCount": 7,
+ "lastToolsRefresh": "2026-06-20T14:02:11.000Z",
+ "lastConnected": "2026-06-20T14:02:11.000Z",
+ "createdAt": "2026-06-01T09:14:00.000Z",
+ "updatedAt": "2026-06-20T14:02:11.000Z",
+ "hasHeaders": true,
+ "headerNames": ["Authorization"],
+ "hasOauthClientSecret": false
+ }
+ ],
+ "nextCursor": null
+ }
+ ]
+ },
+ "CreateMcpServerResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2McpServer"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Create MCP server response",
+ "description": "The registered MCP server without write-only credentials.",
+ "examples": [
+ {
+ "data": {
+ "id": "mcp-3f7a9c21",
+ "name": "Docs server",
+ "description": "Internal documentation tools",
+ "transport": "streamable-http",
+ "authType": "headers",
+ "url": "https://mcp.example.com/sse",
+ "timeout": 30000,
+ "retries": 3,
+ "enabled": true,
+ "connectionStatus": "disconnected",
+ "lastError": null,
+ "toolCount": 0,
+ "createdAt": "2026-06-01T09:14:00.000Z",
+ "updatedAt": "2026-06-20T14:02:11.000Z",
+ "hasHeaders": true,
+ "headerNames": ["Authorization"],
+ "hasOauthClientSecret": false
+ }
+ }
+ ]
+ },
+ "CreateMcpServerRequest": {
+ "type": "object",
+ "properties": {
+ "workspaceId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 128,
+ "description": "Workspace in which to register the server."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "Server display name."
+ },
+ "description": {
+ "description": "Optional server description.",
+ "type": "string",
+ "maxLength": 2000
+ },
+ "transport": {
+ "description": "Transport protocol. Defaults to `streamable-http` on creation.",
+ "default": "streamable-http",
+ "type": "string",
+ "enum": ["streamable-http"]
+ },
+ "url": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2048,
+ "description": "Absolute HTTP or HTTPS endpoint URL without `{{ENV_VAR}}` references. It determines server identity and is immutable: delete and recreate the server to change endpoints."
+ },
+ "authType": {
+ "description": "Authentication method. When omitted, and no `headers` are sent, registration probes the endpoint once to classify it, falling back to `headers` when the probe fails or the server does not advertise OAuth. A server publishing RFC 9728 metadata is therefore stored as `oauth`, and headers configured afterwards will not authenticate — send this field explicitly to pin the method.",
+ "type": "string",
+ "enum": ["none", "headers", "oauth"]
+ },
+ "headers": {
+ "description": "Write-only request headers sent to the server. Replaced wholesale rather than merged on update: sending this field drops every stored header it does not repeat.",
+ "writeOnly": true,
+ "type": "object",
+ "propertyNames": {
+ "type": "string",
+ "minLength": 1
+ },
+ "additionalProperties": {
+ "type": "string",
+ "description": "Header value sent to the MCP server."
+ }
+ },
+ "timeout": {
+ "description": "Per-request timeout in milliseconds. Defaults to 30000 on creation.",
+ "default": 30000,
+ "type": "integer",
+ "minimum": 1000,
+ "maximum": 300000
+ },
+ "retries": {
+ "description": "Number of retries per request. Defaults to 3 on creation.",
+ "default": 3,
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 10
+ },
+ "enabled": {
+ "description": "Whether workflows can use the server's tools. Defaults to true on creation.",
+ "default": true,
+ "type": "boolean"
+ },
+ "oauthClientId": {
+ "description": "Pre-registered OAuth client identifier. Changing it on update revokes the stored OAuth grant and forces reauthorization.",
+ "anyOf": [
+ {
+ "type": "string",
+ "maxLength": 512
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "oauthClientSecret": {
+ "description": "Write-only pre-registered OAuth client secret. Sending it on update as null or a new value revokes the stored OAuth grant and forces reauthorization, as does switching away from OAuth authentication.",
+ "writeOnly": true,
+ "anyOf": [
+ {
+ "type": "string",
+ "maxLength": 2048
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ },
+ "required": ["workspaceId", "name", "url"],
+ "additionalProperties": false,
+ "title": "Create MCP server request",
+ "description": "Configuration for a new MCP server.",
+ "examples": [
+ {
+ "workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
+ "name": "Docs server",
+ "url": "https://mcp.example.com/sse",
+ "authType": "headers",
+ "headers": {
+ "Authorization": "Bearer YOUR_TOKEN"
+ }
+ }
+ ]
+ },
+ "GetMcpServerResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2McpServer"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Get MCP server response",
+ "description": "One MCP server without write-only credentials.",
+ "examples": [
+ {
+ "data": {
+ "id": "mcp-3f7a9c21",
+ "name": "Docs server",
+ "description": "Internal documentation tools",
+ "transport": "streamable-http",
+ "authType": "headers",
+ "url": "https://mcp.example.com/sse",
+ "timeout": 30000,
+ "retries": 3,
+ "enabled": true,
+ "connectionStatus": "connected",
+ "lastError": null,
+ "toolCount": 7,
+ "lastToolsRefresh": "2026-06-20T14:02:11.000Z",
+ "lastConnected": "2026-06-20T14:02:11.000Z",
+ "createdAt": "2026-06-01T09:14:00.000Z",
+ "updatedAt": "2026-06-20T14:02:11.000Z",
+ "hasHeaders": true,
+ "headerNames": ["Authorization"],
+ "hasOauthClientSecret": false
+ }
+ }
+ ]
+ },
+ "UpdateMcpServerResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2McpServer"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Update MCP server response",
+ "description": "The updated MCP server.",
+ "examples": [
+ {
+ "data": {
+ "id": "mcp-3f7a9c21",
+ "name": "Docs server",
+ "description": "Internal documentation tools",
+ "transport": "streamable-http",
+ "authType": "headers",
+ "url": "https://mcp.example.com/sse",
+ "timeout": 30000,
+ "retries": 3,
+ "enabled": false,
+ "connectionStatus": "connected",
+ "lastError": null,
+ "toolCount": 7,
+ "lastToolsRefresh": "2026-06-20T14:02:11.000Z",
+ "lastConnected": "2026-06-20T14:02:11.000Z",
+ "createdAt": "2026-06-01T09:14:00.000Z",
+ "updatedAt": "2026-06-20T14:02:11.000Z",
+ "hasHeaders": true,
+ "headerNames": ["Authorization"],
+ "hasOauthClientSecret": false
+ }
+ }
+ ]
+ },
+ "UpdateMcpServerRequest": {
+ "type": "object",
+ "properties": {
+ "workspaceId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 128,
+ "description": "Workspace that owns the MCP server."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "Server display name."
+ },
+ "description": {
+ "description": "Optional server description.",
+ "type": "string",
+ "maxLength": 2000
+ },
+ "transport": {
+ "description": "Transport protocol. Defaults to `streamable-http` on creation.",
+ "default": "streamable-http",
+ "type": "string",
+ "enum": ["streamable-http"]
+ },
+ "url": {
+ "description": "Immutable server URL. When provided, it must equal the current URL; use delete and create to change endpoints.",
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2048
+ },
+ "authType": {
+ "description": "Authentication method. When omitted, and no `headers` are sent, registration probes the endpoint once to classify it, falling back to `headers` when the probe fails or the server does not advertise OAuth. A server publishing RFC 9728 metadata is therefore stored as `oauth`, and headers configured afterwards will not authenticate — send this field explicitly to pin the method.",
+ "type": "string",
+ "enum": ["none", "headers", "oauth"]
+ },
+ "headers": {
+ "description": "Write-only request headers sent to the server. Replaced wholesale rather than merged on update: sending this field drops every stored header it does not repeat.",
+ "writeOnly": true,
+ "type": "object",
+ "propertyNames": {
+ "type": "string",
+ "minLength": 1
+ },
+ "additionalProperties": {
+ "type": "string",
+ "description": "Header value sent to the MCP server."
+ }
+ },
+ "timeout": {
+ "description": "Per-request timeout in milliseconds. Defaults to 30000 on creation.",
+ "default": 30000,
+ "type": "integer",
+ "minimum": 1000,
+ "maximum": 300000
+ },
+ "retries": {
+ "description": "Number of retries per request. Defaults to 3 on creation.",
+ "default": 3,
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 10
+ },
+ "enabled": {
+ "description": "Whether workflows can use the server's tools. Defaults to true on creation.",
+ "default": true,
+ "type": "boolean"
+ },
+ "oauthClientId": {
+ "description": "Pre-registered OAuth client identifier. Changing it on update revokes the stored OAuth grant and forces reauthorization.",
+ "anyOf": [
+ {
+ "type": "string",
+ "maxLength": 512
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "oauthClientSecret": {
+ "description": "Write-only pre-registered OAuth client secret. Sending it on update as null or a new value revokes the stored OAuth grant and forces reauthorization, as does switching away from OAuth authentication.",
+ "writeOnly": true,
+ "anyOf": [
+ {
+ "type": "string",
+ "maxLength": 2048
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ },
+ "required": ["workspaceId"],
+ "additionalProperties": false,
+ "title": "Update MCP server request",
+ "description": "MCP server fields to change; omitted fields retain their stored values.",
+ "examples": [
+ {
+ "workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
+ "enabled": false
+ }
+ ]
+ },
+ "V2McpServerDeleteData": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Identifier of the deleted MCP server."
+ },
+ "deleted": {
+ "type": "boolean",
+ "const": true,
+ "description": "Whether the server was deleted."
+ }
+ },
+ "required": ["id", "deleted"],
+ "additionalProperties": false,
+ "title": "Delete MCP server data",
+ "description": "MCP server deletion acknowledgement."
+ },
+ "DeleteMcpServerResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2McpServerDeleteData"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Delete MCP server response",
+ "description": "Acknowledgement that the MCP server was deleted.",
+ "examples": [
+ {
+ "data": {
+ "id": "mcp-3f7a9c21",
+ "deleted": true
+ }
+ }
+ ]
+ },
+ "V2McpTool": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Tool name, as the MCP server reports it."
+ },
+ "description": {
+ "description": "Tool description reported by the server.",
+ "type": "string"
+ },
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "object",
+ "description": "JSON Schema type of the argument object. MCP requires `object`."
+ },
+ "properties": {
+ "description": "Argument schemas keyed by argument name.",
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "description": "Server-defined JSON Schema for one tool argument."
+ }
+ },
+ "required": {
+ "description": "Names of the arguments the tool requires.",
+ "type": "array",
+ "items": {
+ "type": "string",
+ "description": "Name of a required argument."
+ }
+ }
+ },
+ "required": ["type"],
+ "additionalProperties": {
+ "description": "Additional JSON Schema keyword reported by the server."
+ },
+ "description": "JSON Schema for the tool's arguments, as reported by the server."
+ },
+ "serverId": {
+ "type": "string",
+ "description": "Identifier of the MCP server exposing the tool."
+ },
+ "serverName": {
+ "type": "string",
+ "description": "Display name of the MCP server exposing the tool."
+ }
+ },
+ "required": ["name", "inputSchema", "serverId", "serverName"],
+ "additionalProperties": false,
+ "title": "MCP tool",
+ "description": "A tool exposed by a registered MCP server."
+ },
+ "ListMcpServerToolsResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/V2McpTool"
+ },
+ "description": "Items in the current page."
+ },
+ "nextCursor": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Always `null` — this list has no `cursor` or `limit` param and returns its whole bounded set in one page. Present so the list can gain pages later without a shape change."
+ }
+ },
+ "required": ["data", "nextCursor"],
+ "additionalProperties": false,
+ "title": "List MCP server tools response",
+ "description": "Tools exposed by the MCP server.",
+ "examples": [
+ {
+ "data": [
+ {
+ "name": "search_docs",
+ "description": "Search the internal documentation",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "query": {
+ "type": "string",
+ "description": "Search terms"
+ }
+ },
+ "required": ["query"]
+ },
+ "serverId": "mcp-3f7a9c21",
+ "serverName": "Docs server"
+ }
+ ],
+ "nextCursor": null
+ }
+ ]
+ },
+ "V2SkillSummary": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique skill identifier. A built-in skill is `builtin-` followed by its name, for example `builtin-research`."
+ },
+ "name": {
+ "type": "string",
+ "description": "Kebab-case name that agents use to reference the skill."
+ },
+ "description": {
+ "type": "string",
+ "description": "One-line summary of when the skill applies."
+ },
+ "readOnly": {
+ "type": "boolean",
+ "description": "Whether this is a built-in skill that cannot be modified or deleted."
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
+ "description": "ISO 8601 timestamp when the skill was created. Built-in skills report the Unix epoch."
+ },
+ "updatedAt": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
+ "description": "ISO 8601 timestamp when the skill was last updated. Built-in skills report the Unix epoch."
+ }
+ },
+ "required": ["id", "name", "description", "readOnly", "createdAt", "updatedAt"],
+ "additionalProperties": false,
+ "title": "Skill summary",
+ "description": "Public summary metadata for a workspace or built-in skill."
+ },
+ "ListSkillsResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/V2SkillSummary"
+ },
+ "description": "Items in the current page."
+ },
+ "nextCursor": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Opaque cursor for the next page. Send it back as `cursor`; `null` means there is nothing further to fetch. Never construct one yourself."
+ }
+ },
+ "required": ["data", "nextCursor"],
+ "additionalProperties": false,
+ "title": "List skills response",
+ "description": "Skill summaries available in the workspace.",
+ "examples": [
+ {
+ "data": [
+ {
+ "id": "V1StGXR8Z5jdHi6BmyT",
+ "name": "refund-policy",
+ "description": "How support should handle refund requests",
+ "readOnly": false,
+ "createdAt": "2026-06-01T09:14:00.000Z",
+ "updatedAt": "2026-06-20T14:02:11.000Z"
+ }
+ ],
+ "nextCursor": null
+ }
+ ]
+ },
+ "V2Skill": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique skill identifier. A built-in skill is `builtin-` followed by its name, for example `builtin-research`."
+ },
+ "name": {
+ "type": "string",
+ "description": "Kebab-case name that agents use to reference the skill."
+ },
+ "description": {
+ "type": "string",
+ "description": "One-line summary of when the skill applies."
+ },
+ "readOnly": {
+ "type": "boolean",
+ "description": "Whether this is a built-in skill that cannot be modified or deleted."
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
+ "description": "ISO 8601 timestamp when the skill was created. Built-in skills report the Unix epoch."
+ },
+ "updatedAt": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
+ "description": "ISO 8601 timestamp when the skill was last updated. Built-in skills report the Unix epoch."
+ },
+ "content": {
+ "type": "string",
+ "description": "Skill body containing the instructions given to the agent."
+ }
+ },
+ "required": ["id", "name", "description", "readOnly", "createdAt", "updatedAt", "content"],
+ "additionalProperties": false,
+ "title": "Skill",
+ "description": "A workspace or built-in skill including its instruction body."
+ },
+ "CreateSkillResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2Skill"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Create skill response",
+ "description": "The created skill including its content.",
+ "examples": [
+ {
+ "data": {
+ "id": "V1StGXR8Z5jdHi6BmyT",
+ "name": "refund-policy",
+ "description": "How support should handle refund requests",
+ "readOnly": false,
+ "createdAt": "2026-06-01T09:14:00.000Z",
+ "updatedAt": "2026-06-20T14:02:11.000Z",
+ "content": "# Refund policy\n\nAlways check the order date first."
+ }
+ }
+ ]
+ },
+ "CreateSkillRequest": {
+ "type": "object",
+ "properties": {
+ "workspaceId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 128,
+ "description": "Workspace in which to create the skill."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64,
+ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
+ "description": "Kebab-case name, unique within the workspace and not reserved by a built-in skill."
+ },
+ "description": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 1024,
+ "description": "One-line summary of when the skill applies."
+ },
+ "content": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 50000,
+ "description": "Skill body containing the instructions given to the agent."
+ }
+ },
+ "required": ["workspaceId", "name", "description", "content"],
+ "additionalProperties": false,
+ "title": "Create skill request",
+ "description": "Definition of a new skill.",
+ "examples": [
+ {
+ "workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
+ "name": "refund-policy",
+ "description": "How support should handle refund requests",
+ "content": "# Refund policy\n\nAlways check the order date first."
+ }
+ ]
+ },
+ "GetSkillResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2Skill"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Get skill response",
+ "description": "One skill including its full content.",
+ "examples": [
+ {
+ "data": {
+ "id": "V1StGXR8Z5jdHi6BmyT",
+ "name": "refund-policy",
+ "description": "How support should handle refund requests",
+ "readOnly": false,
+ "createdAt": "2026-06-01T09:14:00.000Z",
+ "updatedAt": "2026-06-20T14:02:11.000Z",
+ "content": "# Refund policy\n\nAlways check the order date first."
+ }
+ }
+ ]
+ },
+ "UpdateSkillResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2Skill"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Update skill response",
+ "description": "The updated skill including its full content.",
+ "examples": [
+ {
+ "data": {
+ "id": "V1StGXR8Z5jdHi6BmyT",
+ "name": "refund-policy",
+ "description": "Updated refund guidance",
+ "readOnly": false,
+ "createdAt": "2026-06-01T09:14:00.000Z",
+ "updatedAt": "2026-06-20T14:02:11.000Z",
+ "content": "# Refund policy\n\nAlways check the order date first."
+ }
+ }
+ ]
+ },
+ "UpdateSkillRequest": {
+ "type": "object",
+ "properties": {
+ "workspaceId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 128,
+ "description": "Workspace that owns the skill."
+ },
+ "name": {
+ "description": "New kebab-case skill name.",
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64,
+ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
+ },
+ "description": {
+ "description": "New one-line summary of when the skill applies.",
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 1024
+ },
+ "content": {
+ "description": "Replacement skill body.",
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 50000
+ }
+ },
+ "required": ["workspaceId"],
+ "additionalProperties": false,
+ "title": "Update skill request",
+ "description": "Skill fields to change; at least one editable field is required.",
+ "examples": [
+ {
+ "workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
+ "description": "Updated refund guidance"
+ }
+ ]
+ },
+ "V2SkillDeleteData": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Identifier of the deleted skill."
+ },
+ "deleted": {
+ "type": "boolean",
+ "const": true,
+ "description": "Whether the skill was deleted."
+ }
+ },
+ "required": ["id", "deleted"],
+ "additionalProperties": false,
+ "title": "Delete skill data",
+ "description": "Skill deletion acknowledgement."
+ },
+ "DeleteSkillResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2SkillDeleteData"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Delete skill response",
+ "description": "Acknowledgement that the skill was deleted.",
+ "examples": [
+ {
+ "data": {
+ "id": "V1StGXR8Z5jdHi6BmyT",
+ "deleted": true
+ }
+ }
+ ]
+ },
+ "V2SkillEditor": {
+ "type": "object",
+ "properties": {
+ "email": {
+ "type": "string",
+ "format": "email",
+ "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
+ "description": "Email address of the skill editor."
+ },
+ "name": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Display name of the skill editor."
+ },
+ "image": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Profile image URL of the skill editor."
+ },
+ "isWorkspaceAdmin": {
+ "type": "boolean",
+ "description": "Whether editor access is derived from workspace administration."
+ }
+ },
+ "required": ["email", "name", "image", "isWorkspaceAdmin"],
+ "additionalProperties": false,
+ "title": "Skill editor",
+ "description": "Public identity fields for a user who can edit a skill."
+ },
+ "ListSkillEditorsResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/V2SkillEditor"
+ },
+ "description": "Items in the current page."
+ },
+ "nextCursor": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Opaque cursor for the next page. Send it back as `cursor`; `null` means there is nothing further to fetch. Never construct one yourself."
+ }
+ },
+ "required": ["data", "nextCursor"],
+ "additionalProperties": false,
+ "title": "List skill editors response",
+ "description": "Public identity fields for users who can edit the skill.",
+ "examples": [
+ {
+ "data": [
+ {
+ "email": "jane@example.com",
+ "name": "Jane Smith",
+ "image": null,
+ "isWorkspaceAdmin": false
+ }
+ ],
+ "nextCursor": null
+ }
+ ]
+ },
+ "GrantSkillEditorResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2SkillEditor"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Grant skill editor response",
+ "description": "Public identity fields for the editor.",
+ "examples": [
+ {
+ "data": {
+ "email": "jane@example.com",
+ "name": "Jane Smith",
+ "image": null,
+ "isWorkspaceAdmin": false
+ }
+ }
+ ]
+ },
+ "GrantSkillEditorRequest": {
+ "type": "object",
+ "properties": {
+ "workspaceId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 128,
+ "description": "Workspace that owns the skill."
+ },
+ "email": {
+ "type": "string",
+ "format": "email",
+ "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
+ "description": "Email address of a current workspace member."
+ }
+ },
+ "required": ["workspaceId", "email"],
+ "additionalProperties": false,
+ "title": "Grant skill editor request",
+ "description": "Workspace scope and email of the member to grant.",
+ "examples": [
+ {
+ "workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
+ "email": "jane@example.com"
+ }
+ ]
+ },
+ "V2SkillEditorDeleteData": {
+ "type": "object",
+ "properties": {
+ "email": {
+ "type": "string",
+ "format": "email",
+ "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
+ "description": "Email address whose explicit editor grant was revoked."
+ },
+ "revoked": {
+ "type": "boolean",
+ "const": true,
+ "description": "Whether the explicit editor grant was revoked."
+ }
+ },
+ "required": ["email", "revoked"],
+ "additionalProperties": false,
+ "title": "Revoke skill editor data",
+ "description": "Skill editor revocation acknowledgement."
+ },
+ "RevokeSkillEditorResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2SkillEditorDeleteData"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Revoke skill editor response",
+ "description": "Acknowledgement that the explicit editor grant was revoked.",
+ "examples": [
+ {
+ "data": {
+ "email": "jane@example.com",
+ "revoked": true
+ }
+ }
+ ]
+ },
+ "V2CustomTool": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique custom tool identifier."
+ },
+ "title": {
+ "type": "string",
+ "description": "Display title, unique within the workspace."
+ },
+ "schema": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "function",
+ "description": "Function declaration discriminator."
+ },
+ "function": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Function name presented to the model."
+ },
+ "description": {
+ "description": "Optional explanation of what the function does.",
+ "type": "string"
+ },
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "JSON Schema type for the arguments, usually `object`."
+ },
+ "properties": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "description": "Caller-defined JSON Schema for one tool argument."
+ },
+ "description": "Caller-defined argument schemas keyed by argument name."
+ },
+ "required": {
+ "description": "Names of required arguments.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "required": ["type", "properties"],
+ "additionalProperties": {
+ "description": "Caller-defined extension value preserved by the public API."
+ },
+ "description": "JSON Schema describing the arguments accepted by the tool."
+ }
+ },
+ "required": ["name", "parameters"],
+ "additionalProperties": {
+ "description": "Caller-defined extension value preserved by the public API."
+ },
+ "description": "OpenAI-style function definition."
+ }
+ },
+ "required": ["type", "function"],
+ "additionalProperties": {
+ "description": "Caller-defined extension value preserved by the public API."
+ },
+ "description": "OpenAI-style function declaration describing the callable tool surface."
+ },
+ "code": {
+ "type": "string",
+ "description": "Tool implementation executed in the sandboxed function runtime."
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
+ "description": "ISO 8601 timestamp when the tool was created."
+ },
+ "updatedAt": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
+ "description": "ISO 8601 timestamp when the tool was last updated."
+ }
+ },
+ "required": ["id", "title", "schema", "code", "createdAt", "updatedAt"],
+ "additionalProperties": false,
+ "title": "Custom tool",
+ "description": "A workspace custom tool and its callable function declaration."
+ },
+ "ListCustomToolsResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/V2CustomTool"
+ },
+ "description": "Items in the current page."
+ },
+ "nextCursor": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Opaque cursor for the next page. Send it back as `cursor`; `null` means there is nothing further to fetch. Never construct one yourself."
+ }
+ },
+ "required": ["data", "nextCursor"],
+ "additionalProperties": false,
+ "title": "List custom tools response",
+ "description": "Custom tools defined in the workspace.",
+ "examples": [
+ {
+ "data": [
+ {
+ "id": "V1StGXR8Z5jdHi6BmyT",
+ "title": "lookup_order",
+ "schema": {
+ "type": "function",
+ "function": {
+ "name": "lookup_order",
+ "description": "Look up an order by id",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "orderId": {
+ "type": "string"
+ }
+ },
+ "required": ["orderId"]
+ }
+ }
+ },
+ "code": "return { ok: true }",
+ "createdAt": "2026-06-01T09:14:00.000Z",
+ "updatedAt": "2026-06-20T14:02:11.000Z"
+ }
+ ],
+ "nextCursor": null
+ }
+ ]
+ },
+ "CreateCustomToolResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2CustomTool"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Create custom tool response",
+ "description": "The created custom tool.",
+ "examples": [
+ {
+ "data": {
+ "id": "V1StGXR8Z5jdHi6BmyT",
+ "title": "lookup_order",
+ "schema": {
+ "type": "function",
+ "function": {
+ "name": "lookup_order",
+ "description": "Look up an order by id",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "orderId": {
+ "type": "string"
+ }
+ },
+ "required": ["orderId"]
+ }
+ }
+ },
+ "code": "return { ok: true }",
+ "createdAt": "2026-06-01T09:14:00.000Z",
+ "updatedAt": "2026-06-20T14:02:11.000Z"
+ }
+ }
+ ]
+ },
+ "CreateCustomToolRequest": {
+ "type": "object",
+ "properties": {
+ "workspaceId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 128,
+ "description": "Workspace in which to create the custom tool."
+ },
+ "title": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200,
+ "description": "Display title, unique within the workspace."
+ },
+ "schema": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "function",
+ "description": "Function declaration discriminator."
+ },
+ "function": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Function name presented to the model."
+ },
+ "description": {
+ "description": "Optional explanation of what the function does.",
+ "type": "string"
+ },
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "JSON Schema type for the arguments, usually `object`."
+ },
+ "properties": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "description": "Caller-defined JSON Schema for one tool argument."
+ },
+ "description": "Caller-defined argument schemas keyed by argument name."
+ },
+ "required": {
+ "description": "Names of required arguments.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "required": ["type", "properties"],
+ "additionalProperties": {
+ "description": "Caller-defined extension value preserved by the public API."
+ },
+ "description": "JSON Schema describing the arguments accepted by the tool."
+ }
+ },
+ "required": ["name", "parameters"],
+ "additionalProperties": {
+ "description": "Caller-defined extension value preserved by the public API."
+ },
+ "description": "OpenAI-style function definition."
+ }
+ },
+ "required": ["type", "function"],
+ "additionalProperties": {
+ "description": "Caller-defined extension value preserved by the public API."
+ },
+ "description": "OpenAI-style function declaration describing the callable tool surface."
+ },
+ "code": {
+ "type": "string",
+ "maxLength": 100000,
+ "description": "Tool implementation executed in the sandboxed function runtime."
+ }
+ },
+ "required": ["workspaceId", "title", "schema", "code"],
+ "additionalProperties": false,
+ "title": "Create custom tool request",
+ "description": "Definition and implementation of a new custom tool.",
+ "examples": [
+ {
+ "workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
+ "title": "lookup_order",
+ "schema": {
+ "type": "function",
+ "function": {
+ "name": "lookup_order",
+ "description": "Look up an order by id",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "orderId": {
+ "type": "string"
+ }
+ },
+ "required": ["orderId"]
+ }
+ }
+ },
+ "code": "return { ok: true }"
+ }
+ ]
+ },
+ "GetCustomToolResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2CustomTool"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Get custom tool response",
+ "description": "One custom tool.",
+ "examples": [
+ {
+ "data": {
+ "id": "V1StGXR8Z5jdHi6BmyT",
+ "title": "lookup_order",
+ "schema": {
+ "type": "function",
+ "function": {
+ "name": "lookup_order",
+ "description": "Look up an order by id",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "orderId": {
+ "type": "string"
+ }
+ },
+ "required": ["orderId"]
+ }
+ }
+ },
+ "code": "return { ok: true }",
+ "createdAt": "2026-06-01T09:14:00.000Z",
+ "updatedAt": "2026-06-20T14:02:11.000Z"
+ }
+ }
+ ]
+ },
+ "UpdateCustomToolResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2CustomTool"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Update custom tool response",
+ "description": "The updated custom tool.",
+ "examples": [
+ {
+ "data": {
+ "id": "V1StGXR8Z5jdHi6BmyT",
+ "title": "lookup_order",
+ "schema": {
+ "type": "function",
+ "function": {
+ "name": "lookup_order",
+ "description": "Look up an order by id",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "orderId": {
+ "type": "string"
+ }
+ },
+ "required": ["orderId"]
+ }
+ }
+ },
+ "code": "return { ok: false }",
+ "createdAt": "2026-06-01T09:14:00.000Z",
+ "updatedAt": "2026-06-20T14:02:11.000Z"
+ }
+ }
+ ]
+ },
+ "UpdateCustomToolRequest": {
+ "type": "object",
+ "properties": {
+ "workspaceId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 128,
+ "description": "Workspace that owns the custom tool."
+ },
+ "title": {
+ "description": "New display title for the tool.",
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200
+ },
+ "schema": {
+ "description": "Replacement function declaration.",
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "function",
+ "description": "Function declaration discriminator."
+ },
+ "function": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Function name presented to the model."
+ },
+ "description": {
+ "description": "Optional explanation of what the function does.",
+ "type": "string"
+ },
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "JSON Schema type for the arguments, usually `object`."
+ },
+ "properties": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "description": "Caller-defined JSON Schema for one tool argument."
+ },
+ "description": "Caller-defined argument schemas keyed by argument name."
+ },
+ "required": {
+ "description": "Names of required arguments.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "required": ["type", "properties"],
+ "additionalProperties": {
+ "description": "Caller-defined extension value preserved by the public API."
+ },
+ "description": "JSON Schema describing the arguments accepted by the tool."
+ }
+ },
+ "required": ["name", "parameters"],
+ "additionalProperties": {
+ "description": "Caller-defined extension value preserved by the public API."
+ },
+ "description": "OpenAI-style function definition."
+ }
+ },
+ "required": ["type", "function"],
+ "additionalProperties": {
+ "description": "Caller-defined extension value preserved by the public API."
+ }
+ },
+ "code": {
+ "description": "Replacement tool implementation.",
+ "type": "string",
+ "maxLength": 100000
+ }
+ },
+ "required": ["workspaceId"],
+ "additionalProperties": false,
+ "title": "Update custom tool request",
+ "description": "Custom tool fields to change; at least one editable field is required.",
+ "examples": [
+ {
+ "workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
+ "code": "return { ok: false }"
+ }
+ ]
+ },
+ "V2CustomToolDeleteData": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Identifier of the deleted custom tool."
+ },
+ "deleted": {
+ "type": "boolean",
+ "const": true,
+ "description": "Whether the custom tool was deleted."
+ }
+ },
+ "required": ["id", "deleted"],
+ "additionalProperties": false,
+ "title": "Delete custom tool data",
+ "description": "Custom tool deletion acknowledgement."
+ },
+ "DeleteCustomToolResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2CustomToolDeleteData"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Delete custom tool response",
+ "description": "Acknowledgement that the custom tool was deleted.",
+ "examples": [
+ {
+ "data": {
+ "id": "V1StGXR8Z5jdHi6BmyT",
+ "deleted": true
+ }
+ }
+ ]
+ },
+ "V2Sandbox": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique sandbox identifier."
+ },
+ "name": {
+ "type": "string",
+ "description": "Display name, unique within the workspace."
+ },
+ "language": {
+ "type": "string",
+ "enum": ["javascript", "python"],
+ "description": "Dependency ecosystem: `javascript` installs from npm, `python` from PyPI."
+ },
+ "dependencies": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Package specifiers installed into the sandbox, one per entry."
+ },
+ "cliTools": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "google-cloud-cli@577.0.0-r1",
+ "aws-cli@2.36.15-r1",
+ "azure-cli@2.89.0-r1",
+ "doctl@1.166.0-r1",
+ "github-cli@2.97.0-r1",
+ "gitlab-cli@1.111.0-r1",
+ "kubectl@1.36.3-r1",
+ "helm@4.2.3-r1",
+ "kustomize@5.8.1-r1",
+ "argocd@3.4.6-r1",
+ "terraform@1.15.8-r1",
+ "pulumi@3.255.0-r1",
+ "supabase-cli@2.111.0-r1",
+ "firebase-cli@15.25.1-r1",
+ "flyctl@0.4.78-r1",
+ "railway-cli@5.30.4-r1",
+ "stripe-cli@1.45.0-r1",
+ "duckdb@1.5.5-r1",
+ "rclone@1.75.0-r1",
+ "restic@0.19.1-r1",
+ "minio-mc@RELEASE.2025-08-13T08-35-41Z-r1",
+ "mongosh@2.9.2-r1",
+ "sops@3.13.3-r1",
+ "age@1.3.1-r1"
+ ]
+ },
+ "description": "Pinned managed CLI ids installed into the sandbox, at most 10, no duplicates."
+ },
+ "systemPackages": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Debian packages installed into the sandbox, one per entry."
+ },
+ "buildStatus": {
+ "anyOf": [
+ {
+ "type": "string",
+ "enum": ["pending", "building", "ready", "failed"]
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Image build state. `null` when the deployment installs dependencies at run time and has nothing to build."
+ },
+ "errorCode": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Classified build failure code, or `null`."
+ },
+ "errorMessage": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Human-readable build failure summary, or `null`."
+ },
+ "errorDetail": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Tail of the installer log for a failed build, or `null`."
+ },
+ "builtAt": {
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "ISO 8601 timestamp when the current image finished building, or `null`."
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
+ "description": "ISO 8601 timestamp when the sandbox was created."
+ },
+ "updatedAt": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
+ "description": "ISO 8601 timestamp when the sandbox was last updated."
+ }
+ },
+ "required": [
+ "id",
+ "name",
+ "language",
+ "dependencies",
+ "cliTools",
+ "systemPackages",
+ "buildStatus",
+ "errorCode",
+ "errorMessage",
+ "errorDetail",
+ "builtAt",
+ "createdAt",
+ "updatedAt"
+ ],
+ "additionalProperties": false,
+ "title": "Sandbox",
+ "description": "A workspace sandbox: a reusable dependency set that Function blocks execute against."
+ },
+ "ListSandboxesResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/V2Sandbox"
+ },
+ "description": "Items in the current page."
+ },
+ "nextCursor": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Opaque cursor for the next page. Send it back as `cursor`; `null` means there is nothing further to fetch. Never construct one yourself."
+ }
+ },
+ "required": ["data", "nextCursor"],
+ "additionalProperties": false,
+ "title": "List sandboxes response",
+ "description": "Sandboxes defined in the workspace.",
+ "examples": [
+ {
+ "data": [
+ {
+ "id": "V1StGXR8Z5jdHi6BmyT",
+ "name": "data-tools",
+ "language": "python",
+ "dependencies": ["pandas==2.2.2", "requests"],
+ "cliTools": [],
+ "systemPackages": ["graphviz"],
+ "buildStatus": "ready",
+ "errorCode": null,
+ "errorMessage": null,
+ "errorDetail": null,
+ "builtAt": "2026-06-20T14:05:40.000Z",
+ "createdAt": "2026-06-01T09:14:00.000Z",
+ "updatedAt": "2026-06-20T14:02:11.000Z"
+ }
+ ],
+ "nextCursor": null
+ }
+ ]
+ },
+ "CreateSandboxResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2Sandbox"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Create sandbox response",
+ "description": "The created sandbox. `buildStatus` is `pending` while an image builds and `null` where nothing is built.",
+ "examples": [
+ {
+ "data": {
+ "id": "V1StGXR8Z5jdHi6BmyT",
+ "name": "data-tools",
+ "language": "python",
+ "dependencies": ["pandas==2.2.2", "requests"],
+ "cliTools": [],
+ "systemPackages": ["graphviz"],
+ "buildStatus": "pending",
+ "errorCode": null,
+ "errorMessage": null,
+ "errorDetail": null,
+ "builtAt": null,
+ "createdAt": "2026-06-01T09:14:00.000Z",
+ "updatedAt": "2026-06-20T14:02:11.000Z"
+ }
+ }
+ ]
+ },
+ "CreateSandboxRequest": {
+ "type": "object",
+ "properties": {
+ "workspaceId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 128,
+ "description": "Workspace in which to create the sandbox."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64,
+ "description": "Display name, unique within the workspace; 1 to 64 characters."
+ },
+ "language": {
+ "type": "string",
+ "enum": ["javascript", "python"],
+ "description": "Dependency ecosystem: `javascript` installs from npm, `python` from PyPI."
+ },
+ "dependencies": {
+ "default": [],
+ "description": "Package specifiers installed into the sandbox, one per entry.",
+ "maxItems": 1000,
+ "type": "array",
+ "items": {
+ "type": "string",
+ "maxLength": 2000
+ }
+ },
+ "cliTools": {
+ "default": [],
+ "description": "Pinned managed CLI ids installed into the sandbox, at most 10, no duplicates.",
+ "maxItems": 10,
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "google-cloud-cli@577.0.0-r1",
+ "aws-cli@2.36.15-r1",
+ "azure-cli@2.89.0-r1",
+ "doctl@1.166.0-r1",
+ "github-cli@2.97.0-r1",
+ "gitlab-cli@1.111.0-r1",
+ "kubectl@1.36.3-r1",
+ "helm@4.2.3-r1",
+ "kustomize@5.8.1-r1",
+ "argocd@3.4.6-r1",
+ "terraform@1.15.8-r1",
+ "pulumi@3.255.0-r1",
+ "supabase-cli@2.111.0-r1",
+ "firebase-cli@15.25.1-r1",
+ "flyctl@0.4.78-r1",
+ "railway-cli@5.30.4-r1",
+ "stripe-cli@1.45.0-r1",
+ "duckdb@1.5.5-r1",
+ "rclone@1.75.0-r1",
+ "restic@0.19.1-r1",
+ "minio-mc@RELEASE.2025-08-13T08-35-41Z-r1",
+ "mongosh@2.9.2-r1",
+ "sops@3.13.3-r1",
+ "age@1.3.1-r1"
+ ]
+ }
+ },
+ "systemPackages": {
+ "default": [],
+ "description": "Debian packages installed into the sandbox, one per entry.",
+ "maxItems": 1000,
+ "type": "array",
+ "items": {
+ "type": "string",
+ "maxLength": 2000
+ }
+ }
+ },
+ "required": ["workspaceId", "name", "language"],
+ "additionalProperties": false,
+ "title": "Create sandbox request",
+ "description": "Name, language, and dependency set of a new sandbox.",
+ "examples": [
+ {
+ "workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
+ "name": "data-tools",
+ "language": "python",
+ "dependencies": ["pandas==2.2.2", "requests"],
+ "systemPackages": ["graphviz"]
+ }
+ ]
+ },
+ "GetSandboxResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2Sandbox"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Get sandbox response",
+ "description": "One sandbox.",
+ "examples": [
+ {
+ "data": {
+ "id": "V1StGXR8Z5jdHi6BmyT",
+ "name": "data-tools",
+ "language": "python",
+ "dependencies": ["pandas==2.2.2", "requests"],
+ "cliTools": [],
+ "systemPackages": ["graphviz"],
+ "buildStatus": "ready",
+ "errorCode": null,
+ "errorMessage": null,
+ "errorDetail": null,
+ "builtAt": "2026-06-20T14:05:40.000Z",
+ "createdAt": "2026-06-01T09:14:00.000Z",
+ "updatedAt": "2026-06-20T14:02:11.000Z"
+ }
+ }
+ ]
+ },
+ "UpdateSandboxResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2Sandbox"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Update sandbox response",
+ "description": "The updated sandbox. `buildStatus` is `pending` while an image rebuilds and `null` where nothing is built.",
+ "examples": [
+ {
+ "data": {
+ "id": "V1StGXR8Z5jdHi6BmyT",
+ "name": "data-tools",
+ "language": "python",
+ "dependencies": ["pandas==2.2.2", "requests", "pyarrow"],
+ "cliTools": [],
+ "systemPackages": ["graphviz"],
+ "buildStatus": "pending",
+ "errorCode": null,
+ "errorMessage": null,
+ "errorDetail": null,
+ "builtAt": null,
+ "createdAt": "2026-06-01T09:14:00.000Z",
+ "updatedAt": "2026-06-20T14:02:11.000Z"
+ }
+ }
+ ]
+ },
+ "UpdateSandboxRequest": {
+ "type": "object",
+ "properties": {
+ "workspaceId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 128,
+ "description": "Workspace that owns the sandbox."
+ },
+ "name": {
+ "description": "New display name, unique within the workspace; 1 to 64 characters.",
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64
+ },
+ "language": {
+ "description": "Replacement dependency ecosystem. The whole spec is revalidated against it, so a Python dependency list does not survive a switch to JavaScript.",
+ "type": "string",
+ "enum": ["javascript", "python"]
+ },
+ "dependencies": {
+ "description": "Replacement package list; replaces the whole list.",
+ "maxItems": 1000,
+ "type": "array",
+ "items": {
+ "type": "string",
+ "maxLength": 2000
+ }
+ },
+ "cliTools": {
+ "description": "Replacement managed CLI list; replaces the whole list.",
+ "maxItems": 10,
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "google-cloud-cli@577.0.0-r1",
+ "aws-cli@2.36.15-r1",
+ "azure-cli@2.89.0-r1",
+ "doctl@1.166.0-r1",
+ "github-cli@2.97.0-r1",
+ "gitlab-cli@1.111.0-r1",
+ "kubectl@1.36.3-r1",
+ "helm@4.2.3-r1",
+ "kustomize@5.8.1-r1",
+ "argocd@3.4.6-r1",
+ "terraform@1.15.8-r1",
+ "pulumi@3.255.0-r1",
+ "supabase-cli@2.111.0-r1",
+ "firebase-cli@15.25.1-r1",
+ "flyctl@0.4.78-r1",
+ "railway-cli@5.30.4-r1",
+ "stripe-cli@1.45.0-r1",
+ "duckdb@1.5.5-r1",
+ "rclone@1.75.0-r1",
+ "restic@0.19.1-r1",
+ "minio-mc@RELEASE.2025-08-13T08-35-41Z-r1",
+ "mongosh@2.9.2-r1",
+ "sops@3.13.3-r1",
+ "age@1.3.1-r1"
+ ]
+ }
+ },
+ "systemPackages": {
+ "description": "Replacement Debian package list; replaces the whole list.",
+ "maxItems": 1000,
+ "type": "array",
+ "items": {
+ "type": "string",
+ "maxLength": 2000
+ }
+ }
+ },
+ "required": ["workspaceId"],
+ "additionalProperties": false,
+ "title": "Update sandbox request",
+ "description": "Sandbox fields to change; at least one editable field is required.",
+ "examples": [
+ {
+ "workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
+ "dependencies": ["pandas==2.2.2", "requests", "pyarrow"]
+ }
+ ]
+ },
+ "V2SandboxDeleteData": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Identifier of the deleted sandbox."
+ },
+ "deleted": {
+ "type": "boolean",
+ "const": true,
+ "description": "Whether the sandbox was deleted."
+ }
+ },
+ "required": ["id", "deleted"],
+ "additionalProperties": false,
+ "title": "Delete sandbox data",
+ "description": "Sandbox deletion acknowledgement."
+ },
+ "DeleteSandboxResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2SandboxDeleteData"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Delete sandbox response",
+ "description": "Acknowledgement that the sandbox was deleted.",
+ "examples": [
+ {
+ "data": {
+ "id": "V1StGXR8Z5jdHi6BmyT",
+ "deleted": true
+ }
+ }
+ ]
+ },
+ "V2Credential": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique credential identifier."
+ },
+ "type": {
+ "type": "string",
+ "enum": ["oauth", "service_account"],
+ "description": "Authenticated connection type."
+ },
+ "displayName": {
+ "type": "string",
+ "description": "Credential display name."
+ },
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional credential description."
+ },
+ "providerId": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Integration provider authenticated by this credential."
+ },
+ "accountId": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Linked account identifier for OAuth credentials."
+ },
+ "hasServiceAccountKey": {
+ "type": "boolean",
+ "description": "Whether a service-account payload is stored. Its contents are never returned."
+ },
+ "role": {
+ "type": "string",
+ "enum": ["admin", "member"],
+ "description": "Caller role for the credential."
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
+ "description": "ISO 8601 timestamp when the credential was created."
+ },
+ "updatedAt": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
+ "description": "ISO 8601 timestamp when the credential was last updated."
+ }
+ },
+ "required": [
+ "id",
+ "type",
+ "displayName",
+ "description",
+ "providerId",
+ "accountId",
+ "hasServiceAccountKey",
+ "role",
+ "createdAt",
+ "updatedAt"
+ ],
+ "additionalProperties": false,
+ "title": "Credential",
+ "description": "Public authenticated-connection metadata without secret material."
+ },
+ "ListCredentialsResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/V2Credential"
+ },
+ "description": "Items in the current page."
+ },
+ "nextCursor": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Opaque cursor for the next page. Send it back as `cursor`; `null` means there is nothing further to fetch. Never construct one yourself."
+ }
+ },
+ "required": ["data", "nextCursor"],
+ "additionalProperties": false,
+ "title": "List credentials response",
+ "description": "Credential metadata visible to the caller.",
+ "examples": [
+ {
+ "data": [
+ {
+ "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
+ "type": "service_account",
+ "displayName": "Zoom service account",
+ "description": null,
+ "providerId": "zoom-service-account",
+ "accountId": null,
+ "hasServiceAccountKey": true,
+ "role": "admin",
+ "createdAt": "2026-06-01T09:14:00.000Z",
+ "updatedAt": "2026-06-20T14:02:11.000Z"
+ }
+ ],
+ "nextCursor": null
+ }
+ ]
+ },
+ "V2CredentialProvider": {
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "oauth",
+ "description": "Browser-based OAuth connection method."
+ },
+ "serviceId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "Stable credential-provider identifier."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "Credential provider display name."
+ },
+ "description": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 1000,
+ "description": "Credential provider description."
+ },
+ "providerFamily": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "Owning provider family identifier."
+ },
+ "available": {
+ "type": "boolean",
+ "description": "Whether this caller can connect the provider in the current deployment."
+ },
+ "supportsReconnect": {
+ "type": "boolean",
+ "description": "Whether existing credentials for this service can be reconnected."
+ },
+ "authorizationOptions": {
+ "minItems": 1,
+ "maxItems": 10,
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "providerId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "Exact OAuth provider identifier accepted by the connection endpoint."
+ },
+ "label": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "Human-readable authorization-server label."
+ }
+ },
+ "required": ["providerId", "label"],
+ "additionalProperties": false
+ },
+ "description": "Authorization servers available for this OAuth service."
+ },
+ "fields": {
+ "maxItems": 20,
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "Exact create-body field name."
+ },
+ "label": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "Human-readable field label."
+ },
+ "placeholder": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 1000,
+ "description": "Suggested input placeholder."
+ },
+ "required": {
+ "type": "boolean",
+ "description": "Whether the field is required for the selected flow."
+ },
+ "secret": {
+ "type": "boolean",
+ "description": "Whether the submitted field is write-only secret material."
+ },
+ "multiline": {
+ "type": "boolean",
+ "description": "Whether the field is intended for multi-line input."
+ },
+ "requiredForAuthMethods": {
+ "description": "Authentication methods for which this field is required.",
+ "minItems": 1,
+ "maxItems": 10,
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64
+ }
+ },
+ "options": {
+ "description": "Fixed values accepted by a selector field.",
+ "minItems": 1,
+ "maxItems": 20,
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "value": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "Submitted option value."
+ },
+ "label": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "Human-readable option label."
+ }
+ },
+ "required": ["value", "label"],
+ "additionalProperties": false
+ }
+ },
+ "hint": {
+ "description": "Provider-specific setup guidance.",
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2000
+ }
+ },
+ "required": ["id", "label", "placeholder", "required", "secret", "multiline"],
+ "additionalProperties": false
+ },
+ "description": "Write-only setup fields required before starting this OAuth flow."
+ }
+ },
+ "required": [
+ "type",
+ "serviceId",
+ "name",
+ "description",
+ "providerFamily",
+ "available",
+ "supportsReconnect",
+ "authorizationOptions",
+ "fields"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "service_account",
+ "description": "Direct service-account credential method."
+ },
+ "serviceId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "Stable credential-provider identifier."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "Credential provider display name."
+ },
+ "description": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 1000,
+ "description": "Credential provider description."
+ },
+ "providerFamily": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "Owning provider family identifier."
+ },
+ "available": {
+ "type": "boolean",
+ "description": "Whether this caller can connect the provider in the current deployment."
+ },
+ "providerId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "Exact service-account provider ID accepted by credential creation."
+ },
+ "docsUrl": {
+ "type": "string",
+ "format": "uri",
+ "description": "Setup guide for the provider."
+ },
+ "helpText": {
+ "description": "Provider-specific setup guidance.",
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2000
+ },
+ "requiresClientGeneratedCredentialId": {
+ "type": "boolean",
+ "description": "Whether the caller must generate and submit the credential ID before setup."
+ },
+ "fields": {
+ "minItems": 1,
+ "maxItems": 20,
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "Exact create-body field name."
+ },
+ "label": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "Human-readable field label."
+ },
+ "placeholder": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 1000,
+ "description": "Suggested input placeholder."
+ },
+ "required": {
+ "type": "boolean",
+ "description": "Whether the field is required for the selected flow."
+ },
+ "secret": {
+ "type": "boolean",
+ "description": "Whether the submitted field is write-only secret material."
+ },
+ "multiline": {
+ "type": "boolean",
+ "description": "Whether the field is intended for multi-line input."
+ },
+ "requiredForAuthMethods": {
+ "description": "Authentication methods for which this field is required.",
+ "minItems": 1,
+ "maxItems": 10,
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64
+ }
+ },
+ "options": {
+ "description": "Fixed values accepted by a selector field.",
+ "minItems": 1,
+ "maxItems": 20,
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "value": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "Submitted option value."
+ },
+ "label": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "Human-readable option label."
+ }
+ },
+ "required": ["value", "label"],
+ "additionalProperties": false
+ }
+ },
+ "hint": {
+ "description": "Provider-specific setup guidance.",
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2000
+ }
+ },
+ "required": ["id", "label", "placeholder", "required", "secret", "multiline"],
+ "additionalProperties": false
+ },
+ "description": "Create-body fields accepted by this provider. Secret fields are write-only."
+ }
+ },
+ "required": [
+ "type",
+ "serviceId",
+ "name",
+ "description",
+ "providerFamily",
+ "available",
+ "providerId",
+ "docsUrl",
+ "requiresClientGeneratedCredentialId",
+ "fields"
+ ],
+ "additionalProperties": false
+ }
+ ],
+ "title": "Credential Provider",
+ "description": "An OAuth or service-account connection method available to a workspace."
+ },
+ "ListCredentialProvidersResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/V2CredentialProvider"
+ },
+ "description": "Items in the current page."
+ },
+ "nextCursor": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Always `null` — this list has no `cursor` or `limit` param and returns its whole bounded set in one page. Present so the list can gain pages later without a shape change."
+ }
+ },
+ "required": ["data", "nextCursor"],
+ "additionalProperties": false,
+ "title": "List credential providers response",
+ "description": "OAuth and service-account connection methods.",
+ "examples": [
+ {
+ "data": [
+ {
+ "type": "oauth",
+ "serviceId": "salesforce",
+ "name": "Salesforce",
+ "description": "Connect to Salesforce CRM data and operations.",
+ "providerFamily": "salesforce",
+ "available": true,
+ "supportsReconnect": true,
+ "fields": [],
+ "authorizationOptions": [
+ {
+ "providerId": "salesforce",
+ "label": "Production"
+ },
+ {
+ "providerId": "salesforce-sandbox",
+ "label": "Sandbox"
+ }
+ ]
+ },
+ {
+ "type": "service_account",
+ "serviceId": "zoom-service-account",
+ "providerId": "zoom-service-account",
+ "name": "Zoom server-to-server app",
+ "description": "Connect Zoom with a server-to-server app.",
+ "providerFamily": "zoom",
+ "available": true,
+ "docsUrl": "https://docs.sim.ai/integrations/zoom-service-account",
+ "requiresClientGeneratedCredentialId": false,
+ "fields": [
+ {
+ "id": "clientId",
+ "label": "Client ID",
+ "placeholder": "Paste the client ID",
+ "required": true,
+ "secret": false,
+ "multiline": false
+ },
+ {
+ "id": "clientSecret",
+ "label": "Client secret",
+ "placeholder": "Paste the client secret",
+ "required": true,
+ "secret": true,
+ "multiline": false
+ },
+ {
+ "id": "orgId",
+ "label": "Account ID",
+ "placeholder": "Paste the account ID",
+ "required": true,
+ "secret": false,
+ "multiline": false
+ }
+ ]
+ }
+ ],
+ "nextCursor": null
+ }
+ ]
+ },
+ "CreateServiceAccountCredentialResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2Credential"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Create service-account credential response",
+ "description": "Verified credential metadata without secret material.",
+ "examples": [
+ {
+ "data": {
+ "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
+ "type": "service_account",
+ "displayName": "Zoom service account",
+ "description": null,
+ "providerId": "zoom-service-account",
+ "accountId": null,
+ "hasServiceAccountKey": true,
+ "role": "admin",
+ "createdAt": "2026-06-01T09:14:00.000Z",
+ "updatedAt": "2026-06-20T14:02:11.000Z"
+ }
+ }
+ ]
+ },
+ "CreateServiceAccountCredentialRequest": {
+ "type": "object",
+ "properties": {
+ "workspaceId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 128,
+ "description": "Workspace that will own the credential."
+ },
+ "type": {
+ "type": "string",
+ "const": "service_account",
+ "description": "Service-account credential discriminator."
+ },
+ "providerId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "Exact service-account provider ID returned by provider discovery."
+ },
+ "displayName": {
+ "description": "Optional name; providers may derive one from the verified account identity.",
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255
+ },
+ "description": {
+ "description": "Optional credential description.",
+ "type": "string",
+ "maxLength": 500
+ },
+ "id": {
+ "description": "Required only when provider discovery requests a client-generated ID.",
+ "type": "string",
+ "format": "uuid",
+ "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
+ },
+ "credentials": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 131072,
+ "description": "Write-only JSON object string containing the fields declared by credential-provider discovery.",
+ "writeOnly": true
+ }
+ },
+ "required": ["workspaceId", "type", "providerId", "credentials"],
+ "additionalProperties": false,
+ "title": "Create service-account credential request",
+ "description": "Provider identifier, optional display metadata, and a write-only JSON object string containing the fields declared by provider discovery."
+ },
+ "V2CredentialConnectionAuthorization": {
+ "type": "object",
+ "properties": {
+ "authorizationUrl": {
+ "type": "string",
+ "format": "uri",
+ "description": "Short-lived Sim browser URL that starts the OAuth authorization flow."
+ },
+ "expiresAt": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
+ "description": "ISO 8601 timestamp when the connection link expires."
+ }
+ },
+ "required": ["authorizationUrl", "expiresAt"],
+ "additionalProperties": false,
+ "title": "Credential Connection Authorization",
+ "description": "A short-lived browser entrypoint for an OAuth connection flow."
+ },
+ "CreateCredentialConnectionResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2CredentialConnectionAuthorization"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Create credential connection response",
+ "description": "Short-lived Sim browser entrypoint and its expiry.",
+ "examples": [
+ {
+ "data": {
+ "authorizationUrl": "https://www.sim.ai/api/auth/oauth2/authorize?draftId=draft-123",
+ "expiresAt": "2026-06-20T14:17:11.000Z"
+ }
+ }
+ ]
+ },
+ "CreateCredentialConnectionBody": {
+ "anyOf": [
+ {
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "workspaceId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 128,
+ "description": "Workspace that will own the credential."
+ },
+ "displayName": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "Name shown for the new credential in Sim."
+ },
+ "providerId": {
+ "type": "string",
+ "const": "quickbooks",
+ "description": "QuickBooks OAuth provider ID returned by credential-provider discovery."
+ },
+ "oauthClientConfig": {
+ "type": "object",
+ "properties": {
+ "clientId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "Client ID for the caller-managed Intuit OAuth application."
+ },
+ "clientSecret": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 512,
+ "description": "Write-only client secret for the caller-managed Intuit OAuth application.",
+ "writeOnly": true
+ },
+ "environment": {
+ "type": "string",
+ "enum": ["sandbox", "production"],
+ "description": "Intuit company environment used for authorization and API requests."
+ },
+ "webhookVerifierToken": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 512,
+ "description": "Write-only verifier token for webhook signatures from the caller-managed app.",
+ "writeOnly": true
+ }
+ },
+ "required": ["clientId", "clientSecret", "environment", "webhookVerifierToken"],
+ "additionalProperties": false,
+ "description": "Write-only caller-managed Intuit OAuth app configuration."
+ }
+ },
+ "required": ["workspaceId", "displayName", "providerId", "oauthClientConfig"],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "workspaceId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 128,
+ "description": "Workspace that will own the credential."
+ },
+ "displayName": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "Name shown for the new credential in Sim."
+ },
+ "providerId": {
+ "type": "string",
+ "enum": [
+ "github-repositories",
+ "google-email",
+ "google-drive",
+ "google-docs",
+ "google-sheets",
+ "google-forms",
+ "google-calendar",
+ "google-contacts",
+ "google-ads",
+ "google-bigquery",
+ "google-tasks",
+ "google-vault",
+ "google-groups",
+ "google-chat",
+ "google-meet",
+ "vertex-ai",
+ "microsoft-ad",
+ "microsoft-dataverse",
+ "microsoft-excel",
+ "microsoft-planner",
+ "microsoft-teams",
+ "microsoft-word",
+ "outlook",
+ "onedrive",
+ "sharepoint",
+ "x",
+ "tiktok",
+ "confluence",
+ "jira",
+ "airtable",
+ "bitbucket",
+ "notion",
+ "clickup",
+ "linear",
+ "manageengine-sdp",
+ "monday",
+ "box",
+ "dropbox",
+ "shopify",
+ "slack",
+ "reddit",
+ "wealthbox",
+ "webflow",
+ "trello",
+ "asana",
+ "attio",
+ "calcom",
+ "docusign",
+ "pipedrive",
+ "hubspot",
+ "linkedin",
+ "instagram",
+ "salesforce",
+ "salesforce-sandbox",
+ "zoho-desk",
+ "zoom",
+ "wordpress",
+ "spotify"
+ ],
+ "description": "Exact OAuth provider ID returned by credential-provider discovery."
+ }
+ },
+ "required": ["workspaceId", "displayName", "providerId"],
+ "additionalProperties": false
+ }
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "workspaceId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 128,
+ "description": "Workspace expected to own the credential."
+ },
+ "credentialId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "Existing OAuth credential to reconnect in place. QuickBooks reconnects also require oauthClientConfig with the Intuit client ID, client secret, environment, and webhook verifier token."
+ },
+ "oauthClientConfig": {
+ "description": "Write-only Intuit OAuth app configuration. Required when credentialId identifies a QuickBooks credential; omit it for other providers.",
+ "type": "object",
+ "properties": {
+ "clientId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "Client ID for the caller-managed Intuit OAuth application."
+ },
+ "clientSecret": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 512,
+ "description": "Write-only client secret for the caller-managed Intuit OAuth application.",
+ "writeOnly": true
+ },
+ "environment": {
+ "type": "string",
+ "enum": ["sandbox", "production"],
+ "description": "Intuit company environment used for authorization and API requests."
+ },
+ "webhookVerifierToken": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 512,
+ "description": "Write-only verifier token for webhook signatures from the caller-managed app.",
+ "writeOnly": true
+ }
+ },
+ "required": ["clientId", "clientSecret", "environment", "webhookVerifierToken"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["workspaceId", "credentialId"],
+ "additionalProperties": false
+ }
+ ],
+ "title": "Create credential connection body",
+ "description": "For a new connection, provide providerId and displayName. For a reconnect, provide only credentialId; the existing display name is preserved."
+ },
+ "V2CredentialDeleteData": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Disconnected credential identifier."
},
+ "deleted": {
+ "type": "boolean",
+ "const": true,
+ "description": "Whether the credential was disconnected."
+ }
+ },
+ "required": ["id", "deleted"],
+ "additionalProperties": false,
+ "title": "Delete credential data",
+ "description": "Credential disconnection acknowledgement."
+ },
+ "DeleteCredentialResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2CredentialDeleteData"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Disconnect credential response",
+ "description": "Acknowledgement that the credential was disconnected.",
+ "examples": [
+ {
+ "data": {
+ "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
+ "deleted": true
+ }
+ }
+ ]
+ },
+ "V2SecretWithValue": {
+ "type": "object",
+ "properties": {
"name": {
"type": "string",
"minLength": 1,
- "maxLength": 64,
- "description": "Display name, unique within the workspace; 1 to 64 characters."
+ "maxLength": 255,
+ "pattern": "^[A-Za-z0-9_]+$",
+ "description": "Secret name containing only letters, numbers, and underscores."
},
- "language": {
+ "scope": {
"type": "string",
- "enum": ["javascript", "python"],
- "description": "Dependency ecosystem: `javascript` installs from npm, `python` from PyPI."
+ "enum": ["workspace", "personal"],
+ "description": "Whether the secret belongs to the workspace or to the caller. A personal secret belongs to the caller across every workspace, not to one workspace."
},
- "dependencies": {
- "default": [],
- "description": "Package specifiers installed into the sandbox, one per entry.",
- "maxItems": 1000,
- "type": "array",
- "items": {
- "type": "string",
- "maxLength": 2000
- }
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "What the secret is for, as set on the workspace secret. Always null for a personal secret, which has no shared audience."
},
- "cliTools": {
- "default": [],
- "description": "Pinned managed CLI ids installed into the sandbox, at most 10, no duplicates.",
- "maxItems": 10,
- "type": "array",
- "items": {
- "type": "string",
- "enum": [
- "google-cloud-cli@577.0.0-r1",
- "aws-cli@2.36.15-r1",
- "azure-cli@2.89.0-r1",
- "doctl@1.166.0-r1",
- "github-cli@2.97.0-r1",
- "gitlab-cli@1.111.0-r1",
- "kubectl@1.36.3-r1",
- "helm@4.2.3-r1",
- "kustomize@5.8.1-r1",
- "argocd@3.4.6-r1",
- "terraform@1.15.8-r1",
- "pulumi@3.255.0-r1",
- "supabase-cli@2.111.0-r1",
- "firebase-cli@15.25.1-r1",
- "flyctl@0.4.78-r1",
- "railway-cli@5.30.4-r1",
- "stripe-cli@1.45.0-r1",
- "duckdb@1.5.5-r1",
- "rclone@1.75.0-r1",
- "restic@0.19.1-r1",
- "minio-mc@RELEASE.2025-08-13T08-35-41Z-r1",
- "mongosh@2.9.2-r1",
- "sops@3.13.3-r1",
- "age@1.3.1-r1"
- ]
- }
+ "unredacted": {
+ "type": "boolean",
+ "description": "Whether the workspace secret opts out of redaction, so its value appears in plaintext in run logs and model-visible content. Always false for a personal secret."
},
- "systemPackages": {
- "default": [],
- "description": "Debian packages installed into the sandbox, one per entry.",
- "maxItems": 1000,
+ "role": {
+ "type": "string",
+ "enum": ["admin", "member"],
+ "description": "Caller role for the secret."
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
+ "description": "ISO 8601 timestamp when the secret was created."
+ },
+ "updatedAt": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
+ "description": "ISO 8601 timestamp when the secret was last updated."
+ },
+ "value": {
+ "description": "The stored secret value. Present only when the workspace secret is marked visible (unredacted); omitted for every other secret.",
+ "type": "string"
+ }
+ },
+ "required": [
+ "name",
+ "scope",
+ "description",
+ "unredacted",
+ "role",
+ "createdAt",
+ "updatedAt"
+ ],
+ "additionalProperties": false,
+ "title": "Secret metadata with visible value",
+ "description": "Secret metadata; the stored value is included only for a workspace secret marked visible (unredacted)."
+ },
+ "ListSecretsResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
"type": "array",
"items": {
- "type": "string",
- "maxLength": 2000
- }
+ "$ref": "#/components/schemas/V2SecretWithValue"
+ },
+ "description": "Items in the current page."
+ },
+ "nextCursor": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Opaque cursor for the next page. Send it back as `cursor`; `null` means there is nothing further to fetch. Never construct one yourself."
}
},
- "required": ["workspaceId", "name", "language"],
+ "required": ["data", "nextCursor"],
"additionalProperties": false,
- "title": "Create sandbox request",
- "description": "Name, language, and dependency set of a new sandbox.",
+ "title": "List secrets response",
+ "description": "Secret metadata visible to the caller; visible (unredacted) workspace secrets carry their value.",
"examples": [
{
- "workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
- "name": "data-tools",
- "language": "python",
- "dependencies": ["pandas==2.2.2", "requests"],
- "systemPackages": ["graphviz"]
+ "data": [
+ {
+ "name": "STRIPE_API_KEY",
+ "scope": "workspace",
+ "description": "Production billing key — rotate quarterly.",
+ "unredacted": false,
+ "role": "admin",
+ "createdAt": "2026-06-01T09:14:00.000Z",
+ "updatedAt": "2026-06-20T14:02:11.000Z"
+ },
+ {
+ "name": "STAGING_BASE_URL",
+ "scope": "workspace",
+ "description": "Staging environment base URL.",
+ "unredacted": true,
+ "role": "member",
+ "createdAt": "2026-06-03T11:30:00.000Z",
+ "updatedAt": "2026-06-21T08:45:09.000Z",
+ "value": "https://staging.example.com"
+ }
+ ],
+ "nextCursor": null
}
]
},
- "GetSandboxResponse": {
+ "V2Secret": {
"type": "object",
"properties": {
- "data": {
- "description": "Response data.",
- "$ref": "#/components/schemas/V2Sandbox"
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "pattern": "^[A-Za-z0-9_]+$",
+ "description": "Secret name containing only letters, numbers, and underscores."
+ },
+ "scope": {
+ "type": "string",
+ "enum": ["workspace", "personal"],
+ "description": "Whether the secret belongs to the workspace or to the caller. A personal secret belongs to the caller across every workspace, not to one workspace."
+ },
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "What the secret is for, as set on the workspace secret. Always null for a personal secret, which has no shared audience."
+ },
+ "unredacted": {
+ "type": "boolean",
+ "description": "Whether the workspace secret opts out of redaction, so its value appears in plaintext in run logs and model-visible content. Always false for a personal secret."
+ },
+ "role": {
+ "type": "string",
+ "enum": ["admin", "member"],
+ "description": "Caller role for the secret."
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
+ "description": "ISO 8601 timestamp when the secret was created."
+ },
+ "updatedAt": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
+ "description": "ISO 8601 timestamp when the secret was last updated."
}
},
- "required": ["data"],
+ "required": [
+ "name",
+ "scope",
+ "description",
+ "unredacted",
+ "role",
+ "createdAt",
+ "updatedAt"
+ ],
"additionalProperties": false,
- "title": "Get sandbox response",
- "description": "One sandbox.",
- "examples": [
- {
- "data": {
- "id": "V1StGXR8Z5jdHi6BmyT",
- "name": "data-tools",
- "language": "python",
- "dependencies": ["pandas==2.2.2", "requests"],
- "cliTools": [],
- "systemPackages": ["graphviz"],
- "buildStatus": "ready",
- "errorCode": null,
- "errorMessage": null,
- "errorDetail": null,
- "builtAt": "2026-06-20T14:05:40.000Z",
- "createdAt": "2026-06-01T09:14:00.000Z",
- "updatedAt": "2026-06-20T14:02:11.000Z"
- }
- }
- ]
+ "title": "Secret metadata",
+ "description": "Public secret metadata without the stored secret value."
},
- "UpdateSandboxResponse": {
+ "SetSecretResponse": {
"type": "object",
"properties": {
"data": {
"description": "Response data.",
- "$ref": "#/components/schemas/V2Sandbox"
+ "$ref": "#/components/schemas/V2Secret"
}
},
"required": ["data"],
"additionalProperties": false,
- "title": "Update sandbox response",
- "description": "The updated sandbox. `buildStatus` is `pending` while an image rebuilds and `null` where nothing is built.",
+ "title": "Set secret response",
+ "description": "Metadata for the created or replaced secret without its value.",
"examples": [
{
"data": {
- "id": "V1StGXR8Z5jdHi6BmyT",
- "name": "data-tools",
- "language": "python",
- "dependencies": ["pandas==2.2.2", "requests", "pyarrow"],
- "cliTools": [],
- "systemPackages": ["graphviz"],
- "buildStatus": "pending",
- "errorCode": null,
- "errorMessage": null,
- "errorDetail": null,
- "builtAt": null,
+ "name": "STRIPE_API_KEY",
+ "scope": "workspace",
+ "description": "Production billing key — rotate quarterly.",
+ "unredacted": false,
+ "role": "admin",
"createdAt": "2026-06-01T09:14:00.000Z",
"updatedAt": "2026-06-20T14:02:11.000Z"
}
}
]
},
- "UpdateSandboxRequest": {
+ "SetSecretRequest": {
"type": "object",
"properties": {
"workspaceId": {
"type": "string",
"minLength": 1,
"maxLength": 128,
- "description": "Workspace that owns the sandbox."
+ "description": "Workspace the request is authorized against. A workspace secret is written to it; a personal secret is written to the caller and is available in all of their workspaces."
},
- "name": {
- "description": "New display name, unique within the workspace; 1 to 64 characters.",
+ "scope": {
"type": "string",
- "minLength": 1,
- "maxLength": 64
+ "enum": ["workspace", "personal"],
+ "description": "Whether the secret belongs to the workspace or to the caller. A personal secret belongs to the caller across every workspace, not to one workspace."
},
- "language": {
- "description": "Replacement dependency ecosystem. The whole spec is revalidated against it, so a Python dependency list does not survive a switch to JavaScript.",
+ "value": {
+ "description": "Write-only secret value. It is never returned. Omit it on a workspace secret to change description or unredacted alone, leaving the stored value untouched; the secret must already exist. Always required for a personal secret, which carries no other writable field.",
+ "writeOnly": true,
"type": "string",
- "enum": ["javascript", "python"]
- },
- "dependencies": {
- "description": "Replacement package list; replaces the whole list.",
- "maxItems": 1000,
- "type": "array",
- "items": {
- "type": "string",
- "maxLength": 2000
- }
+ "minLength": 1,
+ "maxLength": 65536
},
- "cliTools": {
- "description": "Replacement managed CLI list; replaces the whole list.",
- "maxItems": 10,
- "type": "array",
- "items": {
- "type": "string",
- "enum": [
- "google-cloud-cli@577.0.0-r1",
- "aws-cli@2.36.15-r1",
- "azure-cli@2.89.0-r1",
- "doctl@1.166.0-r1",
- "github-cli@2.97.0-r1",
- "gitlab-cli@1.111.0-r1",
- "kubectl@1.36.3-r1",
- "helm@4.2.3-r1",
- "kustomize@5.8.1-r1",
- "argocd@3.4.6-r1",
- "terraform@1.15.8-r1",
- "pulumi@3.255.0-r1",
- "supabase-cli@2.111.0-r1",
- "firebase-cli@15.25.1-r1",
- "flyctl@0.4.78-r1",
- "railway-cli@5.30.4-r1",
- "stripe-cli@1.45.0-r1",
- "duckdb@1.5.5-r1",
- "rclone@1.75.0-r1",
- "restic@0.19.1-r1",
- "minio-mc@RELEASE.2025-08-13T08-35-41Z-r1",
- "mongosh@2.9.2-r1",
- "sops@3.13.3-r1",
- "age@1.3.1-r1"
- ]
- }
+ "description": {
+ "description": "What the secret is for, shown to teammates. Workspace scope only — sending it for a personal secret is rejected. Omit it to leave an existing description untouched; send null or an empty string to clear one.",
+ "anyOf": [
+ {
+ "type": "string",
+ "maxLength": 500
+ },
+ {
+ "type": "null"
+ }
+ ]
},
- "systemPackages": {
- "description": "Replacement Debian package list; replaces the whole list.",
- "maxItems": 1000,
- "type": "array",
- "items": {
- "type": "string",
- "maxLength": 2000
- }
+ "unredacted": {
+ "description": "Opt the workspace secret out of redaction: its value then appears in plaintext in run logs, model-visible content, and files, including publicly shared log links. Workspace scope only — sending it for a personal secret is rejected. Omit it to leave the current setting untouched.",
+ "type": "boolean"
}
},
- "required": ["workspaceId"],
+ "required": ["workspaceId", "scope"],
"additionalProperties": false,
- "title": "Update sandbox request",
- "description": "Sandbox fields to change; at least one editable field is required.",
+ "title": "Set secret request",
+ "description": "Ownership scope and write-only value for the secret. A workspace secret may instead send description or unredacted alone, without a value.",
"examples": [
{
"workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
- "dependencies": ["pandas==2.2.2", "requests", "pyarrow"]
+ "scope": "workspace",
+ "value": "YOUR_SECRET_VALUE"
+ },
+ {
+ "workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
+ "scope": "workspace",
+ "unredacted": false
}
]
},
- "V2SandboxDeleteData": {
+ "V2SecretDeleteData": {
"type": "object",
"properties": {
- "id": {
+ "name": {
"type": "string",
- "description": "Identifier of the deleted sandbox."
+ "minLength": 1,
+ "maxLength": 255,
+ "pattern": "^[A-Za-z0-9_]+$",
+ "description": "Secret name containing only letters, numbers, and underscores."
+ },
+ "scope": {
+ "type": "string",
+ "enum": ["workspace", "personal"],
+ "description": "Whether the secret belongs to the workspace or to the caller. A personal secret belongs to the caller across every workspace, not to one workspace."
},
"deleted": {
"type": "boolean",
"const": true,
- "description": "Whether the sandbox was deleted."
+ "description": "Whether the secret was deleted."
}
},
- "required": ["id", "deleted"],
+ "required": ["name", "scope", "deleted"],
"additionalProperties": false,
- "title": "Delete sandbox data",
- "description": "Sandbox deletion acknowledgement."
+ "title": "Delete secret data",
+ "description": "Secret deletion acknowledgement without the stored value."
},
- "DeleteSandboxResponse": {
+ "DeleteSecretResponse": {
"type": "object",
"properties": {
"data": {
"description": "Response data.",
- "$ref": "#/components/schemas/V2SandboxDeleteData"
+ "$ref": "#/components/schemas/V2SecretDeleteData"
}
},
"required": ["data"],
"additionalProperties": false,
- "title": "Delete sandbox response",
- "description": "Acknowledgement that the sandbox was deleted.",
+ "title": "Delete secret response",
+ "description": "Acknowledgement that the secret was deleted.",
"examples": [
{
"data": {
- "id": "V1StGXR8Z5jdHi6BmyT",
+ "name": "STRIPE_API_KEY",
+ "scope": "workspace",
"deleted": true
}
}
]
},
- "V2Credential": {
+ "V2Meta": {
"type": "object",
"properties": {
- "id": {
- "type": "string",
- "description": "Unique credential identifier."
- },
- "type": {
- "type": "string",
- "enum": ["oauth", "service_account"],
- "description": "Authenticated connection type."
+ "v2Enabled": {
+ "type": "boolean",
+ "description": "Whether this API version is available. This is true when the endpoint is served."
},
- "displayName": {
+ "keyType": {
"type": "string",
- "description": "Credential display name."
+ "enum": ["personal", "workspace", "oauth_access_token"],
+ "description": "Whether the calling credential is a personal API key carrying the full authority of its owner across their workspaces, a key scoped to one workspace, or an OAuth access token acting for its user within the scopes it was granted."
},
- "description": {
+ "expiresAt": {
"anyOf": [
{
- "type": "string"
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
},
{
"type": "null"
}
],
- "description": "Optional credential description."
+ "description": "ISO 8601 timestamp when the calling credential expires, or null when it does not."
+ }
+ },
+ "required": ["v2Enabled", "keyType", "expiresAt"],
+ "additionalProperties": false,
+ "title": "API capabilities",
+ "description": "API availability and lifecycle facts about the calling credential."
+ },
+ "GetApiMetaResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2Meta"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "API capabilities response",
+ "description": "API availability, credential type, and expiry for the caller.",
+ "examples": [
+ {
+ "data": {
+ "v2Enabled": true,
+ "keyType": "personal",
+ "expiresAt": null
+ }
+ }
+ ]
+ },
+ "WorkflowMcpServerListItem": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique workflow-MCP server identifier."
},
- "providerId": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Integration provider authenticated by this credential."
+ "name": {
+ "type": "string",
+ "description": "Server display name, shown to connecting MCP clients."
},
- "accountId": {
+ "description": {
"anyOf": [
{
"type": "string"
@@ -7508,53 +10728,63 @@
"type": "null"
}
],
- "description": "Linked account identifier for OAuth credentials."
+ "description": "Optional server description, or null when unset."
},
- "hasServiceAccountKey": {
+ "isPublic": {
"type": "boolean",
- "description": "Whether a service-account payload is stored. Its contents are never returned."
+ "description": "Whether the server answers MCP clients without a Sim API key."
},
- "role": {
+ "mcpServerUrl": {
"type": "string",
- "enum": ["admin", "member"],
- "description": "Caller role for the credential."
+ "description": "Endpoint an MCP client connects to. Published here so callers never build it.",
+ "examples": ["https://www.sim.ai/api/mcp/serve/wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2"]
},
"createdAt": {
"type": "string",
- "format": "date-time",
- "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
- "description": "ISO 8601 timestamp when the credential was created."
+ "description": "ISO 8601 timestamp when the server was created.",
+ "format": "date-time"
},
"updatedAt": {
"type": "string",
- "format": "date-time",
- "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
- "description": "ISO 8601 timestamp when the credential was last updated."
+ "description": "ISO 8601 timestamp when the server was last modified.",
+ "format": "date-time"
+ },
+ "toolCount": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 9007199254740991,
+ "description": "Number of workflows published as tools."
+ },
+ "toolNames": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Tool names this server publishes, alphabetically ordered."
}
},
"required": [
"id",
- "type",
- "displayName",
+ "name",
"description",
- "providerId",
- "accountId",
- "hasServiceAccountKey",
- "role",
+ "isPublic",
+ "mcpServerUrl",
"createdAt",
- "updatedAt"
+ "updatedAt",
+ "toolCount",
+ "toolNames"
],
"additionalProperties": false,
- "title": "Credential",
- "description": "Public authenticated-connection metadata without secret material."
+ "title": "Workflow MCP server list item",
+ "description": "A published MCP server together with the tool names it exposes."
},
- "ListCredentialsResponse": {
+ "ListWorkflowMcpServersResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
- "$ref": "#/components/schemas/V2Credential"
+ "$ref": "#/components/schemas/WorkflowMcpServerListItem"
},
"description": "Items in the current page."
},
@@ -7568,827 +10798,424 @@
}
],
"description": "Opaque cursor for the next page. Send it back as `cursor`; `null` means there is nothing further to fetch. Never construct one yourself."
+ },
+ "toolNamesTruncated": {
+ "type": "boolean",
+ "description": "Whether the page-wide tool-name limit left some inventories incomplete. Use List Workflow MCP Tools for one server and check its `truncated` flag before treating the inventory as complete. `nextCursor` paginates servers, not tool names."
}
},
- "required": ["data", "nextCursor"],
+ "required": ["data", "nextCursor", "toolNamesTruncated"],
"additionalProperties": false,
- "title": "List credentials response",
- "description": "Credential metadata visible to the caller.",
+ "title": "List workflow MCP servers response",
+ "description": "A cursor-paginated page of published MCP servers.",
"examples": [
{
"data": [
{
- "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
- "type": "service_account",
- "displayName": "Zoom service account",
- "description": null,
- "providerId": "zoom-service-account",
- "accountId": null,
- "hasServiceAccountKey": true,
- "role": "admin",
- "createdAt": "2026-06-01T09:14:00.000Z",
- "updatedAt": "2026-06-20T14:02:11.000Z"
- }
- ],
- "nextCursor": null
- }
- ]
- },
- "V2CredentialProvider": {
- "oneOf": [
- {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "const": "oauth",
- "description": "Browser-based OAuth connection method."
- },
- "serviceId": {
- "type": "string",
- "minLength": 1,
- "maxLength": 255,
- "description": "Stable credential-provider identifier."
- },
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 255,
- "description": "Credential provider display name."
- },
- "description": {
- "type": "string",
- "minLength": 1,
- "maxLength": 1000,
- "description": "Credential provider description."
- },
- "providerFamily": {
- "type": "string",
- "minLength": 1,
- "maxLength": 255,
- "description": "Owning provider family identifier."
- },
- "available": {
- "type": "boolean",
- "description": "Whether this caller can connect the provider in the current deployment."
- },
- "supportsReconnect": {
- "type": "boolean",
- "description": "Whether existing credentials for this service can be reconnected."
- },
- "authorizationOptions": {
- "minItems": 1,
- "maxItems": 10,
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "providerId": {
- "type": "string",
- "minLength": 1,
- "maxLength": 255,
- "description": "Exact OAuth provider identifier accepted by the connection endpoint."
- },
- "label": {
- "type": "string",
- "minLength": 1,
- "maxLength": 255,
- "description": "Human-readable authorization-server label."
- }
- },
- "required": ["providerId", "label"],
- "additionalProperties": false
- },
- "description": "Authorization servers available for this OAuth service."
- },
- "fields": {
- "maxItems": 20,
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string",
- "minLength": 1,
- "maxLength": 255,
- "description": "Exact create-body field name."
- },
- "label": {
- "type": "string",
- "minLength": 1,
- "maxLength": 255,
- "description": "Human-readable field label."
- },
- "placeholder": {
- "type": "string",
- "minLength": 1,
- "maxLength": 1000,
- "description": "Suggested input placeholder."
- },
- "required": {
- "type": "boolean",
- "description": "Whether the field is required for the selected flow."
- },
- "secret": {
- "type": "boolean",
- "description": "Whether the submitted field is write-only secret material."
- },
- "multiline": {
- "type": "boolean",
- "description": "Whether the field is intended for multi-line input."
- },
- "requiredForAuthMethods": {
- "description": "Authentication methods for which this field is required.",
- "minItems": 1,
- "maxItems": 10,
- "type": "array",
- "items": {
- "type": "string",
- "minLength": 1,
- "maxLength": 64
- }
- },
- "options": {
- "description": "Fixed values accepted by a selector field.",
- "minItems": 1,
- "maxItems": 20,
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "value": {
- "type": "string",
- "minLength": 1,
- "maxLength": 255,
- "description": "Submitted option value."
- },
- "label": {
- "type": "string",
- "minLength": 1,
- "maxLength": 255,
- "description": "Human-readable option label."
- }
- },
- "required": ["value", "label"],
- "additionalProperties": false
- }
- },
- "hint": {
- "description": "Provider-specific setup guidance.",
- "type": "string",
- "minLength": 1,
- "maxLength": 2000
- }
- },
- "required": ["id", "label", "placeholder", "required", "secret", "multiline"],
- "additionalProperties": false
- },
- "description": "Write-only setup fields required before starting this OAuth flow."
+ "id": "wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2",
+ "name": "Support agents",
+ "description": "Ticket triage and escalation workflows.",
+ "isPublic": false,
+ "mcpServerUrl": "https://www.sim.ai/api/mcp/serve/wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2",
+ "createdAt": "2026-06-12T10:30:00.000Z",
+ "updatedAt": "2026-06-12T10:30:00.000Z",
+ "toolCount": 1,
+ "toolNames": ["triage_ticket"]
}
- },
- "required": [
- "type",
- "serviceId",
- "name",
- "description",
- "providerFamily",
- "available",
- "supportsReconnect",
- "authorizationOptions",
- "fields"
],
- "additionalProperties": false
+ "nextCursor": null,
+ "toolNamesTruncated": false
+ }
+ ]
+ },
+ "WorkflowMcpServer": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique workflow-MCP server identifier."
},
- {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "const": "service_account",
- "description": "Direct service-account credential method."
- },
- "serviceId": {
- "type": "string",
- "minLength": 1,
- "maxLength": 255,
- "description": "Stable credential-provider identifier."
- },
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 255,
- "description": "Credential provider display name."
- },
- "description": {
- "type": "string",
- "minLength": 1,
- "maxLength": 1000,
- "description": "Credential provider description."
- },
- "providerFamily": {
- "type": "string",
- "minLength": 1,
- "maxLength": 255,
- "description": "Owning provider family identifier."
- },
- "available": {
- "type": "boolean",
- "description": "Whether this caller can connect the provider in the current deployment."
- },
- "providerId": {
- "type": "string",
- "minLength": 1,
- "maxLength": 255,
- "description": "Exact service-account provider ID accepted by credential creation."
- },
- "docsUrl": {
- "type": "string",
- "format": "uri",
- "description": "Setup guide for the provider."
- },
- "helpText": {
- "description": "Provider-specific setup guidance.",
- "type": "string",
- "minLength": 1,
- "maxLength": 2000
- },
- "requiresClientGeneratedCredentialId": {
- "type": "boolean",
- "description": "Whether the caller must generate and submit the credential ID before setup."
+ "name": {
+ "type": "string",
+ "description": "Server display name, shown to connecting MCP clients."
+ },
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
},
- "fields": {
- "minItems": 1,
- "maxItems": 20,
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string",
- "minLength": 1,
- "maxLength": 255,
- "description": "Exact create-body field name."
- },
- "label": {
- "type": "string",
- "minLength": 1,
- "maxLength": 255,
- "description": "Human-readable field label."
- },
- "placeholder": {
- "type": "string",
- "minLength": 1,
- "maxLength": 1000,
- "description": "Suggested input placeholder."
- },
- "required": {
- "type": "boolean",
- "description": "Whether the field is required for the selected flow."
- },
- "secret": {
- "type": "boolean",
- "description": "Whether the submitted field is write-only secret material."
- },
- "multiline": {
- "type": "boolean",
- "description": "Whether the field is intended for multi-line input."
- },
- "requiredForAuthMethods": {
- "description": "Authentication methods for which this field is required.",
- "minItems": 1,
- "maxItems": 10,
- "type": "array",
- "items": {
- "type": "string",
- "minLength": 1,
- "maxLength": 64
- }
- },
- "options": {
- "description": "Fixed values accepted by a selector field.",
- "minItems": 1,
- "maxItems": 20,
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "value": {
- "type": "string",
- "minLength": 1,
- "maxLength": 255,
- "description": "Submitted option value."
- },
- "label": {
- "type": "string",
- "minLength": 1,
- "maxLength": 255,
- "description": "Human-readable option label."
- }
- },
- "required": ["value", "label"],
- "additionalProperties": false
- }
- },
- "hint": {
- "description": "Provider-specific setup guidance.",
- "type": "string",
- "minLength": 1,
- "maxLength": 2000
- }
- },
- "required": ["id", "label", "placeholder", "required", "secret", "multiline"],
- "additionalProperties": false
- },
- "description": "Create-body fields accepted by this provider. Secret fields are write-only."
+ {
+ "type": "null"
}
- },
- "required": [
- "type",
- "serviceId",
- "name",
- "description",
- "providerFamily",
- "available",
- "providerId",
- "docsUrl",
- "requiresClientGeneratedCredentialId",
- "fields"
],
- "additionalProperties": false
+ "description": "Optional server description, or null when unset."
+ },
+ "isPublic": {
+ "type": "boolean",
+ "description": "Whether the server answers MCP clients without a Sim API key."
+ },
+ "mcpServerUrl": {
+ "type": "string",
+ "description": "Endpoint an MCP client connects to. Published here so callers never build it.",
+ "examples": ["https://www.sim.ai/api/mcp/serve/wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2"]
+ },
+ "createdAt": {
+ "type": "string",
+ "description": "ISO 8601 timestamp when the server was created.",
+ "format": "date-time"
+ },
+ "updatedAt": {
+ "type": "string",
+ "description": "ISO 8601 timestamp when the server was last modified.",
+ "format": "date-time"
}
+ },
+ "required": [
+ "id",
+ "name",
+ "description",
+ "isPublic",
+ "mcpServerUrl",
+ "createdAt",
+ "updatedAt"
],
- "title": "Credential Provider",
- "description": "An OAuth or service-account connection method available to a workspace."
+ "additionalProperties": false,
+ "title": "Workflow MCP server",
+ "description": "A workspace-published MCP server exposing deployed workflows as tools."
},
- "ListCredentialProvidersResponse": {
+ "CreateWorkflowMcpServerResponse": {
"type": "object",
"properties": {
"data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/WorkflowMcpServer"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Create workflow MCP server response",
+ "description": "The published MCP server.",
+ "examples": [
+ {
+ "data": {
+ "id": "wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2",
+ "name": "Support agents",
+ "description": "Ticket triage and escalation workflows.",
+ "isPublic": false,
+ "mcpServerUrl": "https://www.sim.ai/api/mcp/serve/wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2",
+ "createdAt": "2026-06-12T10:30:00.000Z",
+ "updatedAt": "2026-06-12T10:30:00.000Z"
+ }
+ }
+ ]
+ },
+ "CreateWorkflowMcpServerRequest": {
+ "type": "object",
+ "properties": {
+ "workspaceId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 128,
+ "description": "Workspace in which to publish the server."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "Server display name, shown to connecting MCP clients."
+ },
+ "description": {
+ "description": "Optional server description.",
+ "type": "string",
+ "maxLength": 2000
+ },
+ "isPublic": {
+ "description": "Whether the server answers MCP clients without a Sim API key. Defaults to false — a public server executes the workflows it publishes for anyone holding its URL.",
+ "default": false,
+ "type": "boolean"
+ },
+ "workflowIds": {
+ "description": "Deployed workflows to publish as tools on the new server.",
+ "maxItems": 100,
"type": "array",
"items": {
- "$ref": "#/components/schemas/V2CredentialProvider"
- },
- "description": "Items in the current page."
- },
- "nextCursor": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Always `null` — this list has no `cursor` or `limit` param and returns its whole bounded set in one page. Present so the list can gain pages later without a shape change."
+ "type": "string",
+ "minLength": 1
+ }
}
},
- "required": ["data", "nextCursor"],
+ "required": ["workspaceId", "name"],
"additionalProperties": false,
- "title": "List credential providers response",
- "description": "OAuth and service-account connection methods.",
+ "title": "Create workflow MCP server request",
+ "description": "A new workspace-published MCP server and the workflows it exposes.",
"examples": [
{
- "data": [
- {
- "type": "oauth",
- "serviceId": "salesforce",
- "name": "Salesforce",
- "description": "Connect to Salesforce CRM data and operations.",
- "providerFamily": "salesforce",
- "available": true,
- "supportsReconnect": true,
- "fields": [],
- "authorizationOptions": [
- {
- "providerId": "salesforce",
- "label": "Production"
- },
- {
- "providerId": "salesforce-sandbox",
- "label": "Sandbox"
- }
- ]
- },
- {
- "type": "service_account",
- "serviceId": "zoom-service-account",
- "providerId": "zoom-service-account",
- "name": "Zoom server-to-server app",
- "description": "Connect Zoom with a server-to-server app.",
- "providerFamily": "zoom",
- "available": true,
- "docsUrl": "https://docs.sim.ai/integrations/zoom-service-account",
- "requiresClientGeneratedCredentialId": false,
- "fields": [
- {
- "id": "clientId",
- "label": "Client ID",
- "placeholder": "Paste the client ID",
- "required": true,
- "secret": false,
- "multiline": false
- },
- {
- "id": "clientSecret",
- "label": "Client secret",
- "placeholder": "Paste the client secret",
- "required": true,
- "secret": true,
- "multiline": false
- },
- {
- "id": "orgId",
- "label": "Account ID",
- "placeholder": "Paste the account ID",
- "required": true,
- "secret": false,
- "multiline": false
- }
- ]
- }
- ],
- "nextCursor": null
+ "workspaceId": "9f4c2a10-3b7e-4d58-8f6a-2c1d0e5b7a94",
+ "name": "Support agents",
+ "workflowIds": ["3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36"]
}
]
},
- "CreateServiceAccountCredentialResponse": {
+ "GetWorkflowMcpServerResponse": {
"type": "object",
"properties": {
"data": {
"description": "Response data.",
- "$ref": "#/components/schemas/V2Credential"
+ "$ref": "#/components/schemas/WorkflowMcpServer"
}
},
"required": ["data"],
"additionalProperties": false,
- "title": "Create service-account credential response",
- "description": "Verified credential metadata without secret material.",
+ "title": "Get workflow MCP server response",
+ "description": "A single published MCP server.",
"examples": [
{
"data": {
- "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
- "type": "service_account",
- "displayName": "Zoom service account",
- "description": null,
- "providerId": "zoom-service-account",
- "accountId": null,
- "hasServiceAccountKey": true,
- "role": "admin",
- "createdAt": "2026-06-01T09:14:00.000Z",
- "updatedAt": "2026-06-20T14:02:11.000Z"
+ "id": "wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2",
+ "name": "Support agents",
+ "description": "Ticket triage and escalation workflows.",
+ "isPublic": false,
+ "mcpServerUrl": "https://www.sim.ai/api/mcp/serve/wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2",
+ "createdAt": "2026-06-12T10:30:00.000Z",
+ "updatedAt": "2026-06-12T10:30:00.000Z"
}
}
]
},
- "CreateServiceAccountCredentialRequest": {
+ "WorkflowMcpToolListItem": {
"type": "object",
"properties": {
- "workspaceId": {
+ "id": {
"type": "string",
- "minLength": 1,
- "maxLength": 128,
- "description": "Workspace that will own the credential."
+ "description": "Unique tool identifier."
},
- "type": {
+ "serverId": {
"type": "string",
- "const": "service_account",
- "description": "Service-account credential discriminator."
+ "description": "Server that publishes this tool."
},
- "providerId": {
+ "workflowId": {
"type": "string",
- "minLength": 1,
- "maxLength": 255,
- "description": "Exact service-account provider ID returned by provider discovery."
+ "description": "Workflow this tool executes."
},
- "displayName": {
- "description": "Optional name; providers may derive one from the verified account identity.",
+ "toolName": {
"type": "string",
- "minLength": 1,
- "maxLength": 255
+ "description": "Name an MCP client calls. Derived from the supplied name or the workflow name, normalized to the MCP tool-name grammar."
},
- "description": {
- "description": "Optional credential description.",
+ "toolDescription": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Description shown to MCP clients."
+ },
+ "mcpServerUrl": {
"type": "string",
- "maxLength": 500
+ "description": "Endpoint an MCP client connects to."
},
- "id": {
- "description": "Required only when provider discovery requests a client-generated ID.",
+ "apiEndpoint": {
"type": "string",
- "format": "uuid",
- "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
+ "description": "Sim execution endpoint this tool calls through."
},
- "credentials": {
+ "createdAt": {
"type": "string",
- "minLength": 1,
- "maxLength": 131072,
- "description": "Write-only JSON object string containing the fields declared by credential-provider discovery.",
- "writeOnly": true
+ "description": "ISO 8601 timestamp when the tool was created.",
+ "format": "date-time"
+ },
+ "updatedAt": {
+ "type": "string",
+ "description": "ISO 8601 timestamp when the tool was last modified.",
+ "format": "date-time"
}
},
- "required": ["workspaceId", "type", "providerId", "credentials"],
+ "required": [
+ "id",
+ "serverId",
+ "workflowId",
+ "toolName",
+ "toolDescription",
+ "mcpServerUrl",
+ "apiEndpoint",
+ "createdAt",
+ "updatedAt"
+ ],
"additionalProperties": false,
- "title": "Create service-account credential request",
- "description": "Provider identifier, optional display metadata, and a write-only JSON object string containing the fields declared by provider discovery."
+ "title": "Workflow MCP tool list item",
+ "description": "A tool a server publishes, as returned by a read."
},
- "V2CredentialConnectionAuthorization": {
+ "ListWorkflowMcpToolsResponse": {
"type": "object",
"properties": {
- "authorizationUrl": {
- "type": "string",
- "format": "uri",
- "description": "Short-lived Sim browser URL that starts the OAuth authorization flow."
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/WorkflowMcpToolListItem"
+ },
+ "description": "Items in the current page."
},
- "expiresAt": {
- "type": "string",
- "format": "date-time",
- "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
- "description": "ISO 8601 timestamp when the connection link expires."
+ "nextCursor": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Always `null` — this list has no `cursor` or `limit` param and returns its whole bounded set in one page. Present so the list can gain pages later without a shape change."
+ },
+ "truncated": {
+ "type": "boolean",
+ "description": "Whether the tool limit left this inventory incomplete. The list is unpaginated and `nextCursor` remains null even when truncated. Do not treat a truncated inventory as the complete set of published tools."
}
},
- "required": ["authorizationUrl", "expiresAt"],
+ "required": ["data", "nextCursor", "truncated"],
"additionalProperties": false,
- "title": "Credential Connection Authorization",
- "description": "A short-lived browser entrypoint for an OAuth connection flow."
+ "title": "List workflow MCP tools response",
+ "description": "The tools a published MCP server exposes.",
+ "examples": [
+ {
+ "data": [
+ {
+ "id": "wfmcptool_01J8ZK3QW4M6X2R9T7B5C0V3",
+ "serverId": "wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2",
+ "workflowId": "3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36",
+ "toolName": "triage_ticket",
+ "toolDescription": "Execute Ticket triage workflow",
+ "mcpServerUrl": "https://www.sim.ai/api/mcp/serve/wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2",
+ "apiEndpoint": "https://www.sim.ai/api/v2/workflows/3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36/execute",
+ "createdAt": "2026-06-12T10:30:00.000Z",
+ "updatedAt": "2026-06-12T10:30:00.000Z"
+ }
+ ],
+ "nextCursor": null,
+ "truncated": false
+ }
+ ]
},
- "CreateCredentialConnectionResponse": {
+ "UpdateWorkflowMcpServerResponse": {
"type": "object",
"properties": {
"data": {
"description": "Response data.",
- "$ref": "#/components/schemas/V2CredentialConnectionAuthorization"
+ "$ref": "#/components/schemas/WorkflowMcpServer"
}
},
"required": ["data"],
"additionalProperties": false,
- "title": "Create credential connection response",
- "description": "Short-lived Sim browser entrypoint and its expiry.",
+ "title": "Update workflow MCP server response",
+ "description": "The updated MCP server.",
"examples": [
{
"data": {
- "authorizationUrl": "https://www.sim.ai/api/auth/oauth2/authorize?draftId=draft-123",
- "expiresAt": "2026-06-20T14:17:11.000Z"
+ "id": "wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2",
+ "name": "Support agents",
+ "description": "Ticket triage and escalation workflows.",
+ "isPublic": true,
+ "mcpServerUrl": "https://www.sim.ai/api/mcp/serve/wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2",
+ "createdAt": "2026-06-12T10:30:00.000Z",
+ "updatedAt": "2026-06-12T10:30:00.000Z"
}
}
]
},
- "CreateCredentialConnectionBody": {
- "anyOf": [
- {
+ "UpdateWorkflowMcpServerRequest": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "Server display name, shown to connecting MCP clients."
+ },
+ "description": {
+ "description": "New server description, or null to clear it.",
"anyOf": [
{
- "type": "object",
- "properties": {
- "workspaceId": {
- "type": "string",
- "minLength": 1,
- "maxLength": 128,
- "description": "Workspace that will own the credential."
- },
- "displayName": {
- "type": "string",
- "minLength": 1,
- "maxLength": 255,
- "description": "Name shown for the new credential in Sim."
- },
- "providerId": {
- "type": "string",
- "const": "quickbooks",
- "description": "QuickBooks OAuth provider ID returned by credential-provider discovery."
- },
- "oauthClientConfig": {
- "type": "object",
- "properties": {
- "clientId": {
- "type": "string",
- "minLength": 1,
- "maxLength": 255,
- "description": "Client ID for the caller-managed Intuit OAuth application."
- },
- "clientSecret": {
- "type": "string",
- "minLength": 1,
- "maxLength": 512,
- "description": "Write-only client secret for the caller-managed Intuit OAuth application.",
- "writeOnly": true
- },
- "environment": {
- "type": "string",
- "enum": ["sandbox", "production"],
- "description": "Intuit company environment used for authorization and API requests."
- },
- "webhookVerifierToken": {
- "type": "string",
- "minLength": 1,
- "maxLength": 512,
- "description": "Write-only verifier token for webhook signatures from the caller-managed app.",
- "writeOnly": true
- }
- },
- "required": ["clientId", "clientSecret", "environment", "webhookVerifierToken"],
- "additionalProperties": false,
- "description": "Write-only caller-managed Intuit OAuth app configuration."
- }
- },
- "required": ["workspaceId", "displayName", "providerId", "oauthClientConfig"],
- "additionalProperties": false
+ "type": "string",
+ "maxLength": 2000
},
{
- "type": "object",
- "properties": {
- "workspaceId": {
- "type": "string",
- "minLength": 1,
- "maxLength": 128,
- "description": "Workspace that will own the credential."
- },
- "displayName": {
- "type": "string",
- "minLength": 1,
- "maxLength": 255,
- "description": "Name shown for the new credential in Sim."
- },
- "providerId": {
- "type": "string",
- "enum": [
- "github-repositories",
- "google-email",
- "google-drive",
- "google-docs",
- "google-sheets",
- "google-forms",
- "google-calendar",
- "google-contacts",
- "google-ads",
- "google-bigquery",
- "google-tasks",
- "google-vault",
- "google-groups",
- "google-chat",
- "google-meet",
- "vertex-ai",
- "microsoft-ad",
- "microsoft-dataverse",
- "microsoft-excel",
- "microsoft-planner",
- "microsoft-teams",
- "microsoft-word",
- "outlook",
- "onedrive",
- "sharepoint",
- "x",
- "tiktok",
- "confluence",
- "jira",
- "airtable",
- "bitbucket",
- "notion",
- "clickup",
- "linear",
- "manageengine-sdp",
- "monday",
- "box",
- "dropbox",
- "shopify",
- "slack",
- "reddit",
- "wealthbox",
- "webflow",
- "trello",
- "asana",
- "attio",
- "calcom",
- "docusign",
- "pipedrive",
- "hubspot",
- "linkedin",
- "instagram",
- "salesforce",
- "salesforce-sandbox",
- "zoho-desk",
- "zoom",
- "wordpress",
- "spotify"
- ],
- "description": "Exact OAuth provider ID returned by credential-provider discovery."
- }
- },
- "required": ["workspaceId", "displayName", "providerId"],
- "additionalProperties": false
+ "type": "null"
}
]
},
+ "isPublic": {
+ "description": "Whether the server answers MCP clients without a Sim API key.",
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": false,
+ "title": "Update workflow MCP server request",
+ "description": "Merge-patch body for a published MCP server.",
+ "examples": [
{
- "type": "object",
- "properties": {
- "workspaceId": {
- "type": "string",
- "minLength": 1,
- "maxLength": 128,
- "description": "Workspace expected to own the credential."
- },
- "credentialId": {
- "type": "string",
- "minLength": 1,
- "maxLength": 255,
- "description": "Existing OAuth credential to reconnect in place. QuickBooks reconnects also require oauthClientConfig with the Intuit client ID, client secret, environment, and webhook verifier token."
- },
- "oauthClientConfig": {
- "description": "Write-only Intuit OAuth app configuration. Required when credentialId identifies a QuickBooks credential; omit it for other providers.",
- "type": "object",
- "properties": {
- "clientId": {
- "type": "string",
- "minLength": 1,
- "maxLength": 255,
- "description": "Client ID for the caller-managed Intuit OAuth application."
- },
- "clientSecret": {
- "type": "string",
- "minLength": 1,
- "maxLength": 512,
- "description": "Write-only client secret for the caller-managed Intuit OAuth application.",
- "writeOnly": true
- },
- "environment": {
- "type": "string",
- "enum": ["sandbox", "production"],
- "description": "Intuit company environment used for authorization and API requests."
- },
- "webhookVerifierToken": {
- "type": "string",
- "minLength": 1,
- "maxLength": 512,
- "description": "Write-only verifier token for webhook signatures from the caller-managed app.",
- "writeOnly": true
- }
- },
- "required": ["clientId", "clientSecret", "environment", "webhookVerifierToken"],
- "additionalProperties": false
- }
- },
- "required": ["workspaceId", "credentialId"],
- "additionalProperties": false
+ "isPublic": true
}
- ],
- "title": "Create credential connection body",
- "description": "For a new connection, provide providerId and displayName. For a reconnect, provide only credentialId; the existing display name is preserved."
+ ]
},
- "V2CredentialDeleteData": {
+ "DeleteWorkflowMcpServerResult": {
"type": "object",
"properties": {
"id": {
"type": "string",
- "minLength": 1,
- "description": "Disconnected credential identifier."
+ "description": "Identifier of the unpublished server."
},
"deleted": {
"type": "boolean",
"const": true,
- "description": "Whether the credential was disconnected."
+ "description": "Whether the server was unpublished."
}
},
"required": ["id", "deleted"],
"additionalProperties": false,
- "title": "Delete credential data",
- "description": "Credential disconnection acknowledgement."
+ "title": "Delete workflow MCP server result",
+ "description": "Unpublish acknowledgement."
},
- "DeleteCredentialResponse": {
+ "DeleteWorkflowMcpServerResponse": {
"type": "object",
"properties": {
"data": {
"description": "Response data.",
- "$ref": "#/components/schemas/V2CredentialDeleteData"
+ "$ref": "#/components/schemas/DeleteWorkflowMcpServerResult"
}
},
"required": ["data"],
"additionalProperties": false,
- "title": "Disconnect credential response",
- "description": "Acknowledgement that the credential was disconnected.",
+ "title": "Delete workflow MCP server response",
+ "description": "Acknowledgement that the MCP server was unpublished.",
"examples": [
{
"data": {
- "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
+ "id": "wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2",
"deleted": true
}
}
]
},
- "V2SecretWithValue": {
+ "WorkflowMcpTool": {
"type": "object",
"properties": {
- "name": {
+ "id": {
"type": "string",
- "minLength": 1,
- "maxLength": 255,
- "pattern": "^[A-Za-z0-9_]+$",
- "description": "Secret name containing only letters, numbers, and underscores."
+ "description": "Unique tool identifier."
},
- "scope": {
+ "serverId": {
"type": "string",
- "enum": ["workspace", "personal"],
- "description": "Whether the secret belongs to the workspace or to the caller. A personal secret belongs to the caller across every workspace, not to one workspace."
+ "description": "Server that publishes this tool."
},
- "description": {
+ "workflowId": {
+ "type": "string",
+ "description": "Workflow this tool executes."
+ },
+ "toolName": {
+ "type": "string",
+ "description": "Name an MCP client calls. Derived from the supplied name or the workflow name, normalized to the MCP tool-name grammar."
+ },
+ "toolDescription": {
"anyOf": [
{
"type": "string"
@@ -8397,180 +11224,201 @@
"type": "null"
}
],
- "description": "What the secret is for, as set on the workspace secret. Always null for a personal secret, which has no shared audience."
+ "description": "Description shown to MCP clients."
},
- "unredacted": {
- "type": "boolean",
- "description": "Whether the workspace secret opts out of redaction, so its value appears in plaintext in run logs and model-visible content. Always false for a personal secret."
+ "mcpServerUrl": {
+ "type": "string",
+ "description": "Endpoint an MCP client connects to."
},
- "role": {
+ "apiEndpoint": {
"type": "string",
- "enum": ["admin", "member"],
- "description": "Caller role for the secret."
+ "description": "Sim execution endpoint this tool calls through."
+ },
+ "updated": {
+ "type": "boolean",
+ "description": "False when the workflow was newly published on this server, true when an existing tool was replaced. Publishing is idempotent per workflow, so a repeat call answers 200 with true rather than conflicting."
},
"createdAt": {
"type": "string",
- "format": "date-time",
- "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
- "description": "ISO 8601 timestamp when the secret was created."
+ "description": "ISO 8601 timestamp when the tool was created.",
+ "format": "date-time"
},
"updatedAt": {
"type": "string",
- "format": "date-time",
- "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
- "description": "ISO 8601 timestamp when the secret was last updated."
- },
- "value": {
- "description": "The stored secret value. Present only when the workspace secret is marked visible (unredacted); omitted for every other secret.",
- "type": "string"
+ "description": "ISO 8601 timestamp when the tool was last modified.",
+ "format": "date-time"
}
},
"required": [
- "name",
- "scope",
- "description",
- "unredacted",
- "role",
+ "id",
+ "serverId",
+ "workflowId",
+ "toolName",
+ "toolDescription",
+ "mcpServerUrl",
+ "apiEndpoint",
+ "updated",
"createdAt",
"updatedAt"
],
"additionalProperties": false,
- "title": "Secret metadata with visible value",
- "description": "Secret metadata; the stored value is included only for a workspace secret marked visible (unredacted)."
+ "title": "Workflow MCP tool",
+ "description": "A deployed workflow published as a tool on a workflow-MCP server."
},
- "ListSecretsResponse": {
+ "DeployWorkflowMcpToolResponse": {
"type": "object",
"properties": {
"data": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/V2SecretWithValue"
- },
- "description": "Items in the current page."
- },
- "nextCursor": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Opaque cursor for the next page. Send it back as `cursor`; `null` means there is nothing further to fetch. Never construct one yourself."
+ "description": "Response data.",
+ "$ref": "#/components/schemas/WorkflowMcpTool"
}
},
- "required": ["data", "nextCursor"],
+ "required": ["data"],
"additionalProperties": false,
- "title": "List secrets response",
- "description": "Secret metadata visible to the caller; visible (unredacted) workspace secrets carry their value.",
+ "title": "Publish workflow as MCP tool response",
+ "description": "The published tool.",
"examples": [
{
- "data": [
- {
- "name": "STRIPE_API_KEY",
- "scope": "workspace",
- "description": "Production billing key — rotate quarterly.",
- "unredacted": false,
- "role": "admin",
- "createdAt": "2026-06-01T09:14:00.000Z",
- "updatedAt": "2026-06-20T14:02:11.000Z"
- },
- {
- "name": "STAGING_BASE_URL",
- "scope": "workspace",
- "description": "Staging environment base URL.",
- "unredacted": true,
- "role": "member",
- "createdAt": "2026-06-03T11:30:00.000Z",
- "updatedAt": "2026-06-21T08:45:09.000Z",
- "value": "https://staging.example.com"
- }
- ],
- "nextCursor": null
+ "data": {
+ "id": "wfmcptool_01J8ZK3QW4M6X2R9T7B5C0V3",
+ "serverId": "wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2",
+ "workflowId": "3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36",
+ "toolName": "triage_ticket",
+ "toolDescription": "Execute Ticket triage workflow",
+ "mcpServerUrl": "https://www.sim.ai/api/mcp/serve/wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2",
+ "apiEndpoint": "https://www.sim.ai/api/v2/workflows/3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36/execute",
+ "updated": false,
+ "createdAt": "2026-06-12T10:30:00.000Z",
+ "updatedAt": "2026-06-12T10:30:00.000Z"
+ }
}
]
},
- "V2Secret": {
+ "DeployWorkflowMcpToolRequest": {
"type": "object",
"properties": {
- "name": {
+ "workflowId": {
"type": "string",
"minLength": 1,
- "maxLength": 255,
- "pattern": "^[A-Za-z0-9_]+$",
- "description": "Secret name containing only letters, numbers, and underscores."
+ "description": "Deployed workflow to publish. The workflow must already be deployed."
},
- "scope": {
+ "toolName": {
+ "description": "Name MCP clients call. Normalized to the MCP tool-name grammar, and derived from the workflow name when omitted.",
"type": "string",
- "enum": ["workspace", "personal"],
- "description": "Whether the secret belongs to the workspace or to the caller. A personal secret belongs to the caller across every workspace, not to one workspace."
- },
- "description": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "What the secret is for, as set on the workspace secret. Always null for a personal secret, which has no shared audience."
+ "minLength": 1,
+ "maxLength": 128
},
- "unredacted": {
- "type": "boolean",
- "description": "Whether the workspace secret opts out of redaction, so its value appears in plaintext in run logs and model-visible content. Always false for a personal secret."
+ "toolDescription": {
+ "description": "Description shown to MCP clients. Derived from the workflow name when omitted.",
+ "type": "string",
+ "maxLength": 2000
},
- "role": {
+ "parameterDescriptions": {
+ "description": "Per-field description overrides applied to the schema generated from the deployed workflow inputs. A name matching no input field is ignored.",
+ "maxItems": 100,
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Input field of the deployed workflow to describe."
+ },
+ "description": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2000,
+ "description": "Text MCP clients see for that field."
+ }
+ },
+ "required": ["name", "description"],
+ "additionalProperties": false
+ }
+ }
+ },
+ "required": ["workflowId"],
+ "additionalProperties": false,
+ "title": "Publish workflow as MCP tool request",
+ "description": "The workflow to publish and the tool metadata MCP clients see.",
+ "examples": [
+ {
+ "workflowId": "3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36",
+ "toolName": "triage_ticket"
+ }
+ ]
+ },
+ "UndeployWorkflowMcpToolResult": {
+ "type": "object",
+ "properties": {
+ "id": {
"type": "string",
- "enum": ["admin", "member"],
- "description": "Caller role for the secret."
+ "description": "Identifier of the removed tool."
},
- "createdAt": {
+ "serverId": {
"type": "string",
- "format": "date-time",
- "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
- "description": "ISO 8601 timestamp when the secret was created."
+ "description": "Server the tool was removed from."
},
- "updatedAt": {
+ "workflowId": {
"type": "string",
- "format": "date-time",
- "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
- "description": "ISO 8601 timestamp when the secret was last updated."
+ "description": "Workflow that is no longer published."
+ },
+ "deleted": {
+ "type": "boolean",
+ "const": true,
+ "description": "Whether the tool was removed."
}
},
- "required": [
- "name",
- "scope",
- "description",
- "unredacted",
- "role",
- "createdAt",
- "updatedAt"
- ],
+ "required": ["id", "serverId", "workflowId", "deleted"],
"additionalProperties": false,
- "title": "Secret metadata",
- "description": "Public secret metadata without the stored secret value."
+ "title": "Unpublish workflow MCP tool result",
+ "description": "Tool removal acknowledgement."
+ },
+ "UndeployWorkflowMcpToolResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/UndeployWorkflowMcpToolResult"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Unpublish workflow MCP tool response",
+ "description": "Acknowledgement that the tool was removed.",
+ "examples": [
+ {
+ "data": {
+ "id": "wfmcptool_01J8ZK3QW4M6X2R9T7B5C0V3",
+ "serverId": "wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2",
+ "workflowId": "3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36",
+ "deleted": true
+ }
+ }
+ ]
},
- "SetSecretResponse": {
+ "UpdateCredentialResponse": {
"type": "object",
"properties": {
"data": {
"description": "Response data.",
- "$ref": "#/components/schemas/V2Secret"
+ "$ref": "#/components/schemas/V2Credential"
}
},
"required": ["data"],
"additionalProperties": false,
- "title": "Set secret response",
- "description": "Metadata for the created or replaced secret without its value.",
+ "title": "Update credential response",
+ "description": "Updated credential metadata without secret material.",
"examples": [
{
"data": {
- "name": "STRIPE_API_KEY",
- "scope": "workspace",
- "description": "Production billing key — rotate quarterly.",
- "unredacted": false,
+ "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
+ "type": "service_account",
+ "displayName": "Zoom service account",
+ "description": null,
+ "providerId": "zoom-service-account",
+ "accountId": null,
+ "hasServiceAccountKey": true,
"role": "admin",
"createdAt": "2026-06-01T09:14:00.000Z",
"updatedAt": "2026-06-20T14:02:11.000Z"
@@ -8578,29 +11426,17 @@
}
]
},
- "SetSecretRequest": {
+ "UpdateCredentialRequest": {
"type": "object",
"properties": {
- "workspaceId": {
- "type": "string",
- "minLength": 1,
- "maxLength": 128,
- "description": "Workspace the request is authorized against. A workspace secret is written to it; a personal secret is written to the caller and is available in all of their workspaces."
- },
- "scope": {
- "type": "string",
- "enum": ["workspace", "personal"],
- "description": "Whether the secret belongs to the workspace or to the caller. A personal secret belongs to the caller across every workspace, not to one workspace."
- },
- "value": {
- "description": "Write-only secret value. It is never returned. Omit it on a workspace secret to change description or unredacted alone, leaving the stored value untouched; the secret must already exist. Always required for a personal secret, which carries no other writable field.",
- "writeOnly": true,
+ "displayName": {
+ "description": "New name shown for the credential in Sim.",
"type": "string",
"minLength": 1,
- "maxLength": 65536
+ "maxLength": 255
},
"description": {
- "description": "What the secret is for, shown to teammates. Workspace scope only — sending it for a personal secret is rejected. Omit it to leave an existing description untouched; send null or an empty string to clear one.",
+ "description": "New credential description. Send null to clear the stored one.",
"anyOf": [
{
"type": "string",
@@ -8611,973 +11447,1556 @@
}
]
},
- "unredacted": {
- "description": "Opt the workspace secret out of redaction: its value then appears in plaintext in run logs, model-visible content, and files, including publicly shared log links. Workspace scope only — sending it for a personal secret is rejected. Omit it to leave the current setting untouched.",
- "type": "boolean"
+ "serviceAccountJson": {
+ "description": "Write-only Google service-account JSON key.",
+ "writeOnly": true,
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 65536
+ },
+ "apiToken": {
+ "description": "Write-only provider API token.",
+ "writeOnly": true,
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 8192
+ },
+ "domain": {
+ "description": "Provider account domain.",
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2048
+ },
+ "atlassianProduct": {
+ "description": "Atlassian product to verify; defaults to Jira on create and preserves the saved product on reconnect.",
+ "type": "string",
+ "enum": ["jira", "confluence"]
+ },
+ "signingSecret": {
+ "description": "Write-only webhook signing secret.",
+ "writeOnly": true,
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 8192
+ },
+ "botToken": {
+ "description": "Write-only bot token.",
+ "writeOnly": true,
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 8192
+ },
+ "clientId": {
+ "description": "OAuth client identifier.",
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 512
+ },
+ "clientSecret": {
+ "description": "Write-only OAuth client secret.",
+ "writeOnly": true,
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 1024
+ },
+ "certificateId": {
+ "description": "Provider certificate mapping identifier.",
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 512
+ },
+ "orgId": {
+ "description": "Provider organization ID.",
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255
+ },
+ "dataCenter": {
+ "description": "Provider data center.",
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 32
+ },
+ "authMethod": {
+ "description": "Provider authentication method.",
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64
+ },
+ "privateKey": {
+ "description": "Write-only PEM private key.",
+ "writeOnly": true,
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 8192
+ },
+ "username": {
+ "description": "Provider run-as username.",
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255
}
},
- "required": ["workspaceId", "scope"],
"additionalProperties": false,
- "title": "Set secret request",
- "description": "Ownership scope and write-only value for the secret. A workspace secret may instead send description or unredacted alone, without a value.",
+ "title": "Update credential request",
+ "description": "Replacement display metadata and the write-only fields declared by provider discovery.",
"examples": [
{
- "workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
- "scope": "workspace",
- "value": "YOUR_SECRET_VALUE"
- },
- {
- "workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
- "scope": "workspace",
- "unredacted": false
+ "clientSecret": "YOUR_ROTATED_CLIENT_SECRET"
}
]
},
- "V2SecretDeleteData": {
+ "V2BlockSummary": {
"type": "object",
"properties": {
+ "id": {
+ "type": "string",
+ "description": "Block type identifier, used as a workflow block’s `type`."
+ },
"name": {
"type": "string",
- "minLength": 1,
- "maxLength": 255,
- "pattern": "^[A-Za-z0-9_]+$",
- "description": "Secret name containing only letters, numbers, and underscores."
+ "description": "Display name."
},
- "scope": {
+ "description": {
"type": "string",
- "enum": ["workspace", "personal"],
- "description": "Whether the secret belongs to the workspace or to the caller. A personal secret belongs to the caller across every workspace, not to one workspace."
+ "description": "One-line summary of what the block does."
},
- "deleted": {
+ "longDescription": {
+ "description": "Extended explanation, when the block has one.",
+ "type": "string"
+ },
+ "category": {
+ "type": "string",
+ "description": "Toolbar category: `blocks`, `tools`, or `triggers`."
+ },
+ "integrationType": {
+ "description": "Integration category, e.g. `communication`, `databases`.",
+ "type": "string"
+ },
+ "source": {
+ "type": "string",
+ "enum": ["builtin", "custom"],
+ "description": "Block source: `builtin` for built-in blocks, or `custom` for workflows this workspace deployed as blocks."
+ },
+ "authMode": {
+ "description": "How the block authenticates: `oauth`, `api_key`, or `bot_token`.",
+ "type": "string"
+ },
+ "triggerAllowed": {
"type": "boolean",
- "const": true,
- "description": "Whether the secret was deleted."
- }
- },
- "required": ["name", "scope", "deleted"],
- "additionalProperties": false,
- "title": "Delete secret data",
- "description": "Secret deletion acknowledgement without the stored value."
- },
- "DeleteSecretResponse": {
- "type": "object",
- "properties": {
- "data": {
- "description": "Response data.",
- "$ref": "#/components/schemas/V2SecretDeleteData"
+ "description": "Whether the block declares itself usable as a trigger."
+ },
+ "triggerCapable": {
+ "type": "boolean",
+ "description": "Whether the block can start a workflow — a trigger-category block, one declaring `triggerAllowed`, or one with trigger-mode fields."
+ },
+ "triggerIds": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Identifiers of the triggers this block supports."
+ },
+ "toolIds": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Built-in tools this block can run. Read a tool by its id for the full definition."
+ },
+ "operationIds": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Operations this block exposes. Their fields and tools are on the block read."
+ },
+ "preview": {
+ "type": "boolean",
+ "description": "Whether the block is unreleased and revealed only to this caller."
+ },
+ "sunset": {
+ "description": "Post-release lifecycle state. Absent for a block in normal support.",
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "enum": ["legacy", "deprecated"],
+ "description": "`legacy` is superseded but supported; `deprecated` is slated for removal."
+ },
+ "replacedBy": {
+ "description": "Block type to migrate to, when one exists.",
+ "type": "string"
+ }
+ },
+ "required": ["status"],
+ "additionalProperties": false
+ },
+ "docsLink": {
+ "description": "Sim documentation page for the integration.",
+ "type": "string"
+ },
+ "tags": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Catalog tags, e.g. `messaging`, `version-control`."
}
},
- "required": ["data"],
+ "required": [
+ "id",
+ "name",
+ "description",
+ "category",
+ "source",
+ "triggerAllowed",
+ "triggerCapable",
+ "triggerIds",
+ "toolIds",
+ "operationIds",
+ "preview",
+ "tags"
+ ],
"additionalProperties": false,
- "title": "Delete secret response",
- "description": "Acknowledgement that the secret was deleted.",
- "examples": [
- {
- "data": {
- "name": "STRIPE_API_KEY",
- "scope": "workspace",
- "deleted": true
- }
- }
- ]
+ "title": "Block summary",
+ "description": "List view of a block: what it is and what it references, by id."
},
- "V2Meta": {
+ "ListBlocksResponse": {
"type": "object",
"properties": {
- "v2Enabled": {
- "type": "boolean",
- "description": "Whether this API version is available. This is true when the endpoint is served."
- },
- "keyType": {
- "type": "string",
- "enum": ["personal", "workspace", "oauth_access_token"],
- "description": "Whether the calling credential is a personal API key carrying the full authority of its owner across their workspaces, a key scoped to one workspace, or an OAuth access token acting for its user within the scopes it was granted."
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/V2BlockSummary"
+ },
+ "description": "Items in the current page."
},
- "expiresAt": {
+ "nextCursor": {
"anyOf": [
{
- "type": "string",
- "format": "date-time",
- "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
+ "type": "string"
},
{
"type": "null"
}
],
- "description": "ISO 8601 timestamp when the calling credential expires, or null when it does not."
- }
- },
- "required": ["v2Enabled", "keyType", "expiresAt"],
- "additionalProperties": false,
- "title": "API capabilities",
- "description": "API availability and lifecycle facts about the calling credential."
- },
- "GetApiMetaResponse": {
- "type": "object",
- "properties": {
- "data": {
- "description": "Response data.",
- "$ref": "#/components/schemas/V2Meta"
+ "description": "Opaque cursor for the next page. Send it back as `cursor`; `null` means there is nothing further to fetch. Never construct one yourself."
}
},
- "required": ["data"],
+ "required": ["data", "nextCursor"],
"additionalProperties": false,
- "title": "API capabilities response",
- "description": "API availability, credential type, and expiry for the caller.",
+ "title": "List blocks response",
+ "description": "Blocks available in the workspace.",
"examples": [
{
- "data": {
- "v2Enabled": true,
- "keyType": "personal",
- "expiresAt": null
- }
+ "data": [
+ {
+ "id": "slack",
+ "name": "Slack",
+ "description": "Send messages and read channels in Slack.",
+ "category": "tools",
+ "integrationType": "communication",
+ "source": "builtin",
+ "authMode": "oauth",
+ "triggerAllowed": true,
+ "triggerCapable": true,
+ "triggerIds": ["slack_webhook"],
+ "toolIds": ["slack_message", "slack_canvas_read"],
+ "operationIds": ["send", "read"],
+ "preview": false,
+ "docsLink": "https://docs.sim.ai/tools/slack",
+ "tags": ["messaging"]
+ }
+ ],
+ "nextCursor": null
}
]
},
- "WorkflowMcpServerListItem": {
+ "V2BlockField": {
"type": "object",
"properties": {
"id": {
"type": "string",
- "description": "Unique workflow-MCP server identifier."
+ "description": "Field identifier, and the key its value is stored under."
},
- "name": {
+ "type": {
"type": "string",
- "description": "Server display name, shown to connecting MCP clients."
+ "description": "Editor control the field renders as, e.g. `short-input`."
+ },
+ "title": {
+ "description": "Human-readable label.",
+ "type": "string"
+ },
+ "required": {
+ "description": "Whether a value must be supplied. A conditionally required field reports `true` and carries `requiredWhen`.",
+ "type": "boolean"
+ },
+ "requiredWhen": {
+ "description": "Condition under which the field is required.",
+ "$ref": "#/components/schemas/V2CatalogCondition"
},
"description": {
- "anyOf": [
- {
- "type": "string"
+ "description": "Authored explanation of the field.",
+ "type": "string"
+ },
+ "placeholder": {
+ "description": "Placeholder shown in the editor.",
+ "type": "string"
+ },
+ "mode": {
+ "description": "Where the field renders: `basic`, `advanced`, `both`, `trigger`, or `trigger-advanced`.",
+ "type": "string"
+ },
+ "hidden": {
+ "description": "Whether the field is hidden in the editor.",
+ "type": "boolean"
+ },
+ "condition": {
+ "description": "Condition under which the field applies at all.",
+ "$ref": "#/components/schemas/V2CatalogCondition"
+ },
+ "options": {
+ "description": "Selectable options. Absent on fields whose options are fetched per workspace at edit time.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Value stored when this option is selected."
+ },
+ "label": {
+ "description": "Human-readable option label.",
+ "type": "string"
+ },
+ "hasIcon": {
+ "description": "Whether the option renders with an icon. The icon itself is not published.",
+ "type": "boolean"
+ }
},
- {
- "type": "null"
- }
- ],
- "description": "Optional server description, or null when unset."
+ "required": ["id"],
+ "additionalProperties": false
+ }
},
- "isPublic": {
- "type": "boolean",
- "description": "Whether the server answers MCP clients without a Sim API key."
+ "min": {
+ "description": "Minimum accepted numeric value.",
+ "type": "number"
},
- "mcpServerUrl": {
- "type": "string",
- "description": "Endpoint an MCP client connects to. Published here so callers never build it.",
- "examples": ["https://www.sim.ai/api/mcp/serve/wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2"]
+ "max": {
+ "description": "Maximum accepted numeric value.",
+ "type": "number"
},
- "createdAt": {
- "type": "string",
- "description": "ISO 8601 timestamp when the server was created.",
- "format": "date-time"
+ "step": {
+ "description": "Increment for numeric controls.",
+ "type": "number"
},
- "updatedAt": {
- "type": "string",
- "description": "ISO 8601 timestamp when the server was last modified.",
- "format": "date-time"
+ "integer": {
+ "description": "Whether the numeric value must be a whole number.",
+ "type": "boolean"
},
- "toolCount": {
- "type": "integer",
- "minimum": 0,
- "maximum": 9007199254740991,
- "description": "Number of workflows published as tools."
+ "rows": {
+ "description": "Visible row count for multi-line text.",
+ "type": "number"
},
- "toolNames": {
+ "password": {
+ "description": "Whether the stored value is masked in the editor.",
+ "type": "boolean"
+ },
+ "multiSelect": {
+ "description": "Whether more than one option may be selected.",
+ "type": "boolean"
+ },
+ "language": {
+ "description": "Language of a code field.",
+ "type": "string"
+ },
+ "generationType": {
+ "description": "Kind of content AI assistance generates here.",
+ "type": "string"
+ },
+ "serviceId": {
+ "description": "OAuth service this credential field authenticates.",
+ "type": "string"
+ },
+ "requiredScopes": {
+ "description": "OAuth scopes the credential selected here must carry.",
"type": "array",
"items": {
"type": "string"
- },
- "description": "Tool names this server publishes, alphabetically ordered."
- }
- },
- "required": [
- "id",
- "name",
- "description",
- "isPublic",
- "mcpServerUrl",
- "createdAt",
- "updatedAt",
- "toolCount",
- "toolNames"
- ],
- "additionalProperties": false,
- "title": "Workflow MCP server list item",
- "description": "A published MCP server together with the tool names it exposes."
- },
- "ListWorkflowMcpServersResponse": {
- "type": "object",
- "properties": {
- "data": {
+ }
+ },
+ "mimeType": {
+ "description": "MIME type filter applied to a file picker.",
+ "type": "string"
+ },
+ "acceptedTypes": {
+ "description": "Accepted file extensions for an upload field.",
+ "type": "string"
+ },
+ "multiple": {
+ "description": "Whether more than one file may be supplied.",
+ "type": "boolean"
+ },
+ "maxSize": {
+ "description": "Maximum upload size in megabytes.",
+ "type": "number"
+ },
+ "connectionDroppable": {
+ "description": "Whether another block’s output can be dropped onto this field.",
+ "type": "boolean"
+ },
+ "columns": {
+ "description": "Column headings for a table field.",
"type": "array",
"items": {
- "$ref": "#/components/schemas/WorkflowMcpServerListItem"
- },
- "description": "Items in the current page."
+ "type": "string"
+ }
},
- "nextCursor": {
+ "dependsOn": {
+ "description": "Sibling fields this field is cleared by when they change.",
"anyOf": [
{
- "type": "string"
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
},
{
- "type": "null"
- }
- ],
- "description": "Opaque cursor for the next page. Send it back as `cursor`; `null` means there is nothing further to fetch. Never construct one yourself."
- },
- "toolNamesTruncated": {
- "type": "boolean",
- "description": "Whether the page-wide tool-name limit left some inventories incomplete. Use List Workflow MCP Tools for one server and check its `truncated` flag before treating the inventory as complete. `nextCursor` paginates servers, not tool names."
- }
- },
- "required": ["data", "nextCursor", "toolNamesTruncated"],
- "additionalProperties": false,
- "title": "List workflow MCP servers response",
- "description": "A cursor-paginated page of published MCP servers.",
- "examples": [
- {
- "data": [
- {
- "id": "wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2",
- "name": "Support agents",
- "description": "Ticket triage and escalation workflows.",
- "isPublic": false,
- "mcpServerUrl": "https://www.sim.ai/api/mcp/serve/wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2",
- "createdAt": "2026-06-12T10:30:00.000Z",
- "updatedAt": "2026-06-12T10:30:00.000Z",
- "toolCount": 1,
- "toolNames": ["triage_ticket"]
+ "type": "object",
+ "properties": {
+ "all": {
+ "description": "Every listed field must hold a value.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "any": {
+ "description": "At least one listed field must hold a value.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "additionalProperties": false
}
- ],
- "nextCursor": null,
- "toolNamesTruncated": false
- }
- ]
- },
- "WorkflowMcpServer": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string",
- "description": "Unique workflow-MCP server identifier."
+ ]
},
- "name": {
- "type": "string",
- "description": "Server display name, shown to connecting MCP clients."
+ "canonicalParamId": {
+ "description": "Shared key for a picker/manual-entry pair. Both fields write the same value, so supply exactly one of the pair.",
+ "type": "string"
},
- "description": {
+ "defaultValue": {
+ "description": "Value used when the field is left unset.",
"anyOf": [
{
"type": "string"
},
{
- "type": "null"
+ "type": "number"
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "description": "Member of an object-valued default. Shape varies by field type."
+ }
+ },
+ {
+ "type": "array",
+ "items": {
+ "description": "Element of an array-valued default. Shape varies by field type."
+ }
}
- ],
- "description": "Optional server description, or null when unset."
- },
- "isPublic": {
- "type": "boolean",
- "description": "Whether the server answers MCP clients without a Sim API key."
- },
- "mcpServerUrl": {
- "type": "string",
- "description": "Endpoint an MCP client connects to. Published here so callers never build it.",
- "examples": ["https://www.sim.ai/api/mcp/serve/wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2"]
- },
- "createdAt": {
- "type": "string",
- "description": "ISO 8601 timestamp when the server was created.",
- "format": "date-time"
- },
- "updatedAt": {
- "type": "string",
- "description": "ISO 8601 timestamp when the server was last modified.",
- "format": "date-time"
- }
- },
- "required": [
- "id",
- "name",
- "description",
- "isPublic",
- "mcpServerUrl",
- "createdAt",
- "updatedAt"
- ],
- "additionalProperties": false,
- "title": "Workflow MCP server",
- "description": "A workspace-published MCP server exposing deployed workflows as tools."
- },
- "CreateWorkflowMcpServerResponse": {
- "type": "object",
- "properties": {
- "data": {
- "description": "Response data.",
- "$ref": "#/components/schemas/WorkflowMcpServer"
- }
- },
- "required": ["data"],
- "additionalProperties": false,
- "title": "Create workflow MCP server response",
- "description": "The published MCP server.",
- "examples": [
- {
- "data": {
- "id": "wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2",
- "name": "Support agents",
- "description": "Ticket triage and escalation workflows.",
- "isPublic": false,
- "mcpServerUrl": "https://www.sim.ai/api/mcp/serve/wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2",
- "createdAt": "2026-06-12T10:30:00.000Z",
- "updatedAt": "2026-06-12T10:30:00.000Z"
- }
- }
- ]
- },
- "CreateWorkflowMcpServerRequest": {
- "type": "object",
- "properties": {
- "workspaceId": {
- "type": "string",
- "minLength": 1,
- "maxLength": 128,
- "description": "Workspace in which to publish the server."
- },
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 255,
- "description": "Server display name, shown to connecting MCP clients."
- },
- "description": {
- "description": "Optional server description.",
- "type": "string",
- "maxLength": 2000
+ ]
},
- "isPublic": {
- "description": "Whether the server answers MCP clients without a Sim API key. Defaults to false — a public server executes the workflows it publishes for anyone holding its URL.",
- "default": false,
+ "hasComputedDefault": {
+ "description": "Whether the field derives its value from the block’s other values. The deriving function is not published.",
"type": "boolean"
- },
- "workflowIds": {
- "description": "Deployed workflows to publish as tools on the new server.",
- "maxItems": 100,
- "type": "array",
- "items": {
- "type": "string",
- "minLength": 1
- }
- }
- },
- "required": ["workspaceId", "name"],
- "additionalProperties": false,
- "title": "Create workflow MCP server request",
- "description": "A new workspace-published MCP server and the workflows it exposes.",
- "examples": [
- {
- "workspaceId": "9f4c2a10-3b7e-4d58-8f6a-2c1d0e5b7a94",
- "name": "Support agents",
- "workflowIds": ["3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36"]
- }
- ]
- },
- "GetWorkflowMcpServerResponse": {
- "type": "object",
- "properties": {
- "data": {
- "description": "Response data.",
- "$ref": "#/components/schemas/WorkflowMcpServer"
}
},
- "required": ["data"],
+ "required": ["id", "type"],
"additionalProperties": false,
- "title": "Get workflow MCP server response",
- "description": "A single published MCP server.",
- "examples": [
- {
- "data": {
- "id": "wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2",
- "name": "Support agents",
- "description": "Ticket triage and escalation workflows.",
- "isPublic": false,
- "mcpServerUrl": "https://www.sim.ai/api/mcp/serve/wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2",
- "createdAt": "2026-06-12T10:30:00.000Z",
- "updatedAt": "2026-06-12T10:30:00.000Z"
- }
- }
- ]
+ "title": "Block field",
+ "description": "One configuration field on a block."
},
- "WorkflowMcpToolListItem": {
+ "V2CatalogCondition": {
"type": "object",
"properties": {
- "id": {
- "type": "string",
- "description": "Unique tool identifier."
- },
- "serverId": {
- "type": "string",
- "description": "Server that publishes this tool."
- },
- "workflowId": {
- "type": "string",
- "description": "Workflow this tool executes."
- },
- "toolName": {
+ "field": {
"type": "string",
- "description": "Name an MCP client calls. Derived from the supplied name or the workflow name, normalized to the MCP tool-name grammar."
+ "description": "Sibling field id whose value decides this condition."
},
- "toolDescription": {
+ "value": {
"anyOf": [
{
"type": "string"
},
{
- "type": "null"
+ "type": "number"
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "array",
+ "items": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "boolean"
+ }
+ ]
+ }
}
],
- "description": "Description shown to MCP clients."
+ "description": "Value, or set of accepted values, the named field must hold."
},
- "mcpServerUrl": {
- "type": "string",
- "description": "Endpoint an MCP client connects to."
+ "not": {
+ "description": "Invert the match: every value EXCEPT `value`.",
+ "type": "boolean"
},
- "apiEndpoint": {
+ "and": {
+ "description": "A second clause that must hold as well.",
+ "type": "object",
+ "properties": {
+ "field": {
+ "type": "string",
+ "description": "Sibling field id for the second clause."
+ },
+ "value": {
+ "description": "Value the second clause matches. Absent means \"holds any value\".",
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "array",
+ "items": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "boolean"
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "not": {
+ "description": "Invert the second clause.",
+ "type": "boolean"
+ }
+ },
+ "required": ["field"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["field", "value"],
+ "additionalProperties": false,
+ "title": "Catalog condition",
+ "description": "When a configuration field applies, expressed against a sibling field."
+ },
+ "V2OperationInput": {
+ "type": "object",
+ "properties": {
+ "type": {
"type": "string",
- "description": "Sim execution endpoint this tool calls through."
+ "description": "Value type."
},
- "createdAt": {
- "type": "string",
- "description": "ISO 8601 timestamp when the tool was created.",
- "format": "date-time"
+ "required": {
+ "description": "Whether the value must be supplied.",
+ "type": "boolean"
},
- "updatedAt": {
- "type": "string",
- "description": "ISO 8601 timestamp when the tool was last modified.",
- "format": "date-time"
+ "visibility": {
+ "description": "Who may supply the value: `user-or-llm`, `user-only`, `llm-only`, or `hidden`.",
+ "type": "string"
+ },
+ "description": {
+ "description": "What the value means.",
+ "type": "string"
+ },
+ "default": {
+ "description": "Value used when this input is omitted."
+ },
+ "items": {
+ "description": "JSON-Schema-shaped constraints declared by the tool parameter."
+ },
+ "schema": {
+ "description": "JSON-Schema-shaped structure declared by the block input."
}
},
- "required": [
- "id",
- "serverId",
- "workflowId",
- "toolName",
- "toolDescription",
- "mcpServerUrl",
- "apiEndpoint",
- "createdAt",
- "updatedAt"
- ],
+ "required": ["type"],
"additionalProperties": false,
- "title": "Workflow MCP tool list item",
- "description": "A tool a server publishes, as returned by a read."
+ "title": "Operation input",
+ "description": "One value a block operation needs, from its tool or its block-level inputs."
},
- "ListWorkflowMcpToolsResponse": {
+ "V2ToolOutput": {
"type": "object",
"properties": {
- "data": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/WorkflowMcpToolListItem"
+ "type": {
+ "type": "string",
+ "description": "Value type of the output field."
+ },
+ "description": {
+ "description": "What the field holds.",
+ "type": "string"
+ },
+ "optional": {
+ "description": "Whether the field may be absent.",
+ "type": "boolean"
+ },
+ "nullable": {
+ "description": "Whether the field may be null.",
+ "type": "boolean"
+ },
+ "properties": {
+ "description": "Members of an object-typed output, keyed by field name.",
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
},
- "description": "Items in the current page."
+ "additionalProperties": {
+ "description": "Nested output field, in this same shape."
+ }
},
- "nextCursor": {
- "anyOf": [
- {
+ "items": {
+ "description": "Element shape of an array-typed output.",
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Element value type."
+ },
+ "description": {
+ "description": "What an element holds.",
"type": "string"
},
- {
- "type": "null"
+ "properties": {
+ "description": "Members of an object-typed element, keyed by field name.",
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "description": "Nested output field, in this same shape."
+ }
}
- ],
- "description": "Always `null` — this list has no `cursor` or `limit` param and returns its whole bounded set in one page. Present so the list can gain pages later without a shape change."
+ },
+ "required": ["type"],
+ "additionalProperties": false
},
- "truncated": {
- "type": "boolean",
- "description": "Whether the tool limit left this inventory incomplete. The list is unpaginated and `nextCursor` remains null even when truncated. Do not treat a truncated inventory as the complete set of published tools."
- }
- },
- "required": ["data", "nextCursor", "truncated"],
- "additionalProperties": false,
- "title": "List workflow MCP tools response",
- "description": "The tools a published MCP server exposes.",
- "examples": [
- {
- "data": [
- {
- "id": "wfmcptool_01J8ZK3QW4M6X2R9T7B5C0V3",
- "serverId": "wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2",
- "workflowId": "3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36",
- "toolName": "triage_ticket",
- "toolDescription": "Execute Ticket triage workflow",
- "mcpServerUrl": "https://www.sim.ai/api/mcp/serve/wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2",
- "apiEndpoint": "https://www.sim.ai/api/v2/workflows/3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36/execute",
- "createdAt": "2026-06-12T10:30:00.000Z",
- "updatedAt": "2026-06-12T10:30:00.000Z"
- }
- ],
- "nextCursor": null,
- "truncated": false
- }
- ]
- },
- "UpdateWorkflowMcpServerResponse": {
- "type": "object",
- "properties": {
- "data": {
- "description": "Response data.",
- "$ref": "#/components/schemas/WorkflowMcpServer"
+ "fileConfig": {
+ "description": "File metadata for a file-typed output.",
+ "type": "object",
+ "properties": {
+ "mimeType": {
+ "description": "MIME type of the produced file.",
+ "type": "string"
+ },
+ "extension": {
+ "description": "File extension of the produced file.",
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
}
},
- "required": ["data"],
+ "required": ["type"],
"additionalProperties": false,
- "title": "Update workflow MCP server response",
- "description": "The updated MCP server.",
- "examples": [
- {
- "data": {
- "id": "wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2",
- "name": "Support agents",
- "description": "Ticket triage and escalation workflows.",
- "isPublic": true,
- "mcpServerUrl": "https://www.sim.ai/api/mcp/serve/wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2",
- "createdAt": "2026-06-12T10:30:00.000Z",
- "updatedAt": "2026-06-12T10:30:00.000Z"
- }
- }
- ]
+ "title": "Tool output",
+ "description": "One declared output field of a built-in tool."
},
- "UpdateWorkflowMcpServerRequest": {
+ "V2ToolDetail": {
"type": "object",
"properties": {
+ "id": {
+ "type": "string",
+ "description": "Registered tool identifier, including its version suffix."
+ },
"name": {
"type": "string",
- "minLength": 1,
- "maxLength": 255,
- "description": "Server display name, shown to connecting MCP clients."
+ "description": "Display name."
},
"description": {
- "description": "New server description, or null to clear it.",
- "anyOf": [
- {
+ "type": "string",
+ "description": "What the tool does."
+ },
+ "version": {
+ "description": "Tool version.",
+ "type": "string"
+ },
+ "hostedApiKey": {
+ "type": "string",
+ "enum": ["always", "conditional", "none"],
+ "description": "Whether Sim supplies the API key on THIS deployment: `always`, `conditional` (only for some parameter combinations), or `none` (bring your own). Self-hosted deployments supply no hosted keys, so every tool reports `none` there regardless of what it declares."
+ },
+ "oauth": {
+ "description": "OAuth requirement, when the tool has one.",
+ "type": "object",
+ "properties": {
+ "required": {
+ "type": "boolean",
+ "description": "Whether the tool cannot run without an OAuth credential."
+ },
+ "provider": {
"type": "string",
- "maxLength": 2000
+ "description": "OAuth service the credential must authenticate."
},
- {
- "type": "null"
+ "requiredScopes": {
+ "description": "Scopes the credential must carry.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
}
- ]
+ },
+ "required": ["required", "provider"],
+ "additionalProperties": false
},
- "isPublic": {
- "description": "Whether the server answers MCP clients without a Sim API key.",
- "type": "boolean"
+ "params": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "$ref": "#/components/schemas/V2ToolParam"
+ },
+ "description": "Parameters the tool accepts."
+ },
+ "outputs": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "$ref": "#/components/schemas/V2ToolOutput"
+ },
+ "description": "Fields the tool produces."
}
},
+ "required": ["id", "name", "description", "hostedApiKey", "params", "outputs"],
"additionalProperties": false,
- "title": "Update workflow MCP server request",
- "description": "Merge-patch body for a published MCP server.",
- "examples": [
- {
- "isPublic": true
- }
- ]
+ "title": "Tool",
+ "description": "A built-in tool with its declared parameters and outputs."
},
- "DeleteWorkflowMcpServerResult": {
+ "V2ToolParam": {
"type": "object",
"properties": {
- "id": {
+ "type": {
"type": "string",
- "description": "Identifier of the unpublished server."
+ "description": "Parameter value type."
},
- "deleted": {
- "type": "boolean",
- "const": true,
- "description": "Whether the server was unpublished."
- }
- },
- "required": ["id", "deleted"],
- "additionalProperties": false,
- "title": "Delete workflow MCP server result",
- "description": "Unpublish acknowledgement."
- },
- "DeleteWorkflowMcpServerResponse": {
- "type": "object",
- "properties": {
- "data": {
- "description": "Response data.",
- "$ref": "#/components/schemas/DeleteWorkflowMcpServerResult"
+ "required": {
+ "description": "Whether the parameter must be supplied.",
+ "type": "boolean"
+ },
+ "visibility": {
+ "description": "Who may supply the value: `user-or-llm`, `user-only`, `llm-only`, or `hidden`.",
+ "type": "string"
+ },
+ "description": {
+ "description": "What the parameter means.",
+ "type": "string"
+ },
+ "default": {
+ "description": "Value used when the parameter is omitted."
+ },
+ "items": {
+ "description": "JSON-Schema-shaped constraints for structured params."
}
},
- "required": ["data"],
+ "required": ["type"],
"additionalProperties": false,
- "title": "Delete workflow MCP server response",
- "description": "Acknowledgement that the MCP server was unpublished.",
- "examples": [
- {
- "data": {
- "id": "wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2",
- "deleted": true
- }
- }
- ]
+ "title": "Tool parameter",
+ "description": "One declared parameter of a built-in tool."
},
- "WorkflowMcpTool": {
+ "V2BlockDetail": {
"type": "object",
"properties": {
"id": {
"type": "string",
- "description": "Unique tool identifier."
+ "description": "Block type identifier, used as a workflow block’s `type`."
},
- "serverId": {
+ "name": {
"type": "string",
- "description": "Server that publishes this tool."
+ "description": "Display name."
},
- "workflowId": {
+ "description": {
"type": "string",
- "description": "Workflow this tool executes."
+ "description": "One-line summary of what the block does."
+ },
+ "longDescription": {
+ "description": "Extended explanation, when the block has one.",
+ "type": "string"
+ },
+ "category": {
+ "type": "string",
+ "description": "Toolbar category: `blocks`, `tools`, or `triggers`."
+ },
+ "integrationType": {
+ "description": "Integration category, e.g. `communication`, `databases`.",
+ "type": "string"
+ },
+ "source": {
+ "type": "string",
+ "enum": ["builtin", "custom"],
+ "description": "Block source: `builtin` for built-in blocks, or `custom` for workflows this workspace deployed as blocks."
+ },
+ "authMode": {
+ "description": "How the block authenticates: `oauth`, `api_key`, or `bot_token`.",
+ "type": "string"
+ },
+ "triggerAllowed": {
+ "type": "boolean",
+ "description": "Whether the block declares itself usable as a trigger."
+ },
+ "triggerCapable": {
+ "type": "boolean",
+ "description": "Whether the block can start a workflow — a trigger-category block, one declaring `triggerAllowed`, or one with trigger-mode fields."
+ },
+ "triggerIds": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Identifiers of the triggers this block supports."
+ },
+ "toolIds": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Built-in tools this block can run. Read a tool by its id for the full definition."
+ },
+ "operationIds": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Operations this block exposes. Their fields and tools are on the block read."
+ },
+ "preview": {
+ "type": "boolean",
+ "description": "Whether the block is unreleased and revealed only to this caller."
+ },
+ "sunset": {
+ "description": "Post-release lifecycle state. Absent for a block in normal support.",
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "enum": ["legacy", "deprecated"],
+ "description": "`legacy` is superseded but supported; `deprecated` is slated for removal."
+ },
+ "replacedBy": {
+ "description": "Block type to migrate to, when one exists.",
+ "type": "string"
+ }
+ },
+ "required": ["status"],
+ "additionalProperties": false
+ },
+ "docsLink": {
+ "description": "Sim documentation page for the integration.",
+ "type": "string"
+ },
+ "tags": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Catalog tags, e.g. `messaging`, `version-control`."
+ },
+ "bestPractices": {
+ "description": "Authored guidance on using the block correctly.",
+ "type": "string"
},
- "toolName": {
- "type": "string",
- "description": "Name an MCP client calls. Derived from the supplied name or the workflow name, normalized to the MCP tool-name grammar."
+ "inputSchema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/V2BlockField"
+ },
+ "description": "Configuration fields that apply regardless of the selected operation."
},
- "toolDescription": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
+ "operationInputSchema": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/V2BlockField"
}
- ],
- "description": "Description shown to MCP clients."
+ },
+ "description": "Configuration fields keyed by the operation that reveals them."
},
- "mcpServerUrl": {
- "type": "string",
- "description": "Endpoint an MCP client connects to."
+ "inputDefinitions": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Value type: `string`, `number`, `boolean`, `json`, `array`, or `file`."
+ },
+ "description": {
+ "description": "What the input means.",
+ "type": "string"
+ },
+ "schema": {
+ "description": "JSON-Schema-shaped structure for object and array inputs."
+ }
+ },
+ "required": ["type"],
+ "additionalProperties": false
+ },
+ "description": "Block-level input definitions, keyed by parameter name."
},
- "apiEndpoint": {
- "type": "string",
- "description": "Sim execution endpoint this tool calls through."
+ "operations": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "object",
+ "properties": {
+ "toolId": {
+ "description": "Built-in tool that performs this operation.",
+ "type": "string"
+ },
+ "toolName": {
+ "description": "Display name of that tool.",
+ "type": "string"
+ },
+ "description": {
+ "description": "What the operation does.",
+ "type": "string"
+ },
+ "inputs": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "$ref": "#/components/schemas/V2OperationInput"
+ },
+ "description": "Values this operation needs, excluding the ones the block supplies from its own block-level inputs."
+ },
+ "outputs": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "$ref": "#/components/schemas/V2ToolOutput"
+ },
+ "description": "Fields the operation produces."
+ },
+ "inputSchema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/V2BlockField"
+ },
+ "description": "Configuration fields that appear when this operation is selected."
+ }
+ },
+ "required": ["inputs", "outputs", "inputSchema"],
+ "additionalProperties": false
+ },
+ "description": "Operations the block exposes, keyed by operation id."
},
- "updated": {
- "type": "boolean",
- "description": "False when the workflow was newly published on this server, true when an existing tool was replaced. Publishing is idempotent per workflow, so a repeat call answers 200 with true rather than conflicting."
+ "tools": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/V2ToolDetail"
+ },
+ "description": "Every built-in tool the block can run, with parameters and outputs."
},
- "createdAt": {
- "type": "string",
- "description": "ISO 8601 timestamp when the tool was created.",
- "format": "date-time"
+ "triggers": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Trigger identifier."
+ },
+ "outputs": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Value type of the output."
+ },
+ "description": {
+ "description": "What the output holds.",
+ "type": "string"
+ }
+ },
+ "required": ["type"],
+ "additionalProperties": false
+ },
+ "description": "Top-level fields the trigger event delivers."
+ },
+ "configFields": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Editor control the field renders as."
+ },
+ "required": {
+ "type": "boolean",
+ "description": "Whether a value must be supplied."
+ },
+ "title": {
+ "description": "Human-readable label.",
+ "type": "string"
+ },
+ "description": {
+ "description": "Authored explanation of the field.",
+ "type": "string"
+ },
+ "placeholder": {
+ "description": "Placeholder shown in the editor.",
+ "type": "string"
+ },
+ "default": {
+ "description": "Value used when the field is left unset."
+ },
+ "options": {
+ "description": "Selectable options.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Value stored when this option is selected."
+ },
+ "label": {
+ "type": "string",
+ "description": "Human-readable option label."
+ }
+ },
+ "required": ["id", "label"],
+ "additionalProperties": false
+ }
+ },
+ "condition": {
+ "description": "Condition under which the field applies.",
+ "$ref": "#/components/schemas/V2CatalogCondition"
+ }
+ },
+ "required": ["type", "required"],
+ "additionalProperties": false
+ },
+ "description": "Fields that configure the trigger, keyed by field id."
+ }
+ },
+ "required": ["id", "outputs", "configFields"],
+ "additionalProperties": false
+ },
+ "description": "Triggers the block can run on."
},
- "updatedAt": {
- "type": "string",
- "description": "ISO 8601 timestamp when the tool was last modified.",
- "format": "date-time"
+ "outputs": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Value type of the output."
+ },
+ "description": {
+ "description": "What the output holds.",
+ "type": "string"
+ }
+ },
+ "required": ["type"],
+ "additionalProperties": false
+ },
+ "description": "Fields the block produces."
}
},
"required": [
"id",
- "serverId",
- "workflowId",
- "toolName",
- "toolDescription",
- "mcpServerUrl",
- "apiEndpoint",
- "updated",
- "createdAt",
- "updatedAt"
+ "name",
+ "description",
+ "category",
+ "source",
+ "triggerAllowed",
+ "triggerCapable",
+ "triggerIds",
+ "toolIds",
+ "operationIds",
+ "preview",
+ "tags",
+ "inputSchema",
+ "operationInputSchema",
+ "inputDefinitions",
+ "operations",
+ "tools",
+ "triggers",
+ "outputs"
],
"additionalProperties": false,
- "title": "Workflow MCP tool",
- "description": "A deployed workflow published as a tool on a workflow-MCP server."
+ "title": "Block",
+ "description": "A block with its configuration fields, operations, tools, and triggers."
},
- "DeployWorkflowMcpToolResponse": {
+ "GetBlockResponse": {
"type": "object",
"properties": {
"data": {
"description": "Response data.",
- "$ref": "#/components/schemas/WorkflowMcpTool"
+ "$ref": "#/components/schemas/V2BlockDetail"
}
},
"required": ["data"],
"additionalProperties": false,
- "title": "Publish workflow as MCP tool response",
- "description": "The published tool.",
+ "title": "Get block response",
+ "description": "One block with its fields, operations, tools, and triggers.",
"examples": [
{
"data": {
- "id": "wfmcptool_01J8ZK3QW4M6X2R9T7B5C0V3",
- "serverId": "wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2",
- "workflowId": "3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36",
- "toolName": "triage_ticket",
- "toolDescription": "Execute Ticket triage workflow",
- "mcpServerUrl": "https://www.sim.ai/api/mcp/serve/wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2",
- "apiEndpoint": "https://www.sim.ai/api/v2/workflows/3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36/execute",
- "updated": false,
- "createdAt": "2026-06-12T10:30:00.000Z",
- "updatedAt": "2026-06-12T10:30:00.000Z"
- }
- }
- ]
- },
- "DeployWorkflowMcpToolRequest": {
- "type": "object",
- "properties": {
- "workflowId": {
- "type": "string",
- "minLength": 1,
- "description": "Deployed workflow to publish. The workflow must already be deployed."
- },
- "toolName": {
- "description": "Name MCP clients call. Normalized to the MCP tool-name grammar, and derived from the workflow name when omitted.",
- "type": "string",
- "minLength": 1,
- "maxLength": 128
- },
- "toolDescription": {
- "description": "Description shown to MCP clients. Derived from the workflow name when omitted.",
- "type": "string",
- "maxLength": 2000
- },
- "parameterDescriptions": {
- "description": "Per-field description overrides applied to the schema generated from the deployed workflow inputs. A name matching no input field is ignored.",
- "maxItems": 100,
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "name": {
- "type": "string",
- "minLength": 1,
- "description": "Input field of the deployed workflow to describe."
- },
- "description": {
+ "id": "slack",
+ "name": "Slack",
+ "description": "Send messages and read channels in Slack.",
+ "category": "tools",
+ "integrationType": "communication",
+ "source": "builtin",
+ "authMode": "oauth",
+ "triggerAllowed": true,
+ "triggerCapable": true,
+ "triggerIds": ["slack_webhook"],
+ "toolIds": ["slack_message", "slack_canvas_read"],
+ "operationIds": ["send", "read"],
+ "preview": false,
+ "docsLink": "https://docs.sim.ai/tools/slack",
+ "tags": ["messaging"],
+ "inputSchema": [
+ {
+ "id": "operation",
+ "type": "dropdown",
+ "title": "Operation",
+ "required": true,
+ "options": [
+ {
+ "id": "send",
+ "label": "Send message"
+ },
+ {
+ "id": "read",
+ "label": "Read messages"
+ }
+ ]
+ }
+ ],
+ "operationInputSchema": {
+ "send": [
+ {
+ "id": "text",
+ "type": "long-input",
+ "title": "Message",
+ "required": true
+ }
+ ]
+ },
+ "inputDefinitions": {
+ "channel": {
"type": "string",
- "minLength": 1,
- "maxLength": 2000,
- "description": "Text MCP clients see for that field."
+ "description": "Channel to post into."
}
},
- "required": ["name", "description"],
- "additionalProperties": false
+ "operations": {
+ "send": {
+ "toolId": "slack_message",
+ "toolName": "Slack Send Message",
+ "description": "Send a message to a Slack channel.",
+ "inputs": {
+ "text": {
+ "type": "string",
+ "required": true,
+ "description": "Message body."
+ }
+ },
+ "outputs": {
+ "ts": {
+ "type": "string",
+ "description": "Message timestamp."
+ }
+ },
+ "inputSchema": [
+ {
+ "id": "text",
+ "type": "long-input",
+ "title": "Message",
+ "required": true
+ }
+ ]
+ }
+ },
+ "tools": [
+ {
+ "id": "slack_message",
+ "name": "Slack Send Message",
+ "description": "Send a message to a Slack channel.",
+ "version": "1.0.0",
+ "hostedApiKey": "none",
+ "oauth": {
+ "required": true,
+ "provider": "slack",
+ "requiredScopes": ["chat:write"]
+ },
+ "params": {
+ "text": {
+ "type": "string",
+ "required": true,
+ "description": "Message body."
+ }
+ },
+ "outputs": {
+ "ts": {
+ "type": "string",
+ "description": "Message timestamp."
+ }
+ }
+ }
+ ],
+ "triggers": [
+ {
+ "id": "slack_webhook",
+ "outputs": {
+ "text": {
+ "type": "string",
+ "description": "Message text."
+ }
+ },
+ "configFields": {
+ "channels": {
+ "type": "short-input",
+ "required": false,
+ "title": "Channels"
+ }
+ }
+ }
+ ],
+ "outputs": {
+ "ts": {
+ "type": "string",
+ "description": "Message timestamp."
+ }
+ }
}
}
- },
- "required": ["workflowId"],
- "additionalProperties": false,
- "title": "Publish workflow as MCP tool request",
- "description": "The workflow to publish and the tool metadata MCP clients see.",
- "examples": [
- {
- "workflowId": "3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36",
- "toolName": "triage_ticket"
- }
]
},
- "UndeployWorkflowMcpToolResult": {
+ "V2ToolSummary": {
"type": "object",
"properties": {
"id": {
"type": "string",
- "description": "Identifier of the removed tool."
+ "description": "Registered tool identifier, including its version suffix."
},
- "serverId": {
+ "name": {
"type": "string",
- "description": "Server the tool was removed from."
+ "description": "Display name."
},
- "workflowId": {
+ "description": {
"type": "string",
- "description": "Workflow that is no longer published."
+ "description": "What the tool does."
},
- "deleted": {
- "type": "boolean",
- "const": true,
- "description": "Whether the tool was removed."
+ "version": {
+ "description": "Tool version.",
+ "type": "string"
+ },
+ "hostedApiKey": {
+ "type": "string",
+ "enum": ["always", "conditional", "none"],
+ "description": "Whether Sim supplies the API key on THIS deployment: `always`, `conditional` (only for some parameter combinations), or `none` (bring your own). Self-hosted deployments supply no hosted keys, so every tool reports `none` there regardless of what it declares."
+ },
+ "oauth": {
+ "description": "OAuth requirement, when the tool has one.",
+ "type": "object",
+ "properties": {
+ "required": {
+ "type": "boolean",
+ "description": "Whether the tool cannot run without an OAuth credential."
+ },
+ "provider": {
+ "type": "string",
+ "description": "OAuth service the credential must authenticate."
+ },
+ "requiredScopes": {
+ "description": "Scopes the credential must carry.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "required": ["required", "provider"],
+ "additionalProperties": false
}
},
- "required": ["id", "serverId", "workflowId", "deleted"],
+ "required": ["id", "name", "description", "hostedApiKey"],
"additionalProperties": false,
- "title": "Unpublish workflow MCP tool result",
- "description": "Tool removal acknowledgement."
+ "title": "Tool summary",
+ "description": "List view of a built-in tool: identity, auth, and key hosting."
},
- "UndeployWorkflowMcpToolResponse": {
+ "ListToolsResponse": {
"type": "object",
"properties": {
"data": {
- "description": "Response data.",
- "$ref": "#/components/schemas/UndeployWorkflowMcpToolResult"
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/V2ToolSummary"
+ },
+ "description": "Items in the current page."
+ },
+ "nextCursor": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Opaque cursor for the next page. Send it back as `cursor`; `null` means there is nothing further to fetch. Never construct one yourself."
}
},
- "required": ["data"],
+ "required": ["data", "nextCursor"],
"additionalProperties": false,
- "title": "Unpublish workflow MCP tool response",
- "description": "Acknowledgement that the tool was removed.",
+ "title": "List tools response",
+ "description": "Built-in tools available in the workspace.",
"examples": [
{
- "data": {
- "id": "wfmcptool_01J8ZK3QW4M6X2R9T7B5C0V3",
- "serverId": "wfmcp_01J8ZK3QW4M6X2R9T7B5C0V2",
- "workflowId": "3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36",
- "deleted": true
- }
+ "data": [
+ {
+ "id": "slack_message",
+ "name": "Slack Send Message",
+ "description": "Send a message to a Slack channel.",
+ "version": "1.0.0",
+ "hostedApiKey": "none",
+ "oauth": {
+ "required": true,
+ "provider": "slack",
+ "requiredScopes": ["chat:write"]
+ }
+ }
+ ],
+ "nextCursor": null
}
]
},
- "UpdateCredentialResponse": {
+ "GetToolResponse": {
"type": "object",
"properties": {
"data": {
"description": "Response data.",
- "$ref": "#/components/schemas/V2Credential"
+ "$ref": "#/components/schemas/V2ToolDetail"
}
},
"required": ["data"],
"additionalProperties": false,
- "title": "Update credential response",
- "description": "Updated credential metadata without secret material.",
+ "title": "Get tool response",
+ "description": "One built-in tool with its parameters and outputs.",
"examples": [
{
"data": {
- "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
- "type": "service_account",
- "displayName": "Zoom service account",
- "description": null,
- "providerId": "zoom-service-account",
- "accountId": null,
- "hasServiceAccountKey": true,
- "role": "admin",
- "createdAt": "2026-06-01T09:14:00.000Z",
- "updatedAt": "2026-06-20T14:02:11.000Z"
+ "id": "slack_message",
+ "name": "Slack Send Message",
+ "description": "Send a message to a Slack channel.",
+ "version": "1.0.0",
+ "hostedApiKey": "none",
+ "oauth": {
+ "required": true,
+ "provider": "slack",
+ "requiredScopes": ["chat:write"]
+ },
+ "params": {
+ "channel": {
+ "type": "string",
+ "required": true,
+ "description": "Channel ID to post into."
+ },
+ "text": {
+ "type": "string",
+ "required": true,
+ "description": "Message body."
+ }
+ },
+ "outputs": {
+ "ts": {
+ "type": "string",
+ "description": "Message timestamp."
+ }
+ }
}
}
]
},
- "UpdateCredentialRequest": {
+ "V2ToolExecution": {
"type": "object",
"properties": {
- "displayName": {
- "description": "New name shown for the credential in Sim.",
+ "toolId": {
"type": "string",
- "minLength": 1,
- "maxLength": 255
+ "description": "Tool that ran. An unversioned name resolves to the newest version visible in the workspace, so this can differ from the id in the path."
},
- "description": {
- "description": "New credential description. Send null to clear the stored one.",
+ "status": {
+ "type": "string",
+ "enum": ["succeeded", "failed"],
+ "description": "Whether the tool reported success. A failed tool call is still a 200."
+ },
+ "output": {
+ "description": "Whatever the tool produced, shaped by its declared outputs."
+ },
+ "error": {
"anyOf": [
{
- "type": "string",
- "maxLength": 500
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string",
+ "description": "Why the tool call did not succeed."
+ }
+ },
+ "required": ["message"],
+ "additionalProperties": false
},
{
"type": "null"
}
- ]
- },
- "serviceAccountJson": {
- "description": "Write-only Google service-account JSON key.",
- "writeOnly": true,
- "type": "string",
- "minLength": 1,
- "maxLength": 65536
- },
- "apiToken": {
- "description": "Write-only provider API token.",
- "writeOnly": true,
- "type": "string",
- "minLength": 1,
- "maxLength": 8192
- },
- "domain": {
- "description": "Provider account domain.",
- "type": "string",
- "minLength": 1,
- "maxLength": 2048
- },
- "atlassianProduct": {
- "description": "Atlassian product to verify; defaults to Jira on create and preserves the saved product on reconnect.",
- "type": "string",
- "enum": ["jira", "confluence"]
- },
- "signingSecret": {
- "description": "Write-only webhook signing secret.",
- "writeOnly": true,
- "type": "string",
- "minLength": 1,
- "maxLength": 8192
- },
- "botToken": {
- "description": "Write-only bot token.",
- "writeOnly": true,
- "type": "string",
- "minLength": 1,
- "maxLength": 8192
- },
- "clientId": {
- "description": "OAuth client identifier.",
- "type": "string",
- "minLength": 1,
- "maxLength": 512
- },
- "clientSecret": {
- "description": "Write-only OAuth client secret.",
- "writeOnly": true,
- "type": "string",
- "minLength": 1,
- "maxLength": 1024
- },
- "certificateId": {
- "description": "Provider certificate mapping identifier.",
- "type": "string",
- "minLength": 1,
- "maxLength": 512
- },
- "orgId": {
- "description": "Provider organization ID.",
- "type": "string",
- "minLength": 1,
- "maxLength": 255
- },
- "dataCenter": {
- "description": "Provider data center.",
- "type": "string",
- "minLength": 1,
- "maxLength": 32
- },
- "authMethod": {
- "description": "Provider authentication method.",
- "type": "string",
- "minLength": 1,
- "maxLength": 64
- },
- "privateKey": {
- "description": "Write-only PEM private key.",
- "writeOnly": true,
+ ],
+ "description": "Populated only when `status` is `failed`."
+ }
+ },
+ "required": ["toolId", "status", "output", "error"],
+ "additionalProperties": false,
+ "title": "Tool execution",
+ "description": "The result of running one built-in tool."
+ },
+ "ExecuteToolResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2ToolExecution"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Run tool response",
+ "description": "What the tool produced, or why it did not succeed.",
+ "examples": [
+ {
+ "data": {
+ "toolId": "slack_message",
+ "status": "succeeded",
+ "output": {
+ "ts": "1718191234.004500"
+ },
+ "error": null
+ }
+ }
+ ]
+ },
+ "ExecuteToolRequest": {
+ "type": "object",
+ "properties": {
+ "workspaceId": {
"type": "string",
"minLength": 1,
- "maxLength": 8192
+ "maxLength": 128,
+ "description": "Workspace whose integration allowlist, credentials, and environment variables govern this call."
},
- "username": {
- "description": "Provider run-as username.",
+ "input": {
+ "default": {},
+ "description": "Tool arguments keyed by published parameter IDs. For `user-only` parameters, a whole-value `{{VAR_NAME}}` reference resolves a workspace environment variable. Other values pass through unchanged.",
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "description": "One argument value. Its shape is declared by the tool parameter."
+ }
+ },
+ "credentialId": {
+ "description": "Credential to authenticate with. Required when the tool declares an OAuth requirement; the workspace credentials list names the candidates.",
"type": "string",
"minLength": 1,
"maxLength": 255
+ },
+ "timeoutSeconds": {
+ "description": "How long to wait for the tool before abandoning the call.",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 300
}
},
+ "required": ["workspaceId"],
"additionalProperties": false,
- "title": "Update credential request",
- "description": "Replacement display metadata and the write-only fields declared by provider discovery.",
+ "title": "Run tool request",
+ "description": "Workspace, arguments, and the credential to authenticate with.",
"examples": [
{
- "clientSecret": "YOUR_ROTATED_CLIENT_SECRET"
+ "workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
+ "input": {
+ "channel": "C0123456789",
+ "text": "Deploy finished."
+ },
+ "credentialId": "cred_01J8ZK3QW4M6X2R9T7B5C0V2"
}
]
},
- "V2BlockSummary": {
+ "V2ConnectorType": {
"type": "object",
"properties": {
- "id": {
+ "connectorType": {
"type": "string",
- "description": "Block type identifier, used as a workflow block’s `type`."
+ "description": "Exact identifier to send when creating a connector of this type."
},
"name": {
"type": "string",
@@ -9585,205 +13004,144 @@
},
"description": {
"type": "string",
- "description": "One-line summary of what the block does."
- },
- "longDescription": {
- "description": "Extended explanation, when the block has one.",
- "type": "string"
- },
- "category": {
- "type": "string",
- "description": "Toolbar category: `blocks`, `tools`, or `triggers`."
- },
- "integrationType": {
- "description": "Integration category, e.g. `communication`, `databases`.",
- "type": "string"
+ "description": "What the connector syncs."
},
- "source": {
+ "version": {
"type": "string",
- "enum": ["builtin", "custom"],
- "description": "Block source: `builtin` for built-in blocks, or `custom` for workflows this workspace deployed as blocks."
- },
- "authMode": {
- "description": "How the block authenticates: `oauth`, `api_key`, or `bot_token`.",
- "type": "string"
- },
- "triggerAllowed": {
- "type": "boolean",
- "description": "Whether the block declares itself usable as a trigger."
- },
- "triggerCapable": {
- "type": "boolean",
- "description": "Whether the block can start a workflow — a trigger-category block, one declaring `triggerAllowed`, or one with trigger-mode fields."
- },
- "triggerIds": {
- "type": "array",
- "items": {
- "type": "string"
- },
- "description": "Identifiers of the triggers this block supports."
+ "description": "Connector version."
},
- "toolIds": {
- "type": "array",
- "items": {
- "type": "string"
- },
- "description": "Built-in tools this block can run. Read a tool by its id for the full definition."
+ "auth": {
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "mode": {
+ "type": "string",
+ "const": "oauth",
+ "description": "Authenticates with an OAuth credential."
+ },
+ "provider": {
+ "type": "string",
+ "description": "OAuth service the credential must authenticate."
+ },
+ "requiredScopes": {
+ "description": "Scopes the credential must carry.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "required": ["mode", "provider"],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "mode": {
+ "type": "string",
+ "const": "apiKey",
+ "description": "Authenticates with a stored API key."
+ },
+ "label": {
+ "description": "Label shown above the key field.",
+ "type": "string"
+ },
+ "placeholder": {
+ "description": "Placeholder shown in the key field.",
+ "type": "string"
+ },
+ "optional": {
+ "type": "boolean",
+ "description": "Whether the key may be left blank, for a source reachable without authentication."
+ }
+ },
+ "required": ["mode", "optional"],
+ "additionalProperties": false
+ }
+ ],
+ "description": "How the connector authenticates against its source."
},
- "operationIds": {
+ "configFields": {
"type": "array",
"items": {
- "type": "string"
+ "$ref": "#/components/schemas/V2ConnectorConfigField"
},
- "description": "Operations this block exposes. Their fields and tools are on the block read."
+ "description": "Fields that make up the connector’s `sourceConfig`."
},
- "preview": {
+ "supportsIncrementalSync": {
"type": "boolean",
- "description": "Whether the block is unreleased and revealed only to this caller."
- },
- "sunset": {
- "description": "Post-release lifecycle state. Absent for a block in normal support.",
- "type": "object",
- "properties": {
- "status": {
- "type": "string",
- "enum": ["legacy", "deprecated"],
- "description": "`legacy` is superseded but supported; `deprecated` is slated for removal."
- },
- "replacedBy": {
- "description": "Block type to migrate to, when one exists.",
- "type": "string"
- }
- },
- "required": ["status"],
- "additionalProperties": false
- },
- "docsLink": {
- "description": "Sim documentation page for the integration.",
- "type": "string"
+ "description": "Whether syncs after the first fetch only what changed."
},
- "tags": {
+ "tagDefinitions": {
"type": "array",
"items": {
- "type": "string"
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Semantic tag identifier the connector populates."
+ },
+ "displayName": {
+ "type": "string",
+ "description": "Human-readable tag name."
+ },
+ "fieldType": {
+ "type": "string",
+ "enum": ["text", "number", "date", "boolean"],
+ "description": "Value type, which decides the tag slot pool it draws from."
+ }
+ },
+ "required": ["id", "displayName", "fieldType"],
+ "additionalProperties": false
},
- "description": "Catalog tags, e.g. `messaging`, `version-control`."
+ "description": "Tags this connector writes onto the documents it syncs."
}
},
"required": [
- "id",
- "name",
- "description",
- "category",
- "source",
- "triggerAllowed",
- "triggerCapable",
- "triggerIds",
- "toolIds",
- "operationIds",
- "preview",
- "tags"
- ],
- "additionalProperties": false,
- "title": "Block summary",
- "description": "List view of a block: what it is and what it references, by id."
- },
- "ListBlocksResponse": {
- "type": "object",
- "properties": {
- "data": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/V2BlockSummary"
- },
- "description": "Items in the current page."
- },
- "nextCursor": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "null"
- }
- ],
- "description": "Opaque cursor for the next page. Send it back as `cursor`; `null` means there is nothing further to fetch. Never construct one yourself."
- }
- },
- "required": ["data", "nextCursor"],
- "additionalProperties": false,
- "title": "List blocks response",
- "description": "Blocks available in the workspace.",
- "examples": [
- {
- "data": [
- {
- "id": "slack",
- "name": "Slack",
- "description": "Send messages and read channels in Slack.",
- "category": "tools",
- "integrationType": "communication",
- "source": "builtin",
- "authMode": "oauth",
- "triggerAllowed": true,
- "triggerCapable": true,
- "triggerIds": ["slack_webhook"],
- "toolIds": ["slack_message", "slack_canvas_read"],
- "operationIds": ["send", "read"],
- "preview": false,
- "docsLink": "https://docs.sim.ai/tools/slack",
- "tags": ["messaging"]
- }
- ],
- "nextCursor": null
- }
- ]
+ "connectorType",
+ "name",
+ "description",
+ "version",
+ "auth",
+ "configFields",
+ "supportsIncrementalSync",
+ "tagDefinitions"
+ ],
+ "additionalProperties": false,
+ "title": "Connector type",
+ "description": "A knowledge-base connector type and the configuration it accepts."
},
- "V2BlockField": {
+ "V2ConnectorConfigField": {
"type": "object",
"properties": {
"id": {
"type": "string",
- "description": "Field identifier, and the key its value is stored under."
+ "description": "Field identifier."
+ },
+ "title": {
+ "type": "string",
+ "description": "Human-readable label."
},
"type": {
"type": "string",
- "description": "Editor control the field renders as, e.g. `short-input`."
+ "enum": ["short-input", "dropdown", "selector"],
+ "description": "Control the field renders as. A `selector` fetches its options from the connected account."
},
- "title": {
- "description": "Human-readable label.",
+ "placeholder": {
+ "description": "Placeholder shown in the editor.",
"type": "string"
},
"required": {
- "description": "Whether a value must be supplied. A conditionally required field reports `true` and carries `requiredWhen`.",
+ "description": "Whether a value must be supplied.",
"type": "boolean"
},
- "requiredWhen": {
- "description": "Condition under which the field is required.",
- "$ref": "#/components/schemas/V2CatalogCondition"
- },
"description": {
"description": "Authored explanation of the field.",
"type": "string"
},
- "placeholder": {
- "description": "Placeholder shown in the editor.",
- "type": "string"
- },
- "mode": {
- "description": "Where the field renders: `basic`, `advanced`, `both`, `trigger`, or `trigger-advanced`.",
- "type": "string"
- },
- "hidden": {
- "description": "Whether the field is hidden in the editor.",
- "type": "boolean"
- },
- "condition": {
- "description": "Condition under which the field applies at all.",
- "$ref": "#/components/schemas/V2CatalogCondition"
- },
"options": {
- "description": "Selectable options. Absent on fields whose options are fetched per workspace at edit time.",
+ "description": "Static options, for a `dropdown` field.",
"type": "array",
"items": {
"type": "object",
@@ -9793,92 +13151,22 @@
"description": "Value stored when this option is selected."
},
"label": {
- "description": "Human-readable option label.",
- "type": "string"
- },
- "hasIcon": {
- "description": "Whether the option renders with an icon. The icon itself is not published.",
- "type": "boolean"
+ "type": "string",
+ "description": "Human-readable option label."
}
},
- "required": ["id"],
+ "required": ["id", "label"],
"additionalProperties": false
}
},
- "min": {
- "description": "Minimum accepted numeric value.",
- "type": "number"
- },
- "max": {
- "description": "Maximum accepted numeric value.",
- "type": "number"
- },
- "step": {
- "description": "Increment for numeric controls.",
- "type": "number"
- },
- "integer": {
- "description": "Whether the numeric value must be a whole number.",
- "type": "boolean"
- },
- "rows": {
- "description": "Visible row count for multi-line text.",
- "type": "number"
- },
- "password": {
- "description": "Whether the stored value is masked in the editor.",
- "type": "boolean"
- },
- "multiSelect": {
- "description": "Whether more than one option may be selected.",
- "type": "boolean"
- },
- "language": {
- "description": "Language of a code field.",
- "type": "string"
- },
- "generationType": {
- "description": "Kind of content AI assistance generates here.",
- "type": "string"
- },
- "serviceId": {
- "description": "OAuth service this credential field authenticates.",
+ "selectorKey": {
+ "description": "Names the picker a `selector` field renders. Its options are fetched per workspace.",
"type": "string"
},
- "requiredScopes": {
- "description": "OAuth scopes the credential selected here must carry.",
- "type": "array",
- "items": {
- "type": "string"
- }
- },
"mimeType": {
- "description": "MIME type filter applied to a file picker.",
- "type": "string"
- },
- "acceptedTypes": {
- "description": "Accepted file extensions for an upload field.",
+ "description": "MIME type filter applied to the picker.",
"type": "string"
},
- "multiple": {
- "description": "Whether more than one file may be supplied.",
- "type": "boolean"
- },
- "maxSize": {
- "description": "Maximum upload size in megabytes.",
- "type": "number"
- },
- "connectionDroppable": {
- "description": "Whether another block’s output can be dropped onto this field.",
- "type": "boolean"
- },
- "columns": {
- "description": "Column headings for a table field.",
- "type": "array",
- "items": {
- "type": "string"
- }
- },
"dependsOn": {
"description": "Sibling fields this field is cleared by when they change.",
"anyOf": [
@@ -9898,926 +13186,1699 @@
"type": "string"
}
},
- "any": {
- "description": "At least one listed field must hold a value.",
+ "any": {
+ "description": "At least one listed field must hold a value.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "additionalProperties": false
+ }
+ ]
+ },
+ "mode": {
+ "description": "Which half of a canonical pair this field is: `basic` is the picker, `advanced` the manual entry.",
+ "type": "string",
+ "enum": ["basic", "advanced"]
+ },
+ "canonicalParamId": {
+ "description": "Shared `sourceConfig` key for a picker/manual-entry pair. Send exactly one of the pair, keyed by this value rather than by the field’s own `id`.",
+ "type": "string"
+ },
+ "multi": {
+ "description": "When true the stored `sourceConfig` value is a `string[]`, not a `string`: a `selector` renders a multi-select picker and a `short-input` accepts a comma-separated list.",
+ "type": "boolean"
+ }
+ },
+ "required": ["id", "title", "type"],
+ "additionalProperties": false,
+ "title": "Connector config field",
+ "description": "One field of a knowledge-base connector’s source configuration."
+ },
+ "ListConnectorTypesResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/V2ConnectorType"
+ },
+ "description": "Items in the current page."
+ },
+ "nextCursor": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Always `null` — this list has no `cursor` or `limit` param and returns its whole bounded set in one page. Present so the list can gain pages later without a shape change."
+ }
+ },
+ "required": ["data", "nextCursor"],
+ "additionalProperties": false,
+ "title": "List connector types response",
+ "description": "Knowledge-base connector types and their configuration fields.",
+ "examples": [
+ {
+ "data": [
+ {
+ "connectorType": "google_drive",
+ "name": "Google Drive",
+ "description": "Sync documents from a Google Drive folder.",
+ "version": "1.0.0",
+ "auth": {
+ "mode": "oauth",
+ "provider": "google-drive",
+ "requiredScopes": ["https://www.googleapis.com/auth/drive.readonly"]
+ },
+ "configFields": [
+ {
+ "id": "folderSelector",
+ "title": "Folder",
+ "type": "selector",
+ "selectorKey": "google-drive-folder",
+ "mimeType": "application/vnd.google-apps.folder",
+ "mode": "basic",
+ "canonicalParamId": "folderId",
+ "required": true
+ },
+ {
+ "id": "manualFolderId",
+ "title": "Folder ID",
+ "type": "short-input",
+ "placeholder": "Enter the folder ID",
+ "mode": "advanced",
+ "canonicalParamId": "folderId"
+ }
+ ],
+ "supportsIncrementalSync": true,
+ "tagDefinitions": [
+ {
+ "id": "owner",
+ "displayName": "Owner",
+ "fieldType": "text"
+ }
+ ]
+ }
+ ],
+ "nextCursor": null
+ }
+ ]
+ },
+ "V2PermissionGroup": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Permission group identifier."
+ },
+ "organizationId": {
+ "type": "string",
+ "description": "Organization that owns the group."
+ },
+ "name": {
+ "type": "string",
+ "description": "Group name, unique within the organization."
+ },
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Optional description of the group."
+ },
+ "config": {
+ "type": "object",
+ "properties": {
+ "allowedIntegrations": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Integrations and blocks are limited to this list. Null permits every value; an empty list permits none."
+ },
+ "allowedModelProviders": {
+ "anyOf": [
+ {
"type": "array",
"items": {
"type": "string"
}
+ },
+ {
+ "type": "null"
}
- },
- "additionalProperties": false
- }
- ]
- },
- "canonicalParamId": {
- "description": "Shared key for a picker/manual-entry pair. Both fields write the same value, so supply exactly one of the pair.",
- "type": "string"
- },
- "defaultValue": {
- "description": "Value used when the field is left unset.",
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "number"
- },
- {
- "type": "boolean"
+ ],
+ "description": "Model providers are limited to this list. Null permits every value; an empty list permits none."
},
- {
- "type": "object",
- "propertyNames": {
+ "deniedModels": {
+ "default": [],
+ "type": "array",
+ "items": {
"type": "string"
},
- "additionalProperties": {
- "description": "Member of an object-valued default. Shape varies by field type."
- }
+ "description": "Models listed in this list are blocked."
},
- {
+ "deniedTools": {
+ "default": [],
"type": "array",
"items": {
- "description": "Element of an array-valued default. Shape varies by field type."
- }
- }
- ]
- },
- "hasComputedDefault": {
- "description": "Whether the field derives its value from the block’s other values. The deriving function is not published.",
- "type": "boolean"
- }
- },
- "required": ["id", "type"],
- "additionalProperties": false,
- "title": "Block field",
- "description": "One configuration field on a block."
- },
- "V2CatalogCondition": {
- "type": "object",
- "properties": {
- "field": {
- "type": "string",
- "description": "Sibling field id whose value decides this condition."
- },
- "value": {
- "anyOf": [
- {
- "type": "string"
+ "type": "string"
+ },
+ "description": "Integration tools listed in this list are blocked."
},
- {
- "type": "number"
+ "hideTraceSpans": {
+ "type": "boolean",
+ "description": "Withhold per-block trace spans from logs and from the API."
},
- {
- "type": "boolean"
+ "hideKnowledgeBaseTab": {
+ "type": "boolean",
+ "description": "Revoke the Knowledge Base module. Members cannot open, search, or query any knowledge base."
},
- {
- "type": "array",
- "items": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "number"
- },
- {
- "type": "boolean"
- }
- ]
- }
- }
- ],
- "description": "Value, or set of accepted values, the named field must hold."
- },
- "not": {
- "description": "Invert the match: every value EXCEPT `value`.",
- "type": "boolean"
- },
- "and": {
- "description": "A second clause that must hold as well.",
- "type": "object",
- "properties": {
- "field": {
- "type": "string",
- "description": "Sibling field id for the second clause."
+ "hideTablesTab": {
+ "type": "boolean",
+ "description": "Revoke the Tables module. Members cannot read or write any table."
},
- "value": {
- "description": "Value the second clause matches. Absent means \"holds any value\".",
+ "hideCopilot": {
+ "type": "boolean",
+ "description": "Revoke Chat. Members cannot ask Sim to build or edit anything."
+ },
+ "hideIntegrationsTab": {
+ "type": "boolean",
+ "description": "Revoke integration connections. Members cannot view, add, or remove an OAuth connection."
+ },
+ "hideSecretsTab": {
+ "type": "boolean",
+ "description": "Revoke secrets. Members cannot read, add, or change a workspace environment variable."
+ },
+ "hideApiKeysTab": {
+ "type": "boolean",
+ "description": "Revoke workspace API keys. Members cannot list, create, or revoke one."
+ },
+ "hideInboxTab": {
+ "type": "boolean",
+ "description": "Revoke the Sim Mailer inbox. Members cannot read or send mail."
+ },
+ "hideFilesTab": {
+ "type": "boolean",
+ "description": "Revoke the Files module. Members cannot list, upload, or download workspace files."
+ },
+ "disableMcpTools": {
+ "type": "boolean",
+ "description": "Block agents from calling MCP tools."
+ },
+ "disableCustomTools": {
+ "type": "boolean",
+ "description": "Block agents from calling user-defined custom tools."
+ },
+ "disableSkills": {
+ "type": "boolean",
+ "description": "Block agents from loading skills."
+ },
+ "disableInvitations": {
+ "type": "boolean",
+ "description": "Prevent inviting anyone to a workspace or to the organization."
+ },
+ "disablePublicApi": {
+ "type": "boolean",
+ "description": "Revoke public API access. Calls to a deployed workflow are refused."
+ },
+ "disablePublicFileSharing": {
+ "type": "boolean",
+ "description": "Revoke public file sharing. Members cannot create a share link."
+ },
+ "allowedFileShareAuthTypes": {
"anyOf": [
{
- "type": "string"
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": ["public", "password", "email", "sso"]
+ }
},
{
- "type": "number"
- },
+ "type": "null"
+ }
+ ],
+ "description": "Public file-share authentication is limited to this list. Null permits every value; an empty list permits none."
+ },
+ "hideDeployApi": {
+ "type": "boolean",
+ "description": "Prevent deploying a workflow as an API endpoint."
+ },
+ "hideDeployMcp": {
+ "type": "boolean",
+ "description": "Prevent exposing a workflow as an MCP server."
+ },
+ "hideDeployChatbot": {
+ "type": "boolean",
+ "description": "Prevent publishing a workflow as a chat."
+ },
+ "allowedChatDeployAuthTypes": {
+ "anyOf": [
{
- "type": "boolean"
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": ["public", "password", "email", "sso"]
+ }
},
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Chat deployment authentication is limited to this list. Null permits every value; an empty list permits none."
+ },
+ "disablePersonalApiKeys": {
+ "type": "boolean",
+ "description": "Prevent members from using a personal API key against this workspace."
+ },
+ "disableLogExport": {
+ "type": "boolean",
+ "description": "Prevent downloading execution logs as a CSV."
+ },
+ "hideCostInfo": {
+ "type": "boolean",
+ "description": "Withhold execution cost. Logs and member exports omit cost and token spend; organization-level data drains, configurable by org admins only, are not projected."
+ },
+ "disableKnowledgeBaseCreation": {
+ "type": "boolean",
+ "description": "Prevent creating knowledge bases, leaving existing ones queryable."
+ },
+ "disableKnowledgeBaseFileUpload": {
+ "type": "boolean",
+ "description": "Prevent uploading local documents, leaving sanctioned connectors as the only source."
+ },
+ "allowedKnowledgeConnectors": {
+ "anyOf": [
{
"type": "array",
"items": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "number"
- },
- {
- "type": "boolean"
- }
- ]
+ "type": "string"
}
+ },
+ {
+ "type": "null"
}
- ]
+ ],
+ "description": "Knowledge base connectors are limited to this list. Null permits every value; an empty list permits none."
+ },
+ "disableTableCreation": {
+ "type": "boolean",
+ "description": "Prevent creating tables, leaving existing ones usable."
+ },
+ "disableTableExport": {
+ "type": "boolean",
+ "description": "Prevent downloading a whole table as CSV or JSON."
+ },
+ "disableBulkFileDownload": {
+ "type": "boolean",
+ "description": "Prevent downloading folders as an archive."
+ },
+ "disablePersonalCredentials": {
+ "type": "boolean",
+ "description": "Prevent connecting personal credentials, leaving only workspace-shared ones."
+ },
+ "disableWorkspaceCreation": {
+ "type": "boolean",
+ "description": "Prevent creating new workspaces, which no existing group would govern. Read from the organization's default group, because creating a workspace names none."
+ },
+ "hideOrgMemberDirectory": {
+ "type": "boolean",
+ "description": "Withhold the member directory. Members cannot see the names or email addresses of other members. Read from the organization's default group, because the directory belongs to the organization and names no workspace."
+ },
+ "disableCliAccess": {
+ "type": "boolean",
+ "description": "Prevent approving a CLI login or using Sim CLI OAuth tokens for the public API. A login naming one of this group's workspaces is refused; an account-level login names none, so it is read from the organization's default group."
+ },
+ "disableWebhookTriggers": {
+ "type": "boolean",
+ "description": "Prevent making a workflow reachable from an inbound webhook."
+ },
+ "disableToolAutoApproval": {
+ "type": "boolean",
+ "description": "Prevent silencing a tool confirmation, so every call is confirmed again."
+ },
+ "hideSandboxesTab": {
+ "type": "boolean",
+ "description": "Revoke the Sandboxes module. Members cannot view, create, or change a workspace sandbox."
+ },
+ "disableOAuthAppAccess": {
+ "type": "boolean",
+ "description": "Prevent OAuth apps from accessing this group's workspaces. The organization's default group also governs authorizing apps and refreshing their access."
+ },
+ "disableKnowledgeBaseExport": {
+ "type": "boolean",
+ "description": "Prevent downloading a whole knowledge base as an archive."
+ }
+ },
+ "required": [
+ "allowedIntegrations",
+ "allowedModelProviders",
+ "deniedModels",
+ "deniedTools",
+ "hideTraceSpans",
+ "hideKnowledgeBaseTab",
+ "hideTablesTab",
+ "hideCopilot",
+ "hideIntegrationsTab",
+ "hideSecretsTab",
+ "hideApiKeysTab",
+ "hideInboxTab",
+ "hideFilesTab",
+ "disableMcpTools",
+ "disableCustomTools",
+ "disableSkills",
+ "disableInvitations",
+ "disablePublicApi",
+ "disablePublicFileSharing",
+ "allowedFileShareAuthTypes",
+ "hideDeployApi",
+ "hideDeployMcp",
+ "hideDeployChatbot",
+ "allowedChatDeployAuthTypes",
+ "disablePersonalApiKeys",
+ "disableLogExport",
+ "hideCostInfo",
+ "disableKnowledgeBaseCreation",
+ "disableKnowledgeBaseFileUpload",
+ "allowedKnowledgeConnectors",
+ "disableTableCreation",
+ "disableTableExport",
+ "disableBulkFileDownload",
+ "disablePersonalCredentials",
+ "disableWorkspaceCreation",
+ "hideOrgMemberDirectory",
+ "disableCliAccess",
+ "disableWebhookTriggers",
+ "disableToolAutoApproval",
+ "hideSandboxesTab",
+ "disableOAuthAppAccess",
+ "disableKnowledgeBaseExport"
+ ],
+ "additionalProperties": false,
+ "description": "Resolved restrictions. True disables a boolean capability; null allowlists permit every value and empty allowlists permit none."
+ },
+ "isDefault": {
+ "type": "boolean",
+ "description": "Whether this is the organization default, which applies to everyone across all its workspaces regardless of member assignments."
+ },
+ "membershipMode": {
+ "type": "string",
+ "description": "An empty inherit group governs everyone in its workspaces; an empty explicit group governs nobody."
+ },
+ "workspaceIds": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Workspaces governed by a non-default group. Empty for the default group."
+ },
+ "createdBy": {
+ "type": "string",
+ "description": "User who created the group."
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
+ "description": "When the group was created."
+ },
+ "updatedAt": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
+ "description": "When the group was last updated."
+ }
+ },
+ "required": [
+ "id",
+ "organizationId",
+ "name",
+ "description",
+ "config",
+ "isDefault",
+ "membershipMode",
+ "workspaceIds",
+ "createdBy",
+ "createdAt",
+ "updatedAt"
+ ],
+ "additionalProperties": false,
+ "title": "Permission group",
+ "description": "An organization permission group and its resolved restrictions."
+ },
+ "ListPermissionGroupsResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/V2PermissionGroup"
+ },
+ "description": "Items in the current page."
+ },
+ "nextCursor": {
+ "anyOf": [
+ {
+ "type": "string"
},
- "not": {
- "description": "Invert the second clause.",
- "type": "boolean"
+ {
+ "type": "null"
}
- },
- "required": ["field"],
- "additionalProperties": false
+ ],
+ "description": "Opaque cursor for the next page. Send it back as `cursor`; `null` means there is nothing further to fetch. Never construct one yourself."
}
},
- "required": ["field", "value"],
+ "required": ["data", "nextCursor"],
"additionalProperties": false,
- "title": "Catalog condition",
- "description": "When a configuration field applies, expressed against a sibling field."
+ "title": "List Permission Groups response",
+ "description": "List Permission Groups result.",
+ "examples": [
+ {
+ "data": [
+ {
+ "id": "group-123",
+ "organizationId": "org-123",
+ "name": "Restricted",
+ "description": null,
+ "config": {
+ "allowedIntegrations": null,
+ "allowedModelProviders": null,
+ "deniedModels": [],
+ "deniedTools": [],
+ "hideTraceSpans": false,
+ "hideKnowledgeBaseTab": false,
+ "hideTablesTab": false,
+ "hideCopilot": false,
+ "hideIntegrationsTab": false,
+ "hideSecretsTab": false,
+ "hideApiKeysTab": false,
+ "hideInboxTab": false,
+ "hideFilesTab": false,
+ "disableMcpTools": false,
+ "disableCustomTools": false,
+ "disableSkills": false,
+ "disableInvitations": false,
+ "disablePublicApi": false,
+ "disablePublicFileSharing": false,
+ "allowedFileShareAuthTypes": null,
+ "hideDeployApi": false,
+ "hideDeployMcp": false,
+ "hideDeployChatbot": false,
+ "allowedChatDeployAuthTypes": null,
+ "disablePersonalApiKeys": false,
+ "disableLogExport": false,
+ "hideCostInfo": false,
+ "disableKnowledgeBaseCreation": false,
+ "disableKnowledgeBaseFileUpload": false,
+ "allowedKnowledgeConnectors": null,
+ "disableTableCreation": false,
+ "disableTableExport": false,
+ "disableBulkFileDownload": false,
+ "disablePersonalCredentials": false,
+ "disableWorkspaceCreation": false,
+ "hideOrgMemberDirectory": false,
+ "disableCliAccess": false,
+ "disableWebhookTriggers": false,
+ "disableToolAutoApproval": false,
+ "hideSandboxesTab": false,
+ "disableOAuthAppAccess": false,
+ "disableKnowledgeBaseExport": false
+ },
+ "isDefault": false,
+ "membershipMode": "inherit",
+ "workspaceIds": ["workspace-123"],
+ "createdBy": "admin-123",
+ "createdAt": "2026-06-01T09:00:00.000Z",
+ "updatedAt": "2026-06-01T09:00:00.000Z"
+ }
+ ],
+ "nextCursor": null
+ }
+ ]
},
- "V2OperationInput": {
+ "CreatePermissionGroupResponse": {
"type": "object",
"properties": {
- "type": {
- "type": "string",
- "description": "Value type."
- },
- "required": {
- "description": "Whether the value must be supplied.",
- "type": "boolean"
- },
- "visibility": {
- "description": "Who may supply the value: `user-or-llm`, `user-only`, `llm-only`, or `hidden`.",
- "type": "string"
- },
- "description": {
- "description": "What the value means.",
- "type": "string"
- },
- "default": {
- "description": "Value used when this input is omitted."
- },
- "items": {
- "description": "JSON-Schema-shaped constraints declared by the tool parameter."
- },
- "schema": {
- "description": "JSON-Schema-shaped structure declared by the block input."
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2PermissionGroup"
}
},
- "required": ["type"],
+ "required": ["data"],
"additionalProperties": false,
- "title": "Operation input",
- "description": "One value a block operation needs, from its tool or its block-level inputs."
+ "title": "Create Permission Group response",
+ "description": "Create Permission Group result.",
+ "examples": [
+ {
+ "data": {
+ "id": "group-123",
+ "organizationId": "org-123",
+ "name": "Restricted",
+ "description": null,
+ "config": {
+ "allowedIntegrations": null,
+ "allowedModelProviders": null,
+ "deniedModels": [],
+ "deniedTools": [],
+ "hideTraceSpans": false,
+ "hideKnowledgeBaseTab": false,
+ "hideTablesTab": false,
+ "hideCopilot": false,
+ "hideIntegrationsTab": false,
+ "hideSecretsTab": false,
+ "hideApiKeysTab": false,
+ "hideInboxTab": false,
+ "hideFilesTab": false,
+ "disableMcpTools": false,
+ "disableCustomTools": false,
+ "disableSkills": false,
+ "disableInvitations": false,
+ "disablePublicApi": false,
+ "disablePublicFileSharing": false,
+ "allowedFileShareAuthTypes": null,
+ "hideDeployApi": false,
+ "hideDeployMcp": false,
+ "hideDeployChatbot": false,
+ "allowedChatDeployAuthTypes": null,
+ "disablePersonalApiKeys": false,
+ "disableLogExport": false,
+ "hideCostInfo": false,
+ "disableKnowledgeBaseCreation": false,
+ "disableKnowledgeBaseFileUpload": false,
+ "allowedKnowledgeConnectors": null,
+ "disableTableCreation": false,
+ "disableTableExport": false,
+ "disableBulkFileDownload": false,
+ "disablePersonalCredentials": false,
+ "disableWorkspaceCreation": false,
+ "hideOrgMemberDirectory": false,
+ "disableCliAccess": false,
+ "disableWebhookTriggers": false,
+ "disableToolAutoApproval": false,
+ "hideSandboxesTab": false,
+ "disableOAuthAppAccess": false,
+ "disableKnowledgeBaseExport": false
+ },
+ "isDefault": false,
+ "membershipMode": "inherit",
+ "workspaceIds": ["workspace-123"],
+ "createdBy": "admin-123",
+ "createdAt": "2026-06-01T09:00:00.000Z",
+ "updatedAt": "2026-06-01T09:00:00.000Z"
+ }
+ }
+ ]
},
- "V2ToolOutput": {
+ "CreatePermissionGroupRequest": {
"type": "object",
"properties": {
- "type": {
+ "name": {
"type": "string",
- "description": "Value type of the output field."
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Group name, unique within the organization."
},
"description": {
- "description": "What the field holds.",
- "type": "string"
- },
- "optional": {
- "description": "Whether the field may be absent.",
- "type": "boolean"
- },
- "nullable": {
- "description": "Whether the field may be null.",
- "type": "boolean"
- },
- "properties": {
- "description": "Members of an object-typed output, keyed by field name.",
- "type": "object",
- "propertyNames": {
- "type": "string"
- },
- "additionalProperties": {
- "description": "Nested output field, in this same shape."
- }
+ "description": "Optional group description.",
+ "type": "string",
+ "maxLength": 500
},
- "items": {
- "description": "Element shape of an array-typed output.",
+ "config": {
"type": "object",
"properties": {
- "type": {
- "type": "string",
- "description": "Element value type."
+ "allowedIntegrations": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Integrations and blocks are limited to this list. Null permits every value; an empty list permits none."
},
- "description": {
- "description": "What an element holds.",
- "type": "string"
+ "allowedModelProviders": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Model providers are limited to this list. Null permits every value; an empty list permits none."
},
- "properties": {
- "description": "Members of an object-typed element, keyed by field name.",
- "type": "object",
- "propertyNames": {
+ "deniedModels": {
+ "type": "array",
+ "items": {
"type": "string"
},
- "additionalProperties": {
- "description": "Nested output field, in this same shape."
- }
- }
- },
- "required": ["type"],
- "additionalProperties": false
- },
- "fileConfig": {
- "description": "File metadata for a file-typed output.",
- "type": "object",
- "properties": {
- "mimeType": {
- "description": "MIME type of the produced file.",
- "type": "string"
+ "description": "Models listed in this list are blocked."
+ },
+ "deniedTools": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Integration tools listed in this list are blocked."
+ },
+ "hideTraceSpans": {
+ "type": "boolean",
+ "description": "Withhold per-block trace spans from logs and from the API."
+ },
+ "hideKnowledgeBaseTab": {
+ "type": "boolean",
+ "description": "Revoke the Knowledge Base module. Members cannot open, search, or query any knowledge base."
+ },
+ "hideTablesTab": {
+ "type": "boolean",
+ "description": "Revoke the Tables module. Members cannot read or write any table."
+ },
+ "hideCopilot": {
+ "type": "boolean",
+ "description": "Revoke Chat. Members cannot ask Sim to build or edit anything."
+ },
+ "hideIntegrationsTab": {
+ "type": "boolean",
+ "description": "Revoke integration connections. Members cannot view, add, or remove an OAuth connection."
+ },
+ "hideSecretsTab": {
+ "type": "boolean",
+ "description": "Revoke secrets. Members cannot read, add, or change a workspace environment variable."
+ },
+ "hideApiKeysTab": {
+ "type": "boolean",
+ "description": "Revoke workspace API keys. Members cannot list, create, or revoke one."
+ },
+ "hideInboxTab": {
+ "type": "boolean",
+ "description": "Revoke the Sim Mailer inbox. Members cannot read or send mail."
+ },
+ "hideFilesTab": {
+ "type": "boolean",
+ "description": "Revoke the Files module. Members cannot list, upload, or download workspace files."
+ },
+ "disableMcpTools": {
+ "type": "boolean",
+ "description": "Block agents from calling MCP tools."
+ },
+ "disableCustomTools": {
+ "type": "boolean",
+ "description": "Block agents from calling user-defined custom tools."
+ },
+ "disableSkills": {
+ "type": "boolean",
+ "description": "Block agents from loading skills."
+ },
+ "disableInvitations": {
+ "type": "boolean",
+ "description": "Prevent inviting anyone to a workspace or to the organization."
+ },
+ "disablePublicApi": {
+ "type": "boolean",
+ "description": "Revoke public API access. Calls to a deployed workflow are refused."
+ },
+ "disablePublicFileSharing": {
+ "type": "boolean",
+ "description": "Revoke public file sharing. Members cannot create a share link."
+ },
+ "allowedFileShareAuthTypes": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": ["public", "password", "email", "sso"]
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Public file-share authentication is limited to this list. Null permits every value; an empty list permits none."
+ },
+ "hideDeployApi": {
+ "type": "boolean",
+ "description": "Prevent deploying a workflow as an API endpoint."
+ },
+ "hideDeployMcp": {
+ "type": "boolean",
+ "description": "Prevent exposing a workflow as an MCP server."
+ },
+ "hideDeployChatbot": {
+ "type": "boolean",
+ "description": "Prevent publishing a workflow as a chat."
+ },
+ "allowedChatDeployAuthTypes": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": ["public", "password", "email", "sso"]
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Chat deployment authentication is limited to this list. Null permits every value; an empty list permits none."
+ },
+ "disablePersonalApiKeys": {
+ "type": "boolean",
+ "description": "Prevent members from using a personal API key against this workspace."
+ },
+ "disableLogExport": {
+ "type": "boolean",
+ "description": "Prevent downloading execution logs as a CSV."
+ },
+ "hideCostInfo": {
+ "type": "boolean",
+ "description": "Withhold execution cost. Logs and member exports omit cost and token spend; organization-level data drains, configurable by org admins only, are not projected."
+ },
+ "disableKnowledgeBaseCreation": {
+ "type": "boolean",
+ "description": "Prevent creating knowledge bases, leaving existing ones queryable."
+ },
+ "disableKnowledgeBaseFileUpload": {
+ "type": "boolean",
+ "description": "Prevent uploading local documents, leaving sanctioned connectors as the only source."
+ },
+ "allowedKnowledgeConnectors": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Knowledge base connectors are limited to this list. Null permits every value; an empty list permits none."
+ },
+ "disableTableCreation": {
+ "type": "boolean",
+ "description": "Prevent creating tables, leaving existing ones usable."
+ },
+ "disableTableExport": {
+ "type": "boolean",
+ "description": "Prevent downloading a whole table as CSV or JSON."
+ },
+ "disableBulkFileDownload": {
+ "type": "boolean",
+ "description": "Prevent downloading folders as an archive."
+ },
+ "disablePersonalCredentials": {
+ "type": "boolean",
+ "description": "Prevent connecting personal credentials, leaving only workspace-shared ones."
+ },
+ "disableWorkspaceCreation": {
+ "type": "boolean",
+ "description": "Prevent creating new workspaces, which no existing group would govern. Read from the organization's default group, because creating a workspace names none."
+ },
+ "hideOrgMemberDirectory": {
+ "type": "boolean",
+ "description": "Withhold the member directory. Members cannot see the names or email addresses of other members. Read from the organization's default group, because the directory belongs to the organization and names no workspace."
+ },
+ "disableCliAccess": {
+ "type": "boolean",
+ "description": "Prevent approving a CLI login or using Sim CLI OAuth tokens for the public API. A login naming one of this group's workspaces is refused; an account-level login names none, so it is read from the organization's default group."
+ },
+ "disableWebhookTriggers": {
+ "type": "boolean",
+ "description": "Prevent making a workflow reachable from an inbound webhook."
+ },
+ "disableToolAutoApproval": {
+ "type": "boolean",
+ "description": "Prevent silencing a tool confirmation, so every call is confirmed again."
+ },
+ "hideSandboxesTab": {
+ "type": "boolean",
+ "description": "Revoke the Sandboxes module. Members cannot view, create, or change a workspace sandbox."
+ },
+ "disableOAuthAppAccess": {
+ "type": "boolean",
+ "description": "Prevent OAuth apps from accessing this group's workspaces. The organization's default group also governs authorizing apps and refreshing their access."
},
- "extension": {
- "description": "File extension of the produced file.",
- "type": "string"
+ "disableKnowledgeBaseExport": {
+ "type": "boolean",
+ "description": "Prevent downloading a whole knowledge base as an archive."
}
},
- "additionalProperties": false
+ "additionalProperties": false,
+ "description": "Permission restrictions to set. Omitted keys use the default permission configuration."
+ },
+ "isDefault": {
+ "description": "Whether the group is the organization default. Only one group can be the default.",
+ "type": "boolean"
+ },
+ "workspaceIds": {
+ "description": "Workspace IDs targeted by a non-default group. Required when creating a non-default group; omit for a default group.",
+ "maxItems": 500,
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minLength": 1
+ }
}
},
- "required": ["type"],
+ "required": ["name"],
"additionalProperties": false,
- "title": "Tool output",
- "description": "One declared output field of a built-in tool."
+ "title": "Create Permission Group request",
+ "description": "Create Permission Group inputs.",
+ "examples": [
+ {
+ "name": "Restricted",
+ "workspaceIds": ["workspace-123"]
+ }
+ ]
},
- "V2ToolDetail": {
+ "GetPermissionGroupResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2PermissionGroup"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Get Permission Group response",
+ "description": "Get Permission Group result.",
+ "examples": [
+ {
+ "data": {
+ "id": "group-123",
+ "organizationId": "org-123",
+ "name": "Restricted",
+ "description": null,
+ "config": {
+ "allowedIntegrations": null,
+ "allowedModelProviders": null,
+ "deniedModels": [],
+ "deniedTools": [],
+ "hideTraceSpans": false,
+ "hideKnowledgeBaseTab": false,
+ "hideTablesTab": false,
+ "hideCopilot": false,
+ "hideIntegrationsTab": false,
+ "hideSecretsTab": false,
+ "hideApiKeysTab": false,
+ "hideInboxTab": false,
+ "hideFilesTab": false,
+ "disableMcpTools": false,
+ "disableCustomTools": false,
+ "disableSkills": false,
+ "disableInvitations": false,
+ "disablePublicApi": false,
+ "disablePublicFileSharing": false,
+ "allowedFileShareAuthTypes": null,
+ "hideDeployApi": false,
+ "hideDeployMcp": false,
+ "hideDeployChatbot": false,
+ "allowedChatDeployAuthTypes": null,
+ "disablePersonalApiKeys": false,
+ "disableLogExport": false,
+ "hideCostInfo": false,
+ "disableKnowledgeBaseCreation": false,
+ "disableKnowledgeBaseFileUpload": false,
+ "allowedKnowledgeConnectors": null,
+ "disableTableCreation": false,
+ "disableTableExport": false,
+ "disableBulkFileDownload": false,
+ "disablePersonalCredentials": false,
+ "disableWorkspaceCreation": false,
+ "hideOrgMemberDirectory": false,
+ "disableCliAccess": false,
+ "disableWebhookTriggers": false,
+ "disableToolAutoApproval": false,
+ "hideSandboxesTab": false,
+ "disableOAuthAppAccess": false,
+ "disableKnowledgeBaseExport": false
+ },
+ "isDefault": false,
+ "membershipMode": "inherit",
+ "workspaceIds": ["workspace-123"],
+ "createdBy": "admin-123",
+ "createdAt": "2026-06-01T09:00:00.000Z",
+ "updatedAt": "2026-06-01T09:00:00.000Z"
+ }
+ }
+ ]
+ },
+ "UpdatePermissionGroupResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2PermissionGroup"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Update Permission Group response",
+ "description": "Update Permission Group result.",
+ "examples": [
+ {
+ "data": {
+ "id": "group-123",
+ "organizationId": "org-123",
+ "name": "Restricted",
+ "description": null,
+ "config": {
+ "allowedIntegrations": null,
+ "allowedModelProviders": null,
+ "deniedModels": [],
+ "deniedTools": [],
+ "hideTraceSpans": false,
+ "hideKnowledgeBaseTab": false,
+ "hideTablesTab": false,
+ "hideCopilot": false,
+ "hideIntegrationsTab": false,
+ "hideSecretsTab": false,
+ "hideApiKeysTab": false,
+ "hideInboxTab": false,
+ "hideFilesTab": false,
+ "disableMcpTools": false,
+ "disableCustomTools": false,
+ "disableSkills": false,
+ "disableInvitations": false,
+ "disablePublicApi": false,
+ "disablePublicFileSharing": false,
+ "allowedFileShareAuthTypes": null,
+ "hideDeployApi": false,
+ "hideDeployMcp": false,
+ "hideDeployChatbot": false,
+ "allowedChatDeployAuthTypes": null,
+ "disablePersonalApiKeys": false,
+ "disableLogExport": false,
+ "hideCostInfo": false,
+ "disableKnowledgeBaseCreation": false,
+ "disableKnowledgeBaseFileUpload": false,
+ "allowedKnowledgeConnectors": null,
+ "disableTableCreation": false,
+ "disableTableExport": false,
+ "disableBulkFileDownload": false,
+ "disablePersonalCredentials": false,
+ "disableWorkspaceCreation": false,
+ "hideOrgMemberDirectory": false,
+ "disableCliAccess": false,
+ "disableWebhookTriggers": false,
+ "disableToolAutoApproval": false,
+ "hideSandboxesTab": false,
+ "disableOAuthAppAccess": false,
+ "disableKnowledgeBaseExport": false
+ },
+ "isDefault": false,
+ "membershipMode": "inherit",
+ "workspaceIds": ["workspace-123"],
+ "createdBy": "admin-123",
+ "createdAt": "2026-06-01T09:00:00.000Z",
+ "updatedAt": "2026-06-01T09:00:00.000Z"
+ }
+ }
+ ]
+ },
+ "UpdatePermissionGroupRequest": {
"type": "object",
"properties": {
- "id": {
- "type": "string",
- "description": "Registered tool identifier, including its version suffix."
- },
"name": {
"type": "string",
- "description": "Display name."
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Group name, unique within the organization."
},
"description": {
- "type": "string",
- "description": "What the tool does."
- },
- "version": {
- "description": "Tool version.",
- "type": "string"
- },
- "hostedApiKey": {
- "type": "string",
- "enum": ["always", "conditional", "none"],
- "description": "Whether Sim supplies the API key on THIS deployment: `always`, `conditional` (only for some parameter combinations), or `none` (bring your own). Self-hosted deployments supply no hosted keys, so every tool reports `none` there regardless of what it declares."
+ "description": "Group description. Null or an empty string clears it; omission leaves it unchanged.",
+ "anyOf": [
+ {
+ "type": "string",
+ "maxLength": 500
+ },
+ {
+ "type": "null"
+ }
+ ]
},
- "oauth": {
- "description": "OAuth requirement, when the tool has one.",
+ "config": {
"type": "object",
"properties": {
- "required": {
- "type": "boolean",
- "description": "Whether the tool cannot run without an OAuth credential."
+ "allowedIntegrations": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Integrations and blocks are limited to this list. Null permits every value; an empty list permits none."
},
- "provider": {
- "type": "string",
- "description": "OAuth service the credential must authenticate."
+ "allowedModelProviders": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Model providers are limited to this list. Null permits every value; an empty list permits none."
},
- "requiredScopes": {
- "description": "Scopes the credential must carry.",
+ "deniedModels": {
"type": "array",
"items": {
"type": "string"
- }
+ },
+ "description": "Models listed in this list are blocked."
+ },
+ "deniedTools": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Integration tools listed in this list are blocked."
+ },
+ "hideTraceSpans": {
+ "type": "boolean",
+ "description": "Withhold per-block trace spans from logs and from the API."
+ },
+ "hideKnowledgeBaseTab": {
+ "type": "boolean",
+ "description": "Revoke the Knowledge Base module. Members cannot open, search, or query any knowledge base."
+ },
+ "hideTablesTab": {
+ "type": "boolean",
+ "description": "Revoke the Tables module. Members cannot read or write any table."
+ },
+ "hideCopilot": {
+ "type": "boolean",
+ "description": "Revoke Chat. Members cannot ask Sim to build or edit anything."
+ },
+ "hideIntegrationsTab": {
+ "type": "boolean",
+ "description": "Revoke integration connections. Members cannot view, add, or remove an OAuth connection."
+ },
+ "hideSecretsTab": {
+ "type": "boolean",
+ "description": "Revoke secrets. Members cannot read, add, or change a workspace environment variable."
+ },
+ "hideApiKeysTab": {
+ "type": "boolean",
+ "description": "Revoke workspace API keys. Members cannot list, create, or revoke one."
+ },
+ "hideInboxTab": {
+ "type": "boolean",
+ "description": "Revoke the Sim Mailer inbox. Members cannot read or send mail."
+ },
+ "hideFilesTab": {
+ "type": "boolean",
+ "description": "Revoke the Files module. Members cannot list, upload, or download workspace files."
+ },
+ "disableMcpTools": {
+ "type": "boolean",
+ "description": "Block agents from calling MCP tools."
+ },
+ "disableCustomTools": {
+ "type": "boolean",
+ "description": "Block agents from calling user-defined custom tools."
+ },
+ "disableSkills": {
+ "type": "boolean",
+ "description": "Block agents from loading skills."
+ },
+ "disableInvitations": {
+ "type": "boolean",
+ "description": "Prevent inviting anyone to a workspace or to the organization."
+ },
+ "disablePublicApi": {
+ "type": "boolean",
+ "description": "Revoke public API access. Calls to a deployed workflow are refused."
+ },
+ "disablePublicFileSharing": {
+ "type": "boolean",
+ "description": "Revoke public file sharing. Members cannot create a share link."
+ },
+ "allowedFileShareAuthTypes": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": ["public", "password", "email", "sso"]
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Public file-share authentication is limited to this list. Null permits every value; an empty list permits none."
+ },
+ "hideDeployApi": {
+ "type": "boolean",
+ "description": "Prevent deploying a workflow as an API endpoint."
+ },
+ "hideDeployMcp": {
+ "type": "boolean",
+ "description": "Prevent exposing a workflow as an MCP server."
+ },
+ "hideDeployChatbot": {
+ "type": "boolean",
+ "description": "Prevent publishing a workflow as a chat."
+ },
+ "allowedChatDeployAuthTypes": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": ["public", "password", "email", "sso"]
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Chat deployment authentication is limited to this list. Null permits every value; an empty list permits none."
+ },
+ "disablePersonalApiKeys": {
+ "type": "boolean",
+ "description": "Prevent members from using a personal API key against this workspace."
+ },
+ "disableLogExport": {
+ "type": "boolean",
+ "description": "Prevent downloading execution logs as a CSV."
+ },
+ "hideCostInfo": {
+ "type": "boolean",
+ "description": "Withhold execution cost. Logs and member exports omit cost and token spend; organization-level data drains, configurable by org admins only, are not projected."
+ },
+ "disableKnowledgeBaseCreation": {
+ "type": "boolean",
+ "description": "Prevent creating knowledge bases, leaving existing ones queryable."
+ },
+ "disableKnowledgeBaseFileUpload": {
+ "type": "boolean",
+ "description": "Prevent uploading local documents, leaving sanctioned connectors as the only source."
+ },
+ "allowedKnowledgeConnectors": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Knowledge base connectors are limited to this list. Null permits every value; an empty list permits none."
+ },
+ "disableTableCreation": {
+ "type": "boolean",
+ "description": "Prevent creating tables, leaving existing ones usable."
+ },
+ "disableTableExport": {
+ "type": "boolean",
+ "description": "Prevent downloading a whole table as CSV or JSON."
+ },
+ "disableBulkFileDownload": {
+ "type": "boolean",
+ "description": "Prevent downloading folders as an archive."
+ },
+ "disablePersonalCredentials": {
+ "type": "boolean",
+ "description": "Prevent connecting personal credentials, leaving only workspace-shared ones."
+ },
+ "disableWorkspaceCreation": {
+ "type": "boolean",
+ "description": "Prevent creating new workspaces, which no existing group would govern. Read from the organization's default group, because creating a workspace names none."
+ },
+ "hideOrgMemberDirectory": {
+ "type": "boolean",
+ "description": "Withhold the member directory. Members cannot see the names or email addresses of other members. Read from the organization's default group, because the directory belongs to the organization and names no workspace."
+ },
+ "disableCliAccess": {
+ "type": "boolean",
+ "description": "Prevent approving a CLI login or using Sim CLI OAuth tokens for the public API. A login naming one of this group's workspaces is refused; an account-level login names none, so it is read from the organization's default group."
+ },
+ "disableWebhookTriggers": {
+ "type": "boolean",
+ "description": "Prevent making a workflow reachable from an inbound webhook."
+ },
+ "disableToolAutoApproval": {
+ "type": "boolean",
+ "description": "Prevent silencing a tool confirmation, so every call is confirmed again."
+ },
+ "hideSandboxesTab": {
+ "type": "boolean",
+ "description": "Revoke the Sandboxes module. Members cannot view, create, or change a workspace sandbox."
+ },
+ "disableOAuthAppAccess": {
+ "type": "boolean",
+ "description": "Prevent OAuth apps from accessing this group's workspaces. The organization's default group also governs authorizing apps and refreshing their access."
+ },
+ "disableKnowledgeBaseExport": {
+ "type": "boolean",
+ "description": "Prevent downloading a whole knowledge base as an archive."
}
},
- "required": ["required", "provider"],
- "additionalProperties": false
+ "additionalProperties": false,
+ "description": "Patch of permission restrictions. Omitted keys remain unchanged; each supplied array replaces that entire list."
},
- "params": {
- "type": "object",
- "propertyNames": {
- "type": "string"
- },
- "additionalProperties": {
- "$ref": "#/components/schemas/V2ToolParam"
- },
- "description": "Parameters the tool accepts."
+ "isDefault": {
+ "description": "Whether the group is the organization default. Only one group can be the default.",
+ "type": "boolean"
},
- "outputs": {
- "type": "object",
- "propertyNames": {
- "type": "string"
- },
- "additionalProperties": {
- "$ref": "#/components/schemas/V2ToolOutput"
- },
- "description": "Fields the tool produces."
+ "workspaceIds": {
+ "description": "Workspace identifiers for a non-default group. Required on creation; an empty update makes the group inactive.",
+ "maxItems": 500,
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ },
+ "additionalProperties": false,
+ "title": "Update Permission Group request",
+ "description": "Update Permission Group inputs.",
+ "examples": [
+ {
+ "description": "Restricted workspace access"
+ }
+ ]
+ },
+ "V2PermissionGroupDeletion": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Deleted permission group identifier."
+ },
+ "deleted": {
+ "type": "boolean",
+ "const": true,
+ "description": "Whether the group was permanently deleted."
}
},
- "required": ["id", "name", "description", "hostedApiKey", "params", "outputs"],
+ "required": ["id", "deleted"],
"additionalProperties": false,
- "title": "Tool",
- "description": "A built-in tool with its declared parameters and outputs."
+ "title": "Permission group deletion",
+ "description": "Acknowledges permanent group deletion."
},
- "V2ToolParam": {
+ "DeletePermissionGroupResponse": {
"type": "object",
"properties": {
- "type": {
- "type": "string",
- "description": "Parameter value type."
- },
- "required": {
- "description": "Whether the parameter must be supplied.",
- "type": "boolean"
- },
- "visibility": {
- "description": "Who may supply the value: `user-or-llm`, `user-only`, `llm-only`, or `hidden`.",
- "type": "string"
- },
- "description": {
- "description": "What the parameter means.",
- "type": "string"
- },
- "default": {
- "description": "Value used when the parameter is omitted."
- },
- "items": {
- "description": "JSON-Schema-shaped constraints for structured params."
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2PermissionGroupDeletion"
}
},
- "required": ["type"],
+ "required": ["data"],
"additionalProperties": false,
- "title": "Tool parameter",
- "description": "One declared parameter of a built-in tool."
+ "title": "Delete Permission Group response",
+ "description": "Delete Permission Group result.",
+ "examples": [
+ {
+ "data": {
+ "id": "group-123",
+ "deleted": true
+ }
+ }
+ ]
},
- "V2BlockDetail": {
+ "V2PermissionGroupMember": {
"type": "object",
"properties": {
"id": {
"type": "string",
- "description": "Block type identifier, used as a workflow block’s `type`."
- },
- "name": {
- "type": "string",
- "description": "Display name."
- },
- "description": {
- "type": "string",
- "description": "One-line summary of what the block does."
+ "description": "Membership assignment identifier."
},
- "longDescription": {
- "description": "Extended explanation, when the block has one.",
- "type": "string"
- },
- "category": {
+ "userId": {
"type": "string",
- "description": "Toolbar category: `blocks`, `tools`, or `triggers`."
- },
- "integrationType": {
- "description": "Integration category, e.g. `communication`, `databases`.",
- "type": "string"
+ "description": "Organization member assigned to the group."
},
- "source": {
+ "assignedAt": {
"type": "string",
- "enum": ["builtin", "custom"],
- "description": "Block source: `builtin` for built-in blocks, or `custom` for workflows this workspace deployed as blocks."
- },
- "authMode": {
- "description": "How the block authenticates: `oauth`, `api_key`, or `bot_token`.",
- "type": "string"
- },
- "triggerAllowed": {
- "type": "boolean",
- "description": "Whether the block declares itself usable as a trigger."
- },
- "triggerCapable": {
- "type": "boolean",
- "description": "Whether the block can start a workflow — a trigger-category block, one declaring `triggerAllowed`, or one with trigger-mode fields."
- },
- "triggerIds": {
- "type": "array",
- "items": {
- "type": "string"
- },
- "description": "Identifiers of the triggers this block supports."
- },
- "toolIds": {
- "type": "array",
- "items": {
- "type": "string"
- },
- "description": "Built-in tools this block can run. Read a tool by its id for the full definition."
- },
- "operationIds": {
- "type": "array",
- "items": {
- "type": "string"
- },
- "description": "Operations this block exposes. Their fields and tools are on the block read."
- },
- "preview": {
- "type": "boolean",
- "description": "Whether the block is unreleased and revealed only to this caller."
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
+ "description": "When the member was assigned."
},
- "sunset": {
- "description": "Post-release lifecycle state. Absent for a block in normal support.",
- "type": "object",
- "properties": {
- "status": {
- "type": "string",
- "enum": ["legacy", "deprecated"],
- "description": "`legacy` is superseded but supported; `deprecated` is slated for removal."
- },
- "replacedBy": {
- "description": "Block type to migrate to, when one exists.",
+ "userName": {
+ "anyOf": [
+ {
"type": "string"
+ },
+ {
+ "type": "null"
}
- },
- "required": ["status"],
- "additionalProperties": false
- },
- "docsLink": {
- "description": "Sim documentation page for the integration.",
- "type": "string"
- },
- "tags": {
- "type": "array",
- "items": {
- "type": "string"
- },
- "description": "Catalog tags, e.g. `messaging`, `version-control`."
- },
- "bestPractices": {
- "description": "Authored guidance on using the block correctly.",
- "type": "string"
- },
- "inputSchema": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/V2BlockField"
- },
- "description": "Configuration fields that apply regardless of the selected operation."
- },
- "operationInputSchema": {
- "type": "object",
- "propertyNames": {
- "type": "string"
- },
- "additionalProperties": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/V2BlockField"
- }
- },
- "description": "Configuration fields keyed by the operation that reveals them."
+ ],
+ "description": "Member display name."
},
- "inputDefinitions": {
- "type": "object",
- "propertyNames": {
- "type": "string"
- },
- "additionalProperties": {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "description": "Value type: `string`, `number`, `boolean`, `json`, `array`, or `file`."
- },
- "description": {
- "description": "What the input means.",
- "type": "string"
- },
- "schema": {
- "description": "JSON-Schema-shaped structure for object and array inputs."
- }
+ "userEmail": {
+ "anyOf": [
+ {
+ "type": "string"
},
- "required": ["type"],
- "additionalProperties": false
- },
- "description": "Block-level input definitions, keyed by parameter name."
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Member email address."
},
- "operations": {
- "type": "object",
- "propertyNames": {
- "type": "string"
- },
- "additionalProperties": {
- "type": "object",
- "properties": {
- "toolId": {
- "description": "Built-in tool that performs this operation.",
- "type": "string"
- },
- "toolName": {
- "description": "Display name of that tool.",
- "type": "string"
- },
- "description": {
- "description": "What the operation does.",
- "type": "string"
- },
- "inputs": {
- "type": "object",
- "propertyNames": {
- "type": "string"
- },
- "additionalProperties": {
- "$ref": "#/components/schemas/V2OperationInput"
- },
- "description": "Values this operation needs, excluding the ones the block supplies from its own block-level inputs."
- },
- "outputs": {
- "type": "object",
- "propertyNames": {
- "type": "string"
- },
- "additionalProperties": {
- "$ref": "#/components/schemas/V2ToolOutput"
- },
- "description": "Fields the operation produces."
- },
- "inputSchema": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/V2BlockField"
- },
- "description": "Configuration fields that appear when this operation is selected."
- }
+ "userImage": {
+ "anyOf": [
+ {
+ "type": "string"
},
- "required": ["inputs", "outputs", "inputSchema"],
- "additionalProperties": false
- },
- "description": "Operations the block exposes, keyed by operation id."
- },
- "tools": {
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Member avatar URL."
+ }
+ },
+ "required": ["id", "userId", "assignedAt", "userName", "userEmail", "userImage"],
+ "additionalProperties": false,
+ "title": "Permission group member",
+ "description": "An explicit permission-group membership assignment."
+ },
+ "ListPermissionGroupMembersResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
"type": "array",
"items": {
- "$ref": "#/components/schemas/V2ToolDetail"
+ "$ref": "#/components/schemas/V2PermissionGroupMember"
},
- "description": "Every built-in tool the block can run, with parameters and outputs."
+ "description": "Items in the current page."
},
- "triggers": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string",
- "description": "Trigger identifier."
- },
- "outputs": {
- "type": "object",
- "propertyNames": {
- "type": "string"
- },
- "additionalProperties": {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "description": "Value type of the output."
- },
- "description": {
- "description": "What the output holds.",
- "type": "string"
- }
- },
- "required": ["type"],
- "additionalProperties": false
- },
- "description": "Top-level fields the trigger event delivers."
- },
- "configFields": {
- "type": "object",
- "propertyNames": {
- "type": "string"
- },
- "additionalProperties": {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "description": "Editor control the field renders as."
- },
- "required": {
- "type": "boolean",
- "description": "Whether a value must be supplied."
- },
- "title": {
- "description": "Human-readable label.",
- "type": "string"
- },
- "description": {
- "description": "Authored explanation of the field.",
- "type": "string"
- },
- "placeholder": {
- "description": "Placeholder shown in the editor.",
- "type": "string"
- },
- "default": {
- "description": "Value used when the field is left unset."
- },
- "options": {
- "description": "Selectable options.",
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string",
- "description": "Value stored when this option is selected."
- },
- "label": {
- "type": "string",
- "description": "Human-readable option label."
- }
- },
- "required": ["id", "label"],
- "additionalProperties": false
- }
- },
- "condition": {
- "description": "Condition under which the field applies.",
- "$ref": "#/components/schemas/V2CatalogCondition"
- }
- },
- "required": ["type", "required"],
- "additionalProperties": false
- },
- "description": "Fields that configure the trigger, keyed by field id."
- }
+ "nextCursor": {
+ "anyOf": [
+ {
+ "type": "string"
},
- "required": ["id", "outputs", "configFields"],
- "additionalProperties": false
- },
- "description": "Triggers the block can run on."
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Opaque cursor for the next page. Send it back as `cursor`; `null` means there is nothing further to fetch. Never construct one yourself."
+ }
+ },
+ "required": ["data", "nextCursor"],
+ "additionalProperties": false,
+ "title": "List Permission Group Members response",
+ "description": "List Permission Group Members result.",
+ "examples": [
+ {
+ "data": [
+ {
+ "id": "assignment-123",
+ "userId": "user-123",
+ "assignedAt": "2026-06-01T09:00:00.000Z",
+ "userName": "Example Member",
+ "userEmail": "member@example.com",
+ "userImage": null
+ }
+ ],
+ "nextCursor": null
+ }
+ ]
+ },
+ "V2PermissionGroupAssignment": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Membership assignment identifier."
},
- "outputs": {
- "type": "object",
- "propertyNames": {
- "type": "string"
- },
- "additionalProperties": {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "description": "Value type of the output."
- },
- "description": {
- "description": "What the output holds.",
- "type": "string"
- }
- },
- "required": ["type"],
- "additionalProperties": false
- },
- "description": "Fields the block produces."
+ "permissionGroupId": {
+ "type": "string",
+ "description": "Group receiving the member."
+ },
+ "organizationId": {
+ "type": "string",
+ "description": "Organization that owns the group."
+ },
+ "userId": {
+ "type": "string",
+ "description": "User assigned to the group."
+ },
+ "assignedBy": {
+ "type": "string",
+ "description": "User who made the assignment."
+ },
+ "assignedAt": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
+ "description": "When the assignment was created."
}
},
"required": [
"id",
- "name",
- "description",
- "category",
- "source",
- "triggerAllowed",
- "triggerCapable",
- "triggerIds",
- "toolIds",
- "operationIds",
- "preview",
- "tags",
- "inputSchema",
- "operationInputSchema",
- "inputDefinitions",
- "operations",
- "tools",
- "triggers",
- "outputs"
+ "permissionGroupId",
+ "organizationId",
+ "userId",
+ "assignedBy",
+ "assignedAt"
],
"additionalProperties": false,
- "title": "Block",
- "description": "A block with its configuration fields, operations, tools, and triggers."
+ "title": "Permission group assignment",
+ "description": "The newly created membership assignment."
+ },
+ "AddPermissionGroupMemberResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2PermissionGroupAssignment"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Add Permission Group Member response",
+ "description": "Add Permission Group Member result.",
+ "examples": [
+ {
+ "data": {
+ "id": "assignment-123",
+ "permissionGroupId": "group-123",
+ "organizationId": "org-123",
+ "userId": "user-123",
+ "assignedBy": "admin-123",
+ "assignedAt": "2026-06-01T09:00:00.000Z"
+ }
+ }
+ ]
+ },
+ "AddPermissionGroupMemberRequest": {
+ "type": "object",
+ "properties": {
+ "userId": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Existing organization member to add."
+ }
+ },
+ "required": ["userId"],
+ "additionalProperties": false,
+ "title": "Add Permission Group Member request",
+ "description": "Add Permission Group Member inputs.",
+ "examples": [
+ {
+ "userId": "user-123"
+ }
+ ]
+ },
+ "V2PermissionGroupMemberDeletion": {
+ "type": "object",
+ "properties": {
+ "userId": {
+ "type": "string",
+ "description": "User whose membership assignment was removed."
+ },
+ "deleted": {
+ "type": "boolean",
+ "const": true,
+ "description": "Whether the assignment was removed."
+ }
+ },
+ "required": ["userId", "deleted"],
+ "additionalProperties": false,
+ "title": "Permission group member deletion",
+ "description": "Acknowledges membership removal."
+ },
+ "RemovePermissionGroupMemberResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2PermissionGroupMemberDeletion"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Remove Permission Group Member response",
+ "description": "Remove Permission Group Member result.",
+ "examples": [
+ {
+ "data": {
+ "userId": "user-123",
+ "deleted": true
+ }
+ }
+ ]
+ },
+ "V2PermissionGroupBulkAdd": {
+ "type": "object",
+ "properties": {
+ "added": {
+ "type": "number",
+ "description": "Number of members added."
+ },
+ "skipped": {
+ "type": "number",
+ "description": "Number of selected organization members already in the group."
+ }
+ },
+ "required": ["added", "skipped"],
+ "additionalProperties": false,
+ "title": "Permission group bulk addition",
+ "description": "Counts of added and already assigned organization members."
+ },
+ "BulkAddPermissionGroupMembersResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2PermissionGroupBulkAdd"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Bulk Add Permission Group Members response",
+ "description": "Bulk Add Permission Group Members result.",
+ "examples": [
+ {
+ "data": {
+ "added": 1,
+ "skipped": 0
+ }
+ }
+ ]
},
- "GetBlockResponse": {
+ "BulkAddPermissionGroupMembersRequest": {
"type": "object",
"properties": {
- "data": {
- "description": "Response data.",
- "$ref": "#/components/schemas/V2BlockDetail"
+ "userIds": {
+ "description": "Organization member identifiers. Existing group members are skipped; users outside the organization are ignored.",
+ "minItems": 1,
+ "maxItems": 1000,
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "addAllOrganizationMembers": {
+ "description": "Add every current organization member in bounded batches within one transaction. Cannot be combined with userIds.",
+ "type": "boolean"
}
},
- "required": ["data"],
"additionalProperties": false,
- "title": "Get block response",
- "description": "One block with its fields, operations, tools, and triggers.",
+ "title": "Bulk Add Permission Group Members request",
+ "description": "Bulk Add Permission Group Members inputs.",
"examples": [
{
- "data": {
- "id": "slack",
- "name": "Slack",
- "description": "Send messages and read channels in Slack.",
- "category": "tools",
- "integrationType": "communication",
- "source": "builtin",
- "authMode": "oauth",
- "triggerAllowed": true,
- "triggerCapable": true,
- "triggerIds": ["slack_webhook"],
- "toolIds": ["slack_message", "slack_canvas_read"],
- "operationIds": ["send", "read"],
- "preview": false,
- "docsLink": "https://docs.sim.ai/tools/slack",
- "tags": ["messaging"],
- "inputSchema": [
- {
- "id": "operation",
- "type": "dropdown",
- "title": "Operation",
- "required": true,
- "options": [
- {
- "id": "send",
- "label": "Send message"
- },
- {
- "id": "read",
- "label": "Read messages"
- }
- ]
- }
- ],
- "operationInputSchema": {
- "send": [
- {
- "id": "text",
- "type": "long-input",
- "title": "Message",
- "required": true
- }
- ]
- },
- "inputDefinitions": {
- "channel": {
- "type": "string",
- "description": "Channel to post into."
- }
- },
- "operations": {
- "send": {
- "toolId": "slack_message",
- "toolName": "Slack Send Message",
- "description": "Send a message to a Slack channel.",
- "inputs": {
- "text": {
- "type": "string",
- "required": true,
- "description": "Message body."
- }
- },
- "outputs": {
- "ts": {
- "type": "string",
- "description": "Message timestamp."
- }
- },
- "inputSchema": [
- {
- "id": "text",
- "type": "long-input",
- "title": "Message",
- "required": true
- }
- ]
- }
- },
- "tools": [
- {
- "id": "slack_message",
- "name": "Slack Send Message",
- "description": "Send a message to a Slack channel.",
- "version": "1.0.0",
- "hostedApiKey": "none",
- "oauth": {
- "required": true,
- "provider": "slack",
- "requiredScopes": ["chat:write"]
- },
- "params": {
- "text": {
- "type": "string",
- "required": true,
- "description": "Message body."
- }
- },
- "outputs": {
- "ts": {
- "type": "string",
- "description": "Message timestamp."
- }
- }
- }
- ],
- "triggers": [
- {
- "id": "slack_webhook",
- "outputs": {
- "text": {
- "type": "string",
- "description": "Message text."
- }
- },
- "configFields": {
- "channels": {
- "type": "short-input",
- "required": false,
- "title": "Channels"
- }
- }
- }
- ],
- "outputs": {
- "ts": {
- "type": "string",
- "description": "Message timestamp."
- }
- }
- }
+ "userIds": ["user-123"]
}
]
},
- "V2ToolSummary": {
+ "V2Organization": {
"type": "object",
"properties": {
"id": {
"type": "string",
- "description": "Registered tool identifier, including its version suffix."
+ "description": "Organization identifier."
},
"name": {
"type": "string",
- "description": "Display name."
+ "description": "Organization display name."
},
- "description": {
+ "slug": {
"type": "string",
- "description": "What the tool does."
+ "description": "Organization slug."
},
- "version": {
- "description": "Tool version.",
- "type": "string"
+ "logo": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Organization logo URL, or null when unset."
},
- "hostedApiKey": {
+ "role": {
"type": "string",
- "enum": ["always", "conditional", "none"],
- "description": "Whether Sim supplies the API key on THIS deployment: `always`, `conditional` (only for some parameter combinations), or `none` (bring your own). Self-hosted deployments supply no hosted keys, so every tool reports `none` there regardless of what it declares."
+ "enum": ["owner", "admin", "member"],
+ "description": "The acting user’s role in this organization."
},
- "oauth": {
- "description": "OAuth requirement, when the tool has one.",
- "type": "object",
- "properties": {
- "required": {
- "type": "boolean",
- "description": "Whether the tool cannot run without an OAuth credential."
- },
- "provider": {
- "type": "string",
- "description": "OAuth service the credential must authenticate."
- },
- "requiredScopes": {
- "description": "Scopes the credential must carry.",
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- },
- "required": ["required", "provider"],
- "additionalProperties": false
+ "createdAt": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
+ "description": "When the organization was created."
}
},
- "required": ["id", "name", "description", "hostedApiKey"],
+ "required": ["id", "name", "slug", "logo", "role", "createdAt"],
"additionalProperties": false,
- "title": "Tool summary",
- "description": "List view of a built-in tool: identity, auth, and key hosting."
+ "title": "Organization",
+ "description": "An organization the acting user belongs to."
},
- "ListToolsResponse": {
+ "ListOrganizationsResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
- "$ref": "#/components/schemas/V2ToolSummary"
+ "$ref": "#/components/schemas/V2Organization"
},
"description": "Items in the current page."
},
@@ -10835,421 +14896,329 @@
},
"required": ["data", "nextCursor"],
"additionalProperties": false,
- "title": "List tools response",
- "description": "Built-in tools available in the workspace.",
+ "title": "List Organizations response",
+ "description": "List Organizations result.",
"examples": [
{
"data": [
{
- "id": "slack_message",
- "name": "Slack Send Message",
- "description": "Send a message to a Slack channel.",
- "version": "1.0.0",
- "hostedApiKey": "none",
- "oauth": {
- "required": true,
- "provider": "slack",
- "requiredScopes": ["chat:write"]
- }
+ "id": "org-123",
+ "name": "Example Organization",
+ "slug": "example",
+ "logo": null,
+ "role": "admin",
+ "createdAt": "2026-06-01T09:00:00.000Z"
}
],
"nextCursor": null
}
]
},
- "GetToolResponse": {
+ "GetOrganizationResponse": {
"type": "object",
"properties": {
"data": {
"description": "Response data.",
- "$ref": "#/components/schemas/V2ToolDetail"
+ "$ref": "#/components/schemas/V2Organization"
}
},
"required": ["data"],
"additionalProperties": false,
- "title": "Get tool response",
- "description": "One built-in tool with its parameters and outputs.",
+ "title": "Get Organization response",
+ "description": "Get Organization result.",
+ "examples": [
+ {
+ "data": {
+ "id": "org-123",
+ "name": "Example Organization",
+ "slug": "example",
+ "logo": null,
+ "role": "admin",
+ "createdAt": "2026-06-01T09:00:00.000Z"
+ }
+ }
+ ]
+ },
+ "V2OrganizationWorkspace": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Workspace identifier."
+ },
+ "name": {
+ "type": "string",
+ "description": "Workspace display name."
+ }
+ },
+ "required": ["id", "name"],
+ "additionalProperties": false,
+ "title": "Organization workspace",
+ "description": "A workspace owned by the organization."
+ },
+ "ListOrganizationWorkspacesResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/V2OrganizationWorkspace"
+ },
+ "description": "Items in the current page."
+ },
+ "nextCursor": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Opaque cursor for the next page. Send it back as `cursor`; `null` means there is nothing further to fetch. Never construct one yourself."
+ }
+ },
+ "required": ["data", "nextCursor"],
+ "additionalProperties": false,
+ "title": "List Organization Workspaces response",
+ "description": "List Organization Workspaces result.",
"examples": [
{
- "data": {
- "id": "slack_message",
- "name": "Slack Send Message",
- "description": "Send a message to a Slack channel.",
- "version": "1.0.0",
- "hostedApiKey": "none",
- "oauth": {
- "required": true,
- "provider": "slack",
- "requiredScopes": ["chat:write"]
- },
- "params": {
- "channel": {
- "type": "string",
- "required": true,
- "description": "Channel ID to post into."
- },
- "text": {
- "type": "string",
- "required": true,
- "description": "Message body."
- }
- },
- "outputs": {
- "ts": {
- "type": "string",
- "description": "Message timestamp."
- }
+ "data": [
+ {
+ "id": "workspace-123",
+ "name": "Engineering"
}
- }
+ ],
+ "nextCursor": null
}
]
},
- "V2ToolExecution": {
+ "V2OrganizationMember": {
"type": "object",
"properties": {
- "toolId": {
+ "userId": {
"type": "string",
- "description": "Tool that ran. An unversioned name resolves to the newest version visible in the workspace, so this can differ from the id in the path."
+ "description": "User identifier; use this identifier to update or remove the member."
},
- "status": {
+ "name": {
"type": "string",
- "enum": ["succeeded", "failed"],
- "description": "Whether the tool reported success. A failed tool call is still a 200."
+ "description": "Member display name."
},
- "output": {
- "description": "Whatever the tool produced, shaped by its declared outputs."
+ "email": {
+ "type": "string",
+ "description": "Member email address."
},
- "error": {
+ "role": {
+ "type": "string",
+ "enum": ["owner", "admin", "member"],
+ "description": "Organization role; separate from workspace permissions."
+ },
+ "joinedAt": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
+ "description": "When the user joined the organization."
+ }
+ },
+ "required": ["userId", "name", "email", "role", "joinedAt"],
+ "additionalProperties": false,
+ "title": "Organization member",
+ "description": "An organization membership identified by user ID."
+ },
+ "ListOrganizationMembersResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/V2OrganizationMember"
+ },
+ "description": "Items in the current page."
+ },
+ "nextCursor": {
"anyOf": [
{
- "type": "object",
- "properties": {
- "message": {
- "type": "string",
- "description": "Why the tool call did not succeed."
- }
- },
- "required": ["message"],
- "additionalProperties": false
+ "type": "string"
},
{
"type": "null"
}
],
- "description": "Populated only when `status` is `failed`."
+ "description": "Opaque cursor for the next page. Send it back as `cursor`; `null` means there is nothing further to fetch. Never construct one yourself."
}
},
- "required": ["toolId", "status", "output", "error"],
+ "required": ["data", "nextCursor"],
"additionalProperties": false,
- "title": "Tool execution",
- "description": "The result of running one built-in tool."
+ "title": "List Organization Members response",
+ "description": "List Organization Members result.",
+ "examples": [
+ {
+ "data": [
+ {
+ "userId": "user-123",
+ "name": "Example Member",
+ "email": "member@example.com",
+ "role": "member",
+ "joinedAt": "2026-06-01T09:00:00.000Z"
+ }
+ ],
+ "nextCursor": null
+ }
+ ]
},
- "ExecuteToolResponse": {
+ "UpdateOrganizationMemberResponse": {
"type": "object",
"properties": {
"data": {
"description": "Response data.",
- "$ref": "#/components/schemas/V2ToolExecution"
+ "$ref": "#/components/schemas/V2OrganizationMember"
}
},
"required": ["data"],
"additionalProperties": false,
- "title": "Run tool response",
- "description": "What the tool produced, or why it did not succeed.",
+ "title": "Update Organization Member response",
+ "description": "Update Organization Member result.",
"examples": [
{
"data": {
- "toolId": "slack_message",
- "status": "succeeded",
- "output": {
- "ts": "1718191234.004500"
- },
- "error": null
+ "userId": "user-123",
+ "name": "Example Member",
+ "email": "member@example.com",
+ "role": "admin",
+ "joinedAt": "2026-06-01T09:00:00.000Z"
}
}
]
},
- "ExecuteToolRequest": {
+ "UpdateOrganizationMemberBody": {
"type": "object",
"properties": {
- "workspaceId": {
- "type": "string",
- "minLength": 1,
- "maxLength": 128,
- "description": "Workspace whose integration allowlist, credentials, and environment variables govern this call."
- },
- "input": {
- "default": {},
- "description": "Tool arguments keyed by published parameter IDs. For `user-only` parameters, a whole-value `{{VAR_NAME}}` reference resolves a workspace environment variable. Other values pass through unchanged.",
- "type": "object",
- "propertyNames": {
- "type": "string"
- },
- "additionalProperties": {
- "description": "One argument value. Its shape is declared by the tool parameter."
- }
- },
- "credentialId": {
- "description": "Credential to authenticate with. Required when the tool declares an OAuth requirement; the workspace credentials list names the candidates.",
+ "role": {
"type": "string",
- "minLength": 1,
- "maxLength": 255
- },
- "timeoutSeconds": {
- "description": "How long to wait for the tool before abandoning the call.",
- "type": "integer",
- "minimum": 1,
- "maximum": 300
+ "enum": ["member", "admin"],
+ "description": "New organization role. Ownership transfers use a separate operation."
}
},
- "required": ["workspaceId"],
+ "required": ["role"],
"additionalProperties": false,
- "title": "Run tool request",
- "description": "Workspace, arguments, and the credential to authenticate with.",
+ "title": "Update Organization Member body",
+ "description": "Update Organization Member input.",
"examples": [
{
- "workspaceId": "a91c4b2e-6d3f-4e8a-b5c7-0d9e2f1a8c64",
- "input": {
- "channel": "C0123456789",
- "text": "Deploy finished."
- },
- "credentialId": "cred_01J8ZK3QW4M6X2R9T7B5C0V2"
+ "role": "admin"
}
]
},
- "V2ConnectorType": {
+ "V2OrganizationMemberDeletion": {
"type": "object",
"properties": {
- "connectorType": {
+ "userId": {
"type": "string",
- "description": "Exact identifier to send when creating a connector of this type."
- },
- "name": {
- "type": "string",
- "description": "Display name."
- },
- "description": {
- "type": "string",
- "description": "What the connector syncs."
- },
- "version": {
- "type": "string",
- "description": "Connector version."
- },
- "auth": {
- "oneOf": [
- {
- "type": "object",
- "properties": {
- "mode": {
- "type": "string",
- "const": "oauth",
- "description": "Authenticates with an OAuth credential."
- },
- "provider": {
- "type": "string",
- "description": "OAuth service the credential must authenticate."
- },
- "requiredScopes": {
- "description": "Scopes the credential must carry.",
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- },
- "required": ["mode", "provider"],
- "additionalProperties": false
- },
- {
- "type": "object",
- "properties": {
- "mode": {
- "type": "string",
- "const": "apiKey",
- "description": "Authenticates with a stored API key."
- },
- "label": {
- "description": "Label shown above the key field.",
- "type": "string"
- },
- "placeholder": {
- "description": "Placeholder shown in the key field.",
- "type": "string"
- },
- "optional": {
- "type": "boolean",
- "description": "Whether the key may be left blank, for a source reachable without authentication."
- }
- },
- "required": ["mode", "optional"],
- "additionalProperties": false
- }
- ],
- "description": "How the connector authenticates against its source."
- },
- "configFields": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/V2ConnectorConfigField"
- },
- "description": "Fields that make up the connector’s `sourceConfig`."
+ "description": "User removed from the organization."
},
- "supportsIncrementalSync": {
+ "deleted": {
"type": "boolean",
- "description": "Whether syncs after the first fetch only what changed."
- },
- "tagDefinitions": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string",
- "description": "Semantic tag identifier the connector populates."
- },
- "displayName": {
- "type": "string",
- "description": "Human-readable tag name."
- },
- "fieldType": {
- "type": "string",
- "enum": ["text", "number", "date", "boolean"],
- "description": "Value type, which decides the tag slot pool it draws from."
- }
- },
- "required": ["id", "displayName", "fieldType"],
- "additionalProperties": false
- },
- "description": "Tags this connector writes onto the documents it syncs."
+ "const": true,
+ "description": "Whether membership and organization workspace access were removed."
}
},
- "required": [
- "connectorType",
- "name",
- "description",
- "version",
- "auth",
- "configFields",
- "supportsIncrementalSync",
- "tagDefinitions"
- ],
+ "required": ["userId", "deleted"],
+ "additionalProperties": false,
+ "title": "Organization member removal",
+ "description": "Acknowledges removal of an organization member."
+ },
+ "RemoveOrganizationMemberResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2OrganizationMemberDeletion"
+ }
+ },
+ "required": ["data"],
"additionalProperties": false,
- "title": "Connector type",
- "description": "A knowledge-base connector type and the configuration it accepts."
+ "title": "Remove Organization Member response",
+ "description": "Remove Organization Member result.",
+ "examples": [
+ {
+ "data": {
+ "userId": "user-123",
+ "deleted": true
+ }
+ }
+ ]
},
- "V2ConnectorConfigField": {
+ "V2OrganizationInvitation": {
"type": "object",
"properties": {
"id": {
"type": "string",
- "description": "Field identifier."
+ "description": "Invitation identifier."
},
- "title": {
+ "organizationId": {
"type": "string",
- "description": "Human-readable label."
+ "description": "Organization that owns the invitation."
},
- "type": {
+ "email": {
"type": "string",
- "enum": ["short-input", "dropdown", "selector"],
- "description": "Control the field renders as. A `selector` fetches its options from the connected account."
- },
- "placeholder": {
- "description": "Placeholder shown in the editor.",
- "type": "string"
- },
- "required": {
- "description": "Whether a value must be supplied.",
- "type": "boolean"
+ "description": "Email address of the invitee."
},
- "description": {
- "description": "Authored explanation of the field.",
- "type": "string"
- },
- "options": {
- "description": "Static options, for a `dropdown` field.",
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string",
- "description": "Value stored when this option is selected."
- },
- "label": {
- "type": "string",
- "description": "Human-readable option label."
- }
- },
- "required": ["id", "label"],
- "additionalProperties": false
- }
- },
- "selectorKey": {
- "description": "Names the picker a `selector` field renders. Its options are fetched per workspace.",
- "type": "string"
+ "role": {
+ "type": "string",
+ "enum": ["member", "admin"],
+ "description": "Organization role offered to an internal invitee."
},
- "mimeType": {
- "description": "MIME type filter applied to the picker.",
- "type": "string"
+ "kind": {
+ "type": "string",
+ "enum": ["organization", "workspace"],
+ "description": "Whether the invitation originated from organization or workspace administration."
},
- "dependsOn": {
- "description": "Sibling fields this field is cleared by when they change.",
- "anyOf": [
- {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- {
- "type": "object",
- "properties": {
- "all": {
- "description": "Every listed field must hold a value.",
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "any": {
- "description": "At least one listed field must hold a value.",
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- },
- "additionalProperties": false
- }
- ]
+ "membershipIntent": {
+ "type": "string",
+ "enum": ["internal", "external"],
+ "description": "Whether acceptance joins the organization or grants workspace access only."
},
- "mode": {
- "description": "Which half of a canonical pair this field is: `basic` is the picker, `advanced` the manual entry.",
+ "status": {
"type": "string",
- "enum": ["basic", "advanced"]
+ "enum": ["pending", "accepted", "rejected", "cancelled", "expired"],
+ "description": "Current invitation status; elapsed pending invitations are reported as expired."
},
- "canonicalParamId": {
- "description": "Shared `sourceConfig` key for a picker/manual-entry pair. Send exactly one of the pair, keyed by this value rather than by the field’s own `id`.",
- "type": "string"
+ "createdAt": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
+ "description": "When the invitation was created."
},
- "multi": {
- "description": "When true the stored `sourceConfig` value is a `string[]`, not a `string`: a `selector` renders a multi-select picker and a `short-input` accepts a comma-separated list.",
- "type": "boolean"
+ "expiresAt": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
+ "description": "When the invitation expires."
}
},
- "required": ["id", "title", "type"],
+ "required": [
+ "id",
+ "organizationId",
+ "email",
+ "role",
+ "kind",
+ "membershipIntent",
+ "status",
+ "createdAt",
+ "expiresAt"
+ ],
"additionalProperties": false,
- "title": "Connector config field",
- "description": "One field of a knowledge-base connector’s source configuration."
+ "title": "Organization invitation",
+ "description": "Invitation metadata without its acceptance token."
},
- "ListConnectorTypesResponse": {
+ "ListOrganizationInvitationsResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
- "$ref": "#/components/schemas/V2ConnectorType"
+ "$ref": "#/components/schemas/V2OrganizationInvitation"
},
"description": "Items in the current page."
},
@@ -11262,59 +15231,192 @@
"type": "null"
}
],
- "description": "Always `null` — this list has no `cursor` or `limit` param and returns its whole bounded set in one page. Present so the list can gain pages later without a shape change."
+ "description": "Opaque cursor for the next page. Send it back as `cursor`; `null` means there is nothing further to fetch. Never construct one yourself."
}
},
"required": ["data", "nextCursor"],
"additionalProperties": false,
- "title": "List connector types response",
- "description": "Knowledge-base connector types and their configuration fields.",
+ "title": "List Organization Invitations response",
+ "description": "List Organization Invitations result.",
"examples": [
{
"data": [
{
- "connectorType": "google_drive",
- "name": "Google Drive",
- "description": "Sync documents from a Google Drive folder.",
- "version": "1.0.0",
- "auth": {
- "mode": "oauth",
- "provider": "google-drive",
- "requiredScopes": ["https://www.googleapis.com/auth/drive.readonly"]
- },
- "configFields": [
- {
- "id": "folderSelector",
- "title": "Folder",
- "type": "selector",
- "selectorKey": "google-drive-folder",
- "mimeType": "application/vnd.google-apps.folder",
- "mode": "basic",
- "canonicalParamId": "folderId",
- "required": true
- },
- {
- "id": "manualFolderId",
- "title": "Folder ID",
- "type": "short-input",
- "placeholder": "Enter the folder ID",
- "mode": "advanced",
- "canonicalParamId": "folderId"
- }
- ],
- "supportsIncrementalSync": true,
- "tagDefinitions": [
- {
- "id": "owner",
- "displayName": "Owner",
- "fieldType": "text"
- }
- ]
+ "id": "invitation-123",
+ "organizationId": "org-123",
+ "email": "member@example.com",
+ "role": "member",
+ "kind": "organization",
+ "membershipIntent": "internal",
+ "status": "pending",
+ "createdAt": "2026-06-01T09:00:00.000Z",
+ "expiresAt": "2026-06-08T09:00:00.000Z"
}
],
"nextCursor": null
}
]
+ },
+ "CreateOrganizationInvitationResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2OrganizationInvitation"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Create Organization Invitation response",
+ "description": "Create Organization Invitation result.",
+ "examples": [
+ {
+ "data": {
+ "id": "invitation-123",
+ "organizationId": "org-123",
+ "email": "member@example.com",
+ "role": "member",
+ "kind": "organization",
+ "membershipIntent": "internal",
+ "status": "pending",
+ "createdAt": "2026-06-01T09:00:00.000Z",
+ "expiresAt": "2026-06-08T09:00:00.000Z"
+ }
+ }
+ ]
+ },
+ "CreateOrganizationInvitationBody": {
+ "type": "object",
+ "properties": {
+ "email": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 254,
+ "format": "email",
+ "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
+ "description": "Email address of the person to invite."
+ },
+ "role": {
+ "default": "member",
+ "description": "Organization role to offer. Defaults to member; grants no workspace-specific permissions.",
+ "type": "string",
+ "enum": ["member", "admin"]
+ }
+ },
+ "required": ["email"],
+ "additionalProperties": false,
+ "title": "Create Organization Invitation body",
+ "description": "Create Organization Invitation input.",
+ "examples": [
+ {
+ "email": "member@example.com",
+ "role": "member"
+ }
+ ]
+ },
+ "GetOrganizationInvitationResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2OrganizationInvitation"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Get Organization Invitation response",
+ "description": "Get Organization Invitation result.",
+ "examples": [
+ {
+ "data": {
+ "id": "invitation-123",
+ "organizationId": "org-123",
+ "email": "member@example.com",
+ "role": "member",
+ "kind": "organization",
+ "membershipIntent": "internal",
+ "status": "pending",
+ "createdAt": "2026-06-01T09:00:00.000Z",
+ "expiresAt": "2026-06-08T09:00:00.000Z"
+ }
+ }
+ ]
+ },
+ "ResendOrganizationInvitationResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2OrganizationInvitation"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Resend Organization Invitation response",
+ "description": "Resend Organization Invitation result.",
+ "examples": [
+ {
+ "data": {
+ "id": "invitation-123",
+ "organizationId": "org-123",
+ "email": "member@example.com",
+ "role": "member",
+ "kind": "organization",
+ "membershipIntent": "internal",
+ "status": "pending",
+ "createdAt": "2026-06-01T09:00:00.000Z",
+ "expiresAt": "2026-06-08T09:00:00.000Z"
+ }
+ }
+ ]
+ },
+ "ResendOrganizationInvitationBody": {
+ "default": {},
+ "title": "Resend Organization Invitation body",
+ "description": "Resend Organization Invitation input.",
+ "examples": [{}],
+ "type": "object",
+ "properties": {},
+ "additionalProperties": false
+ },
+ "V2OrganizationInvitationRevocation": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Revoked invitation identifier."
+ },
+ "status": {
+ "type": "string",
+ "const": "cancelled",
+ "description": "Revocation cancels the invitation and prevents acceptance."
+ }
+ },
+ "required": ["id", "status"],
+ "additionalProperties": false,
+ "title": "Organization invitation revocation",
+ "description": "Acknowledges cancellation of a pending invitation."
+ },
+ "RevokeOrganizationInvitationResponse": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "Response data.",
+ "$ref": "#/components/schemas/V2OrganizationInvitationRevocation"
+ }
+ },
+ "required": ["data"],
+ "additionalProperties": false,
+ "title": "Revoke Organization Invitation response",
+ "description": "Revoke Organization Invitation result.",
+ "examples": [
+ {
+ "data": {
+ "id": "invitation-123",
+ "status": "cancelled"
+ }
+ }
+ ]
}
}
},
diff --git a/apps/sim/app/api/invitations/[id]/resend/route.test.ts b/apps/sim/app/api/invitations/[id]/resend/route.test.ts
index 4418ace293b..14f7f9a4e0d 100644
--- a/apps/sim/app/api/invitations/[id]/resend/route.test.ts
+++ b/apps/sim/app/api/invitations/[id]/resend/route.test.ts
@@ -1,7 +1,9 @@
/**
* @vitest-environment node
*/
-import { authMockFns, createMockRequest } from '@sim/testing'
+import { db } from '@sim/db'
+import { member, user } from '@sim/db/schema'
+import { authMockFns, createMockRequest, queueTableRows, resetDbChainMock } from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const {
@@ -15,7 +17,7 @@ const {
mockValidateInvitationsAllowed,
mockSendInvitationEmail,
mockPrepareInvitationResend,
- mockPersistInvitationResend,
+ mockRevertInvitationResend,
mockGetOrganizationSubscription,
} = vi.hoisted(() => ({
MockInvitationsNotAllowedError: class extends Error {
@@ -33,7 +35,7 @@ const {
mockValidateInvitationsAllowed: vi.fn(),
mockSendInvitationEmail: vi.fn(),
mockPrepareInvitationResend: vi.fn(),
- mockPersistInvitationResend: vi.fn(),
+ mockRevertInvitationResend: vi.fn(),
mockGetOrganizationSubscription: vi.fn(),
}))
@@ -51,11 +53,12 @@ vi.mock('@/ee/access-control/utils/permission-check', () => ({
vi.mock('@/lib/invitations/core', () => ({
getInvitationById: mockGetInvitationById,
resolveInvitationAdmissionOrganizationId: mockResolveInvitationAdmissionOrganizationId,
+ requireInvitationResendAuthority: vi.fn(),
}))
vi.mock('@/lib/invitations/send', () => ({
sendInvitationEmail: mockSendInvitationEmail,
prepareInvitationResend: mockPrepareInvitationResend,
- persistInvitationResend: mockPersistInvitationResend,
+ revertInvitationResend: mockRevertInvitationResend,
}))
vi.mock('@/lib/billing/core/organization', () => ({
isOrganizationOwnerOrAdmin: mockIsOrganizationOwnerOrAdmin,
@@ -69,8 +72,16 @@ vi.mock('@/lib/workspaces/permissions/utils', () => ({
}))
vi.mock('@/lib/workspaces/policy', () => ({
getWorkspaceInvitePolicy: mockGetWorkspaceInvitePolicy,
+ WORKSPACE_MODE: { ORGANIZATION: 'organization' },
}))
+vi.mock('@/lib/permission-groups/resolve.server', () => ({
+ getUserPermissionConfigForOrganization: vi.fn().mockResolvedValue(null),
+}))
+
+import { OrchestrationError } from '@/lib/core/orchestration/types'
+import { lockInvitationResendPolicy } from '@/lib/invitations/resend-policy'
+import type { PreparedInvitationResend } from '@/lib/invitations/send'
import { POST } from '@/app/api/invitations/[id]/resend/route'
const mockGetSession = authMockFns.mockGetSession
@@ -94,11 +105,24 @@ const workspaceInvitation = {
email: 'invitee@example.com',
role: 'member',
token: 'token-1',
+ expiresAt: new Date('2099-01-01'),
+ createdAt: new Date('2026-01-01'),
+ updatedAt: new Date('2026-01-01'),
organizationId: 'organization-1',
membershipIntent: 'internal',
grants: [{ workspaceId: 'workspace-1', permission: 'read' }],
}
+const preparedResend: PreparedInvitationResend = {
+ invitationId: workspaceInvitation.id,
+ organizationId: workspaceInvitation.organizationId,
+ tokenForEmail: 'token-2',
+ nextExpiresAt: new Date('2099-02-01'),
+ mutationUpdatedAt: new Date('2026-02-01'),
+ previousToken: workspaceInvitation.token,
+ previousExpiresAt: workspaceInvitation.expiresAt,
+}
+
/**
* A resend re-delivers a working link and pushes the expiry forward, so it is a
* send: without the gate an organization that has withheld invitations still
@@ -107,7 +131,13 @@ const workspaceInvitation = {
describe('POST /api/invitations/[id]/resend', () => {
beforeEach(() => {
vi.clearAllMocks()
- mockGetSession.mockResolvedValue({ user: { id: 'user-1', email: 'admin@example.com' } })
+ resetDbChainMock()
+ queueTableRows(member, [{ role: 'admin' }])
+ queueTableRows(user, [{ name: 'Admin', email: 'admin@example.com' }])
+ mockGetSession.mockResolvedValue({
+ user: { id: 'user-1', email: 'admin@example.com' },
+ session: { id: 'session-1' },
+ })
mockGetInvitationById.mockResolvedValue(workspaceInvitation)
mockResolveInvitationAdmissionOrganizationId.mockResolvedValue('organization-1')
mockIsOrganizationOwnerOrAdmin.mockResolvedValue(true)
@@ -115,26 +145,40 @@ describe('POST /api/invitations/[id]/resend', () => {
mockGetWorkspaceWithOwner.mockResolvedValue({
id: 'workspace-1',
organizationId: 'organization-1',
+ workspaceMode: 'organization',
+ billedAccountUserId: 'owner',
+ ownerId: 'owner',
})
mockGetWorkspaceInvitePolicy.mockResolvedValue({ allowed: true })
mockValidateInvitationsAllowed.mockResolvedValue(undefined)
- mockPrepareInvitationResend.mockResolvedValue({
- tokenForEmail: 'token-2',
- nextToken: 'token-2',
- nextExpiresAt: new Date('2026-09-30T00:00:00.000Z'),
+ mockPrepareInvitationResend.mockImplementation(async (params) => {
+ await lockInvitationResendPolicy(
+ db,
+ await mockGetInvitationById(params.invitationId),
+ params.actorUserId,
+ params.expectedOrganizationId
+ )
+ return preparedResend
})
mockSendInvitationEmail.mockResolvedValue({ success: true })
- mockPersistInvitationResend.mockResolvedValue(undefined)
+ mockRevertInvitationResend.mockResolvedValue(true)
})
it('resends when no group withholds invitations', async () => {
const response = await callResend()
expect(response.status).toBe(200)
- expect(mockValidateInvitationsAllowed).toHaveBeenCalledWith('user-1', {
- workspaceId: 'workspace-1',
- })
+ expect(mockValidateInvitationsAllowed).toHaveBeenCalledWith(
+ 'user-1',
+ {
+ workspaceId: 'workspace-1',
+ },
+ db
+ )
expect(mockSendInvitationEmail).toHaveBeenCalled()
+ expect(mockPrepareInvitationResend.mock.invocationCallOrder[0]).toBeLessThan(
+ mockSendInvitationEmail.mock.invocationCallOrder[0]
+ )
})
/**
@@ -148,12 +192,12 @@ describe('POST /api/invitations/[id]/resend', () => {
const response = await callResend()
expect(response.status).toBe(403)
- expect(await response.json()).toEqual({
+ expect(await response.json()).toMatchObject({
error: "Sending invitations is not available under your organization's permission group",
details: { code: 'PERMISSION_GROUP_CAPABILITY_BLOCKED' },
})
expect(mockSendInvitationEmail).not.toHaveBeenCalled()
- expect(mockPersistInvitationResend).not.toHaveBeenCalled()
+ expect(mockRevertInvitationResend).not.toHaveBeenCalled()
})
/**
@@ -161,6 +205,8 @@ describe('POST /api/invitations/[id]/resend', () => {
* someone with no admin standing to hear it.
*/
it('checks admin standing before the permission group', async () => {
+ resetDbChainMock()
+ queueTableRows(member, [{ role: 'member' }])
mockIsOrganizationOwnerOrAdmin.mockResolvedValue(false)
mockHasWorkspaceAdminAccess.mockResolvedValue(false)
@@ -183,12 +229,20 @@ describe('POST /api/invitations/[id]/resend', () => {
const response = await callResend()
expect(response.status).toBe(200)
- expect(mockValidateInvitationsAllowed).toHaveBeenCalledWith('user-1', {
- organizationId: 'organization-1',
- })
- expect(mockValidateInvitationsAllowed).toHaveBeenCalledWith('user-1', {
- workspaceId: 'workspace-1',
- })
+ expect(mockValidateInvitationsAllowed).toHaveBeenCalledWith(
+ 'user-1',
+ {
+ organizationId: 'organization-1',
+ },
+ db
+ )
+ expect(mockValidateInvitationsAllowed).toHaveBeenCalledWith(
+ 'user-1',
+ {
+ workspaceId: 'workspace-1',
+ },
+ db
+ )
})
it('refuses an organization invitation the organization default group withholds, even when its granted workspace allows', async () => {
@@ -203,7 +257,7 @@ describe('POST /api/invitations/[id]/resend', () => {
expect(response.status).toBe(403)
expect(mockSendInvitationEmail).not.toHaveBeenCalled()
- expect(mockPersistInvitationResend).not.toHaveBeenCalled()
+ expect(mockRevertInvitationResend).not.toHaveBeenCalled()
})
/**
@@ -218,13 +272,24 @@ describe('POST /api/invitations/[id]/resend', () => {
const response = await callResend()
expect(response.status).toBe(200)
- expect(mockResolveInvitationAdmissionOrganizationId).toHaveBeenCalledWith(workspaceInvitation)
- expect(mockValidateInvitationsAllowed).toHaveBeenCalledWith('user-1', {
- organizationId: 'organization-1',
- })
- expect(mockValidateInvitationsAllowed).toHaveBeenCalledWith('user-1', {
- workspaceId: 'workspace-1',
- })
+ expect(mockResolveInvitationAdmissionOrganizationId).toHaveBeenCalledWith(
+ workspaceInvitation,
+ db
+ )
+ expect(mockValidateInvitationsAllowed).toHaveBeenCalledWith(
+ 'user-1',
+ {
+ organizationId: 'organization-1',
+ },
+ db
+ )
+ expect(mockValidateInvitationsAllowed).toHaveBeenCalledWith(
+ 'user-1',
+ {
+ workspaceId: 'workspace-1',
+ },
+ db
+ )
})
it('refuses a workspace invitation whose admitting organization withholds invitations', async () => {
@@ -238,7 +303,7 @@ describe('POST /api/invitations/[id]/resend', () => {
expect(response.status).toBe(403)
expect(mockSendInvitationEmail).not.toHaveBeenCalled()
- expect(mockPersistInvitationResend).not.toHaveBeenCalled()
+ expect(mockRevertInvitationResend).not.toHaveBeenCalled()
})
/**
@@ -254,9 +319,13 @@ describe('POST /api/invitations/[id]/resend', () => {
expect(response.status).toBe(200)
expect(mockValidateInvitationsAllowed).toHaveBeenCalledTimes(1)
- expect(mockValidateInvitationsAllowed).toHaveBeenCalledWith('user-1', {
- workspaceId: 'workspace-1',
- })
+ expect(mockValidateInvitationsAllowed).toHaveBeenCalledWith(
+ 'user-1',
+ {
+ workspaceId: 'workspace-1',
+ },
+ db
+ )
})
it('resolves the organization default group for an invitation with no grants', async () => {
@@ -271,8 +340,51 @@ describe('POST /api/invitations/[id]/resend', () => {
const response = await callResend()
expect(response.status).toBe(200)
- expect(mockValidateInvitationsAllowed).toHaveBeenCalledWith('user-1', {
- organizationId: 'organization-1',
- })
+ expect(mockValidateInvitationsAllowed).toHaveBeenCalledWith(
+ 'user-1',
+ {
+ organizationId: 'organization-1',
+ },
+ db
+ )
+ })
+ it.each(['pending', 'expired'])(
+ 'rejects an expired %s invitation consistently',
+ async (status) => {
+ mockGetInvitationById.mockResolvedValue({
+ ...workspaceInvitation,
+ status,
+ expiresAt: new Date('2000-01-01'),
+ })
+ expect((await callResend()).status).toBe(400)
+ expect(mockSendInvitationEmail).not.toHaveBeenCalled()
+ }
+ )
+
+ it('restores the previous token when delivery fails', async () => {
+ mockSendInvitationEmail.mockResolvedValue({ success: false, error: 'Delivery unavailable' })
+ expect((await callResend()).status).toBe(502)
+ expect(mockRevertInvitationResend).toHaveBeenCalledWith(preparedResend)
+ })
+
+ it('does not deliver a token when a concurrent change prevents persistence', async () => {
+ mockPrepareInvitationResend.mockRejectedValueOnce(
+ new OrchestrationError('conflict', 'Invitation changed')
+ )
+ expect((await callResend()).status).toBe(409)
+ expect(mockSendInvitationEmail).not.toHaveBeenCalled()
+ expect(mockRevertInvitationResend).not.toHaveBeenCalled()
+ })
+
+ it('compensates when delivery throws', async () => {
+ mockSendInvitationEmail.mockRejectedValueOnce(new Error('Mail transport unavailable'))
+ expect((await callResend()).status).toBe(502)
+ expect(mockRevertInvitationResend).toHaveBeenCalledOnce()
+ })
+
+ it('reports a conflict when failed delivery cannot be compensated over newer state', async () => {
+ mockSendInvitationEmail.mockResolvedValueOnce({ success: false })
+ mockRevertInvitationResend.mockResolvedValueOnce(false)
+ expect((await callResend()).status).toBe(409)
})
})
diff --git a/apps/sim/app/api/invitations/[id]/resend/route.ts b/apps/sim/app/api/invitations/[id]/resend/route.ts
index 2d8bb7f511b..7ada3ddac7e 100644
--- a/apps/sim/app/api/invitations/[id]/resend/route.ts
+++ b/apps/sim/app/api/invitations/[id]/resend/route.ts
@@ -1,223 +1,22 @@
-import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
-import { db } from '@sim/db'
-import { user } from '@sim/db/schema'
-import { createLogger } from '@sim/logger'
-import { eq } from 'drizzle-orm'
-import { type NextRequest, NextResponse } from 'next/server'
-import { invitationParamsSchema } from '@/lib/api/contracts/invitations'
-import { getValidationErrorMessage } from '@/lib/api/server'
-import { getSession } from '@/lib/auth'
-import { getOrganizationSubscription } from '@/lib/billing/core/billing'
-import { isOrganizationOwnerOrAdmin } from '@/lib/billing/core/organization'
-import { isEnterprise, isTeam } from '@/lib/billing/plan-helpers'
-import { hasUsableSubscriptionStatus } from '@/lib/billing/subscriptions/utils'
-import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
-import { getInvitationById, resolveInvitationAdmissionOrganizationId } from '@/lib/invitations/core'
+import { resendInvitationContract } from '@/lib/api/contracts/invitations'
import {
- persistInvitationResend,
- prepareInvitationResend,
- sendInvitationEmail,
-} from '@/lib/invitations/send'
-import { capabilityRefusalResponse } from '@/lib/permission-groups/capability-response'
-import { getWorkspaceWithOwner, hasWorkspaceAdminAccess } from '@/lib/workspaces/permissions/utils'
-import { getWorkspaceInvitePolicy } from '@/lib/workspaces/policy'
-import {
- InvitationsNotAllowedError,
- validateInvitationsAllowed,
-} from '@/ee/access-control/utils/permission-check'
-
-const logger = createLogger('InvitationResendAPI')
-
-export const POST = withRouteHandler(
- async (request: NextRequest, { params }: { params: Promise<{ id: string }> }) => {
- const parsedParams = invitationParamsSchema.safeParse(await params)
- if (!parsedParams.success) {
- return NextResponse.json(
- { error: getValidationErrorMessage(parsedParams.error) },
- { status: 400 }
- )
- }
- const { id } = parsedParams.data
- const session = await getSession()
-
- if (!session?.user?.id) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
- }
-
- try {
- const inv = await getInvitationById(id)
- if (!inv) {
- return NextResponse.json({ error: 'Invitation not found' }, { status: 404 })
- }
- if (inv.status !== 'pending') {
- return NextResponse.json({ error: 'Can only resend pending invitations' }, { status: 400 })
- }
-
- let canResend = false
- if (inv.organizationId) {
- canResend = await isOrganizationOwnerOrAdmin(session.user.id, inv.organizationId)
- }
- if (!canResend && inv.grants.length > 0) {
- const adminChecks = await Promise.all(
- inv.grants.map((grant) => hasWorkspaceAdminAccess(session.user.id, grant.workspaceId))
- )
- canResend = adminChecks.some(Boolean)
- }
- if (!canResend) {
- return NextResponse.json(
- { error: 'Only an organization or workspace admin can resend this invitation' },
- { status: 403 }
- )
- }
-
- /**
- * permission-group-enforced: invitations.send — a resend is a send.
- *
- * It re-delivers a working link and pushes `expiresAt` forward, so an
- * organization that has withheld invitations would otherwise still admit
- * new people: every pending invitation stays revivable indefinitely by
- * anyone who can reach this route, and each resend mints a fresh token.
- * The invitee has not joined yet — resend is the step that gets them in —
- * which is why this is not the webhook active-config carve-out, where the
- * reachability already exists and the edit only adjusts it.
- *
- * Each granted workspace resolves the group governing the caller there,
- * exactly as creation does. The organization scope is checked *as well*,
- * not instead, whenever the invitation ADMITS TO an organization — which
- * is not the same question as its `kind`. A workspace-kind invitation
- * whose granted workspace belongs to an organization joins the invitee to
- * that organization exactly as an organization-kind one does, so keying
- * this on the kind left every organization-backed workspace invitation
- * performing an ungated organization admission. `resolveInvitationAdmission-
- * OrganizationId` answers it from acceptance's own derivation: the live
- * organization of the granted workspace for a workspace-kind invitation,
- * the stamped one otherwise, and nobody at all when the intent is external
- * or the stamped organization refuses the escalation — the three cases
- * where acceptance creates no member row. Gating only the grants would let
- * an explicit workspace group that permits invitations carry a member into
- * an organization whose default group withholds them.
- *
- * Run after the admin check above, for the reason
- * `resolveWorkspaceInvitationContext` records — the refusal names an
- * organization setting, so it must not reach someone with no admin reach.
- */
- try {
- const admissionOrganizationId = await resolveInvitationAdmissionOrganizationId(inv)
- if (admissionOrganizationId) {
- await validateInvitationsAllowed(session.user.id, {
- organizationId: admissionOrganizationId,
- })
- }
- for (const grant of inv.grants) {
- await validateInvitationsAllowed(session.user.id, { workspaceId: grant.workspaceId })
- }
- } catch (error) {
- if (error instanceof InvitationsNotAllowedError) {
- logger.warn('Invitation resend blocked by permission group', { invitationId: id })
- return capabilityRefusalResponse('invitations.send')
- }
- throw error
- }
-
- for (const grant of inv.grants) {
- const workspaceDetails = await getWorkspaceWithOwner(grant.workspaceId)
- if (!workspaceDetails) {
- return NextResponse.json(
- { error: 'Invitation references a workspace that no longer exists' },
- { status: 409 }
- )
- }
- const policy = await getWorkspaceInvitePolicy(workspaceDetails)
- if (!policy.allowed) {
- return NextResponse.json(
- {
- error: policy.reason ?? 'Invites are no longer allowed on this workspace',
- upgradeRequired: policy.upgradeRequired,
- },
- { status: 403 }
- )
- }
- }
-
- if (inv.kind === 'organization' && inv.grants.length === 0 && inv.organizationId) {
- const orgSubscription = await getOrganizationSubscription(inv.organizationId)
- const orgOnTeamOrEnterprise =
- !!orgSubscription &&
- hasUsableSubscriptionStatus(orgSubscription.status) &&
- (isTeam(orgSubscription.plan) || isEnterprise(orgSubscription.plan))
- if (!orgOnTeamOrEnterprise) {
- return NextResponse.json(
- {
- error: 'Invites are no longer allowed on this organization',
- upgradeRequired: true,
- },
- { status: 403 }
- )
- }
- }
-
- const { tokenForEmail, nextToken, nextExpiresAt } = await prepareInvitationResend({
- invitationId: id,
- rotateToken: true,
- currentToken: inv.token,
- })
-
- const [inviterRow] = await db
- .select({ name: user.name, email: user.email })
- .from(user)
- .where(eq(user.id, session.user.id))
- .limit(1)
-
- const emailResult = await sendInvitationEmail({
- invitationId: inv.id,
- token: tokenForEmail,
- kind: inv.kind,
- email: inv.email,
- inviterName: inviterRow?.name || inviterRow?.email || 'A user',
- organizationId: inv.organizationId,
- organizationRole: (inv.role as 'admin' | 'member') || 'member',
- grants: inv.grants.map((grant) => ({
- workspaceId: grant.workspaceId,
- permission: grant.permission,
- })),
- })
-
- if (!emailResult.success) {
- return NextResponse.json(
- { error: emailResult.error || 'Failed to send invitation email' },
- { status: 502 }
- )
- }
-
- await persistInvitationResend({ invitationId: id, nextToken, nextExpiresAt })
-
- recordAudit({
- workspaceId: inv.grants[0]?.workspaceId ?? null,
- actorId: session.user.id,
- actorName: session.user.name ?? undefined,
- actorEmail: session.user.email ?? undefined,
- action:
- inv.kind === 'workspace'
- ? AuditAction.INVITATION_RESENT
- : AuditAction.ORG_INVITATION_RESENT,
- resourceType:
- inv.kind === 'workspace' ? AuditResourceType.WORKSPACE : AuditResourceType.ORGANIZATION,
- resourceId: inv.organizationId ?? inv.grants[0]?.workspaceId ?? inv.id,
- description: `Resent ${inv.kind} invitation to ${inv.email}`,
- metadata: {
- invitationId: inv.id,
- targetEmail: inv.email,
- targetRole: inv.role,
- kind: inv.kind,
- membershipIntent: inv.membershipIntent,
- },
- request,
- })
-
- return NextResponse.json({ success: true })
- } catch (error) {
- logger.error('Failed to resend invitation', { invitationId: id, error })
- return NextResponse.json({ error: 'Failed to resend invitation' }, { status: 500 })
- }
- }
-)
+ defineInternalJsonRoute,
+ internalRateLimits,
+ internalSessionAuth,
+} from '@/lib/api/server/routes'
+import { internalOrganizationErrorPolicy } from '@/lib/api/server/routes/organizations'
+import { resendInvitation } from '@/lib/invitations/application/mutations'
+import { invitationOperations } from '@/lib/invitations/application/operations'
+
+export const POST = defineInternalJsonRoute({
+ contract: resendInvitationContract,
+ auth: internalSessionAuth,
+ operation: invitationOperations.resend,
+ rateLimit: internalRateLimits.none({
+ reason: 'Preserve existing invitation management admission',
+ }),
+ errorPolicy: internalOrganizationErrorPolicy,
+ mapInput: ({ params }) => ({ invitationId: params.id }),
+ useCase: resendInvitation,
+ present: () => ({ success: true }),
+})
diff --git a/apps/sim/app/api/invitations/[id]/route.ts b/apps/sim/app/api/invitations/[id]/route.ts
index 4e4dd46b9f2..1dd511f7aea 100644
--- a/apps/sim/app/api/invitations/[id]/route.ts
+++ b/apps/sim/app/api/invitations/[id]/route.ts
@@ -3,20 +3,26 @@ import { createLogger } from '@sim/logger'
import { normalizeEmail } from '@sim/utils/string'
import { type NextRequest, NextResponse } from 'next/server'
import {
- cancelInvitationQuerySchema,
+ cancelInvitationContract,
getInvitationContract,
- invitationParamsSchema,
updateInvitationContract,
} from '@/lib/api/contracts/invitations'
-import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
+import { parseRequest } from '@/lib/api/server'
+import {
+ defineInternalJsonRoute,
+ internalRateLimits,
+ internalSessionAuth,
+} from '@/lib/api/server/routes'
+import { internalOrganizationErrorPolicy } from '@/lib/api/server/routes/organizations'
import { getSession } from '@/lib/auth'
import { isOrganizationOwnerOrAdmin } from '@/lib/billing/core/organization'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
+import { revokeInvitation } from '@/lib/invitations/application/mutations'
+import { invitationOperations } from '@/lib/invitations/application/operations'
import {
getInvitationById,
getInvitationJoinPreview,
isInvitationExpired,
- revokeInvitationAsAdmin,
updateInvitation,
} from '@/lib/invitations/core'
import { hasWorkspaceAdminAccess } from '@/lib/workspaces/permissions/utils'
@@ -192,133 +198,15 @@ export const PATCH = withRouteHandler(
}
)
-export const DELETE = withRouteHandler(
- async (request: NextRequest, { params }: { params: Promise<{ id: string }> }) => {
- const parsedParams = invitationParamsSchema.safeParse(await params)
- if (!parsedParams.success) {
- return NextResponse.json(
- { error: getValidationErrorMessage(parsedParams.error) },
- { status: 400 }
- )
- }
- const { id } = parsedParams.data
- const parsedQuery = cancelInvitationQuerySchema.safeParse(
- Object.fromEntries(request.nextUrl.searchParams.entries())
- )
- if (!parsedQuery.success) {
- return NextResponse.json(
- { error: getValidationErrorMessage(parsedQuery.error, 'Invalid query parameters') },
- { status: 400 }
- )
- }
- const scopedWorkspaceId = parsedQuery.data.workspaceId
- const session = await getSession()
-
- if (!session?.user?.id) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
- }
-
- try {
- const result = await revokeInvitationAsAdmin({
- actorId: session.user.id,
- invitationId: id,
- workspaceId: scopedWorkspaceId,
- })
- if (!result.success) {
- if (result.kind === 'not-found') {
- return NextResponse.json({ error: 'Invitation not found' }, { status: 404 })
- }
- if (result.kind === 'not-pending') {
- return NextResponse.json(
- { error: 'Can only cancel pending invitations' },
- { status: 400 }
- )
- }
- if (result.kind === 'grant-not-found') {
- return NextResponse.json(
- { error: 'Invitation does not grant access to that workspace' },
- { status: 400 }
- )
- }
- if (result.kind === 'scoped-forbidden') {
- return NextResponse.json(
- { error: 'You need admin permissions on that workspace to revoke its invitation' },
- { status: 403 }
- )
- }
- if (result.kind === 'whole-forbidden') {
- return NextResponse.json(
- {
- error: result.spansMultipleWorkspaces
- ? 'This invitation spans several workspaces. Revoke it from a workspace you administer, or ask an organization admin.'
- : 'Only an organization or workspace admin can cancel this invitation',
- },
- { status: 403 }
- )
- }
- return NextResponse.json({ error: 'Invitation not cancellable' }, { status: 400 })
- }
-
- /**
- * Scoped revocation: an admin of this one workspace may withdraw its own
- * grant. Authority over the invitation's other workspaces is not implied,
- * so only that grant is removed.
- */
- if (scopedWorkspaceId) {
- recordAudit({
- workspaceId: scopedWorkspaceId,
- actorId: session.user.id,
- actorName: session.user.name ?? undefined,
- actorEmail: session.user.email ?? undefined,
- action: AuditAction.INVITATION_REVOKED,
- resourceType: AuditResourceType.WORKSPACE,
- resourceId: scopedWorkspaceId,
- description: `Revoked ${result.invitation.email}'s pending invitation to this workspace`,
- metadata: {
- invitationId: id,
- targetEmail: result.invitation.email,
- workspaceId: scopedWorkspaceId,
- invitationCancelled: result.invitationCancelled,
- },
- request,
- })
-
- return NextResponse.json({
- success: true,
- invitationCancelled: result.invitationCancelled,
- })
- }
-
- const inv = result.invitation
- recordAudit({
- workspaceId: inv.grants[0]?.workspaceId ?? null,
- actorId: session.user.id,
- actorName: session.user.name ?? undefined,
- actorEmail: session.user.email ?? undefined,
- action:
- inv.kind === 'workspace'
- ? AuditAction.INVITATION_REVOKED
- : AuditAction.ORG_INVITATION_REVOKED,
- resourceType:
- inv.kind === 'workspace' ? AuditResourceType.WORKSPACE : AuditResourceType.ORGANIZATION,
- resourceId: inv.organizationId ?? inv.grants[0]?.workspaceId ?? id,
- description: `Cancelled ${inv.kind} invitation for ${inv.email}`,
- metadata: {
- invitationId: id,
- targetEmail: inv.email,
- targetRole: inv.role,
- kind: inv.kind,
- },
- request,
- })
-
- return NextResponse.json({
- success: true,
- invitationCancelled: result.invitationCancelled,
- })
- } catch (error) {
- logger.error('Failed to cancel invitation', { invitationId: id, error })
- return NextResponse.json({ error: 'Failed to cancel invitation' }, { status: 500 })
- }
- }
-)
+export const DELETE = defineInternalJsonRoute({
+ contract: cancelInvitationContract,
+ auth: internalSessionAuth,
+ operation: invitationOperations.revoke,
+ rateLimit: internalRateLimits.none({
+ reason: 'Preserve existing invitation management admission',
+ }),
+ errorPolicy: internalOrganizationErrorPolicy,
+ mapInput: ({ params, query }) => ({ invitationId: params.id, workspaceId: query.workspaceId }),
+ useCase: revokeInvitation,
+ present: (result) => ({ success: true, invitationCancelled: result.invitationCancelled }),
+})
diff --git a/apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts b/apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts
index 50e265f208f..63165da87e8 100644
--- a/apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts
+++ b/apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts
@@ -1,29 +1,29 @@
-import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
import { db, dbReplica } from '@sim/db'
import { member, user, userStats } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { isOrgAdminRole } from '@sim/platform-authz/workspace'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
-import { updateOrganizationMemberRoleContract } from '@/lib/api/contracts/organization'
-import { parseRequest } from '@/lib/api/server'
+import {
+ removeOrganizationMemberContract,
+ updateOrganizationMemberRoleContract,
+} from '@/lib/api/contracts/organization'
+import {
+ defineInternalJsonRoute,
+ internalRateLimits,
+ internalSessionAuth,
+} from '@/lib/api/server/routes'
+import { internalOrganizationErrorPolicy } from '@/lib/api/server/routes/organizations'
import { getSession } from '@/lib/auth'
import { setActiveOrganizationForCurrentSession } from '@/lib/auth/active-organization'
import { getOrganizationMemberUsageSnapshot } from '@/lib/billing/core/organization'
-import {
- acquireOrganizationUserMutationLocks,
- removeExternalUserFromOrganizationWorkspaces,
- removeUserFromOrganization,
- WORKSPACE_BILLING_ACCOUNT_REMOVAL_ERROR,
-} from '@/lib/billing/organizations/membership'
-import { reconcileOrganizationSeats } from '@/lib/billing/organizations/seats'
-import { ForbiddenOperationError } from '@/lib/core/application'
-import { OrchestrationError, statusForOrchestrationError } from '@/lib/core/orchestration/types'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
-import { isRetryableTransactionError } from '@/lib/db/transaction'
-import { changeMemberRoleTx } from '@/lib/organizations/members/lifecycle'
+import {
+ removeOrganizationMember,
+ updateOrganizationMember,
+} from '@/lib/organizations/application/members'
+import { organizationOperations } from '@/lib/organizations/application/operations'
import { captureServerEvent } from '@/lib/posthog/server'
-import { assertMembershipNotScimManaged } from '@/ee/scim/lib/managed-membership'
const logger = createLogger('OrganizationMemberAPI')
@@ -147,409 +147,84 @@ export const GET = withRouteHandler(
}
)
-/**
- * PUT /api/organizations/[id]/members/[memberId]
- * Update organization member role
- */
-export const PUT = withRouteHandler(
- async (request: NextRequest, context: { params: Promise<{ id: string; memberId: string }> }) => {
- try {
- const session = await getSession()
-
- if (!session?.user?.id) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
- }
-
- const parsed = await parseRequest(updateOrganizationMemberRoleContract, request, context)
- if (!parsed.success) return parsed.response
-
- const { id: organizationId, memberId } = parsed.data.params
- const { role } = parsed.data.body
-
- const userMember = await db
- .select()
- .from(member)
- .where(and(eq(member.organizationId, organizationId), eq(member.userId, session.user.id)))
- .limit(1)
-
- if (userMember.length === 0) {
- return NextResponse.json(
- { error: 'Forbidden - Not a member of this organization' },
- { status: 403 }
- )
- }
-
- if (!isOrgAdminRole(userMember[0].role)) {
- return NextResponse.json({ error: 'Forbidden - Admin access required' }, { status: 403 })
- }
-
- const targetMember = await db
- .select({
- id: member.id,
- role: member.role,
- userId: member.userId,
- email: user.email,
- name: user.name,
- })
- .from(member)
- .innerJoin(user, eq(member.userId, user.id))
- .where(and(eq(member.organizationId, organizationId), eq(member.userId, memberId)))
- .limit(1)
-
- if (targetMember.length === 0) {
- return NextResponse.json({ error: 'Member not found' }, { status: 404 })
- }
-
- if (targetMember[0].role === 'owner') {
- return NextResponse.json({ error: 'Cannot change owner role' }, { status: 400 })
- }
-
- if (role === 'owner') {
- return NextResponse.json(
- {
- error:
- 'Ownership transfer is not supported via this endpoint. Use POST /organizations/[id]/transfer-ownership instead.',
- },
- { status: 400 }
- )
- }
-
- /**
- * The member is re-read under the organization's mutation lock, so a
- * concurrent promotion to owner — or a directory provisioning this very
- * member — cannot slip between the checks and the write. When the
- * organization has made its identity provider the source of truth, a role
- * set here is reverted by the next sync; refusing says so.
- */
- const roleChange = await db.transaction(async (tx) => {
- await acquireOrganizationUserMutationLocks(tx, {
- userId: memberId,
- organizationIds: [organizationId],
- })
- await assertMembershipNotScimManaged({ organizationId, userId: memberId, executor: tx })
- return changeMemberRoleTx(tx, { organizationId, userId: memberId, role })
- })
-
- /**
- * The audit row and analytics event fire whether or not the role actually
- * moved, exactly as this route did before the write went through the
- * shared primitive. Callers assert on those side effects.
- */
- logger.info('Organization member role updated', {
- organizationId,
- memberId,
- newRole: role,
- updatedBy: session.user.id,
- })
-
- recordAudit({
- workspaceId: null,
- actorId: session.user.id,
- action: AuditAction.ORG_MEMBER_ROLE_CHANGED,
- resourceType: AuditResourceType.ORGANIZATION,
- resourceId: organizationId,
- actorName: session.user.name ?? undefined,
- actorEmail: session.user.email ?? undefined,
- description: `Changed role for member ${memberId} to ${role}`,
- metadata: {
- targetUserId: memberId,
- targetEmail: targetMember[0].email ?? undefined,
- targetName: targetMember[0].name ?? undefined,
- changes: [{ field: 'role', from: targetMember[0].role, to: role }],
- },
- request,
- })
-
- captureServerEvent(
- session.user.id,
- 'org_member_role_changed',
- { organization_id: organizationId, new_role: role },
- { groups: { organization: organizationId } }
- )
-
- return NextResponse.json({
- success: true,
- message: 'Member role updated successfully',
- data: {
- id: targetMember[0].id,
- userId: targetMember[0].userId,
- role: roleChange.changed ? roleChange.to : roleChange.role,
- updatedBy: session.user.id,
- },
- })
- } catch (error) {
- if (error instanceof ForbiddenOperationError) {
- return NextResponse.json(
- { error: error.message, details: { code: error.detailCode } },
- { status: 403 }
- )
- }
- if (error instanceof OrchestrationError) {
- return NextResponse.json(
- { error: error.message },
- { status: statusForOrchestrationError(error.code) }
- )
- }
- /** The role change now serializes on the organization lock; a timeout is "retry", not a fault. */
- if (isRetryableTransactionError(error)) {
- return NextResponse.json(
- { error: 'The organization is busy; retry in a moment' },
- { status: 409 }
- )
- }
-
- logger.error('Failed to update organization member role', {
- organizationId: (await context.params).id,
- memberId: (await context.params).memberId,
- error,
- })
-
- return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
- }
- }
-)
-
-/**
- * DELETE /api/organizations/[id]/members/[memberId]
- * Remove member from organization
- */
-export const DELETE = withRouteHandler(
- async (
- request: NextRequest,
- { params }: { params: Promise<{ id: string; memberId: string }> }
- ) => {
- try {
- const session = await getSession()
-
- if (!session?.user?.id) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
- }
-
- const { id: organizationId, memberId: targetUserId } = await params
-
- const userMember = await db
- .select()
- .from(member)
- .where(and(eq(member.organizationId, organizationId), eq(member.userId, session.user.id)))
- .limit(1)
-
- if (userMember.length === 0) {
- return NextResponse.json(
- { error: 'Forbidden - Not a member of this organization' },
- { status: 403 }
- )
- }
-
- const canRemoveMembers =
- isOrgAdminRole(userMember[0].role) || session.user.id === targetUserId
-
- if (!canRemoveMembers) {
- return NextResponse.json({ error: 'Forbidden - Insufficient permissions' }, { status: 403 })
- }
-
- const targetMember = await db
- .select({ id: member.id, role: member.role, email: user.email, name: user.name })
- .from(member)
- .innerJoin(user, eq(member.userId, user.id))
- .where(and(eq(member.organizationId, organizationId), eq(member.userId, targetUserId)))
- .limit(1)
-
- if (targetMember.length === 0) {
- const [targetUser] = await db
- .select({ id: user.id, email: user.email, name: user.name })
- .from(user)
- .where(eq(user.id, targetUserId))
- .limit(1)
-
- if (!targetUser) {
- return NextResponse.json({ error: 'Member not found' }, { status: 404 })
- }
-
- const externalResult = await removeExternalUserFromOrganizationWorkspaces({
- userId: targetUserId,
- organizationId,
- })
-
- if (!externalResult.success) {
- const error = externalResult.error || 'External workspace member not found'
- const status =
- error === 'External workspace member not found'
- ? 404
- : error === 'User is an organization member'
- ? 409
- : error === WORKSPACE_BILLING_ACCOUNT_REMOVAL_ERROR
- ? 400
- : 500
-
- return NextResponse.json({ error }, { status })
- }
-
- logger.info('External workspace member removed from organization workspaces', {
- organizationId,
- removedMemberId: targetUserId,
- removedBy: session.user.id,
- workspaceAccessRevoked: externalResult.workspaceAccessRevoked,
- permissionGroupsRevoked: externalResult.permissionGroupsRevoked,
- credentialMembershipsRevoked: externalResult.credentialMembershipsRevoked,
- pendingInvitationsCancelled: externalResult.pendingInvitationsCancelled,
- })
-
- recordAudit({
- workspaceId: null,
- actorId: session.user.id,
- action: AuditAction.ORG_MEMBER_REMOVED,
- resourceType: AuditResourceType.ORGANIZATION,
- resourceId: organizationId,
- actorName: session.user.name ?? undefined,
- actorEmail: session.user.email ?? undefined,
- description: `Removed external workspace member ${targetUserId} from organization`,
- metadata: {
- targetUserId,
- targetEmail: targetUser.email ?? undefined,
- targetName: targetUser.name ?? undefined,
+export const PUT = defineInternalJsonRoute({
+ contract: updateOrganizationMemberRoleContract,
+ auth: internalSessionAuth,
+ operation: organizationOperations.updateMember,
+ rateLimit: internalRateLimits.none({
+ reason: 'Preserve existing organization member administration behavior.',
+ }),
+ errorPolicy: internalOrganizationErrorPolicy,
+ mapInput: ({ params, body }) => ({
+ organizationId: params.id,
+ userId: params.memberId,
+ role: body.role,
+ }),
+ useCase: updateOrganizationMember,
+ present: ({ member }, { principal }) => ({
+ success: true,
+ message: 'Member role updated successfully',
+ data: { id: member.id, userId: member.userId, role: member.role, updatedBy: principal.userId },
+ }),
+ onSuccess: ({ principal, input }) => {
+ captureServerEvent(
+ principal.userId,
+ 'org_member_role_changed',
+ { organization_id: input.organizationId, new_role: input.role },
+ { groups: { organization: input.organizationId } }
+ )
+ },
+})
+
+export const DELETE = defineInternalJsonRoute({
+ contract: removeOrganizationMemberContract,
+ auth: internalSessionAuth,
+ operation: organizationOperations.removeMember,
+ rateLimit: internalRateLimits.none({
+ reason: 'Preserve existing organization member administration behavior.',
+ }),
+ errorPolicy: internalOrganizationErrorPolicy,
+ mapInput: ({ params }) => ({ organizationId: params.id, userId: params.memberId }),
+ useCase: removeOrganizationMember,
+ present: (result) => ({
+ success: true,
+ message:
+ result.membershipType === 'external'
+ ? 'External member removed successfully'
+ : result.wasSelfRemoval
+ ? 'You have left the organization'
+ : 'Member removed successfully',
+ data: {
+ removedMemberId: result.target.userId,
+ removedBy: result.removedBy,
+ removedAt: result.removedAt,
+ ...(result.membershipType === 'external'
+ ? {
membershipType: 'external',
- workspaceAccessRevoked: externalResult.workspaceAccessRevoked,
- permissionGroupsRevoked: externalResult.permissionGroupsRevoked,
- credentialMembershipsRevoked: externalResult.credentialMembershipsRevoked,
- pendingInvitationsCancelled: externalResult.pendingInvitationsCancelled,
- },
- request,
- })
-
- captureServerEvent(
- session.user.id,
- 'org_member_removed',
- { organization_id: organizationId, is_self_removal: session.user.id === targetUserId },
- { groups: { organization: organizationId } }
- )
-
- return NextResponse.json({
- success: true,
- message: 'External member removed successfully',
- data: {
- removedMemberId: targetUserId,
- removedBy: session.user.id,
- removedAt: new Date().toISOString(),
- membershipType: 'external',
- workspaceAccessRevoked: externalResult.workspaceAccessRevoked,
- permissionGroupsRevoked: externalResult.permissionGroupsRevoked,
- credentialMembershipsRevoked: externalResult.credentialMembershipsRevoked,
- pendingInvitationsCancelled: externalResult.pendingInvitationsCancelled,
- },
- })
- }
-
- const result = await removeUserFromOrganization({
- userId: targetUserId,
- organizationId,
- memberId: targetMember[0].id,
- spareSessionToken: session.session.token,
- })
-
- if (!result.success) {
- if (result.error === 'Cannot remove organization owner') {
- return NextResponse.json({ error: result.error }, { status: 400 })
- }
- if (result.error === 'Member not found') {
- return NextResponse.json({ error: result.error }, { status: 404 })
- }
- if (result.error === WORKSPACE_BILLING_ACCOUNT_REMOVAL_ERROR) {
- return NextResponse.json({ error: result.error }, { status: 400 })
- }
- return NextResponse.json({ error: result.error }, { status: 500 })
- }
-
- let seatReduction: Awaited> | null = null
+ workspaceAccessRevoked: result.removal.workspaceAccessRevoked,
+ permissionGroupsRevoked: result.removal.permissionGroupsRevoked,
+ credentialMembershipsRevoked: result.removal.credentialMembershipsRevoked,
+ pendingInvitationsCancelled: result.removal.pendingInvitationsCancelled,
+ }
+ : { seatReduction: result.seatReduction }),
+ },
+ }),
+ async onSuccess({ principal, input, result }) {
+ if (result.wasSelfRemoval) {
try {
- seatReduction = await reconcileOrganizationSeats({
- organizationId,
- reason: 'member-removed',
- actorId: session.user.id,
+ await setActiveOrganizationForCurrentSession(null)
+ } catch (error) {
+ logger.warn('Failed to clear active organization after self-removal', {
+ organizationId: input.organizationId,
+ error,
})
- } catch (seatError) {
- logger.error('Failed to reduce seats after member removal', {
- organizationId,
- removedMemberId: targetUserId,
- removedBy: session.user.id,
- error: seatError,
- })
- seatReduction = {
- changed: false,
- reason: 'Failed to reduce seats after member removal',
- }
}
-
- if (session.user.id === targetUserId) {
- try {
- await setActiveOrganizationForCurrentSession(null)
- } catch (clearError) {
- logger.warn('Failed to clear active organization after self-removal', {
- userId: session.user.id,
- organizationId,
- error: clearError,
- })
- }
- }
-
- logger.info('Organization member removed', {
- organizationId,
- removedMemberId: targetUserId,
- removedBy: session.user.id,
- wasSelfRemoval: session.user.id === targetUserId,
- billingActions: result.billingActions,
- seatReduction,
- })
-
- recordAudit({
- workspaceId: null,
- actorId: session.user.id,
- action: AuditAction.ORG_MEMBER_REMOVED,
- resourceType: AuditResourceType.ORGANIZATION,
- resourceId: organizationId,
- actorName: session.user.name ?? undefined,
- actorEmail: session.user.email ?? undefined,
- description:
- session.user.id === targetUserId
- ? 'Left the organization'
- : `Removed member ${targetUserId} from organization`,
- metadata: {
- targetUserId,
- targetEmail: targetMember[0].email ?? undefined,
- targetName: targetMember[0].name ?? undefined,
- wasSelfRemoval: session.user.id === targetUserId,
- seatReduction,
- },
- request,
- })
-
- captureServerEvent(
- session.user.id,
- 'org_member_removed',
- { organization_id: organizationId, is_self_removal: session.user.id === targetUserId },
- { groups: { organization: organizationId } }
- )
-
- return NextResponse.json({
- success: true,
- message:
- session.user.id === targetUserId
- ? 'You have left the organization'
- : 'Member removed successfully',
- data: {
- removedMemberId: targetUserId,
- removedBy: session.user.id,
- removedAt: new Date().toISOString(),
- seatReduction,
- },
- })
- } catch (error) {
- logger.error('Failed to remove organization member', {
- organizationId: (await params).id,
- memberId: (await params).memberId,
- error,
- })
-
- return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
- }
-)
+ captureServerEvent(
+ principal.userId,
+ 'org_member_removed',
+ { organization_id: input.organizationId, is_self_removal: result.wasSelfRemoval },
+ { groups: { organization: input.organizationId } }
+ )
+ },
+})
diff --git a/apps/sim/app/api/organizations/[id]/members/route.test.ts b/apps/sim/app/api/organizations/[id]/members/route.test.ts
index 5eab80f930b..248783cec4f 100644
--- a/apps/sim/app/api/organizations/[id]/members/route.test.ts
+++ b/apps/sim/app/api/organizations/[id]/members/route.test.ts
@@ -56,7 +56,10 @@ describe('GET /api/organizations/[id]/members', () => {
beforeEach(() => {
vi.clearAllMocks()
resetDbChainMock()
- mockGetSession.mockResolvedValue(createSession({ userId: 'user-reader' }))
+ mockGetSession.mockResolvedValue({
+ ...createSession({ userId: 'user-reader' }),
+ session: { id: 'session-reader' },
+ })
mockGetOrgPermissionConfig.mockResolvedValue(null)
})
@@ -91,7 +94,7 @@ describe('GET /api/organizations/[id]/members', () => {
const response = await request()
expect(response.status).toBe(403)
- await expect(response.json()).resolves.toEqual({
+ await expect(response.json()).resolves.toMatchObject({
error: capabilityRefusal('organization.member_directory'),
})
})
diff --git a/apps/sim/app/api/organizations/[id]/members/route.ts b/apps/sim/app/api/organizations/[id]/members/route.ts
index 858c33f74ae..1d7b7701de2 100644
--- a/apps/sim/app/api/organizations/[id]/members/route.ts
+++ b/apps/sim/app/api/organizations/[id]/members/route.ts
@@ -1,201 +1,64 @@
-import { db } from '@sim/db'
-import { member, user, userStats } from '@sim/db/schema'
-import { createLogger } from '@sim/logger'
-import { isOrgAdminRole } from '@sim/platform-authz/workspace'
-import { and, count, eq } from 'drizzle-orm'
-import { type NextRequest, NextResponse } from 'next/server'
import {
- organizationMemberQuerySchema,
- organizationParamsSchema,
+ listOrganizationMembersContract,
+ organizationMemberUsageSchema,
} from '@/lib/api/contracts/organization'
-import { getValidationErrorMessage } from '@/lib/api/server'
-import { getSession } from '@/lib/auth'
-import { getOrganizationMemberUsageSnapshot } from '@/lib/billing/core/organization'
-import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
+import { organizationRoleSchema } from '@/lib/api/contracts/primitives'
import {
- capabilityRefusal,
- isOrganizationCapabilityWithheld,
-} from '@/lib/permission-groups/capability-assertions'
-
-const logger = createLogger('OrganizationMembersAPI')
-
-/**
- * GET /api/organizations/[id]/members
- * Get organization members with optional usage data
- */
-export const GET = withRouteHandler(
- async (request: NextRequest, { params }: { params: Promise<{ id: string }> }) => {
- try {
- const session = await getSession()
-
- if (!session?.user?.id) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
- }
-
- const paramsResult = organizationParamsSchema.safeParse(await params)
- if (!paramsResult.success) {
- return NextResponse.json(
- { error: getValidationErrorMessage(paramsResult.error, 'Invalid route parameters') },
- { status: 400 }
- )
- }
-
- const { id: organizationId } = paramsResult.data
- const queryResult = organizationMemberQuerySchema.safeParse(
- Object.fromEntries(request.nextUrl.searchParams.entries())
- )
- if (!queryResult.success) {
- return NextResponse.json(
- { error: getValidationErrorMessage(queryResult.error, 'Invalid query parameters') },
- { status: 400 }
- )
- }
- const { limit, offset } = queryResult.data
- const includeUsage = queryResult.data.include === 'usage'
-
- // Verify user has access to this organization
- const memberEntry = await db
- .select()
- .from(member)
- .where(and(eq(member.organizationId, organizationId), eq(member.userId, session.user.id)))
- .limit(1)
-
- if (memberEntry.length === 0) {
- return NextResponse.json(
- { error: 'Forbidden - Not a member of this organization' },
- { status: 403 }
- )
- }
-
- const userRole = memberEntry[0].role
- const hasAdminAccess = isOrgAdminRole(userRole)
-
- /**
- * permission-group-enforced: organization.member_directory — an
- * organization-scoped read with no workspace for the funnel to authorize.
- *
- * Admins and owners are exempt. This response is the only source for the
- * team-management page and the seat-usage snapshot it renders, so
- * withholding it from an admin would take away the page they would use to
- * change the setting, and their seat management with it.
- */
- if (
- !hasAdminAccess &&
- (await isOrganizationCapabilityWithheld(organizationId, 'organization.member_directory'))
- ) {
- logger.warn('Organization member directory blocked by permission group', {
- organizationId,
- userId: session.user.id,
- })
- return NextResponse.json(
- { error: capabilityRefusal('organization.member_directory') },
- { status: 403 }
- )
- }
-
- // Get organization members
- const memberPageQuery = db
- .select({
- id: member.id,
- userId: member.userId,
- organizationId: member.organizationId,
- role: member.role,
- createdAt: member.createdAt,
- userName: user.name,
- userEmail: user.email,
- })
- .from(member)
- .innerJoin(user, eq(member.userId, user.id))
- .where(eq(member.organizationId, organizationId))
- .orderBy(user.name, user.id)
- .limit(limit)
- .offset(offset)
-
- const totalQuery = db
- .select({ value: count() })
- .from(member)
- .where(eq(member.organizationId, organizationId))
-
- // Include usage data if requested and user has admin access
- if (includeUsage && hasAdminAccess) {
- const [base, totalRows] = await Promise.all([
- db
- .select({
- id: member.id,
- userId: member.userId,
- organizationId: member.organizationId,
- role: member.role,
- createdAt: member.createdAt,
- userName: user.name,
- userEmail: user.email,
- currentUsageLimit: userStats.currentUsageLimit,
- usageLimitUpdatedAt: userStats.usageLimitUpdatedAt,
- })
- .from(member)
- .innerJoin(user, eq(member.userId, user.id))
- .leftJoin(userStats, eq(user.id, userStats.userId))
- .where(eq(member.organizationId, organizationId))
- .orderBy(user.name, user.id)
- .limit(limit)
- .offset(offset),
- totalQuery,
- ])
-
- const { billingPeriod, usageByUser } = await getOrganizationMemberUsageSnapshot(
- organizationId,
- {
- userIds: base.map((row) => row.userId),
- }
- )
- const billingPeriodStart = billingPeriod?.start ?? null
- const billingPeriodEnd = billingPeriod?.end ?? null
-
- const membersWithUsage = base.map((row) => ({
+ defineInternalJsonRoute,
+ internalRateLimits,
+ internalSessionAuth,
+} from '@/lib/api/server/routes'
+import { internalOrganizationErrorPolicy } from '@/lib/api/server/routes/organizations'
+import { organizationOperations } from '@/lib/organizations/application/operations'
+import { listOrganizationMembers } from '@/lib/organizations/application/reads'
+
+export const GET = defineInternalJsonRoute({
+ contract: listOrganizationMembersContract,
+ auth: internalSessionAuth,
+ operation: organizationOperations.listMembers,
+ rateLimit: internalRateLimits.none({
+ reason: 'Preserve existing organization member-directory behavior.',
+ }),
+ errorPolicy: internalOrganizationErrorPolicy,
+ mapInput: ({ params, query }) => ({
+ organizationId: params.id,
+ limit: query.limit,
+ offset: query.offset,
+ sortBy: 'name' as const,
+ sortOrder: 'asc' as const,
+ includeUsage: query.include === 'usage',
+ }),
+ useCase: listOrganizationMembers,
+ present: ({ data, total, userRole, hasAdminAccess }, { input }) => {
+ if (total === undefined || input.offset === undefined)
+ throw new Error('Internal directory requires offset pagination')
+ return {
+ success: true,
+ data: data.map((row) =>
+ organizationMemberUsageSchema.parse({
...row,
- currentPeriodCost: (usageByUser.get(row.userId) ?? 0).toString(),
- billingPeriodStart,
- billingPeriodEnd,
- }))
-
- const total = totalRows[0]?.value ?? 0
- return NextResponse.json({
- success: true,
- data: membersWithUsage,
- total,
- pagination: {
- total,
- limit,
- offset,
- hasMore: offset + membersWithUsage.length < total,
- },
- userRole,
- hasAdminAccess,
+ role: organizationRoleSchema.parse(row.role),
+ createdAt: row.createdAt.toISOString(),
+ ...(row.usageLimitUpdatedAt === undefined
+ ? {}
+ : { usageLimitUpdatedAt: row.usageLimitUpdatedAt?.toISOString() ?? null }),
+ ...(row.billingPeriodStart === undefined
+ ? {}
+ : { billingPeriodStart: row.billingPeriodStart?.toISOString() ?? null }),
+ ...(row.billingPeriodEnd === undefined
+ ? {}
+ : { billingPeriodEnd: row.billingPeriodEnd?.toISOString() ?? null }),
})
- }
-
- const [members, totalRows] = await Promise.all([memberPageQuery, totalQuery])
- const total = totalRows[0]?.value ?? 0
-
- return NextResponse.json({
- success: true,
- data: members,
+ ),
+ total,
+ pagination: {
total,
- pagination: {
- total,
- limit,
- offset,
- hasMore: offset + members.length < total,
- },
- userRole,
- hasAdminAccess,
- })
- } catch (error) {
- logger.error('Failed to get organization members', {
- organizationId: (await params).id,
- error,
- })
-
- return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
+ limit: input.limit,
+ offset: input.offset,
+ hasMore: input.offset + data.length < total,
+ },
+ userRole: organizationRoleSchema.parse(userRole),
+ hasAdminAccess,
}
- }
-)
+ },
+})
diff --git a/apps/sim/app/api/organizations/[id]/permission-groups/[groupId]/members/bulk/route.ts b/apps/sim/app/api/organizations/[id]/permission-groups/[groupId]/members/bulk/route.ts
index 5f7257295d2..8d95ae133ae 100644
--- a/apps/sim/app/api/organizations/[id]/permission-groups/[groupId]/members/bulk/route.ts
+++ b/apps/sim/app/api/organizations/[id]/permission-groups/[groupId]/members/bulk/route.ts
@@ -1,211 +1,22 @@
-import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
-import { db } from '@sim/db'
-import { member, permissionGroupMember } from '@sim/db/schema'
-import { createLogger } from '@sim/logger'
-import { getPostgresConstraintName, getPostgresErrorCode } from '@sim/utils/errors'
-import { generateId } from '@sim/utils/id'
-import { and, eq, inArray } from 'drizzle-orm'
-import { type NextRequest, NextResponse } from 'next/server'
import { bulkAddPermissionGroupMembersContract } from '@/lib/api/contracts/permission-groups'
-import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
-import { getSession } from '@/lib/auth'
-import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import {
- findScopeConflicts,
- type ScopeConflict,
-} from '@/lib/permission-groups/application/group-membership'
-import { PERMISSION_GROUP_MEMBER_CONSTRAINTS } from '@/lib/permission-groups/constraints'
-import { acquirePermissionGroupOrgLock } from '@/lib/permission-groups/locks'
-import {
- authorizeOrgAccessControl,
- formatScopeConflictError,
- getGroupWorkspaces,
- loadGroupInOrganization,
-} from '@/app/api/organizations/[id]/permission-groups/utils'
-
-const logger = createLogger('OrganizationPermissionGroupBulkMembers')
-
-export const POST = withRouteHandler(
- async (req: NextRequest, context: { params: Promise<{ id: string; groupId: string }> }) => {
- const session = await getSession()
- if (!session?.user?.id) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
- }
-
- const { id: organizationId, groupId: id } = await context.params
-
- // Populated inside the transaction when a scope conflict is detected, so the
- // catch can format the 409 after the rollback.
- let scopeConflicts: ScopeConflict[] = []
-
- try {
- const denied = await authorizeOrgAccessControl(session.user.id, organizationId)
- if (denied) return denied
-
- const group = await loadGroupInOrganization(id, organizationId)
- if (!group) {
- return NextResponse.json({ error: 'Permission group not found' }, { status: 404 })
- }
-
- const parsed = await parseRequest(bulkAddPermissionGroupMembersContract, req, context, {
- validationErrorResponse: (error) =>
- NextResponse.json({ error: getValidationErrorMessage(error) }, { status: 400 }),
- })
- if (!parsed.success) return parsed.response
- const { userIds, addAllOrganizationMembers } = parsed.data.body
-
- let targetUserIds: string[] = []
-
- if (addAllOrganizationMembers) {
- const orgMembers = await db
- .select({ userId: member.userId })
- .from(member)
- .where(eq(member.organizationId, organizationId))
-
- targetUserIds = Array.from(new Set(orgMembers.map((m) => m.userId)))
- } else if (userIds && userIds.length > 0) {
- const uniqueUserIds = Array.from(new Set(userIds))
- const validMembers = await db
- .select({ userId: member.userId })
- .from(member)
- .where(
- and(eq(member.organizationId, organizationId), inArray(member.userId, uniqueUserIds))
- )
-
- targetUserIds = Array.from(new Set(validMembers.map((m) => m.userId)))
- }
-
- if (targetUserIds.length === 0) {
- return NextResponse.json({ added: 0, skipped: 0 })
- }
-
- const { addedUserIds } = await db.transaction(async (tx) => {
- // Serialize all permission-group writes for this org so the conflict
- // check and inserts are atomic against concurrent adds or scope changes.
- await acquirePermissionGroupOrgLock(tx, organizationId)
-
- // Re-read the group under the lock: a concurrent scope change may have
- // changed its workspaces since the pre-transaction load, so the conflict
- // check uses one consistent snapshot.
- const lockedGroup = await loadGroupInOrganization(id, organizationId, tx)
- if (!lockedGroup) {
- throw new Error('GROUP_NOT_FOUND')
- }
-
- // Bulk add is all-or-nothing for conflicts: if any selected user is
- // already an explicit member of another group sharing one of this group's
- // workspaces, add nobody and surface the conflict so the admin can fix the
- // selection. Members already in this group are no-ops.
- const groupWorkspaceIds = (await getGroupWorkspaces(id, tx)).map((ws) => ws.id)
- const conflicts = await findScopeConflicts(
- {
- organizationId,
- excludeGroupId: id,
- workspaceIds: groupWorkspaceIds,
- candidateUserIds: targetUserIds,
- },
- tx
- )
- if (conflicts.length > 0) {
- scopeConflicts = conflicts
- throw new Error('SCOPE_CONFLICT')
- }
-
- const existingInGroup = await tx
- .select({ userId: permissionGroupMember.userId })
- .from(permissionGroupMember)
- .where(
- and(
- eq(permissionGroupMember.permissionGroupId, id),
- inArray(permissionGroupMember.userId, targetUserIds)
- )
- )
- const alreadyInThisGroup = new Set(existingInGroup.map((m) => m.userId))
-
- const usersToAdd = targetUserIds.filter((uid) => !alreadyInThisGroup.has(uid))
-
- if (usersToAdd.length === 0) {
- return { addedUserIds: [] as string[] }
- }
-
- const newMembers = usersToAdd.map((userId) => ({
- id: generateId(),
- permissionGroupId: id,
- organizationId,
- userId,
- assignedBy: session.user.id,
- assignedAt: new Date(),
- }))
-
- await tx.insert(permissionGroupMember).values(newMembers)
-
- return { addedUserIds: usersToAdd }
- })
-
- const skipped = targetUserIds.length - addedUserIds.length
-
- if (addedUserIds.length === 0) {
- return NextResponse.json({ added: 0, skipped })
- }
-
- logger.info('Bulk added members to permission group', {
- permissionGroupId: id,
- organizationId,
- addedCount: addedUserIds.length,
- skipped,
- assignedBy: session.user.id,
- })
-
- recordAudit({
- actorId: session.user.id,
- action: AuditAction.PERMISSION_GROUP_MEMBER_ADDED,
- resourceType: AuditResourceType.PERMISSION_GROUP,
- resourceId: id,
- resourceName: group.name,
- actorName: session.user.name ?? undefined,
- actorEmail: session.user.email ?? undefined,
- description: `Bulk added ${addedUserIds.length} member(s) to permission group "${group.name}"`,
- metadata: {
- organizationId,
- permissionGroupId: id,
- addedUserIds,
- skipped,
- },
- request: req,
- })
-
- return NextResponse.json({ added: addedUserIds.length, skipped })
- } catch (error) {
- if (error instanceof Error && error.message === 'GROUP_NOT_FOUND') {
- return NextResponse.json({ error: 'Permission group not found' }, { status: 404 })
- }
- if (error instanceof Error && error.message === 'SCOPE_CONFLICT') {
- return NextResponse.json(
- { error: formatScopeConflictError(scopeConflicts) },
- { status: 409 }
- )
- }
- if (
- getPostgresErrorCode(error) === '23505' &&
- getPostgresConstraintName(error) === PERMISSION_GROUP_MEMBER_CONSTRAINTS.groupUser
- ) {
- return NextResponse.json(
- {
- error:
- 'One or more users were concurrently added to this group. Please refresh and try again.',
- },
- { status: 409 }
- )
- }
- // Advisory lock wait exceeded (lock_timeout) — transient contention.
- if (getPostgresErrorCode(error) === '55P03') {
- return NextResponse.json(
- { error: 'This group is being updated by another request. Please try again.' },
- { status: 503 }
- )
- }
- logger.error('Error bulk adding members to permission group', error)
- return NextResponse.json({ error: 'Failed to add members' }, { status: 500 })
- }
- }
-)
+ defineInternalJsonRoute,
+ internalRateLimits,
+ internalSessionAuth,
+} from '@/lib/api/server/routes'
+import { internalPermissionGroupErrorPolicy } from '@/lib/api/server/routes/permission-groups'
+import { permissionGroupOperations } from '@/lib/permission-groups/application/operations'
+import { bulkAddPermissionGroupMembers } from '@/lib/permission-groups/application/use-cases'
+
+export const POST = defineInternalJsonRoute({
+ contract: bulkAddPermissionGroupMembersContract,
+ auth: internalSessionAuth,
+ operation: permissionGroupOperations.bulkAddMembers,
+ rateLimit: internalRateLimits.none({
+ reason: 'Preserve existing permission group settings behavior',
+ }),
+ errorPolicy: internalPermissionGroupErrorPolicy,
+ mapInput: ({ params, body }) => ({ organizationId: params.id, groupId: params.groupId, ...body }),
+ useCase: bulkAddPermissionGroupMembers,
+ present: ({ added, skipped }) => ({ added, skipped }),
+})
diff --git a/apps/sim/app/api/organizations/[id]/permission-groups/[groupId]/members/route.ts b/apps/sim/app/api/organizations/[id]/permission-groups/[groupId]/members/route.ts
index b2a6ee6fc42..45f286e6b25 100644
--- a/apps/sim/app/api/organizations/[id]/permission-groups/[groupId]/members/route.ts
+++ b/apps/sim/app/api/organizations/[id]/permission-groups/[groupId]/members/route.ts
@@ -1,362 +1,65 @@
-import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
-import { db } from '@sim/db'
-import { permissionGroupMember, user } from '@sim/db/schema'
-import { createLogger } from '@sim/logger'
-import { getPostgresConstraintName, getPostgresErrorCode } from '@sim/utils/errors'
-import { generateId } from '@sim/utils/id'
-import { and, count, eq } from 'drizzle-orm'
-import { type NextRequest, NextResponse } from 'next/server'
-import { addPermissionGroupMemberContract } from '@/lib/api/contracts/permission-groups'
-import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
-import { getSession } from '@/lib/auth'
-import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import {
- type AllMembersConflict,
- findAllMembersWorkspaceConflict,
- findScopeConflicts,
- type ScopeConflict,
-} from '@/lib/permission-groups/application/group-membership'
-import { PERMISSION_GROUP_MEMBER_CONSTRAINTS } from '@/lib/permission-groups/constraints'
-import { acquirePermissionGroupOrgLock } from '@/lib/permission-groups/locks'
-import { isOrganizationMember } from '@/lib/workspaces/permissions/utils'
+ addPermissionGroupMemberContract,
+ listPermissionGroupMembersContract,
+ removePermissionGroupMemberContract,
+} from '@/lib/api/contracts/permission-groups'
+import { presentPermissionGroupMember } from '@/lib/api/server/permission-group-presenters'
import {
- authorizeOrgAccessControl,
- formatAllMembersConflictError,
- formatScopeConflictError,
- getGroupWorkspaces,
- loadGroupInOrganization,
-} from '@/app/api/organizations/[id]/permission-groups/utils'
-
-const logger = createLogger('OrganizationPermissionGroupMembers')
-
-export const GET = withRouteHandler(
- async (_req: NextRequest, { params }: { params: Promise<{ id: string; groupId: string }> }) => {
- const session = await getSession()
- if (!session?.user?.id) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
- }
-
- const { id: organizationId, groupId: id } = await params
-
- const denied = await authorizeOrgAccessControl(session.user.id, organizationId)
- if (denied) return denied
-
- const group = await loadGroupInOrganization(id, organizationId)
- if (!group) {
- return NextResponse.json({ error: 'Permission group not found' }, { status: 404 })
- }
-
- const members = await db
- .select({
- id: permissionGroupMember.id,
- userId: permissionGroupMember.userId,
- assignedAt: permissionGroupMember.assignedAt,
- userName: user.name,
- userEmail: user.email,
- userImage: user.image,
- })
- .from(permissionGroupMember)
- .leftJoin(user, eq(permissionGroupMember.userId, user.id))
- .where(eq(permissionGroupMember.permissionGroupId, id))
-
- return NextResponse.json({ members })
- }
-)
-
-export const POST = withRouteHandler(
- async (req: NextRequest, context: { params: Promise<{ id: string; groupId: string }> }) => {
- const session = await getSession()
- if (!session?.user?.id) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
- }
-
- const { id: organizationId, groupId: id } = await context.params
-
- // Populated inside the transaction when a scope conflict is detected, so the
- // catch can format the 409 after the rollback.
- let scopeConflicts: ScopeConflict[] = []
-
- try {
- const denied = await authorizeOrgAccessControl(session.user.id, organizationId)
- if (denied) return denied
-
- const group = await loadGroupInOrganization(id, organizationId)
- if (!group) {
- return NextResponse.json({ error: 'Permission group not found' }, { status: 404 })
- }
-
- const parsed = await parseRequest(addPermissionGroupMemberContract, req, context, {
- validationErrorResponse: (error) =>
- NextResponse.json({ error: getValidationErrorMessage(error) }, { status: 400 }),
- })
- if (!parsed.success) return parsed.response
- const { userId } = parsed.data.body
-
- const isMember = await isOrganizationMember(userId, organizationId)
- if (!isMember) {
- return NextResponse.json(
- { error: 'User is not a member of this organization' },
- { status: 400 }
- )
- }
-
- const newMember = await db.transaction(async (tx) => {
- // Serialize all permission-group writes for this org so the conflict
- // check and insert are atomic. Without it, two concurrent adds (or a
- // concurrent scope change) could both pass findScopeConflicts and place
- // the user in two groups that overlap on a workspace.
- await acquirePermissionGroupOrgLock(tx, organizationId)
-
- // Re-read the group under the lock: a concurrent scope change may have
- // changed its workspaces since the pre-transaction load, so the conflict
- // check uses one consistent snapshot.
- const lockedGroup = await loadGroupInOrganization(id, organizationId, tx)
- if (!lockedGroup) {
- throw new Error('GROUP_NOT_FOUND')
- }
-
- const [existingInGroup] = await tx
- .select({ id: permissionGroupMember.id })
- .from(permissionGroupMember)
- .where(
- and(
- eq(permissionGroupMember.permissionGroupId, id),
- eq(permissionGroupMember.userId, userId)
- )
- )
- .limit(1)
-
- if (existingInGroup) {
- throw new Error('ALREADY_IN_GROUP')
- }
-
- // A user may belong to multiple groups, but only one may govern any given
- // workspace. Reject when the user is already an explicit member of another
- // group that shares one of this group's workspaces.
- const groupWorkspaceIds = (await getGroupWorkspaces(id, tx)).map((ws) => ws.id)
- const conflicts = await findScopeConflicts(
- {
- organizationId,
- excludeGroupId: id,
- workspaceIds: groupWorkspaceIds,
- candidateUserIds: [userId],
- },
- tx
- )
- if (conflicts.length > 0) {
- scopeConflicts = conflicts
- throw new Error('SCOPE_CONFLICT')
- }
-
- const memberData = {
- id: generateId(),
- permissionGroupId: id,
- organizationId,
- userId,
- assignedBy: session.user.id,
- assignedAt: new Date(),
- }
-
- await tx.insert(permissionGroupMember).values(memberData)
- return memberData
- })
-
- logger.info('Added member to permission group', {
- permissionGroupId: id,
- organizationId,
- userId,
- assignedBy: session.user.id,
- })
-
- recordAudit({
- actorId: session.user.id,
- action: AuditAction.PERMISSION_GROUP_MEMBER_ADDED,
- resourceType: AuditResourceType.PERMISSION_GROUP,
- resourceId: id,
- resourceName: group.name,
- actorName: session.user.name ?? undefined,
- actorEmail: session.user.email ?? undefined,
- description: `Added member ${userId} to permission group "${group.name}"`,
- metadata: {
- organizationId,
- targetUserId: userId,
- permissionGroupId: id,
- },
- request: req,
- })
-
- return NextResponse.json({ member: newMember }, { status: 201 })
- } catch (error) {
- if (error instanceof Error && error.message === 'GROUP_NOT_FOUND') {
- return NextResponse.json({ error: 'Permission group not found' }, { status: 404 })
- }
- if (error instanceof Error && error.message === 'ALREADY_IN_GROUP') {
- return NextResponse.json(
- { error: 'User is already in this permission group' },
- { status: 409 }
- )
- }
- if (error instanceof Error && error.message === 'SCOPE_CONFLICT') {
- return NextResponse.json(
- { error: formatScopeConflictError(scopeConflicts) },
- { status: 409 }
- )
- }
- if (
- getPostgresErrorCode(error) === '23505' &&
- getPostgresConstraintName(error) === PERMISSION_GROUP_MEMBER_CONSTRAINTS.groupUser
- ) {
- return NextResponse.json(
- { error: 'User is already in this permission group' },
- { status: 409 }
- )
- }
- // Advisory lock wait exceeded (lock_timeout) — transient contention.
- if (getPostgresErrorCode(error) === '55P03') {
- return NextResponse.json(
- { error: 'This group is being updated by another request. Please try again.' },
- { status: 503 }
- )
- }
- logger.error('Error adding member to permission group', error)
- return NextResponse.json({ error: 'Failed to add member' }, { status: 500 })
- }
- }
-)
-
-export const DELETE = withRouteHandler(
- async (req: NextRequest, { params }: { params: Promise<{ id: string; groupId: string }> }) => {
- const session = await getSession()
- if (!session?.user?.id) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
- }
-
- const { id: organizationId, groupId: id } = await params
- const { searchParams } = new URL(req.url)
- const memberId = searchParams.get('memberId')
-
- if (!memberId) {
- return NextResponse.json({ error: 'memberId is required' }, { status: 400 })
- }
-
- // Populated inside the transaction when an all-members scope conflict is
- // detected, so the catch can format the 409 after the rollback.
- let allMembersConflict: AllMembersConflict | null = null
-
- try {
- const denied = await authorizeOrgAccessControl(session.user.id, organizationId)
- if (denied) return denied
-
- const group = await loadGroupInOrganization(id, organizationId)
- if (!group) {
- return NextResponse.json({ error: 'Permission group not found' }, { status: 404 })
- }
-
- const memberToRemove = await db.transaction(async (tx) => {
- // Serialize permission-group writes for this org so the last-member check
- // and the delete commit atomically: removing the last member turns a
- // workspace group into an all-members group, which is unique per workspace.
- await acquirePermissionGroupOrgLock(tx, organizationId)
-
- const lockedGroup = await loadGroupInOrganization(id, organizationId, tx)
- if (!lockedGroup) {
- throw new Error('GROUP_NOT_FOUND')
- }
-
- const [member] = await tx
- .select({
- id: permissionGroupMember.id,
- userId: permissionGroupMember.userId,
- email: user.email,
- })
- .from(permissionGroupMember)
- .innerJoin(user, eq(permissionGroupMember.userId, user.id))
- .where(
- and(
- eq(permissionGroupMember.id, memberId),
- eq(permissionGroupMember.permissionGroupId, id)
- )
- )
- .limit(1)
-
- if (!member) {
- throw new Error('MEMBER_NOT_FOUND')
- }
-
- if (!lockedGroup.isDefault && lockedGroup.membershipMode === 'inherit') {
- const [memberCountRow] = await tx
- .select({ value: count() })
- .from(permissionGroupMember)
- .where(eq(permissionGroupMember.permissionGroupId, id))
- if ((memberCountRow?.value ?? 0) <= 1) {
- const workspaceIds = (await getGroupWorkspaces(id, tx)).map((ws) => ws.id)
- const conflict = await findAllMembersWorkspaceConflict(
- { organizationId, excludeGroupId: id, workspaceIds },
- tx
- )
- if (conflict) {
- allMembersConflict = conflict
- throw new Error('ALL_MEMBERS_CONFLICT')
- }
- }
- }
-
- await tx.delete(permissionGroupMember).where(eq(permissionGroupMember.id, memberId))
- return member
- })
-
- logger.info('Removed member from permission group', {
- permissionGroupId: id,
- organizationId,
- memberId,
- userId: session.user.id,
- })
-
- recordAudit({
- actorId: session.user.id,
- action: AuditAction.PERMISSION_GROUP_MEMBER_REMOVED,
- resourceType: AuditResourceType.PERMISSION_GROUP,
- resourceId: id,
- resourceName: group.name,
- actorName: session.user.name ?? undefined,
- actorEmail: session.user.email ?? undefined,
- description: `Removed member ${memberToRemove.userId} from permission group "${group.name}"`,
- metadata: {
- organizationId,
- targetUserId: memberToRemove.userId,
- targetEmail: memberToRemove.email ?? undefined,
- memberId,
- permissionGroupId: id,
- },
- request: req,
- })
-
- return NextResponse.json({ success: true })
- } catch (error) {
- if (error instanceof Error && error.message === 'GROUP_NOT_FOUND') {
- return NextResponse.json({ error: 'Permission group not found' }, { status: 404 })
- }
- if (error instanceof Error && error.message === 'MEMBER_NOT_FOUND') {
- return NextResponse.json({ error: 'Member not found' }, { status: 404 })
- }
- if (
- error instanceof Error &&
- error.message === 'ALL_MEMBERS_CONFLICT' &&
- allMembersConflict
- ) {
- return NextResponse.json(
- { error: formatAllMembersConflictError(allMembersConflict) },
- { status: 409 }
- )
- }
- if (getPostgresErrorCode(error) === '55P03') {
- return NextResponse.json(
- { error: 'This group is being updated by another request. Please try again.' },
- { status: 503 }
- )
- }
- logger.error('Error removing member from permission group', error)
- return NextResponse.json({ error: 'Failed to remove member' }, { status: 500 })
- }
- }
-)
+ defineInternalJsonRoute,
+ internalRateLimits,
+ internalSessionAuth,
+} from '@/lib/api/server/routes'
+import { internalPermissionGroupErrorPolicy } from '@/lib/api/server/routes/permission-groups'
+import { permissionGroupOperations } from '@/lib/permission-groups/application/operations'
+import {
+ addPermissionGroupMember,
+ listPermissionGroupMembers,
+ removePermissionGroupMember,
+} from '@/lib/permission-groups/application/use-cases'
+
+export const GET = defineInternalJsonRoute({
+ contract: listPermissionGroupMembersContract,
+ auth: internalSessionAuth,
+ operation: permissionGroupOperations.listMembers,
+ rateLimit: internalRateLimits.none({
+ reason: 'Preserve existing permission group settings behavior',
+ }),
+ errorPolicy: internalPermissionGroupErrorPolicy,
+ mapInput: ({ params }) => ({ organizationId: params.id, groupId: params.groupId }),
+ useCase: listPermissionGroupMembers,
+ present: ({ data }) => ({ members: data.map(presentPermissionGroupMember) }),
+})
+
+export const POST = defineInternalJsonRoute({
+ contract: addPermissionGroupMemberContract,
+ auth: internalSessionAuth,
+ operation: permissionGroupOperations.addMember,
+ rateLimit: internalRateLimits.none({
+ reason: 'Preserve existing permission group settings behavior',
+ }),
+ errorPolicy: internalPermissionGroupErrorPolicy,
+ mapInput: ({ params, body }) => ({
+ organizationId: params.id,
+ groupId: params.groupId,
+ userId: body.userId,
+ }),
+ useCase: addPermissionGroupMember,
+ present: ({ member }) => ({ member: presentPermissionGroupMember(member) }),
+})
+
+export const DELETE = defineInternalJsonRoute({
+ contract: removePermissionGroupMemberContract,
+ auth: internalSessionAuth,
+ operation: permissionGroupOperations.removeMember,
+ rateLimit: internalRateLimits.none({
+ reason: 'Preserve existing permission group settings behavior',
+ }),
+ errorPolicy: internalPermissionGroupErrorPolicy,
+ mapInput: ({ params, query }) => ({
+ organizationId: params.id,
+ groupId: params.groupId,
+ memberId: query.memberId,
+ }),
+ useCase: removePermissionGroupMember,
+ present: () => ({ success: true as const }),
+})
diff --git a/apps/sim/app/api/organizations/[id]/permission-groups/[groupId]/route.test.ts b/apps/sim/app/api/organizations/[id]/permission-groups/[groupId]/route.test.ts
index 02948dc1ee5..6197aea0283 100644
--- a/apps/sim/app/api/organizations/[id]/permission-groups/[groupId]/route.test.ts
+++ b/apps/sim/app/api/organizations/[id]/permission-groups/[groupId]/route.test.ts
@@ -28,12 +28,9 @@ vi.mock('@/lib/permission-groups/application/group-membership', () => ({
findScopeConflicts: vi.fn(),
}))
-vi.mock('@/app/api/organizations/[id]/permission-groups/utils', () => ({
- authorizeOrgAccessControl: mocks.authorize,
+vi.mock('@/lib/permission-groups/repository', () => ({
loadGroupInOrganization: mocks.loadGroup,
findWorkspacesNotInOrganization: vi.fn(),
- formatAllMembersConflictError: vi.fn(),
- formatScopeConflictError: vi.fn(),
getGroupWorkspaces: vi.fn(),
}))
@@ -43,6 +40,13 @@ vi.mock('@sim/audit', () => ({
AuditResourceType: { PERMISSION_GROUP: 'permission_group' },
}))
+vi.mock('@/lib/core/application/organization-authorization', () => ({
+ authorizeOrganizationOperation: mocks.authorize,
+}))
+vi.mock('@/lib/permission-groups/resolve.server', () => ({
+ isOrganizationPermissionRegimeActive: vi.fn().mockResolvedValue(true),
+}))
+
import { PUT } from '@/app/api/organizations/[id]/permission-groups/[groupId]/route'
const ORGANIZATION_ID = 'org-1'
@@ -54,6 +58,10 @@ const GROUP = {
description: null,
isDefault: true,
config: { disableOAuthAppAccess: false },
+ membershipMode: 'inherit',
+ createdBy: 'admin-1',
+ createdAt: new Date(),
+ updatedAt: new Date(),
}
async function updateUnderLock(body: UpdatePermissionGroupBody) {
@@ -69,7 +77,9 @@ async function updateUnderLock(body: UpdatePermissionGroupBody) {
})
try {
expect(await Promise.race([lockEntered.promise, pendingResponse.then(() => false)])).toBe(true)
- expect(mocks.acquireLock).toHaveBeenCalledExactlyOnceWith(db, ORGANIZATION_ID)
+ expect(mocks.acquireLock).toHaveBeenCalledExactlyOnceWith(db, ORGANIZATION_ID, {
+ lockTimeoutAlreadyBounded: true,
+ })
expect(dbChainMockFns.update).not.toHaveBeenCalled()
} finally {
lockReleased.resolve()
@@ -86,13 +96,22 @@ describe('permission group PUT policy serialization', () => {
beforeEach(() => {
vi.clearAllMocks()
resetDbChainMock()
- authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'admin-1' } })
- mocks.authorize.mockResolvedValue(null)
+ authMockFns.mockGetSession.mockResolvedValue({
+ user: { id: 'admin-1' },
+ session: { id: 'session-1' },
+ })
+ mocks.authorize.mockResolvedValue({
+ userId: 'admin-1',
+ organizationId: ORGANIZATION_ID,
+ role: 'admin',
+ })
mocks.loadGroup.mockResolvedValue(GROUP)
})
it('locks a config-only update and writes the requested OAuth restriction', async () => {
- queueTableRows(permissionGroup, [{ ...GROUP, config: { disableOAuthAppAccess: true } }])
+ dbChainMockFns.returning.mockResolvedValueOnce([
+ { ...GROUP, config: { disableOAuthAppAccess: true } },
+ ])
await updateUnderLock({ config: { disableOAuthAppAccess: true } })
@@ -105,7 +124,7 @@ describe('permission group PUT policy serialization', () => {
'locks metadata-only update %j without restoring stale policy',
async (metadata) => {
if ('name' in metadata) queueTableRows(permissionGroup, [])
- queueTableRows(permissionGroup, [
+ dbChainMockFns.returning.mockResolvedValueOnce([
{ ...GROUP, ...metadata, config: { disableOAuthAppAccess: true } },
])
@@ -120,10 +139,8 @@ describe('permission group PUT policy serialization', () => {
)
it('merges a config patch with the policy reloaded under the lock', async () => {
- mocks.loadGroup
- .mockResolvedValueOnce(GROUP)
- .mockResolvedValueOnce({ ...GROUP, config: { disableOAuthAppAccess: true } })
- queueTableRows(permissionGroup, [
+ mocks.loadGroup.mockResolvedValueOnce({ ...GROUP, config: { disableOAuthAppAccess: true } })
+ dbChainMockFns.returning.mockResolvedValueOnce([
{ ...GROUP, config: { disableOAuthAppAccess: true, disableCliAccess: true } },
])
@@ -136,8 +153,8 @@ describe('permission group PUT policy serialization', () => {
)
})
- it('does not write when the group disappears before the locked reload', async () => {
- mocks.loadGroup.mockResolvedValueOnce(GROUP).mockResolvedValueOnce(null)
+ it('does not write when the group disappears before the locked read', async () => {
+ mocks.loadGroup.mockResolvedValueOnce(null)
mocks.acquireLock.mockResolvedValueOnce(undefined)
const response = await PUT(createMockRequest('PUT', { description: 'Updated description' }), {
@@ -145,8 +162,10 @@ describe('permission group PUT policy serialization', () => {
})
expect(response.status).toBe(404)
- await expect(response.json()).resolves.toEqual({ error: 'Permission group not found' })
- expect(mocks.acquireLock).toHaveBeenCalledExactlyOnceWith(db, ORGANIZATION_ID)
+ await expect(response.json()).resolves.toMatchObject({ error: 'Permission group not found' })
+ expect(mocks.acquireLock).toHaveBeenCalledExactlyOnceWith(db, ORGANIZATION_ID, {
+ lockTimeoutAlreadyBounded: true,
+ })
expect(mocks.loadGroup).toHaveBeenLastCalledWith(GROUP_ID, ORGANIZATION_ID, db)
expect(dbChainMockFns.update).not.toHaveBeenCalled()
})
diff --git a/apps/sim/app/api/organizations/[id]/permission-groups/[groupId]/route.ts b/apps/sim/app/api/organizations/[id]/permission-groups/[groupId]/route.ts
index a245f86f430..f8b14d440ac 100644
--- a/apps/sim/app/api/organizations/[id]/permission-groups/[groupId]/route.ts
+++ b/apps/sim/app/api/organizations/[id]/permission-groups/[groupId]/route.ts
@@ -1,409 +1,61 @@
-import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
-import { db } from '@sim/db'
-import { permissionGroup, permissionGroupMember, permissionGroupWorkspace } from '@sim/db/schema'
-import { createLogger } from '@sim/logger'
-import { getPostgresConstraintName, getPostgresErrorCode } from '@sim/utils/errors'
-import { generateId } from '@sim/utils/id'
-import { and, eq } from 'drizzle-orm'
-import { type NextRequest, NextResponse } from 'next/server'
-import { updatePermissionGroupContract } from '@/lib/api/contracts/permission-groups'
-import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
-import { getSession } from '@/lib/auth'
-import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import {
- type AllMembersConflict,
- findAllMembersWorkspaceConflict,
- findScopeConflicts,
- type ScopeConflict,
-} from '@/lib/permission-groups/application/group-membership'
-import { PERMISSION_GROUP_CONSTRAINTS } from '@/lib/permission-groups/constraints'
+ deletePermissionGroupContract,
+ getPermissionGroupContract,
+ updatePermissionGroupContract,
+} from '@/lib/api/contracts/permission-groups'
+import { presentPermissionGroup } from '@/lib/api/server/permission-group-presenters'
import {
- type PermissionGroupConfig,
- parsePermissionGroupConfig,
-} from '@/lib/permission-groups/fields'
-import { acquirePermissionGroupOrgLock } from '@/lib/permission-groups/locks'
+ defineInternalJsonRoute,
+ internalRateLimits,
+ internalSessionAuth,
+} from '@/lib/api/server/routes'
+import { internalPermissionGroupErrorPolicy } from '@/lib/api/server/routes/permission-groups'
+import { permissionGroupOperations } from '@/lib/permission-groups/application/operations'
import {
- authorizeOrgAccessControl,
- findWorkspacesNotInOrganization,
- formatAllMembersConflictError,
- formatScopeConflictError,
- getGroupWorkspaces,
- loadGroupInOrganization,
-} from '@/app/api/organizations/[id]/permission-groups/utils'
-
-const logger = createLogger('OrganizationPermissionGroup')
-
-export const GET = withRouteHandler(
- async (_req: NextRequest, { params }: { params: Promise<{ id: string; groupId: string }> }) => {
- const session = await getSession()
- if (!session?.user?.id) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
- }
-
- const { id: organizationId, groupId: id } = await params
-
- const denied = await authorizeOrgAccessControl(session.user.id, organizationId)
- if (denied) return denied
-
- const group = await loadGroupInOrganization(id, organizationId)
- if (!group) {
- return NextResponse.json({ error: 'Permission group not found' }, { status: 404 })
- }
-
- const workspaces = group.isDefault ? [] : await getGroupWorkspaces(id)
-
- return NextResponse.json({
- permissionGroup: {
- ...group,
- config: parsePermissionGroupConfig(group.config),
- workspaces,
- },
- })
- }
-)
-
-export const PUT = withRouteHandler(
- async (req: NextRequest, context: { params: Promise<{ id: string; groupId: string }> }) => {
- const session = await getSession()
- if (!session?.user?.id) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
- }
-
- const { id: organizationId, groupId: id } = await context.params
-
- // Populated inside the transaction when a scope conflict is detected, so the
- // catch can format the 409 after the rollback.
- let scopeConflicts: ScopeConflict[] = []
- let allMembersConflict: AllMembersConflict | null = null
-
- try {
- const denied = await authorizeOrgAccessControl(session.user.id, organizationId)
- if (denied) return denied
-
- const group = await loadGroupInOrganization(id, organizationId)
- if (!group) {
- return NextResponse.json({ error: 'Permission group not found' }, { status: 404 })
- }
-
- const parsed = await parseRequest(updatePermissionGroupContract, req, context, {
- validationErrorResponse: (error) =>
- NextResponse.json({ error: getValidationErrorMessage(error) }, { status: 400 }),
- })
- if (!parsed.success) return parsed.response
- const updates = parsed.data.body
-
- if (updates.name) {
- const existingGroup = await db
- .select({ id: permissionGroup.id })
- .from(permissionGroup)
- .where(
- and(
- eq(permissionGroup.organizationId, organizationId),
- eq(permissionGroup.name, updates.name)
- )
- )
- .limit(1)
-
- if (existingGroup.length > 0 && existingGroup[0].id !== id) {
- return NextResponse.json(
- { error: 'A permission group with this name already exists' },
- { status: 409 }
- )
- }
- }
-
- // Demoting the org default with no new scope: it becomes a non-default
- // group with no workspaces (inert) until an admin re-scopes it. The client
- // sends only `isDefault: false`, so this never forwards a workspace list.
- const demotingDefaultToInert =
- group.isDefault && updates.isDefault === false && updates.workspaceIds === undefined
-
- // "Org-wide" is definitionally `isDefault` (the default group), so the
- // effective scope follows it: a default group targets no specific
- // workspaces; a non-default group targets its `workspaceIds`.
- const effectiveIsDefault =
- updates.isDefault !== undefined ? updates.isDefault : group.isDefault
-
- // Scope is rewritten when the group is promoted to default, demoted to
- // inert, or handed an explicit workspace list.
- const scopeProvided =
- demotingDefaultToInert || updates.workspaceIds !== undefined || updates.isDefault === true
-
- // The default group governs every workspace, so it can't also name specific
- // ones. The contract rejects `isDefault: true` + workspaceIds, but a direct
- // API caller can still send workspaceIds against a group that is already the
- // default — reject rather than silently dropping them.
- if (effectiveIsDefault && updates.workspaceIds !== undefined) {
- return NextResponse.json(
- {
- error: 'The default group governs all workspaces and cannot target specific workspaces',
- },
- { status: 400 }
- )
- }
-
- // Resolve and validate explicitly-provided workspaceIds before the
- // transaction. When the request omits them for a specific-scope group
- // ("keep current"), they're read under the lock instead (see below) so the
- // conflict check and the write share one consistent snapshot.
- let providedWorkspaceIds: string[] | null = null
- if (!effectiveIsDefault && updates.workspaceIds !== undefined) {
- // Zero workspaces is allowed on update: the group then governs nothing
- // (the resolver inner-joins on the link table, so an empty group never
- // matches any workspace). No "at least one" floor here.
- providedWorkspaceIds = Array.from(new Set(updates.workspaceIds))
- const invalid = await findWorkspacesNotInOrganization(providedWorkspaceIds, organizationId)
- if (invalid.length > 0) {
- return NextResponse.json(
- { error: 'One or more selected workspaces do not belong to this organization' },
- { status: 400 }
- )
- }
- }
-
- const now = new Date()
-
- await db.transaction(async (tx) => {
- await acquirePermissionGroupOrgLock(tx, organizationId)
- const currentGroup = await loadGroupInOrganization(id, organizationId, tx)
- if (!currentGroup) throw new Error('GROUP_NOT_FOUND')
- const newConfig: PermissionGroupConfig | undefined = updates.config
- ? { ...parsePermissionGroupConfig(currentGroup.config), ...updates.config }
- : undefined
-
- // For a specific-scope group the target workspaces are the request's
- // explicit ids, or — when omitted ("keep current") — the group's current
- // workspaces read under the lock so the conflict check and write share
- // one snapshot.
- let resolvedWorkspaceIds: string[] = []
-
- if (scopeProvided) {
- if (!effectiveIsDefault) {
- // May resolve to an empty list — a non-default group is allowed to
- // target zero workspaces (governs nothing). The write below deletes
- // the old links and inserts none.
- resolvedWorkspaceIds =
- providedWorkspaceIds ?? (await getGroupWorkspaces(id, tx)).map((ws) => ws.id)
- }
-
- const members = await tx
- .select({ userId: permissionGroupMember.userId })
- .from(permissionGroupMember)
- .where(eq(permissionGroupMember.permissionGroupId, id))
- const conflicts = await findScopeConflicts(
- {
- organizationId,
- excludeGroupId: id,
- workspaceIds: resolvedWorkspaceIds,
- candidateUserIds: members.map((m) => m.userId),
- },
- tx
- )
- if (conflicts.length > 0) {
- scopeConflicts = conflicts
- throw new Error('SCOPE_CONFLICT')
- }
-
- // With no explicit members the group governs all members of its
- // workspaces; reject when another all-members group already does.
- if (!effectiveIsDefault && members.length === 0) {
- const conflict = await findAllMembersWorkspaceConflict(
- { organizationId, excludeGroupId: id, workspaceIds: resolvedWorkspaceIds },
- tx
- )
- if (conflict) {
- allMembersConflict = conflict
- throw new Error('ALL_MEMBERS_CONFLICT')
- }
- }
- }
-
- if (updates.isDefault === true) {
- // Demote the prior default to a non-default group (only the default may
- // be org-wide); it ends up with no workspaces (inert) until an admin
- // re-scopes it.
- await tx
- .update(permissionGroup)
- .set({ isDefault: false, updatedAt: now })
- .where(
- and(
- eq(permissionGroup.organizationId, organizationId),
- eq(permissionGroup.isDefault, true)
- )
- )
- }
-
- await tx
- .update(permissionGroup)
- .set({
- ...(updates.name !== undefined && { name: updates.name }),
- ...(updates.description !== undefined && { description: updates.description }),
- ...(updates.isDefault !== undefined && { isDefault: updates.isDefault }),
- ...(newConfig !== undefined && { config: newConfig }),
- updatedAt: now,
- })
- .where(eq(permissionGroup.id, id))
-
- if (scopeProvided) {
- await tx
- .delete(permissionGroupWorkspace)
- .where(eq(permissionGroupWorkspace.permissionGroupId, id))
- if (!effectiveIsDefault && resolvedWorkspaceIds.length > 0) {
- await tx.insert(permissionGroupWorkspace).values(
- resolvedWorkspaceIds.map((workspaceId) => ({
- id: generateId(),
- permissionGroupId: id,
- workspaceId,
- organizationId,
- createdAt: now,
- }))
- )
- }
- }
- })
-
- const [updated] = await db
- .select()
- .from(permissionGroup)
- .where(eq(permissionGroup.id, id))
- .limit(1)
-
- const finalWorkspaceIds = updated.isDefault
- ? []
- : (await getGroupWorkspaces(id)).map((ws) => ws.id)
-
- recordAudit({
- actorId: session.user.id,
- action: AuditAction.PERMISSION_GROUP_UPDATED,
- resourceType: AuditResourceType.PERMISSION_GROUP,
- resourceId: id,
- actorName: session.user.name ?? undefined,
- actorEmail: session.user.email ?? undefined,
- resourceName: updated.name,
- description: `Updated permission group "${updated.name}"`,
- metadata: {
- organizationId,
- updatedFields: Object.keys(updates).filter(
- (k) => updates[k as keyof typeof updates] !== undefined
- ),
- },
- request: req,
- })
-
- return NextResponse.json({
- permissionGroup: {
- ...updated,
- config: parsePermissionGroupConfig(updated.config),
- workspaceIds: finalWorkspaceIds,
- },
- })
- } catch (error) {
- if (error instanceof Error && error.message === 'GROUP_NOT_FOUND') {
- return NextResponse.json({ error: 'Permission group not found' }, { status: 404 })
- }
- if (error instanceof Error && error.message === 'SCOPE_CONFLICT') {
- return NextResponse.json(
- { error: formatScopeConflictError(scopeConflicts) },
- { status: 409 }
- )
- }
- if (
- error instanceof Error &&
- error.message === 'ALL_MEMBERS_CONFLICT' &&
- allMembersConflict
- ) {
- return NextResponse.json(
- { error: formatAllMembersConflictError(allMembersConflict) },
- { status: 409 }
- )
- }
- if (getPostgresErrorCode(error) === '23505') {
- const constraint = getPostgresConstraintName(error)
- if (constraint === PERMISSION_GROUP_CONSTRAINTS.organizationName) {
- return NextResponse.json(
- { error: 'A permission group with this name already exists' },
- { status: 409 }
- )
- }
- if (constraint === PERMISSION_GROUP_CONSTRAINTS.organizationDefault) {
- return NextResponse.json(
- {
- error:
- 'Another group was concurrently set as the default. Please refresh and try again.',
- },
- { status: 409 }
- )
- }
- }
- // Advisory lock wait exceeded (lock_timeout) — transient contention.
- if (getPostgresErrorCode(error) === '55P03') {
- return NextResponse.json(
- { error: 'This group is being updated by another request. Please try again.' },
- { status: 503 }
- )
- }
- logger.error('Error updating permission group', error)
- return NextResponse.json({ error: 'Failed to update permission group' }, { status: 500 })
- }
- }
-)
-
-export const DELETE = withRouteHandler(
- async (req: NextRequest, { params }: { params: Promise<{ id: string; groupId: string }> }) => {
- const session = await getSession()
- if (!session?.user?.id) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
- }
-
- const { id: organizationId, groupId: id } = await params
-
- try {
- const denied = await authorizeOrgAccessControl(session.user.id, organizationId)
- if (denied) return denied
-
- const group = await loadGroupInOrganization(id, organizationId)
- if (!group) {
- return NextResponse.json({ error: 'Permission group not found' }, { status: 404 })
- }
-
- await db.transaction(async (tx) => {
- await acquirePermissionGroupOrgLock(tx, organizationId)
- await tx
- .delete(permissionGroupMember)
- .where(eq(permissionGroupMember.permissionGroupId, id))
- await tx.delete(permissionGroup).where(eq(permissionGroup.id, id))
- })
-
- logger.info('Deleted permission group', {
- permissionGroupId: id,
- organizationId,
- userId: session.user.id,
- })
-
- recordAudit({
- actorId: session.user.id,
- action: AuditAction.PERMISSION_GROUP_DELETED,
- resourceType: AuditResourceType.PERMISSION_GROUP,
- resourceId: id,
- actorName: session.user.name ?? undefined,
- actorEmail: session.user.email ?? undefined,
- resourceName: group.name,
- description: `Deleted permission group "${group.name}"`,
- metadata: { organizationId },
- request: req,
- })
-
- return NextResponse.json({ success: true })
- } catch (error) {
- // Advisory lock wait exceeded (lock_timeout) — transient contention.
- if (getPostgresErrorCode(error) === '55P03') {
- return NextResponse.json(
- { error: 'This group is being updated by another request. Please try again.' },
- { status: 503 }
- )
- }
- logger.error('Error deleting permission group', error)
- return NextResponse.json({ error: 'Failed to delete permission group' }, { status: 500 })
- }
- }
-)
+ deletePermissionGroup,
+ getPermissionGroup,
+ updatePermissionGroup,
+} from '@/lib/permission-groups/application/use-cases'
+
+export const GET = defineInternalJsonRoute({
+ contract: getPermissionGroupContract,
+ auth: internalSessionAuth,
+ operation: permissionGroupOperations.read,
+ rateLimit: internalRateLimits.none({
+ reason: 'Preserve existing permission group settings behavior',
+ }),
+ errorPolicy: internalPermissionGroupErrorPolicy,
+ mapInput: ({ params }) => ({ organizationId: params.id, groupId: params.groupId }),
+ useCase: getPermissionGroup,
+ present: (group) => ({ permissionGroup: presentPermissionGroup(group) }),
+})
+
+export const PUT = defineInternalJsonRoute({
+ contract: updatePermissionGroupContract,
+ auth: internalSessionAuth,
+ operation: permissionGroupOperations.update,
+ rateLimit: internalRateLimits.none({
+ reason: 'Preserve existing permission group settings behavior',
+ }),
+ errorPolicy: internalPermissionGroupErrorPolicy,
+ mapInput: ({ params, body }) => ({
+ organizationId: params.id,
+ groupId: params.groupId,
+ changes: body,
+ }),
+ useCase: updatePermissionGroup,
+ present: (group) => ({ permissionGroup: presentPermissionGroup(group) }),
+})
+
+export const DELETE = defineInternalJsonRoute({
+ contract: deletePermissionGroupContract,
+ auth: internalSessionAuth,
+ operation: permissionGroupOperations.delete,
+ rateLimit: internalRateLimits.none({
+ reason: 'Preserve existing permission group settings behavior',
+ }),
+ errorPolicy: internalPermissionGroupErrorPolicy,
+ mapInput: ({ params }) => ({ organizationId: params.id, groupId: params.groupId }),
+ useCase: deletePermissionGroup,
+ present: () => ({ success: true as const }),
+})
diff --git a/apps/sim/app/api/organizations/[id]/permission-groups/route.ts b/apps/sim/app/api/organizations/[id]/permission-groups/route.ts
index 42cfc1ae1a4..d251c10d8a8 100644
--- a/apps/sim/app/api/organizations/[id]/permission-groups/route.ts
+++ b/apps/sim/app/api/organizations/[id]/permission-groups/route.ts
@@ -1,282 +1,42 @@
-import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
-import { db } from '@sim/db'
import {
- permissionGroup,
- permissionGroupMember,
- permissionGroupWorkspace,
- user,
-} from '@sim/db/schema'
-import { createLogger } from '@sim/logger'
-import { getPostgresConstraintName, getPostgresErrorCode } from '@sim/utils/errors'
-import { generateId } from '@sim/utils/id'
-import { and, count, desc, eq, inArray } from 'drizzle-orm'
-import { type NextRequest, NextResponse } from 'next/server'
-import { createPermissionGroupContract } from '@/lib/api/contracts/permission-groups'
-import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
-import { getSession } from '@/lib/auth'
-import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
+ createPermissionGroupContract,
+ listPermissionGroupsContract,
+} from '@/lib/api/contracts/permission-groups'
+import { presentPermissionGroup } from '@/lib/api/server/permission-group-presenters'
import {
- type AllMembersConflict,
- findAllMembersWorkspaceConflict,
-} from '@/lib/permission-groups/application/group-membership'
-import { PERMISSION_GROUP_CONSTRAINTS } from '@/lib/permission-groups/constraints'
+ defineInternalJsonRoute,
+ internalRateLimits,
+ internalSessionAuth,
+} from '@/lib/api/server/routes'
+import { internalPermissionGroupErrorPolicy } from '@/lib/api/server/routes/permission-groups'
+import { permissionGroupOperations } from '@/lib/permission-groups/application/operations'
import {
- DEFAULT_PERMISSION_GROUP_CONFIG,
- type PermissionGroupConfig,
- parsePermissionGroupConfig,
-} from '@/lib/permission-groups/fields'
-import { acquirePermissionGroupOrgLock } from '@/lib/permission-groups/locks'
-import {
- authorizeOrgAccessControl,
- findWorkspacesNotInOrganization,
- formatAllMembersConflictError,
- getWorkspacesForGroups,
-} from '@/app/api/organizations/[id]/permission-groups/utils'
-
-const logger = createLogger('OrganizationPermissionGroups')
-
-export const GET = withRouteHandler(
- async (_req: NextRequest, { params }: { params: Promise<{ id: string }> }) => {
- const session = await getSession()
- if (!session?.user?.id) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
- }
-
- const { id: organizationId } = await params
-
- const denied = await authorizeOrgAccessControl(session.user.id, organizationId)
- if (denied) return denied
-
- const groups = await db
- .select({
- id: permissionGroup.id,
- name: permissionGroup.name,
- description: permissionGroup.description,
- config: permissionGroup.config,
- createdBy: permissionGroup.createdBy,
- createdAt: permissionGroup.createdAt,
- updatedAt: permissionGroup.updatedAt,
- isDefault: permissionGroup.isDefault,
- creatorName: user.name,
- creatorEmail: user.email,
- })
- .from(permissionGroup)
- .leftJoin(user, eq(permissionGroup.createdBy, user.id))
- .where(eq(permissionGroup.organizationId, organizationId))
- .orderBy(desc(permissionGroup.createdAt))
-
- const groupIds = groups.map((group) => group.id)
- const memberCounts = groupIds.length
- ? await db
- .select({
- permissionGroupId: permissionGroupMember.permissionGroupId,
- count: count(),
- })
- .from(permissionGroupMember)
- .where(inArray(permissionGroupMember.permissionGroupId, groupIds))
- .groupBy(permissionGroupMember.permissionGroupId)
- : []
- const countByGroupId = new Map(memberCounts.map((row) => [row.permissionGroupId, row.count]))
- const workspacesByGroupId = await getWorkspacesForGroups(groupIds)
-
- const groupsWithCounts = groups.map((group) => ({
- ...group,
- config: parsePermissionGroupConfig(group.config),
- memberCount: countByGroupId.get(group.id) ?? 0,
- workspaces: workspacesByGroupId.get(group.id) ?? [],
- }))
-
- return NextResponse.json({ permissionGroups: groupsWithCounts })
- }
-)
-
-export const POST = withRouteHandler(
- async (req: NextRequest, context: { params: Promise<{ id: string }> }) => {
- const session = await getSession()
- if (!session?.user?.id) {
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
- }
-
- const { id: organizationId } = await context.params
-
- // Populated inside the transaction when an all-members scope conflict is
- // detected, so the catch can format the 409 after the rollback.
- let allMembersConflict: AllMembersConflict | null = null
-
- try {
- const denied = await authorizeOrgAccessControl(session.user.id, organizationId)
- if (denied) return denied
-
- const parsed = await parseRequest(createPermissionGroupContract, req, context, {
- validationErrorResponse: (error) =>
- NextResponse.json({ error: getValidationErrorMessage(error) }, { status: 400 }),
- })
- if (!parsed.success) return parsed.response
- const { name, description, config, isDefault } = parsed.data.body
-
- // Only the organization default group is org-wide; every other group
- // targets specific workspaces. "Org-wide" is definitionally `isDefault`.
- const isDefaultGroup = isDefault === true
- const workspaceIds = isDefaultGroup
- ? []
- : Array.from(new Set(parsed.data.body.workspaceIds ?? []))
-
- if (!isDefaultGroup && workspaceIds.length === 0) {
- return NextResponse.json(
- { error: 'Select at least one workspace when the group targets specific workspaces' },
- { status: 400 }
- )
- }
-
- if (!isDefaultGroup) {
- const invalid = await findWorkspacesNotInOrganization(workspaceIds, organizationId)
- if (invalid.length > 0) {
- return NextResponse.json(
- { error: 'One or more selected workspaces do not belong to this organization' },
- { status: 400 }
- )
- }
- }
-
- const existingGroup = await db
- .select({ id: permissionGroup.id })
- .from(permissionGroup)
- .where(
- and(eq(permissionGroup.organizationId, organizationId), eq(permissionGroup.name, name))
- )
- .limit(1)
-
- if (existingGroup.length > 0) {
- return NextResponse.json(
- { error: 'A permission group with this name already exists' },
- { status: 409 }
- )
- }
-
- const groupConfig: PermissionGroupConfig = {
- ...DEFAULT_PERMISSION_GROUP_CONFIG,
- ...config,
- }
-
- const now = new Date()
- const newGroup = {
- id: generateId(),
- organizationId,
- name,
- description: description || null,
- config: groupConfig,
- createdBy: session.user.id,
- createdAt: now,
- updatedAt: now,
- isDefault: isDefault || false,
- }
-
- await db.transaction(async (tx) => {
- await acquirePermissionGroupOrgLock(tx, organizationId)
-
- // A new non-default group has no members, so it governs all members of
- // its workspaces; reject when another all-members group already does.
- if (!isDefaultGroup) {
- const conflict = await findAllMembersWorkspaceConflict(
- { organizationId, excludeGroupId: newGroup.id, workspaceIds },
- tx
- )
- if (conflict) {
- allMembersConflict = conflict
- throw new Error('ALL_MEMBERS_CONFLICT')
- }
- }
-
- if (isDefault) {
- // Demote the prior default to a non-default group (only the default may
- // be org-wide); it ends up with no workspaces (inert) until an admin
- // re-scopes it.
- await tx
- .update(permissionGroup)
- .set({ isDefault: false, updatedAt: now })
- .where(
- and(
- eq(permissionGroup.organizationId, organizationId),
- eq(permissionGroup.isDefault, true)
- )
- )
- }
- await tx.insert(permissionGroup).values(newGroup)
- if (workspaceIds.length > 0) {
- await tx.insert(permissionGroupWorkspace).values(
- workspaceIds.map((workspaceId) => ({
- id: generateId(),
- permissionGroupId: newGroup.id,
- workspaceId,
- organizationId,
- createdAt: now,
- }))
- )
- }
- })
-
- logger.info('Created permission group', {
- permissionGroupId: newGroup.id,
- organizationId,
- userId: session.user.id,
- workspaceCount: workspaceIds.length,
- })
-
- recordAudit({
- actorId: session.user.id,
- action: AuditAction.PERMISSION_GROUP_CREATED,
- resourceType: AuditResourceType.PERMISSION_GROUP,
- resourceId: newGroup.id,
- actorName: session.user.name ?? undefined,
- actorEmail: session.user.email ?? undefined,
- resourceName: name,
- description: `Created permission group "${name}"`,
- metadata: {
- organizationId,
- isDefault: isDefault || false,
- workspaceCount: workspaceIds.length,
- },
- request: req,
- })
-
- return NextResponse.json({ permissionGroup: { ...newGroup, workspaceIds } }, { status: 201 })
- } catch (error) {
- if (
- error instanceof Error &&
- error.message === 'ALL_MEMBERS_CONFLICT' &&
- allMembersConflict
- ) {
- return NextResponse.json(
- { error: formatAllMembersConflictError(allMembersConflict) },
- { status: 409 }
- )
- }
- if (getPostgresErrorCode(error) === '55P03') {
- return NextResponse.json(
- { error: 'This organization is being updated by another request. Please try again.' },
- { status: 503 }
- )
- }
- if (getPostgresErrorCode(error) === '23505') {
- const constraint = getPostgresConstraintName(error)
- if (constraint === PERMISSION_GROUP_CONSTRAINTS.organizationName) {
- return NextResponse.json(
- { error: 'A permission group with this name already exists' },
- { status: 409 }
- )
- }
- if (constraint === PERMISSION_GROUP_CONSTRAINTS.organizationDefault) {
- return NextResponse.json(
- {
- error:
- 'Another group was concurrently set as the default. Please refresh and try again.',
- },
- { status: 409 }
- )
- }
- }
- logger.error('Error creating permission group', error)
- return NextResponse.json({ error: 'Failed to create permission group' }, { status: 500 })
- }
- }
-)
+ createPermissionGroup,
+ listPermissionGroups,
+} from '@/lib/permission-groups/application/use-cases'
+
+export const GET = defineInternalJsonRoute({
+ contract: listPermissionGroupsContract,
+ auth: internalSessionAuth,
+ operation: permissionGroupOperations.list,
+ rateLimit: internalRateLimits.none({
+ reason: 'Preserve existing permission group settings behavior',
+ }),
+ errorPolicy: internalPermissionGroupErrorPolicy,
+ mapInput: ({ params }) => ({ organizationId: params.id }),
+ useCase: listPermissionGroups,
+ present: ({ data }) => ({ permissionGroups: data.map(presentPermissionGroup) }),
+})
+
+export const POST = defineInternalJsonRoute({
+ contract: createPermissionGroupContract,
+ auth: internalSessionAuth,
+ operation: permissionGroupOperations.create,
+ rateLimit: internalRateLimits.none({
+ reason: 'Preserve existing permission group settings behavior',
+ }),
+ errorPolicy: internalPermissionGroupErrorPolicy,
+ mapInput: ({ params, body }) => ({ organizationId: params.id, changes: body }),
+ useCase: createPermissionGroup,
+ present: (group) => ({ permissionGroup: presentPermissionGroup(group) }),
+})
diff --git a/apps/sim/app/api/organizations/[id]/permission-groups/utils.test.ts b/apps/sim/app/api/organizations/[id]/permission-groups/utils.test.ts
deleted file mode 100644
index c426e067815..00000000000
--- a/apps/sim/app/api/organizations/[id]/permission-groups/utils.test.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-/**
- * @vitest-environment node
- */
-import { resetDbChainMock } from '@sim/testing'
-import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
-
-const { mockIsOrganizationAdminOrOwner, mockIsOrganizationPermissionRegimeActive } = vi.hoisted(
- () => ({
- mockIsOrganizationAdminOrOwner: vi.fn<() => Promise>(),
- mockIsOrganizationPermissionRegimeActive: vi.fn<() => Promise>(),
- })
-)
-
-vi.mock('@/lib/permission-groups/resolve.server', () => ({
- isOrganizationPermissionRegimeActive: mockIsOrganizationPermissionRegimeActive,
-}))
-
-vi.mock('@/lib/workspaces/permissions/utils', () => ({
- isOrganizationAdminOrOwner: mockIsOrganizationAdminOrOwner,
-}))
-
-import { authorizeOrgAccessControl } from '@/app/api/organizations/[id]/permission-groups/utils'
-
-afterAll(resetDbChainMock)
-
-describe('authorizeOrgAccessControl', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- resetDbChainMock()
- })
-
- it('returns a 403 when the user is not an organization admin/owner', async () => {
- mockIsOrganizationAdminOrOwner.mockResolvedValue(false)
- mockIsOrganizationPermissionRegimeActive.mockResolvedValue(true)
-
- const response = await authorizeOrgAccessControl('user-1', 'org-1')
-
- expect(response).not.toBeNull()
- expect(response?.status).toBe(403)
- await expect(response?.json()).resolves.toEqual({ error: 'Admin permissions required' })
- // Entitlement is only checked after the admin gate passes.
- expect(mockIsOrganizationPermissionRegimeActive).not.toHaveBeenCalled()
- })
-
- it('returns a 403 when the organization is not on an enterprise plan', async () => {
- mockIsOrganizationAdminOrOwner.mockResolvedValue(true)
- mockIsOrganizationPermissionRegimeActive.mockResolvedValue(false)
-
- const response = await authorizeOrgAccessControl('user-1', 'org-1')
-
- expect(response?.status).toBe(403)
- await expect(response?.json()).resolves.toEqual({
- error: 'Access Control is an Enterprise feature',
- })
- })
-
- it('returns null when the user is an admin and the org is entitled', async () => {
- mockIsOrganizationAdminOrOwner.mockResolvedValue(true)
- mockIsOrganizationPermissionRegimeActive.mockResolvedValue(true)
-
- const response = await authorizeOrgAccessControl('user-1', 'org-1')
-
- expect(response).toBeNull()
- })
-})
diff --git a/apps/sim/app/api/organizations/[id]/permission-groups/utils.ts b/apps/sim/app/api/organizations/[id]/permission-groups/utils.ts
deleted file mode 100644
index 026963a3a5a..00000000000
--- a/apps/sim/app/api/organizations/[id]/permission-groups/utils.ts
+++ /dev/null
@@ -1,159 +0,0 @@
-import { db } from '@sim/db'
-import { permissionGroup, permissionGroupWorkspace, workspace } from '@sim/db/schema'
-import { and, asc, eq, inArray } from 'drizzle-orm'
-import { NextResponse } from 'next/server'
-import type { DbOrTx } from '@/lib/db/types'
-import type {
- AllMembersConflict,
- ScopeConflict,
-} from '@/lib/permission-groups/application/group-membership'
-import { isOrganizationPermissionRegimeActive } from '@/lib/permission-groups/resolve.server'
-import { isOrganizationAdminOrOwner } from '@/lib/workspaces/permissions/utils'
-
-/** A workspace reference (id + display name). */
-export interface WorkspaceRef {
- id: string
- name: string
-}
-
-/**
- * Authorize an organization-scoped access-control management request. The caller
- * must be an organization owner/admin and the organization must be entitled to
- * the Access Control (Permission Groups) enterprise feature. Returns a
- * `NextResponse` to short-circuit on failure, or `null` when authorized.
- */
-export async function authorizeOrgAccessControl(
- userId: string,
- organizationId: string
-): Promise {
- const isAdmin = await isOrganizationAdminOrOwner(userId, organizationId)
- if (!isAdmin) {
- return NextResponse.json({ error: 'Admin permissions required' }, { status: 403 })
- }
-
- /**
- * The active permission regime, which is what the Access Control page now reads too: an
- * organization whose restrictions still apply has to be able to see and loosen them, and a
- * deployment with Access Control switched off governs nobody, so neither should manage anything.
- */
- const governed = await isOrganizationPermissionRegimeActive(organizationId)
- if (!governed) {
- return NextResponse.json({ error: 'Access Control is an Enterprise feature' }, { status: 403 })
- }
-
- return null
-}
-
-/** Load a permission group only if it belongs to the given organization. */
-export async function loadGroupInOrganization(
- groupId: string,
- organizationId: string,
- executor: DbOrTx = db
-) {
- const [group] = await executor
- .select({
- id: permissionGroup.id,
- organizationId: permissionGroup.organizationId,
- name: permissionGroup.name,
- description: permissionGroup.description,
- config: permissionGroup.config,
- createdBy: permissionGroup.createdBy,
- createdAt: permissionGroup.createdAt,
- updatedAt: permissionGroup.updatedAt,
- isDefault: permissionGroup.isDefault,
- membershipMode: permissionGroup.membershipMode,
- })
- .from(permissionGroup)
- .where(and(eq(permissionGroup.id, groupId), eq(permissionGroup.organizationId, organizationId)))
- .limit(1)
-
- return group ?? null
-}
-
-/** The workspaces ({id, name}) a specific-scope group targets. */
-export async function getGroupWorkspaces(
- groupId: string,
- executor: DbOrTx = db
-): Promise {
- return executor
- .select({ id: workspace.id, name: workspace.name })
- .from(permissionGroupWorkspace)
- .innerJoin(workspace, eq(permissionGroupWorkspace.workspaceId, workspace.id))
- .where(eq(permissionGroupWorkspace.permissionGroupId, groupId))
- .orderBy(asc(workspace.name))
-}
-
-/** Batched map of `groupId -> targeted workspaces` for a list of groups. */
-export async function getWorkspacesForGroups(
- groupIds: string[]
-): Promise