Skip to main content
Not every Speckle server is the same — workspace support, server version, and user permissions all vary. Speckle connectors ask the server what it supports before attempting an operation, rather than catching broad failures after the fact. When to read this: multi-deployment apps, connectors, or any code calling version-gated APIs. Read next: Exceptions for AggregateException vs direct throws.

Prerequisites

Complete Example

This mirrors the pattern Speckle’s own connectors use to decide whether a server supports connector-scale model ingestion, adapted from SendOperation.CheckUseModelIngestionSend in speckle-sharp-connectors:
https://mintcdn.com/speckle/VtRPWzmN-ULoLfIv/images/developers/sdks/csharp.svg?fit=max&auto=format&n=VtRPWzmN-ULoLfIv&q=85&s=f2970e3b8bd4a2c7899b46211a42f251Example
using Speckle.Sdk;
using Speckle.Sdk.Api;

async Task<bool> CanUseModelIngestion(IClient client, string projectId, string modelId)
{
    try
    {
        var permissionCheck = await client.Model.CanCreateModelIngestion(projectId, modelId);
        permissionCheck.EnsureAuthorised(); // throws WorkspacePermissionException if authorized == false
        return true;
    }
    catch (AggregateException ex) when (ex.InnerExceptions.OfType<SpeckleGraphQLInvalidQueryException>().Any())
    {
        // The server's GraphQL schema doesn't have this field at all — it predates
        // model ingestion support. This is a capability gap, not a permission denial.
        return false;
    }
    // WorkspacePermissionException (a real "no" from a capable server) is intentionally
    // NOT caught here — let it propagate so the caller can tell the user why they were denied.
}
Two outcomes look similar from the outside (both mean “don’t use this feature”) but mean very different things to a user: “your server is too old” versus “you don’t have permission.” Catching only the specific schema-level exception, and letting a genuine permission failure surface as WorkspacePermissionException, keeps that distinction intact.

Step-by-Step Explanation

Server-Level Capability Checks

The simplest form: a boolean method that tells you whether a whole feature area exists on this deployment, with no exceptions involved.
https://mintcdn.com/speckle/VtRPWzmN-ULoLfIv/images/developers/sdks/csharp.svg?fit=max&auto=format&n=VtRPWzmN-ULoLfIv&q=85&s=f2970e3b8bd4a2c7899b46211a42f251Example
if (await client.Server.IsWorkspaceEnabled())
{
    var workspaces = await client.ActiveUser.GetWorkspaces();
    // workspace-scoped flow
}
else
{
    // Personal-project flow — most self-hosted community servers land here
    var projects = await client.ActiveUser.GetProjects();
}
Speckle’s Grasshopper connector uses exactly this check before even attempting to populate a workspace picker — if the server doesn’t have workspaces enabled, it skips the workspace UI entirely rather than calling a workspace query and handling the failure.

Permission Checks Returned Alongside Resources

Several resources return permission information with the data, so you can gate UI and code paths before the user attempts anything:
  • client.Project.GetPermissions(projectId)ProjectPermissionChecks (canCreateModel, canDelete, canLoad; canPublish is obsolete — use ModelPermissionChecks.canCreateVersion)
  • client.Model.GetPermissions(projectId, modelId)ModelPermissionChecks (canUpdate, canDelete, canCreateVersion)
  • client.ActiveUser.GetProjectsWithPermissions() → each ProjectWithPermissions.permissions (canCreateModel, canDelete, canLoad only)
  • Workspace.canCreateProject on a fetched workspace
Speckle’s own connectors use these to disable menu items and show a tooltip explaining why, rather than letting the user click “send” and then showing an error:
https://mintcdn.com/speckle/VtRPWzmN-ULoLfIv/images/developers/sdks/csharp.svg?fit=max&auto=format&n=VtRPWzmN-ULoLfIv&q=85&s=f2970e3b8bd4a2c7899b46211a42f251Example
if (!project.permissions.canLoad.authorized)
{
    projectMenuItem.Enabled = false;
    projectMenuItem.ToolTipText = "You do not have permission to do operation on this project.";
}

Explicit “Can I Do This” Checks

For actions that aren’t tied to an existing permission field on a resource, some methods ask the question directly and return a PermissionCheckResult:
  • client.Model.CanCreateModelIngestion(projectId, modelId)
  • client.ActiveUser.CanCreatePersonalProjects()
PermissionCheckResult exposes authorized, code, message, and a convenience method:
https://mintcdn.com/speckle/VtRPWzmN-ULoLfIv/images/developers/sdks/csharp.svg?fit=max&auto=format&n=VtRPWzmN-ULoLfIv&q=85&s=f2970e3b8bd4a2c7899b46211a42f251Example
public void EnsureAuthorised(); // throws WorkspacePermissionException if authorized == false
The property is authorized (US spelling); the guard method is EnsureAuthorised() (UK spelling) — both match the SDK.
Call EnsureAuthorised() as a guard clause instead of hand-rolling an if (!result.authorized) throw ... at every call site.

Telling “Not Allowed” Apart From “Server Doesn’t Support This”

This is the part that’s easy to get wrong with a blanket catch. A server old enough to predate a permission field (like canCreateIngestion, added in server 3.0.11) doesn’t return authorized: false — its GraphQL schema doesn’t have the field at all, so the query fails at the schema-validation level. That surfaces as AggregateException wrapping a SpeckleGraphQLInvalidQueryException, which is structurally different from WorkspacePermissionException (thrown directly by EnsureAuthorised() when a capable server says no). Catch only the narrow exception that represents “this server doesn’t know this concept,” as shown in the Complete Example above. Don’t catch SpeckleException or AggregateException broadly around the whole check — that would also swallow a genuine WorkspacePermissionException (if you don’t re-throw it) or mask unrelated failures (network errors, auth expiry) as capability gaps.
Some older, internal Speckle tooling also uses raw HTTP endpoint probing (checking for a 404 response) to detect feature availability outside GraphQL. That’s an implementation detail of specific legacy code paths, not a documented part of the Speckle.Sdk public surface — prefer the GraphQL capability and permission checks above, which are part of the supported API.

Common Pitfalls

A blanket catch can’t tell a user “your server needs an upgrade” apart from “you don’t have permission” apart from “the network dropped.” Check server capability and permissions first, and let genuinely unexpected exceptions propagate — see Exceptions.
Many self-hosted and community servers run without the workspaces module. Code that only exercises client.Workspace and workspace-scoped client.ActiveUser methods will throw SpeckleGraphQLWorkspaceNotEnabledException (wrapped in AggregateException) on those servers. Check client.Server.IsWorkspaceEnabled() and support the personal-project path too.
An unqualified catch (AggregateException) around a capability check treats old-server incompatibility and a real permission denial identically. Filter with an exception when clause (as in the Complete Example) or inspect InnerExceptions before deciding how to respond.
PermissionCheckResult.EnsureAuthorised() throws WorkspacePermissionException directly — it is not wrapped, because it isn’t a GraphQL response error. Don’t put it inside a catch (AggregateException) block expecting to catch it there. See Exceptions: Workspace Permission Errors.

Next Steps

Core concepts

Server capability patterns in context

Publish large models (connectors)

CanCreateModelIngestion in upload flow

ServerResource

IsWorkspaceEnabled API surface
Last modified on July 10, 2026