Namespaces & tables
Browse the node tree, manage Namespaces, and inspect Tables and Views by stable node ID.
One catalog tree
A workspace contains one tree of generic catalog nodes. A node is a Namespace, Table, or View. Only Namespaces can contain children; Tables and Views are leaves. The tree is the single source of truth for each node's name, parent, current SQL path, UI location, and permission ancestry.
Every node also has a stable regional public ID. Keep that ID in applications, integration configuration, and other durable references. The ID does not change when the node or an ancestor Namespace is renamed or moved. SQL uses names; non-SQL catalog APIs use node IDs.
Absolute SQL paths
SQL paths can contain any Namespace depth. They are always absolute from the current workspace's hidden root, and the workspace itself is not a path component. For example:
SELECT *
FROM commerce.stripe.production.charges;Loam resolves that path as Namespace commerce, Namespacestripe, Namespace production, and Tablecharges. A quoted dot remains part of one identifier, socommerce."stripe.prod".charges has three components.
There is no current Namespace, search path, implicit parent, or special multipart fallback. Every component is resolved from the workspace Root.
List Root or Namespace children
GET /v1/workspaces/{workspacePublicId}/nodes
List direct children of the workspace Root. Results are permission filtered.
/v1/workspaces/{workspacePublicId}/nodes{
"items": [
{
"id": "rw-us-east1-node-01980c54f7a27d318eb1566cf1b44aa0",
"workspaceId": "rw-us-east1-ws-0195a3c47d3e7ab18c4f123456789abc",
"kind": "namespace",
"name": "commerce",
"parentId": null,
"description": "Imported and synchronized data",
"hierarchyVersion": 3,
"effectiveRole": "admin"
}
],
"nextCursor": null
}Pass parentId to expand a Namespace. The API returns direct children only; recursively follow Namespace nodes to browse the whole tree.
curl "https://rw-us-east1.api.loamdata.com/v1/workspaces/rw-us-east1-ws-0195a3c47d3e7ab18c4f123456789abc/nodes?parentId=rw-us-east1-node-01980c54f7a27d318eb1566cf1b44aa1" \
-H "Authorization: Bearer $JWT"{
"items": [
{
"id": "rw-us-east1-node-01980c54f7a47f119d672ac0e011f2c1",
"workspaceId": "rw-us-east1-ws-0195a3c47d3e7ab18c4f123456789abc",
"kind": "table",
"name": "charges",
"parentId": "rw-us-east1-node-01980c54f7a27d318eb1566cf1b44aa1",
"description": null,
"hierarchyVersion": 1,
"effectiveRole": "editor"
}
],
"nextCursor": null
}Read a node
GET /v1/workspaces/{workspacePublicId}/nodes/{nodePublicId}
Read a Namespace, Table, or View through the same generic endpoint. Node kind is immutable.
curl https://rw-us-east1.api.loamdata.com/v1/workspaces/rw-us-east1-ws-0195a3c47d3e7ab18c4f123456789abc/nodes/rw-us-east1-node-01980c54f7a47f119d672ac0e011f2c1 \
-H "Authorization: Bearer $JWT"{
"id": "rw-us-east1-node-01980c54f7a47f119d672ac0e011f2c1",
"workspaceId": "rw-us-east1-ws-0195a3c47d3e7ab18c4f123456789abc",
"kind": "table",
"name": "charges",
"parentId": "rw-us-east1-node-01980c54f7a27d318eb1566cf1b44aa1",
"description": "Stripe charges",
"hierarchyVersion": 1,
"effectiveRole": "editor"
}Create a Namespace
POST /v1/workspaces/{workspacePublicId}/namespaces
Supply the stable ID of the immediate parent Namespace. Use a null parent to create a top-level Namespace. Missing ancestors are never created implicitly.
curl -X POST https://rw-us-east1.api.loamdata.com/v1/workspaces/rw-us-east1-ws-0195a3c47d3e7ab18c4f123456789abc/namespaces \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{
"parentId": "rw-us-east1-node-01980c54f7a27d318eb1566cf1b44aa1",
"name": "finance",
"description": "Finance-owned Stripe data"
}'{
"node": {
"id": "rw-us-east1-node-01980c54f7a27d338eb1566cf1b44aa2",
"kind": "namespace",
"name": "finance",
"workspaceId": "rw-us-east1-ws-0195a3c47d3e7ab18c4f123456789abc",
"parentId": "rw-us-east1-node-01980c54f7a27d318eb1566cf1b44aa1",
"description": "Finance-owned Stripe data",
"hierarchyVersion": 1,
"effectiveRole": "admin"
}
}Rename, move, or describe a node
PATCH /v1/workspaces/{workspacePublicId}/nodes/{nodePublicId}
Patch a node by stable ID. Include the current hierarchyVersion so a concurrent move or rename is rejected rather than overwritten. Change name to rename,parentId to move, or description to update its description. Do not submit a canonical path or ancestor list.
curl -X PATCH https://rw-us-east1.api.loamdata.com/v1/workspaces/rw-us-east1-ws-0195a3c47d3e7ab18c4f123456789abc/nodes/rw-us-east1-node-01980c54f7a27d318eb1566cf1b44aa1 \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{
"name": "live",
"expectedHierarchyVersion": 3
}'{
"id": "rw-us-east1-node-01980c54f7a27d318eb1566cf1b44aa1",
"kind": "namespace",
"name": "live",
"workspaceId": "rw-us-east1-ws-0195a3c47d3e7ab18c4f123456789abc",
"parentId": "rw-us-east1-node-01980c54f7a27d318eb1566cf1b44aa9",
"description": null,
"hierarchyVersion": 4,
"effectiveRole": "admin"
}Rename and move change the current SQL path immediately but never change the node ID. Durable Views, commerce, and ingestion targets continue to follow that ID.
Delete a node
DELETE /v1/workspaces/{workspacePublicId}/nodes/{nodePublicId}
Namespace deletion defaults to RESTRICT and fails if it has children. Use cascade=true only for an explicit recursive deletion. Tables and Views do not accept cascade deletion. External durable references to anything in a Namespace subtree block recursive deletion until they are removed or rebound.
curl -X DELETE "https://rw-us-east1.api.loamdata.com/v1/workspaces/rw-us-east1-ws-0195a3c47d3e7ab18c4f123456789abc/nodes/rw-us-east1-node-01980c54f7a27d318eb1566cf1b44aa1?expectedHierarchyVersion=3&cascade=false" \
-H "Authorization: Bearer $JWT"A successful deletion returns 204 No Content.
Inspect a Table schema
GET /v1/workspaces/{workspacePublicId}/nodes/{nodePublicId}/schemaAddress a Table schema by stable node ID, not by repeating its current Namespace path in the URL. The endpoint keeps working after an ancestor is renamed or moved. Read a View definition through the View detail endpoint instead.
curl https://rw-us-east1.api.loamdata.com/v1/workspaces/rw-us-east1-ws-0195a3c47d3e7ab18c4f123456789abc/nodes/rw-us-east1-node-01980c54f7a47f119d672ac0e011f2c1/schema \
-H "Authorization: Bearer $JWT"{
"nodeId": "rw-us-east1-node-01980c54f7a47f119d672ac0e011f2c1",
"kind": "table",
"columns": [
{ "name": "id", "physicalType": "String", "nullable": false },
{ "name": "amount", "physicalType": "Int64", "nullable": false },
{ "name": "currency", "physicalType": "String", "nullable": true },
{ "name": "created_at", "physicalType": "DateTime", "nullable": false }
]
}Inspect a View definition
GET /v1/workspaces/{workspacePublicId}/nodes/{viewNodeId}/viewViews use their dedicated detail endpoint. It returns the authored SQL and stable relation bindings; the Table-only schema endpoint does not accept a View node.
Namespace SQL
Namespace DDL operates on the same tree used by the node API. Create each parent before its child.
CREATE NAMESPACE commerce;
CREATE NAMESPACE commerce.stripe;
CREATE NAMESPACE commerce.stripe.production;
CREATE TABLE commerce.stripe.production.charges (
id String,
amount Int64
);
ALTER NAMESPACE commerce.stripe.production RENAME TO live;
ALTER NAMESPACE commerce.stripe.live MOVE TO commerce.finance;
-- RESTRICT is the default and fails when the Namespace has children.
DROP NAMESPACE commerce.finance.live RESTRICT;
-- Explicitly delete its full Namespace, Table, and View subtree.
DROP NAMESPACE commerce.finance.live CASCADE;Recursively browse with stable IDs
Applications should render organization from the node list rather than rebuilding it from SQL strings. This browser example uses the public@loam/sdk client so the workspace public ID selects its allow-listed regional data plane.
import { LoamClient } from "@loam/sdk";
const client = new LoamClient({
accessToken: jwt,
dataPlaneUrl: "https://rw-us-east1.api.loamdata.com",
workspaceId: "rw-us-east1-ws-0195a3c47d3e7ab18c4f123456789abc",
});
async function walk(parentId?: string) {
let cursor: string | undefined;
do {
const page = await client.listNodes({ parentId, cursor, limit: 100 });
for (const node of page.items) {
// Store node.id for durable actions. The displayed path is derived.
console.log(node.id, node.kind, node.name);
if (node.kind === "namespace") {
await walk(node.id);
}
}
cursor = page.nextCursor ?? undefined;
} while (cursor);
}
await walk();Last updated 2026-08-21