AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
Test application for Spice.js browser functionality
Translation pending
Developer toolsTypeScript · JavaScriptGitHub @spiceaiOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Node.js SDK for Spice.ai
Translation pending
Taken from the repository README
Loading the full README. The saved excerpt is shown below.
See full documentation at docs.spice.ai.
npm install @spiceai/spice or yarn add @spiceai/spice
High-Performance Apache Arrow Flight Query with cloud
Querying data is done through a SpiceClient object that initializes the connection with Spice endpoint. SpiceClient has the following arguments:Read the full README ↗Spice.ai client library for Node.JS
See full documentation at docs.spice.ai.
npm install @spiceai/spice or yarn add @spiceai/spice
import { SpiceClient } from '@spiceai/spice';
const main = async () => {
const spiceClient = new SpiceClient({
apiKey: 'API_KEY', // spice.ai api key,
httpUrl: 'https://data.spiceai.io',
flightUrl: 'flight.spiceai.io:443',
});
const table = await spiceClient.sql('SHOW TABLES;');
console.table(table.toArray());
};
main();
Querying data is done through a SpiceClient object that initializes the connection with Spice endpoint. SpiceClient has the following arguments:
apiKey (string, optional): API key to authenticate with the endpoint.flightUrl (string, optional): URL of the Flight endpoint to use (default: 127.0.0.1:50051)httpUrl (string, optional): URL of the HTTP endpoint to use (default: http://127.0.0.1:8090)logging (boolean, optional): Enable or disable logging output (default: true). Set to false to silence all library console output.The runtime serves Flight and the HTTP API as one deployment, so the two endpoints
are resolved together: naming one leaves the other pointing at the same runtime.
Give only flightUrl: 'flight.spiceai.io:443' and the HTTP calls — health, readiness,
status, refreshDataset, search, nsql, active and async queries — address
Spice Cloud too; give only httpUrl: 'http://127.0.0.1:8090' and queries go to the
local Flight endpoint. Only the local runtime and Spice Cloud have a known pairing;
any other address may serve its HTTP API elsewhere, so name both when self-hosting.
An endpoint given as an empty or blank string is rejected when the client is
constructed, rather than read as unset. Pass undefined, not '', for an endpoint
you have not configured. An environment variable can be passed through as it is —
flightUrl: process.env.SPICE_FLIGHT_URL is undefined when the variable is unset,
and an error when it is set but empty — but don't default it with ?? '', and don't
turn '' into undefined with || undefined.
Read more about the Spice.ai Apache Arrow Flight API at docs.spice.ai.
Follow the quickstart guide to install and run spice locally
import { SpiceClient } from '@spiceai/spice';
const main = async () => {
// uses connection to local runtime by default
const spiceClient = new SpiceClient();
// or use custom connection params:
// const spiceClient = new SpiceClient({
// httpUrl: 'http://my_spice_http_host',
// flightUrl: 'my_spice_flight_host',
// });
const table = await spiceClient.sql(
'SELECT trip_distance, total_amount FROM taxi_trips ORDER BY trip_distance DESC LIMIT 10;',
);
console.table(table.toArray());
};
main();
The SpiceClient automatically selects the best available transport protocol in this order:
For parameterized queries, the SDK provides secure parameter binding:
// Parameterized query using Flight SQL or HTTP
const table = await client.sql(
'SELECT * FROM taxi_trips WHERE passenger_count = $1 AND trip_distance > $2 LIMIT 10',
{ parameters: [2, 5.0] },
);
The SDK handles all protocol negotiation automatically - you just write standard SQL with parameters.
Pass an AbortSignal to cancel a query. Over HTTP the request is aborted; over
Arrow Flight the result stream is cancelled. An aborted query is never retried.
// Give the query five seconds, then cancel it
const table = await client.sql('SELECT * FROM taxi_trips', {
signal: AbortSignal.timeout(5000),
});
// Or cancel it yourself
const controller = new AbortController();
const pending = client.sqlJson('SELECT * FROM taxi_trips', {
signal: controller.signal,
});
controller.abort();
The promise rejects with the signal's own reason — a TimeoutError from
AbortSignal.timeout(), an AbortError from a bare controller.abort(), or
whatever value you pass to abort(reason). Branch on err.name; an aborted
query is never retried.
Racing the returned promise against a timer is not equivalent: that only stops your code waiting for the result, while the query keeps running, so a retry or a subsequent call stacks more work on top of it.
Over Arrow Flight the SDK also asks the runtime to stop executing the query it
started. Over HTTP, aborting ends the request but the runtime may still run the
statement to completion — cancel it explicitly with cancelActiveQuery() if
that matters.
search() runs vector similarity, keyword, and hybrid search against datasets that have
an embedding column and a loaded embedding model.
const results = await client.search('trips near the airport', {
datasets: ['taxi_trips'],
limit: 5,
additional_columns: ['trip_distance'],
keywords: ['airport'],
});
console.log(`${results.results.length} matches in ${results.duration_ms}ms`);
for (const match of results.results) {
console.log(match.dataset, match.score, match.primary_key, match.data);
}
Each match carries the dataset it was found in, its similarity score, the matched
column values in matches, the dataset's primary_key, any additional_columns you
requested in data, and metadata. The four object fields are always present — they
default to {} when the runtime returns nothing for them, so you can read into them
without a guard.
Version 3.0 represents a major evolution of the SDK with cross-platform support, new APIs, and enhanced reliability.
Minimum Node.js Version: 20+
# Check your Node.js version
node --version
# Upgrade if needed (using nvm)
nvm install 20
nvm use 20
If upgrading from v1.x: Update parameter names from snake_case to camelCase
// ❌ v1.x (snake_case)
const client = new SpiceClient({
api_key: 'your-api-key',
http_url: 'https://data.spiceai.io',
flight_url: 'flight.spiceai.io:443',
});
// ✅ v2.x/v3.x (camelCase)
const client = new SpiceClient({
apiKey: 'your-api-key',
httpUrl: 'https://data.spiceai.io',
flightUrl: 'flight.spiceai.io:443',
});
If upgrading from v1.x: Replace removed async query methods
// ❌ v1.x
const response = await client.queryAsync('my-query', sql, webhookUrl);
const results = await client.getQueryResults(response.query_id);
// ✅ v3.x
const table = await client.sql(sql);
// Or for JSON results:
const results = await client.sqlJson(sql);
Package Installation
npm install @spiceai/spice@latest
sql(), sqlJson(), and nsql() for flexible queryingisSpiceHealthy() and isSpiceReady() for monitoringruntimeStatus() for per-component staterefreshDataset() for on-demand dataset updatesv2.x: Node.js 18+
v3.x: Node.js 20+
# Check your Node.js version
node --version
# Upgrade if needed
nvm install 20
nvm use 20
The package now includes separate builds for different platforms:
v2.x: Single build at dist/
dist/
index.js
client.js
...
v3.x: Platform-specific builds
dist/
node/ # Node.js with gRPC support
browser/ # Browser with HTTP-only
The correct version is automatically loaded via package.json exports field. No code changes needed unless you were importing from dist/ directly.
// ✅ Recommended (works in v2 and v3)
import { SpiceClient } from '@spiceai/spice';
// ❌ Don't do this (breaks in v3)
import { SpiceClient } from '@spiceai/spice/dist/client';
v1.x: snake_case parameters
const client = new SpiceClient({
api_key: 'your-api-key',
http_url: 'https://data.spiceai.io',
flight_url: 'flight.spiceai.io:443',
});
v2.x/v3.x: camelCase parameters
const client = new SpiceClient({
apiKey: 'your-api-key', // was: api_key
httpUrl: 'https://data.spiceai.io', // was: http_url
flightUrl: 'flight.spiceai.io:443', // was: flight_url
});
This applies to all configuration parameters including apiKey, httpUrl, flightUrl, and method options like refreshDataset().
v1.x: API key only
const client = new SpiceClient('your-api-key');
v2.x/v3.x: Configuration object (API key string still supported for backwards compatibility)
// New recommended way
const client = new SpiceClient({
apiKey: 'your-api-key',
httpUrl: 'https://data.spiceai.io',
flightUrl: 'flight.spiceai.io:443',
});
// Still works (backwards compatible)
const client = new SpiceClient('your-api-key');
If you're upgrading from v1.x, these methods were removed in v2.0:
queryAsync(), getQueryResults(), getQueryResultsAll(), getQueryResultsFromNotification()Migration: Use sql() or sqlJson() for direct queries instead of the old async query pattern.
// Recommended: sql() with Apache Arrow
const table = await client.sql('SELECT * FROM my_table LIMIT 10');
console.table(table.toArray());
// Streaming for large results
await client.sql('SELECT * FROM large_table', (chunk) => {
console.log('Chunk:', chunk.numRows, 'rows');
});
// JSON results with schema
const result = await client.sqlJson('SELECT name, age FROM users');
console.log(`${result.row_count} rows`, result.data);
// Natural language queries
const result = await client.nsql('Show top 10 customers by revenue');
console.log('Generated SQL:', result.sql);
console.log('Results:', result.data);
// Check if Spice runtime is healthy (unauthenticated)
const isHealthy = await client.isSpiceHealthy();
// Check if ready to accept queries (authenticated)
const isReady = await client.isSpiceReady();
// Wait for Spice to be ready before querying
async function waitForSpice(client, maxAttempts = 10) {
for (let i = 0; i < maxAttempts; i++) {
if (await client.isSpiceReady()) return true;
await new Promise((resolve) => setTimeout(resolve, 1000));
}
return false;
}
// Basic refresh
await client.refreshDataset('my_dataset');
// Advanced with options
await client.refreshDataset('my_dataset', {
refresh_mode: 'append',
refresh_sql: 'SELECT * FROM source WHERE updated_at > NOW() - INTERVAL 1 DAY',
refresh_jitter_max: '1s',
});
// Works in browser environments!
import { SpiceClient } from '@spiceai/spice';
const client = new SpiceClient({
apiKey: 'your-api-key',
httpUrl: 'https://data.spiceai.io',
});
const result = await client.sql('SELECT * FROM my_table LIMIT 10');
console.table(result.toArray());
Required:
npm install @spiceai/spice@latestRecommended:
sql() instead of query() — query() now submits async jobs; see the Async queries sectionisSpiceHealthy() and isSpiceReady()nsql() for natural language queriesrefreshDataset() for on-demand dataset refreshIf deploying to browsers:
exports fieldIf upgrading from v1.x:
queryAsync(), getQueryResults(), etc. with sql() or sqlJson()new SpiceClient('api-key') to config object if neededIf you're using these standard patterns, your code will work without changes:
✅ SpiceClient initialization with API key string or config object
✅ Connection retry configuration via setMaxRetries()
✅ Custom headers
✅ Flight and HTTP URL configuration
# Upgrade to v3.x
npm install @spiceai/spice@latest
# Or with yarn
yarn add @spiceai/spice@latest
# Or with pnpm
pnpm add @spiceai/spice@latest
Cannot find module '@spiceai/spice'
node_modules and reinstall: rm -rf node_modules package-lock.json && npm installFlight proto file not found in serverless environments
data.spiceai.ioImport errors in TypeScript
@spiceai/spice, not internal pathstsconfig.json with "moduleResolution": "node16" or "bundler"sql(query: string, options?: SqlQueryOptions, onData?: callback) - Execute SQL queriesThe sql() method executes SQL queries and returns results as Apache Arrow tables. This is the recommended method for querying data.
// Full result (recommended for most use cases)
const table = await spiceClient.sql('SELECT * FROM my_table LIMIT 10');
console.table(table.toArray());
// Streaming results (for large datasets)
await spiceClient.sql('SELECT * FROM large_table', (chunk) => {
console.log('Received chunk with', chunk.numRows, 'rows');
// Process chunk immediately
});
sqlJson(query: string) - Execute SQL queries with JSON resultsThe sqlJson() method executes SQL queries and returns results in a JSON format with schema information.
const result = await spiceClient.sqlJson('SELECT name, age FROM users LIMIT 5');
console.log(`Returned ${result.row_count} rows`);
console.log('Schema:', result.schema);
console.log('Data:', result.data);
console.log(`Query took ${result.execution_time_ms}ms`);
// Access individual rows
result.data.forEach((row) => {
console.log(`${row.name} is ${row.age} years old`);
});
The response includes:
row_count: Number of rows returnedschema: Schema information with field names and typesdata: Array of row objectsexecution_time_ms: Query execution time in millisecondsBoth sql() and sqlJson() methods support passing custom headers that are automatically translated to the appropriate transport mechanism:
// Define custom headers
const headers = {
'X-Custom-Header': 'custom-value',
'X-Request-ID': '12345',
'X-Tenant-ID': 'tenant-abc',
};
// Use with sql() - headers as 3rd parameter
const table = await spiceClient.sql(
'SELECT * FROM my_table LIMIT 10',
undefined, // no streaming callback
headers, // custom headers
);
// Use with sqlJson() - headers as 2nd parameter
const result = await spiceClient.sqlJson(
'SELECT * FROM my_table LIMIT 10',
headers, // custom headers
);
// With streaming and custom headers
await spiceClient.sql(
'SELECT * FROM large_table',
(chunk) => {
console.log('Chunk received:', chunk.numRows, 'rows');
},
headers, // custom headers
);
TypeScript usage:
import { SpiceClient, QueryHeaders } from '@spiceai/spice';
const headers: QueryHeaders = {
'X-Custom-Header': 'value',
'X-Request-ID': '12345',
};
const result = await spiceClient.sqlJson('SELECT * FROM table', headers);
This is useful for:
isSpiceHealthy() - Check Spice runtime healthThe isSpiceHealthy() method checks if the Spice runtime is healthy. This endpoint is unauthenticated and does not require an API key.
const isHealthy = await spiceClient.isSpiceHealthy();
if (isHealthy) {
console.log('✅ Spice runtime is healthy');
} else {
console.log('❌ Spice runtime is unhealthy');
}
isSpiceReady() - Check if Spice is readyThe isSpiceReady() method checks if the Spice runtime is ready to accept requests. This endpoint is authenticated and requires an API key if configured on the Spice runtime.
const isReady = await spiceClient.isSpiceReady();
if (isReady) {
console.log('✅ Spice is ready to accept queries');
} else {
console.log('❌ Spice is not ready yet');
}
// Example: Wait for Spice to be ready before querying
async function waitForSpice(maxAttempts = 10) {
for (let i = 0; i < maxAttempts; i++) {
if (await spiceClient.isSpiceReady()) {
return true;
}
await new Promise((resolve) => setTimeout(resolve, 1000));
}
return false;
}
if (await waitForSpice()) {
const result = await spiceClient.sql('SELECT * FROM my_table');
}
runtimeStatus() - Per-component runtime statusThe runtimeStatus() method reports the status of each runtime connection. Where isSpiceReady() collapses the whole runtime to one boolean, this reports per-component state, so a runtime that is still initializing can be told apart from one whose Flight endpoint is failing.
const details = await spiceClient.runtimeStatus();
for (const d of details) {
console.log(`${d.name} @ ${d.endpoint}: ${d.status}`);
}
// http @ http://127.0.0.1:8090: Ready
// flight @ 127.0.0.1:50051: Ready
// metrics @ N/A: Disabled
// Example: fail fast when the data path specifically is unhealthy
const flight = details.find((d) => d.name === 'flight');
if (flight && flight.status !== 'Ready') {
throw new Error(`Flight endpoint is ${flight.status}`);
}
Each entry is a ConnectionDetails:
name: the connection — 'http', 'flight', 'metrics' or 'opentelemetry'endpoint: where it is served, or 'N/A' when the component is disabledstatus: a ComponentStatus — 'Initializing', 'Ready', 'Disabled', 'Error', 'Refreshing', 'ShuttingDown' or 'NotLoaded'. A status added by a newer runtime is passed through rather than dropped.refreshDataset(dataset: string, options?) - Trigger dataset refreshThe refreshDataset() method triggers an on-demand refresh for an accelerated dataset.
refreshAcceleration(), its earlier name, still works as a deprecated alias.
// Basic refresh
const result = await spiceClient.refreshDataset('my_dataset');
console.log(result.message);
// Refresh with custom options
const result = await spiceClient.refreshDataset('my_dataset', {
refresh_mode: 'full', // 'full', 'append', 'changes', or 'disabled'
refresh_sql: 'SELECT * FROM source WHERE updated_at > NOW() - INTERVAL 1 DAY',
refresh_jitter_max: '1s',
});
Options:
refresh_mode: Controls how data is refreshed ('full', 'append', 'changes', 'disabled')refresh_sql: Custom SQL query to use for the refreshrefresh_jitter_max: Maximum jitter time for refresh schedulinglistActiveQueries() / cancelActiveQuery(queryId) - List and cancel running querieslistActiveQueries() reports the synchronous queries this client currently has running — those started by sql(), sqlJson(), FlightSQL, nsql() and search(), but not query()'s async jobs (see listQueries() for those) — and cancelActiveQuery() stops one by id.
The runtime does not hand a query's id back to the client that submitted it, so the two are used together: list to find the query, then cancel it. Both are scoped to the caller, so a client only ever sees and cancels its own queries.
const queries = await spiceClient.listActiveQueries();
for (const query of queries) {
console.log(`${query.query_id} [${query.protocol}] ${query.sql_preview}`);
console.log(` started at ${new Date(query.started_at_ms).toISOString()}`);
}
// Cancel a long-running query by id.
if (queries.length > 0) {
const result = await spiceClient.cancelActiveQuery(queries[0].query_id);
console.log(`${result.query_id} is now ${result.status}`);
}
Each ActiveQuery carries query_id, protocol (http, flight, flightsql, or internal), a truncated sql_preview, and started_at_ms as milliseconds since the Unix epoch.
cancelActiveQuery() throws when the id is not a UUID, when the API key lacks write access, or when no such query is running — including the case where the id belongs to a different caller, which the runtime reports as not found rather than cancelling.
The boundary is the caller's identity, not the client instance: the runtime scopes both listActiveQueries() and cancelActiveQuery() to the authenticated principal. Two clients using the same API key therefore share one set and can cancel each other's queries, and unauthenticated requests all share the runtime's public scope. Do not rely on one SpiceClient seeing only its own queries.
Both work on Node and in the browser, since they use the HTTP control plane rather than Flight.
nsql(request) - Natural language to SQL (NSQL)The nsql() method converts natural language queries into SQL and executes them, returning both the results and the generated SQL.
// Basic natural language query
const result = await spiceClient.nsql(
'Show me the top 5 customers by total sales',
);
console.log('Generated SQL:', result.sql);
console.log('Results:', result.data);
console.log(`Returned ${result.row_count} rows`);
// Advanced usage with options
const result = await spiceClient.nsql(
'What are the average trip distances by payment type?',
{
datasets: ['taxi_trips'], // Limit to specific datasets
model: 'nql', // Specify the model (default: 'nql')
sample_data_enabled: true, // Include sample data in context (default: true)
},
);
// Access the generated SQL
console.log('AI generated this SQL:', result.sql);
// Process results
result.data.forEach((row) => {
console.log(row);
});
Parameters:
query (required): The natural language query to convert to SQLoptions (optional): Configuration object with the following properties:
datasets (optional): Array of dataset names to sample from, or null to use all datasetsmodel (optional): Name of the model to use for SQL generation (default: "nql")sample_data_enabled (optional): Whether to include sample data in context (default: true)The response includes:
row_count: Number of rows returnedschema: Schema information with field names and typesdata: Array of row objectssql: The SQL query generated by the AI modelnsqlGenerateSql(query, options) - Generate SQL without running itnsqlGenerateSql() takes the same arguments as nsql(), but only generates the SQL — it never runs it. Use it to inspect or edit the query before running it, or to run it through sql()/sqlJson() for Arrow-typed results instead of nsql()'s decoded JSON rows.
const generatedSql = await spiceClient.nsqlGenerateSql(
'Show me the top 5 customers by total sales',
);
console.log(generatedSql); // "SELECT ... FROM ... ORDER BY ... LIMIT 5"
// Run it yourself once you're happy with it
const table = await spiceClient.sql(generatedSql);
query(sql, options) / queryWithParams(sql, parameters) - Async queriesBreaking change: query() no longer executes synchronously. It now submits the query for asynchronous execution via the runtime's /v1/queries API and returns an AsyncQuery handle. Use sql() for the normal synchronous, streaming path — including with parameters, via sql()'s existing options.parameters.
Async queries require the runtime to be running in distributed/scheduler mode (spiced --role scheduler with runtime.scheduler.state_location configured).
const job = await spiceClient.query('SELECT * FROM large_table');
// Wait for completion and fetch results as an Arrow Table
const table = await job.results();
// Or poll manually
const status = await job.status(); // 'PENDING' | 'RUNNING' | 'SUCCEEDED' | 'FAILED' | 'CANCELLED' | 'CLOSED'
await job.waitForCompletion({ timeoutMs: 60_000 });
await job.cancel();
// Parameterized
const job2 = await spiceClient.queryWithParams(
'SELECT * FROM large_table WHERE status = $1',
['active'],
);
listQueries({ status?, limit? }) lists async jobs submitted to the runtime — distinct from listActiveQueries(), which lists synchronous queries.
From version 1.0.1 the SpiceClient implements connection retry mechanism (3 attempts by default).
The number of attempts can be configured via setMaxRetries:
const spiceClient = new SpiceClient('API_KEY');
spiceClient.setMaxRetries(5); // Setting to 0 will disable retries
Retries are performed for connection and system internal errors. It is the SDK user's responsibility to properly handle other errors, for example RESOURCE_EXHAUSTED (HTTP 429).
The SpiceClient automatically handles environments where Apache Arrow Flight gRPC cannot be used (e.g., serverless environments like AWS Lambda, Vercel, Netlify). The fallback strategy is:
https://data.spiceai.io/v1/proto/flight and cached/v1/sql endpointBoth gRPC and HTTP modes support compression (gzip, deflate) to reduce bandwidth usage. This ensures the SDK works efficiently in any environment without configuration changes.
Note: mTLS (client certificate authentication) is an Enterprise feature of the Spice.ai runtime.
The client accepts PEM certificate file paths for custom server verification and mutual TLS:
const client = new SpiceClient({
flightUrl: 'my-spice-host:50051',
httpUrl: 'https://my-spice-host:8090',
tlsRootCertFile: './certs/ca.pem', // custom CA for server verification (optional)
tlsClientCertFile: './certs/client.pem', // ┐ provide both to enable mTLS
tlsClientKeyFile: './certs/client.key', // ┘
});
tlsClientCertFile and tlsClientKeyFile must be provided together; the client certificate is presented during the TLS handshake on both the gRPC and HTTP transports.client_auth_mode: request or required. See the mTLS cookbook recipe for a complete walkthrough.The SpiceClient automatically supports parameterized queries through its .sql() method. Parameters are handled transparently using the best available protocol (Flight SQL → HTTP).
Basic usage:
import { SpiceClient } from '@spiceai/spice';
const client = new SpiceClient({
apiKey: 'YOUR_API_KEY',
httpUrl: 'https://data.spiceai.io',
flightUrl: 'flight.spiceai.io:443',
});
// Positional parameters (using $1, $2, etc.)
const table = await client.sql(
'SELECT * FROM taxi_trips WHERE trip_distance > $1 AND passenger_count >= $2 LIMIT 10',
{ parameters: [5.0, 2] },
);
console.table(table.toArray());
Named parameters work too, using $name placeholders:
const table = await client.sql(
'SELECT * FROM taxi_trips WHERE passenger_count = $passengers LIMIT 10',
{ parameters: { passengers: 2 } },
);
Transport Hierarchy:
When parameters are provided, the SDK automatically:
Both paths bind on the server: values travel separately from the SQL text, as a typed Arrow record batch over Flight or as JSON over HTTP. Nothing is substituted into the query string on the client.
Key benefits:
For more information, see docs/PARAMETERIZED_QUERIES.md.
Check out our API documentation to learn more about how to use the Node.js SDK.
The SpiceClient is optimized for high-performance query execution. Performance tests verify that .sql() and .sqlJson() operations meet strict performance thresholds across various scenarios including type conversions, streaming, and concurrent queries.
To run performance tests:
npm run test:perf
For more details, see docs/PERFORMANCE_TESTING.md.
For development and testing, you'll need to set up environment variables:
Copy the example environment file:
cp .env.example .env
Edit .env and add your Spice.ai API key:
SPICEAI_API_KEY=your_api_key_here
The .env file is automatically loaded by the test suite and can be used by examples.
Run the tests with make test. For more information, see CONTRIBUTING.md