Skip to content

SDK reference

12 min read · Updated 30 Oct 2026

@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-runtime and uses the returned app.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, and URL.
  • 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.

For an externally hosted app or direct SDK use:

Terminal window
npm install @astrato-io/sdk

For a Data App, install @astrato-io/app-runtime instead. It depends on and configures the SDK through the hosted browser contract.

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.

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.

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.

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.

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 your query value to the transport without rewriting it or adding an envelope. The default transport JSON-serializes object bodies. Your payload must contain databaseID (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-level client.request() accepts an AbortSignal.
  • HTTP errors reject with AstratoTransportError carrying .status and .responseBody.

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:

  • data is an object keyed by row index. Use Object.values(data) to obtain the row arrays.
  • fields[i].name is a display label, not a stable field key. A raw dimension commonly has a dotted label such as Cars.Manufacturer; an aggregate or calculated column may be labelled SUM of Sales or CPC. 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.

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.

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();
}

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.

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.

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 load
engine.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 semanticLayerId throws until you load it. Loading a cached ID activates its snapshot.
  • engine.capabilities, engine.databaseInfo, engine.localeSettings throw before the first load. They are not undefined.
  • 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 to 0 on a fresh metadata load and can be changed with setGroupedDimensions(). Cache hits preserve those choices separately for each cached snapshot.
  • getLastQueryMetadata(objectId) is keyed by context.objectId and returns the last successful query’s queryID, statement, rows, warnings, and more.

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.

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 group
selections.selectValues('dashboard', {
field: { fieldKey: 'your-engine-field-key' },
operator: 'inList',
values: ['Sweden', 'Norway'],
});
selections.applyPendingSelections('dashboard'); // pending -> applied
selections.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) selectValues writes directly to applied. In 'manual' mode it writes to pending until applyPendingSelections runs.
  • Identity is kind + fieldKey. A second selectValues on the same field and kind replaces the first. It does not append.
  • Three observables, one payload. onActiveSelectionChange, onActiveSelectionChangeApplied, and onActiveSelectionChangePending all emit the full { applied, pending } state. They differ only in when they fire. All replay the current value on subscribe.
  • The selectionMode property setter affects only the 'default' group. Use setSelectionMode(mode, groupId) for named groups.
  • removeSelection with a bare { fieldKey } removes across both kinds. Pass a full selection to remove one kind only.
  • A locked field (setFieldLocked) silently ignores selectValues.
  • Selections are in-memory only and do not survive a page reload.
  • singleSelectFields$ always emits [] and has no supported use.

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.

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-check

Built-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.

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 a baseUrl with a path prefix is preserved.
  • Bodies: null and undefined produce no body. string, FormData, and URLSearchParams pass through. Other values are JSON-encoded with content-type: application/json.
  • fetch option: supply your own fetch (for CSRF handling, logging, or testing).

The following features are not supported:

  • Authentication flows such as login, refresh, expiry checks, or retry after a 401 response.
  • Cross-origin browser requests.
  • Persistence. model.saveProperties() and model.saveLocalVSpec() are no-ops.
  • Listing or discovery APIs. Semantic layers and datasources are fetched by ID.
  • Business questions.
  • Cancellation on query() or getData(), or built-in timeout, retry, pagination, or streaming.
  • Node.js. The SDK is browser-only.