SDK reference
@astrato-io/sdk is a browser-only ESM client for Astrato. It provides governed data queries,
semantic-layer metadata, in-memory selections and variables, and an event bus. It also provides
the host services used by @astrato-io/visualizations.
How the client is created depends on the app model selected in Getting started with Data Apps:
- An externally hosted app configures
createAstratoClient()and owns its trusted authentication/server boundary. - A Data App calls
createAstratoApp()from@astrato-io/app-runtimeand uses the returnedapp.client. It must not configure another client, credentials or platform IDs.
Package details:
- Name:
@astrato-io/sdk. - ESM only. No CommonJS, no UMD. One root export, no subpaths.
- Browser only. It requires
fetch,Headers, andURL. - Install from npmjs.
SDK 0.6.0 supports visualization field metadata from custom SQL views and table views without a physical schema name. Field references continue to use their canonical semantic-layer keys.
Install
Section titled “Install”For an externally hosted app or direct SDK use:
npm install @astrato-io/sdkFor a Data App, install @astrato-io/app-runtime instead. It depends on and configures the
SDK through the hosted browser contract.
Runtime exports
Section titled “Runtime exports”The root package exports these runtime values. Most other exports are types.
import { createAstratoClient, createAstratoEventBus, createDirectDataService, createVisualizationEngineService, createMemorySelectionService, createMemoryVariableService, createFetchTransport, createConsoleLogger, AstratoTransportError,} from '@astrato-io/sdk';Most externally hosted applications only need createAstratoClient. The other factories exist for
advanced composition and testing. Data Apps use the configured client returned by
createAstratoApp().
createAstratoClient for externally hosted apps
Section titled “createAstratoClient for externally hosted apps”const client = createAstratoClient(options);baseUrl is the only required option. Do not call this factory in a Data App; see the
Data App runtime reference instead.
The examples use window.location.origin for baseUrl because browsers cannot call the Astrato
API from a third-party origin. During local development, configure your dev server to proxy /api
requests to https://app.astrato.io, or to your organization’s Astrato host. See
Authentication for supported authentication flows and the
externally hosted app quickstart for a complete Vite configuration.
| Option | Type | Behavior |
|---|---|---|
baseUrl |
string |
Required. Base for the default transport. Ignored if you pass your own transport. |
tenantId |
string |
Merged into the request context. Wins over context.tenantId. |
context |
AstratoDataRequestContext |
Merged into query and query-insights context. Seven fields map to headers: tenantId, workbookVersionContainerId, sheetId, objectId, role, userId, workbookState. datasourceType remains engine context. Metadata GETs receive no context headers. |
auth |
AstratoAuthProvider |
{ token?, headers?, getAccessToken?() }. See Authentication. |
transport |
AstratoTransport |
Replaces the default fetch transport. auth still applies on top. |
visualizationEngine |
boolean |
Default false. Set true to enable client.services.engine. Without it, synchronous engine members throw and asynchronous members reject with "Astrato SDK engine service is not configured. Provide an engine service." |
hostServices |
PartialAstratoHostServices |
Per-service overrides: data, engine, model, selections, variables. A custom data service replaces direct endpoint calls, but a default visualization engine still merges client context into requests passed to it. |
logger |
AstratoLogger |
Default: console logger. Supply your own to silence output. |
events |
AstratoEventBus |
Default: a client-owned bus. |
endpoints |
{ data? } |
Override individual data endpoint URL builders. |
sessionId |
string |
Stamped as scope.sessionId on emitted events. No transport effect. |
createAstratoClient returns:
interface AstratoClient { baseUrl: string; services: AstratoHostServices; // same object as hostServices hostServices: AstratoHostServices; logger: AstratoLogger; events: AstratoEventBus; request<TResponse, TBody>(request: AstratoRequest<TBody>): Promise<TResponse>; reportError(error: unknown, context?: AstratoLogContext): void; destroy(): void;}destroy() invalidates metadata in an engine created by the client and clears subscriptions on an
event bus created by the client. A supplied engine or event bus remains owned by its caller.
In a Data App, retain the object returned by createAstratoApp() and call
app.destroy() instead. It destroys both the SDK client and runtime transport.
Iframe connection availability
Section titled “Iframe connection availability”SDK 0.5.0 adds a versioned iframe connection for hosts that keep authentication outside embedded application code. These exports are unavailable in SDK 0.4.0.
The host uses createAstratoIframeHostBridge() with the exact iframe, expected child origin,
authorized context and a callback that validates each requested operation. Trusted child bootstrap
uses connectAstratoIframeBridge() with the expected parent origin. After connection,
getAstratoIframeBridge() returns the connection, whose createClient() supplies an ordinary
SDK client with the visualization engine enabled.
An arbitrary iframe does not activate this connection. Invalid origins, incompatible protocol versions and failed connections do not fall back to direct HTTP. The bridge bounds requests and responses and supports cancellation, timeouts and destruction. The host must enforce permitted operations and resource access; the context’s capability list does not replace authorization. Host credentials and raw host errors are not sent to the child.
The host queues request bursts in arrival order while preserving maxConcurrentRequests.
At most 32 requests may be active or waiting; excess requests fail with limit. The timeout
includes time spent waiting. Cancelling a waiting request removes it before execution;
cancelling an active request aborts it, and its slot remains occupied until the host operation
settles. Disconnecting clears waiting work and aborts active operations.
Data App source continues to use createAstratoApp() from the app runtime. Host connection
setup belongs to the embedding platform, not individual app source.
Authentication
Section titled “Authentication”The SDK does not sign users in. In externally hosted code, it adds the authentication headers that you provide:
interface AstratoAuthProvider { token?: string; // wins over getAccessToken headers?: Record<string, string>; getAccessToken?: () => string | Promise<string>; // resolved per request or default engine operation}Individual SDK requests resolve getAccessToken independently when auth.token is absent. A
default engine metadata operation or query resolves authentication once and reuses those headers
for all requests within that operation; concurrent engine operations share a pending resolution.
The SDK does not reuse a completed callback result for later operations.
The resolved token supplies authorization: Bearer <token>. An authorization value in
auth.headers overrides the generated header, and a per-request header overrides both.
Bearer token
Section titled “Bearer token”For a quick local experiment, pass a PAT into the client from your development setup:
import { createAstratoClient } from '@astrato-io/sdk';
function createLocalDevelopmentClient(token: string) { return createAstratoClient({ baseUrl: window.location.origin, tenantId: 'your-tenant-id', auth: { token }, });}Bearer tokens do not require a CSRF header. Anything passed through auth.token, however, is
visible to the user and to scripts running on the page. Never use this pattern in a page delivered
to other users. The externally hosted app quickstart shows how to keep the PAT
in a development proxy instead.
The SDK does not provide login, token refresh, expiry handling, or automatic retry after a 401
response. A token also does not bypass the browser’s same-origin restriction.
Queries
Section titled “Queries”client.services.data.query() posts a governed query and returns the engine response
unmodified.
query(request: AstratoQueryRequest): Promise<AstratoQueryResult>
interface AstratoQueryRequest { tenantId?: string; // falls back to client options semanticLayerId?: string; // NOT sent by query(); used by the engine service only query: unknown; // passed to the transport without an added envelope context?: AstratoDataRequestContext;}Important behaviors:
- Endpoint:
POST {baseUrl}/api/message/tenants/{tenantId}/data/execute/query. - The low-level
query()passes yourqueryvalue to the transport without rewriting it or adding an envelope. The default transport JSON-serializes object bodies. Your payload must containdatabaseID(the semantic layer ID) itself. - Prefer
client.services.engine.getData()— it wraps your query into the{ query: { ...query, databaseID } }envelope the engine expects. - The response is not validated or unwrapped. Type:
AstratoQueryResult, a loose{ response, correlationID?, telemetry?, ... }object. - No cancellation, timeout, or retry on
query(). Only the low-levelclient.request()accepts anAbortSignal. - HTTP errors reject with
AstratoTransportErrorcarrying.statusand.responseBody.
The engine response
Section titled “The engine response”query() and getData() return the engine response unchanged. A typical result looks like this:
{ "response": { // One entry per returned column, in the engine's order. "fields": [ { "name": "Cars.Manufacturer", "dataType": "string", "isDimension": true, "isMeasure": false }, { "name": "SUM of Sales", "dataType": "int", "isDimension": false, "isMeasure": true }, ], // Row arrays keyed by row index. "data": { "0": ["Ford", 1234], "1": ["Kia", 987] }, "meta": { "rows": 2, "cols": 2 }, "timezone": "UTC", },}When reading the result:
datais an object keyed by row index. UseObject.values(data)to obtain the row arrays.fields[i].nameis a display label, not a stable field key. A raw dimension commonly has a dotted label such asCars.Manufacturer; an aggregate or calculated column may be labelledSUM of SalesorCPC. Do not match result columns by these labels. Keep the field keys and selection order from the query you sent, then validate that the response has the expected number of columns, row widths and dimension/measure roles before reading values.
Ordering
Section titled “Ordering”orders addresses a result column by its zero-based position, not by its field key. The result
columns are dimensions first, followed by measures or calculations:
const query = { dimensions: [{ fieldKey: '[Cars].[Manufacturer]' }, { fieldKey: '[Cars].[Year]' }], measures: [], orders: [{ columnIdx: 1, direction: 'desc' }], // by Year};Calculate columnIdx from the current result-column order so sorting remains correct when you
add or remove columns. The response fields array and each row’s value array use that same order.
Handling errors
Section titled “Handling errors”import { AstratoTransportError } from '@astrato-io/sdk';
// `client` is configured with one of the authentication flows described above.
try { const result = await client.services.data.query({ query: { query: { databaseID: 'your-semantic-layer-id', dimensions: [], measures: [], }, }, }); console.log(result);} catch (error) { if (error instanceof AstratoTransportError) { console.error(`HTTP ${error.status}`, error.responseBody); } else { throw error; }} finally { client.destroy();}Writeback and stored procedures
Section titled “Writeback and stored procedures”Use generic client.request() with an authenticated platform transport for writeback inserts
and updates, stored procedures and SQL SELECT readback. The SDK has no dedicated methods for
these operations. See Writeback and SQL for scopes, payloads and complete examples.
For local Data Apps during astrato dev, use the App runtime’s capability-gated
app.executeProcedure() and app.writeback() methods. SQL SELECT requests are unavailable
through the CLI development gateway, and the hosted publication transport does not provide
these operations. See App runtime availability.
Metadata
Section titled “Metadata”The following methods are available on client.services.data. Every method except query is
optional in the TypeScript interface, so call them with ?.. The default client implements all of
them.
| Method | Returns |
|---|---|
getSemanticLayerMetadata(semanticLayerId, tenantId?) |
The composite semantic-layer metadata, raw. |
getDatasource(datasourceId, tenantId?) |
Datasource details, raw. |
getAggregations(datasourceType, tenantId?) |
Available aggregation functions, unwrapped to an array. |
getCapabilities(datasourceType, tenantId?) |
Datasource capabilities, unwrapped to an object. |
getQueryInsights({ semanticLayerId, queryID, startTime?, tenantId? }) |
Warehouse execution insights. The only validated response: it must carry six booleans (cacheUsed, metaDataQuery, joinIssue, unionIssue, memoryIssue, pruningIssue) and four numbers (creditsBestCase, creditsWorstCase, creditsFairValue, rowsScanned). |
The SDK fetches semantic layers and datasources by ID; it does not provide listing APIs. Use the Nash MCP gateway to discover available semantic layers.
The visualization engine service
Section titled “The visualization engine service”Enable this service with visualizationEngine: true. It holds one active semantic-layer snapshot at
a time and provides synchronous field lookup from that snapshot. Loading another layer activates
its snapshot. @astrato-io/visualizations uses the same service.
Starting with SDK 0.4.0, metadata loads for the same tenant, semantic layer, datasource type and
authentication headers share pending work and reuse completed snapshots for five minutes. One
client retains at most 16 completed snapshots. Reuse the same client across charts to benefit
from this cache; low-level client.services.data calls and query results are not cached by the SDK.
Externally hosted apps can configure these bounds with
createAstratoClient({ ...options, metadataCache: { maxEntries: 16, ttlMs: 300_000 } }).
The default engine resolves authentication once per metadata operation or query and uses those
headers for the operation’s requests. Concurrent operations share pending authentication resolution;
callbacks that return a new token on each invocation are supported. When any SDK request observes
changed authentication headers, the default engine clears its cached and active metadata. Reload
metadata before using synchronous field or connectivity lookups. If authentication is managed
entirely by a custom transport, invalidate metadata when that transport’s authority changes.
A metadata operation or engine query rejects if a subsequent SDK operation observes changed authentication before it completes. Rejected query results do not update query metadata. An older authentication lookup also rejects if it finishes after a newer successful lookup with different credentials. Retry these operations using the current authentication.
After editing a model in an externally hosted app, refresh its metadata explicitly:
await engine.fields.refreshMetadata?.({ semanticLayerId: 'your-semantic-layer-id', tenantId: 'your-tenant-id',});invalidateMetadata(request) removes matching snapshots without reloading them; omit the request
to clear all snapshots. These methods are optional for custom engine implementations. For an
Data App, reload the page after a model change to refresh both SDK and local runtime metadata.
const client = createAstratoClient({ baseUrl: window.location.origin, tenantId: 'your-tenant-id', visualizationEngine: true, context: { objectId: 'sales-by-manufacturer' }, // used by getLastQueryMetadata()});
const engine = client.services.engine;
// Load or replace the snapshot. Field and connectivity reads below are synchronous.await engine.fields.getFieldMetadata?.({ semanticLayerId: 'your-semantic-layer-id', tenantId: 'your-tenant-id',});
engine.fields.getDimensionsList(); // synchronous after the loadengine.fields.getMeasuresList();engine.areFieldsConnected(fieldA, fieldB); // join-graph connectivity
await engine.getData({ semanticLayerId: 'your-semantic-layer-id', query: { dimensions: [], measures: [] }, // getData adds the databaseID envelope});
const metadata = engine.getLastQueryMetadata?.('sales-by-manufacturer');Important behavior:
- The engine exposes one active snapshot at a time. Reading fields for a different
semanticLayerIdthrows until you load it. Loading a cached ID activates its snapshot. engine.capabilities,engine.databaseInfo,engine.localeSettingsthrow before the first load. They are notundefined.- Field identity is the opaque engine
fieldKey(for example[Cars].[Manufacturer]). Never parse or construct field keys from names. getGroupedDimensions()returns the active level of each grouped dimension. Levels default to0on a fresh metadata load and can be changed withsetGroupedDimensions(). Cache hits preserve those choices separately for each cached snapshot.getLastQueryMetadata(objectId)is keyed bycontext.objectIdand returns the last successful query’squeryID,statement,rows,warnings, and more.
Showing the SQL behind a visualization
Section titled “Showing the SQL behind a visualization”In an Astrato App, statement is the engine SQL for a query. The SDK can pass it through only after a
successful query for that objectId has settled. No Astrato host currently grants it — not
astrato dev, the App Builder preview or published Apps — so the SDK reports it as undefined.
Hiding it is a presentation control, not a security boundary: the query response in the browser
still carries the SQL, visible in browser developer tools. Render a “Show SQL” control only when it
is present:
const sql = engine.getLastQueryMetadata?.('sales-by-manufacturer')?.statement;if (sql) { const button = document.createElement('button'); button.type = 'button'; button.textContent = 'Show SQL'; const output = document.createElement('pre'); output.hidden = true; output.textContent = sql; button.addEventListener('click', () => { output.hidden = !output.hidden; }); chartElement.append(button, output); // the element that holds the visualization}Read statement again after each query settles; the App Builder starter’s ShowSql component
does this for you.
Selections
Section titled “Selections”client.services.selections is an in-memory, group-scoped selection store. It is the basis
for cross-chart interactivity — see the Interactivity guide for the full
picture with charts.
A selection group is a plain string. Groups are created lazily the first time you name one.
The default group ID is the literal string 'default'.
interface AstratoSelection { field: { fieldKey: string }; values: (string | number | boolean | null)[]; operator?: AstratoSelectionOperator; // 'equals' | 'inList' | 'gt' | 'between' | ... kind?: 'VALUE' | 'ASSOCIATIVE'; // defaults to 'VALUE' sourceInstanceId?: string;}Core API:
const selections = client.services.selections;
selections.setSelectionMode('manual', 'dashboard'); // 'auto' | 'manual', per groupselections.selectValues('dashboard', { field: { fieldKey: 'your-engine-field-key' }, operator: 'inList', values: ['Sweden', 'Norway'],});selections.applyPendingSelections('dashboard'); // pending -> appliedselections.getSelection('dashboard'); // { applied, pending }selections.getActiveSelection('VALUE', 'dashboard');selections.removeSelection('dashboard', { fieldKey: 'your-engine-field-key' });selections.clearAllSelections('dashboard');
const subscription = selections .onActiveSelectionChangeApplied('dashboard') .subscribe((state) => console.log(state.applied, state.pending));Behaviors to know:
- Modes. In
'auto'mode (the default)selectValueswrites directly toapplied. In'manual'mode it writes topendinguntilapplyPendingSelectionsruns. - Identity is
kind+fieldKey. A secondselectValueson the same field and kind replaces the first. It does not append. - Three observables, one payload.
onActiveSelectionChange,onActiveSelectionChangeApplied, andonActiveSelectionChangePendingall emit the full{ applied, pending }state. They differ only in when they fire. All replay the current value on subscribe. - The
selectionModeproperty setter affects only the'default'group. UsesetSelectionMode(mode, groupId)for named groups. removeSelectionwith a bare{ fieldKey }removes across both kinds. Pass a full selection to remove one kind only.- A locked field (
setFieldLocked) silently ignoresselectValues. - Selections are in-memory only and do not survive a page reload.
singleSelectFields$always emits[]and has no supported use.
Variables
Section titled “Variables”client.services.variables is read-only, with two methods:
getVariables(contextId?: string): Promise<AstratoVariable[]>;resolveValue(nameOrId: string, contextId?: string): Promise<unknown>;
interface AstratoVariable { id: string; name: string; value: unknown; contextId?: string }The default service is empty. Seed values at client creation:
import { createAstratoClient, createMemoryVariableService } from '@astrato-io/sdk';
const client = createAstratoClient({ baseUrl: window.location.origin, tenantId: 'your-tenant-id', hostServices: { variables: createMemoryVariableService([{ id: 'region', name: 'Region', value: 'EMEA' }]), },});
await client.services.variables.resolveValue('Region'); // 'EMEA'There is no variable write API and no server backing. To change variables at runtime, supply
your own AstratoVariableService implementation.
Events
Section titled “Events”client.events is an RxJS-based event bus. Events are not replayed, so subscribe before triggering
the operation you want to observe.
const unsubscribe = client.events.on('transport:error', (event) => { console.error(event.payload.method, event.payload.url, event.payload.error);});
client.events.emit('my-app:refresh', { reason: 'user' }); // custom events type-checkBuilt-in events, all emitted by the client:
| Event | Payload | When |
|---|---|---|
transport:request |
{ method, url } |
Before every request. |
transport:response |
{ method, url } |
After a successful response. |
transport:error |
{ error, method, url } |
After a failed request, before the rejection reaches you. |
sdk:error |
{ context, error } |
When client.reportError() is called. |
There are no client events for selections, variables, or engine loads. Selection changes are available through the three RxJS observables described above. Variables and engine loads expose no event or observable stream.
Transport
Section titled “Transport”The default transport wraps fetch:
createFetchTransport({ baseUrl?, defaultHeaders?, fetch? })- URL joining uses
new URL(url, baseUrl + '/'). The SDK’s own endpoint paths are relative without a leading slash, so abaseUrlwith a path prefix is preserved. - Bodies:
nullandundefinedproduce no body.string,FormData, andURLSearchParamspass through. Other values are JSON-encoded withcontent-type: application/json. fetchoption: supply your own fetch (for CSRF handling, logging, or testing).
SDK limitations
Section titled “SDK limitations”The following features are not supported:
- Authentication flows such as login, refresh, expiry checks, or retry after a
401response. - Cross-origin browser requests.
- Persistence.
model.saveProperties()andmodel.saveLocalVSpec()are no-ops. - Listing or discovery APIs. Semantic layers and datasources are fetched by ID.
- Business questions.
- Cancellation on
query()orgetData(), or built-in timeout, retry, pagination, or streaming. - Node.js. The SDK is browser-only.