Reference
Kotlin SDK Reference
One-page reference for common Onyx SDK calls. Select a language to see the syntax.
Onyx Environment Variables
Set these to control how the SDK scopes requests and where it fetches credentials.
| Env Var | Description | Default |
|---|---|---|
| ONYX_DATABASE_ID | Database UUID used to scope requests; required. | none |
| ONYX_DATABASE_BASE_URL | HTTP base for DB API. | https://api.onyx.dev |
| ONYX_DATABASE_API_KEY | API key for the database; required. | none |
| ONYX_DATABASE_API_SECRET | API secret for the database; required. | none |
| ONYX_AI_BASE_URL | Base URL for AI/chat endpoints. | https://ai.onyx.dev |
| ONYX_DEFAULT_MODEL | Model used by db.chat() shorthand. | onyx |
| ONYX_CONFIG_PATH | Path to JSON credentials file; checked after env vars. | unset |
| ONYX_DEBUG | When 'true', enables request/response logging and config debug output. | false |
| ONYX_STREAM_DEBUG | When 'true'/'1', logs streaming connection details. | false |
Initialization Patterns
Initialize the SDK with configuration.
Auto resolution
Resolution order: env vars → ONYX_CONFIG_PATH file → ./onyx-database.json (default locations).
import com.onyx.cloud.api.onyx
val db = onyx.init<Any>()Initialize with config object
Pass credentials directly when you can’t rely on env/file resolution.
import com.onyx.cloud.api.onyx
val db = onyx.init<Any>(
OnyxConfig(
apiKey = ONYX_DATABASE_API_KEY,
apiSecret = ONYX_DATABASE_API_SECRET,
)
)Optional config fields
| Attribute | Description | Default |
|---|---|---|
| baseUrl | REST base URL for database operations. | https://api.onyx.dev |
| databaseId | Database ID (inferred for DB-scoped keys). | resolved from API key |
| aiBaseUrl | Base URL for AI endpoints. | https://ai.onyx.dev |
| defaultModel | Fallback AI model for shorthand chat. | onyx |
| partition | Default partition for queries/find/delete. | entity partition |
| requestLoggingEnabled | Log HTTP requests and bodies. | false |
| responseLoggingEnabled | Log HTTP responses and bodies. | false |
| ttl | Milliseconds to cache resolved credentials. | 300000 (5 minutes) |
| retry | Retry config for idempotent GET (honors Retry-After). | enabled; 3 retries; 300/600/1200ms |
Core CRUD
Upsert, fetch, update, and delete entities by primary key.
import com.onyx.cloud.api.onyx
val db = onyx.init<Any>()
val account = Account(id = "acct_1", name = "Checking", balance = 1250.0)
db.save(account)
val fetched: Account? = db.findById("acct_1")
db.save(account.copy(balance = 1400.0))
db.delete("Account", "acct_1")Atomic Saving
Persist a parent plus related records in one call—no explicit transactions needed.
Cascade save
Save Account with related Transactions (all-or-none commit).
val account = Account(
id = "acct_cascade",
name = "Primary",
balance = 0.0,
transactions = listOf(
Transaction(id = "txn_1", accountId = "acct_cascade", amount = 250.0, currency = "USD"),
Transaction(id = "txn_2", accountId = "acct_cascade", amount = 125.0, currency = "USD"),
)
)
val saved = db
.cascade("transactions:Transaction(accountId, id)")
.save(Account::class, account)Cascade builder
Compose cascade strings fluently.
val rel = db
.cascadeBuilder()
.graph("transactions")
.graphType("Transaction")
.targetField("accountId")
.sourceField("id")
println(rel.toString()) // transactions:Transaction(accountId, id)Cascade string syntax
<field>:<RelatedTable>(<targetField>, <sourceField>) — field: property on the parent holding related rows; RelatedTable: table to upsert; targetField: FK on the related table; sourceField: parent field to copy.
Resolvers
Hydrate schema-defined relations inline to avoid extra round trips.
Fetch by id with resolver
Load a transaction and its account in one call.
val tx: Transaction? = db
.findById<Transaction>("txn_123", resolvers = listOf("account"))Query with resolver
Include related data while filtering the base table.
import com.onyx.cloud.api.eq
val rows = db.from<Transaction>()
.where("status" eq "posted")
.resolve("account")
.list<Transaction>()Filter by resolver field
Filter using fields exposed by the resolver.
import com.onyx.cloud.api.eq
val rows = db.from<Transaction>()
.where("account.name" eq "Primary")
.resolve("account")
.list<Transaction>()Bulk Save
Insert many records efficiently by passing a list to save.
val txns = listOf(
Transaction(id = "txn_batch_1", accountId = "acct_batch", amount = 1.99, currency = "USD"),
Transaction(id = "txn_batch_2", accountId = "acct_batch", amount = 12.5, currency = "USD"),
Transaction(id = "txn_batch_3", accountId = "acct_batch", amount = 45.0, currency = "USD"),
Transaction(id = "txn_batch_4", accountId = "acct_batch", amount = 7.25, currency = "USD"),
)
db.save(Transaction::class, txns)Query & Filter
Boolean filters, ranges, text contains, ordering, and limits.
import com.onyx.cloud.api.*
val results = db.from<Transaction>()
.where("status" eq "posted")
.and("amount" gt 100.0)
.and("createdAt" gte "2025-01-01")
.and(contains("merchant", "aws"))
.orderBy(desc("createdAt"))
.limit(25)
.list<Transaction>()Defaults: if you omit limit() and pageSize, the API uses its server-side default page size (capped at 1000); responses include nextPage when more rows remain.
Query clauses
| Clause | Signature | What it does |
|---|---|---|
| where | where(condition) | Sets the primary filter. |
| and | and(condition) | Adds an AND condition. |
| or | or(condition) | Adds an OR condition. |
| orderBy | orderBy(asc/desc(...)) | Sorts results. |
| groupBy | groupBy(...fields) | Groups for aggregates. |
| distinct | distinct() | Return unique rows. |
| limit | limit(n) | Cap result size. |
| pageSize | pageSize(n) | Set page size for streaming/paged queries. |
| inPartition | inPartition(partition) | Scope query to a partition. |
| nextPage | nextPage(token) | Continue a paged query. |
Filters
| Helper | Signature | What it does |
|---|---|---|
| eq | "field" eq value | Equals. |
| ne | "field" ne value | Not equals. |
| gt | "field" gt value | Greater than. |
| gte | "field" gte value | Greater than or equal. |
| lt | "field" lt value | Less than. |
| lte | "field" lte value | Less than or equal. |
| between | between("field", low, high) | Inclusive range. |
| contains | contains("field", text) | Case-sensitive substring. |
| startsWith | startsWith("field", prefix) | Prefix match. |
| endsWith | endsWith("field", suffix) | Suffix match. |
| inOp | inOp("field", list) | Field in list. |
| notIn | notIn("field", list) | Field not in list. |
| isNull | isNull("field") | Field is null. |
| notNull | notNull("field") | Field is not null. |
| within | within("field", subquery) | Field in subquery results. |
| search | search(queryText, SearchOptions) | High-level lexical, semantic, or hybrid search. The one-argument and numeric overloads remain legacy exhaustive lexical search. |
Select Query
Pick specific columns to return from a table.
val rows = db.select("id", "accountId", "amount", "createdAt")
.from<Transaction>()
.list<Map<String, Any>>()First or Null
When you expect a single row, use firstOrNull() or its alias one().
import com.onyx.cloud.api.eq
val tx: Transaction? = db.from<Transaction>()
.where("id" eq "txn_123")
.firstOrNull()
val txOne: Transaction? = db.from<Transaction>()
.where("id" eq "txn_123")
.one()Inner Queries
Filter a table using sub-selects returned from another query.
import com.onyx.cloud.api.*
val accounts = db.from<Account>()
.where(
within(
"id",
db.select("accountId")
.from<Transaction>()
.where(gt("amount", 1000.0))
)
)
.list<Account>()Update Query
Set partial updates for all rows matching a condition.
import com.onyx.cloud.api.eq
val updatedCount = db.from<Transaction>()
.where("status" eq "pending")
.and("amount" gt 100.0)
.setUpdates("status" to "posted")
.update()Delete Query
Delete all rows that match a filter.
import com.onyx.cloud.api.eq
val deleted = db.from<Transaction>()
.where("status" eq "archived")
.delete()Group By
Group results by one or more fields to segment metrics.
import com.onyx.cloud.api.eq
val grouped = db.select("merchant", "currency")
.from<Transaction>()
.where("status" eq "posted")
.groupBy("merchant", "currency")
.list<Map<String, Any>>()Aggregations
Server-side rollups for dashboards and billing summaries.
import com.onyx.cloud.api.*
val totals = db.select(sum("amount"), "merchant")
.from<Transaction>()
.where("status" eq "posted")
.list<Map<String, Any>>()Server-side aggregate helpers
| Function | Signature | What it does |
|---|---|---|
| sum | sum("field") | Numeric sum of a column. |
| count | count("field"|"*") | Row count. |
| avg | avg("field") | Arithmetic mean. |
| min | min("field") | Smallest value. |
| max | max("field") | Largest value. |
| median | median("field") | 50th percentile. |
| percentile | percentile("field", p) | p-th percentile (0–100). |
| std | std("field") | Sample standard deviation. |
| variance | variance("field") | Sample variance. |
| upper | upper("field") | Uppercase before grouping/aggregation. |
| lower | lower("field") | Lowercase before grouping/aggregation. |
| format | format("field", "pattern") | Apply Java-style format (e.g., yyyy-MM). |
| substring | substring("field", from, length) | Substring prior to grouping. |
| replace | replace("field", pattern, repl) | Regex/substring replacement. |
Streaming Queries
Consume large result sets incrementally and react to live query responses.
import com.onyx.cloud.api.eq
val events = mutableListOf<Transaction>()
val subscription = db.from<Transaction>()
.where("status" eq "posted")
.onItem<Transaction> { events.add(it) }
.onItemAdded<Transaction> { events.add(it) }
.onItemUpdated<Transaction> { events.add(it) }
.onItemDeleted<Transaction> { events.add(it) }
.stream<Transaction>(includeQueryResults = true, keepAlive = true)
// later
subscription.cancelAndJoin()Lexical, Semantic & Hybrid Search
Passing SearchOptions opts into the high-level SEARCH API. Defaults are HYBRID mode, ANY lexical matching, a null minimum score, and 1000 candidates. The one-argument and numeric search overloads remain the legacy all-term MATCHES API. Semantic and hybrid modes require a server SearchEmbeddingProvider; marking an entity searchable does not configure one or backfill old rows.
Natural-language lexical search
SearchMatch.ANY ignores harmless extra question words. minScore is normalized from 0 through 1, and maxCandidates bounds admission work.
import com.onyx.cloud.api.SearchMatch
import com.onyx.cloud.api.SearchMode
import com.onyx.cloud.api.SearchOptions
val hits = db.from<ActiveDocumentChunk>()
.search(
"how do i calculate cost per horse",
SearchOptions(
mode = SearchMode.LEXICAL,
match = SearchMatch.ANY,
minScore = 0.4f,
maxCandidates = 500,
),
)
.list<ActiveDocumentChunk>()Semantic search from ordinary text
The server embeds query text in the same model and vector space used when searchable rows are saved.
val related = db.from<ActiveDocumentChunk>()
.search(
"expense for each animal",
SearchOptions(mode = SearchMode.SEMANTIC),
)
.list<ActiveDocumentChunk>()Hybrid search
Fuses lexical and semantic candidates. SearchOptions() selects the canonical hybrid/any defaults.
val results = db.from<ActiveDocumentChunk>()
.search(
"how do i calculate cost per horse",
SearchOptions(),
)
.and("isActive" eq true)
.limit(50)
.list<ActiveDocumentChunk>()Search across eligible tables
Database-wide high-level search covers eligible unpartitioned searchable tables and returns FullTextSearchResult envelopes.
val acrossTables = db.search(
"how do i calculate cost per horse",
SearchOptions(mode = SearchMode.HYBRID),
)
.limit(50)
.list<FullTextSearchResult>()Legacy exhaustive lexical search
Existing one-argument and numeric calls keep their MATCHES wire shape and require every normalized term.
val legacy = db.from<ActiveDocumentChunk>()
.search("cost per horse")
.list<ActiveDocumentChunk>()VectorSearchQuery controls
Use text and/or semantic; minScore defaults to null. The signature must use the table's active calibration.
val signature = SemanticVectorSignature(
calibrationId = 73L,
bucketId = 6,
cells = intArrayOf(1, 2),
cellCounts = intArrayOf(4, 4),
fingerprint = longArrayOf(0x123456789abcdef0L),
)
val query = VectorSearchQuery(
text = "AWS spend",
semantic = signature,
minScore = 0.2f,
nearbyBucketRadius = 2,
maxCandidates = 256,
requireAllTerms = false,
)
val hits = db.from<Transaction>()
.search(query)
.list<Transaction>()Bounded lexical candidates
Caps lexical admission work. SEARCH_CANDIDATES must remain the only root condition and cannot update or delete rows.
val candidates = db.from<Transaction>()
.approximateSearch(
"AWS spend",
minScore = 0.1f,
maxCandidates = 250,
requireAllTerms = false,
)
.list<Transaction>()Native HNSW candidates
Bounds nearest-neighbor work. Defaults: maxCandidates 1000, efSearch max(1000, maxCandidates), minScore null, formatVersion 1. Add inPartition(...) for partitioned tables.
val hnsw = HnswSearchQuery(
calibrationId = 73L,
vector = embedding,
maxCandidates = 256,
efSearch = 1024,
minScore = 0.2f,
)
val nearest = db.from<Transaction>()
.hnswCandidates(hnsw)
.list<Transaction>()Ordinary-index candidates
Use approximateCandidates as a normal where condition. A scalar uses bounded EQUAL admission and a collection uses bounded IN admission; exactly one candidate condition may be combined with non-negated AND filters. maxCandidates defaults to 1000.
import com.onyx.cloud.api.approximateCandidates
import com.onyx.cloud.api.eq
val routed = db.from<Transaction>()
.where(approximateCandidates("merchant", listOf("AWS", "GCP"), 200))
.and("status" eq "posted")
.list<Transaction>()Pagination
Page through ordered results with stable cursors (offset-free).
import com.onyx.cloud.api.asc
val page1 = db.from<Transaction>()
.orderBy(asc("createdAt"))
.limit(50)
.page<Transaction>()
val next = page1.nextPage
if (next != null) {
val page2 = db.from<Transaction>()
.orderBy(asc("createdAt"))
.page<Transaction>(nextPage = next)
}Published Model Predictions (REST)
Call models published from Model Builder with the database API origin and database read credentials. This standalone HTTP example covers raw inputs and saved scripts; it does not add methods to the Kotlin SDK. Replace the published model key, input fields, and saved script with your own. Script parameter values must be strings. Match outputs to returned inputs or records because sequence windows can reduce the prediction count.
// JVM example using the Java 11+ HTTP client.
import java.net.URI
import java.net.URLEncoder
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.nio.charset.StandardCharsets.UTF_8
import java.time.Duration
fun main() {
fun env(name: String) = requireNotNull(System.getenv(name)) { "Set " + name }
fun path(value: String) = URLEncoder.encode(value, UTF_8).replace("+", "%20")
val baseUrl = (System.getenv("ONYX_DATABASE_BASE_URL") ?: "https://api.onyx.dev").trimEnd('/')
val endpoint = baseUrl + "/data/" + path(env("ONYX_DATABASE_ID")) +
"/model-builder/published-model/" + path("churn-model") + "/predict"
val client = HttpClient.newHttpClient()
fun predictRequest(body: String, suffix: String = ""): String {
val request = HttpRequest.newBuilder(URI.create(endpoint + suffix))
.timeout(Duration.ofSeconds(60))
.header("Content-Type", "application/json")
.header("x-onyx-key", env("ONYX_DATABASE_API_KEY"))
.header("x-onyx-secret", env("ONYX_DATABASE_API_SECRET"))
.POST(HttpRequest.BodyPublishers.ofString(body))
.build()
val response = client.send(request, HttpResponse.BodyHandlers.ofString())
check(response.statusCode() in 200..299) {
"Prediction failed: " + response.statusCode()
}
return response.body()
}
val result = predictRequest("""{
"inputs": [
{"accountAgeDays": 420, "usageScore": 0.87},
{"accountAgeDays": 90, "usageScore": 0.42}
]
}""")
println(result)
val scriptResult = predictRequest("""{
"scriptId": "score-active-customers",
"scriptParameters": {"segment": "enterprise", "limit": "50"}
}""", "/script")
println(scriptResult)
}AI Chat
Use the AI endpoint to answer questions that reference your data.
Chat completions
Full request with model and messages.
val response = db.chat(
model = "onyx",
messages = listOf(
AIChatMessage(role = "system", content = "You are a finance assistant."),
AIChatMessage(role = "assistant", content = "I summarize trends each Friday."),
AIChatMessage(role = "user", content = "Draft a status update for finance stakeholders."),
)
)
println(response)Streaming chat
Stream tokens as they arrive.
val stream = db.chatStream(
model = "onyx",
messages = listOf(AIChatMessage(role = "user", content = "List the top 3 spend categories this week."))
)
for (chunk in stream) {
val delta = chunk.choices.firstOrNull()?.delta?.content
if (delta != null) print(delta)
}Shorthand chat (string in, string out)
Quick one-liner for common prompts.
val summary = db.chat("Summarize yesterday's transactions")List available models
Discover model IDs before picking one.
val models = db.getModels()
models.data.forEach { println(it.id) }Get model details
Inspect a specific model's capabilities and limits.
val model = db.getModel("onyx")
println(model)Request script approval
Validate a mutation script before execution.
val approval = db.requestScriptApproval(
script = "db.save({ id: 'acct_1', name: 'Checking' })"
)
println(approval)Documents
Upload binary files with metadata, fetch with optional resizing, or delete stored documents.
Save a document
Stores metadata in the DB and writes bytes to the database’s _documents path.
val payload = "hello".toByteArray()
val doc = OnyxDocument(
documentId = "hello.txt",
path = "/docs/hello.txt",
mimeType = "text/plain",
content = payload
)
db.saveDocument(doc)Get a document (optional resize for images)
Fetches by documentId; width/height resize images on the fly.
val doc = db.getDocument("hello.txt")Delete a document
Removes the file and its metadata.
db.deleteDocument("hello.txt")Storage & search
Files are written to your database’s filesystem under _documents/; only metadata (path, mimeType, timestamps) is stored in the Document record. If you provide plaintext content, it is chunked into vector-indexed parts so full-text search can find the document.
Secrets
Manage encrypted secrets stored per database; db calls never persist plaintext.
List secrets (metadata only)
Returns keys, purposes, and timestamps—no values.
val secrets = db.listSecrets()
println(secrets)Fetch a secret value
Decrypts and returns the plaintext for a single key.
val secret = db.getSecret("stripe_api_key")Create or rotate a secret
Stores the value encrypted at rest; re-encrypts on each update.
db.putSecret(
key = "stripe_api_key",
value = "sk_live_...",
purpose = "Stripe server key"
)Delete a secret
Removes the record from the database.
db.deleteSecret("stripe_api_key")How Onyx secures secrets
- Each secret is encrypted with a random AES-256-GCM key; that key is wrapped using the database’s 4096-bit RSA public key. Only ciphertext and wrapped keys are stored.
- Per-database RSA private keys are themselves encrypted with a master key.
- API routes require valid database credentials; plaintext values are only returned on authenticated
GET /secret/{key}and are never persisted decrypted. - Back up the keystore directory (
secret-keystore) together with the master key to preserve decryption ability.
Schema
Fetch, diff, validate, and publish schema revisions.
val current = db.getSchema() ?: emptyMap<String, Any>()
val next = current.toMutableMap().apply {
// modify entities/attributes here
put("revisionDescription", "Describe your change")
}
val diff = db.diffSchema(next)
val validation = db.validateSchema(next)
if (validation.valid == true) {
db.updateSchema(next, publish = true)
}Need the JSON shape for entities, attributes, and resolvers? See Schema Example.