Workspaces

A workspace here is a catentio deployment record keyed by the Huudis workspace id (huudis_workspace_id, e.g. acc_01HX...) — it tracks whether that Huudis workspace has a provisioned catentio instance (internal/managed/byo plan), and what state that provisioning is in. It is not a generic "tenant" object with a display name and an owner user id.

Catentio is multi-tenant — workspaces, per-tenant runners, billing, and customer onboarding are all shipped. There is no single-user gate on workspace creation; ignore any older reference to one.

For the wire-level reference, see API → Workspaces.

Auth. list/show/create/destroy use RequireSessionOrAgent, which honours a real Huudis-issued JWT in Authorization: Bearer — one of the few namespaces where that's true. setState is different: PATCH /v1/workspaces/{id}/state is gated by RequireAdmin (an X-Catentio-Admin HMAC or the legacy x-operator-token), which no session or Bearer JWT can satisfy. This SDK's apiKey mode (cat_pk_...) is not a Huudis JWT and isn't honoured on any of these routes. See SDKs → Auth: what actually works.

Namespace

catentio.workspaces — every method on this namespace:

catentio.workspaces.list()
catentio.workspaces.show(id)
catentio.workspaces.create(input)
catentio.workspaces.destroy(id)
catentio.workspaces.setState(id, { state, reason? })

There is no update for the mutable fields.

Methods

workspaces.list

Signature. catentio.workspaces.list(token?): Promise<{ rows: WorkspaceDeployment[] }>

The response shape is { rows: [...] }, not { data, meta } and not a bare array — ListResponse is the one list endpoint in this SDK that doesn't follow the usual data/meta convention.

import { CatentioSaasClient } from '@forjio/catentio-saas-node';

const catentio = new CatentioSaasClient({ session });

const { rows } = await catentio.workspaces.list();
console.log(rows[0].huudis_workspace_id, rows[0].status);

workspaces.show

Signature. catentio.workspaces.show(id, token?): Promise<WorkspaceDeploymentDetail>

id is the huudis_workspace_id. 404 {"detail": "not_found"} if there's no deployment row for it.

const ws = await catentio.workspaces.show('acc_01HXAB7K3M9N2P5QRS8TVWXY3Z');
console.log(ws.status, ws.plan, ws.runtime_base_url);

workspaces.create

Signature. catentio.workspaces.create(input, token?): Promise<WorkspaceDeploymentDetail>

Real required fields: huudis_workspace_id (1–64 chars), plan ("managed" or "byo""internal" is rejected with 400 invalid_plan for customer calls). config is optional and plan-specific.

const ws = await catentio.workspaces.create({
  huudis_workspace_id: 'acc_01HX...',
  plan: 'managed',
});

There is no displayName or ownerHuudisUserId field — those don't exist. Real failure modes: 409 {"code": "already_exists", "status": ...} if a non-archived deployment already exists for that Huudis workspace; 402 {"code": "insufficient_credit", ...} if a managed plan needs a wallet top-up that hasn't happened; 400 {"code": "managed_anthropic_not_offered"} if config.manage_anthropic is set (Anthropic is BYO-only).

workspaces.destroy

Signature. catentio.workspaces.destroy(id, token?): Promise<WorkspaceDeploymentDetail>

Synchronously tears the deployment down — the CP awaits the full destroy sequence before responding, it does not return early and finish in the background. 400 {"detail": "cannot_destroy_internal"} on the internal/seed deployment. 502 {"code": "destroy_failed", ...} if teardown fails partway (partial progress is still committed).

await catentio.workspaces.destroy('acc_01HX...');

workspaces.setState — admin-only

Signature. catentio.workspaces.setState(id, input, token?): Promise<WorkspaceDeploymentDetail>

This route requires RequireAdmin, not a session or Bearer JWT. A normal caller of this SDK cannot authenticate against it at all, regardless of credential type.

The real body is also different from what the type suggests: { status: "pending" | "provisioning" | "ready" | "failed" | "archived", runtime_base_url?, error_code?, error_message? }. There is no state field (it's status) and no reason field — those don't exist.

// Only works with an admin-level credential this SDK cannot produce:
await catentio.workspaces.setState('acc_01HX...', {
  status: 'ready',
  runtime_base_url: 'https://acme.catentio.internal',
});

Types

interface WorkspaceDeployment {
  huudis_workspace_id: string;   // primary key, e.g. 'acc_01HX...' — never 'ws_...'
  customer_id: string;
  plan: 'internal' | 'managed' | 'byo';
  status: 'pending' | 'provisioning' | 'ready' | 'failed' | 'archived';
  runtime_base_url: string | null;
  error_code: string | null;
  error_message: string | null;
  created_at: string;
  ready_at: string | null;
  archived_at: string | null;
}

interface WorkspaceDeploymentDetail extends WorkspaceDeployment {
  config: Record<string, unknown> | null;   // present on show/create/destroy; secrets redacted
}

There is no id (it's huudis_workspace_id), displayName, state (it's status, and the enum values differ — no active/suspended), ownerHuudisUserId, runtimeDropletId (it's runtime_base_url, a URL not a droplet id), plugipayPartnerKey, or updatedAt field (there's no updated_at at all) — none of those exist.

Common patterns

Resolve "my" deployment at boot

async function resolveDeployment(huudisWorkspaceId: string) {
  const ws = await catentio.workspaces.show(huudisWorkspaceId);
  if (ws.status !== 'ready') {
    throw new Error(`Deployment for ${huudisWorkspaceId} is ${ws.status}, not ready`);
  }
  return ws.runtime_base_url;
}

Errors

The real error body is FastAPI's {"detail": ...} or a nested {"detail": {"code": ..., ...}}. Because the wire response never has an error key, the SDK doesn't parse out a structured code/message — you get CatentioError with a generic HTTP_ERROR code and the HTTP reason phrase as the message.

Status detail Cause
404 "not_found" show()/destroy() on an id with no deployment row.
400 "invalid_plan" create() with plan: "internal".
400 "cannot_destroy_internal" destroy() on the internal/seed deployment.
402 {"code": "insufficient_credit", ...} create() for a managed plan without enough wallet balance.
409 {"code": "already_exists", ...} create() for a Huudis workspace that already has a live deployment.
502 {"code": "destroy_failed", ...} destroy() failed partway.

Next

  • Agents — what runs inside a workspace's runtime.
  • Billing — the Plugipay subscription tied to a workspace.
  • API → Workspaces — HTTP-level reference.