Smart Queries¶
Smart Queries are reusable, predefined queries that you create in the Centrali console and execute programmatically via the API or SDK. They allow you to define complex filters, joins, and sorting once, then run them with dynamic parameters at runtime.
Overview¶
Smart Queries support: - Dynamic Variables - Use {{variableName}} syntax for runtime parameter substitution - Joins - Join data across structures, including joining on record IDs - Complex Filters - Use MongoDB-style operators ($eq, $gt, $in, etc.) - Sorting & Pagination - Define sort order, limit, and skip in the query definition
Creating a Smart Query¶
Smart queries are created in the Centrali console by navigating to a structure and clicking "Smart Queries". You define:
- Name: A unique identifier for the query
- Query Definition: The filter conditions, joins, sorting, and pagination
Query Definition Structure¶
{
"select": ["title", "status", "createdAt"],
"where": {
"status": { "$eq": "active" },
"priority": { "$gte": 5 }
},
"sort": [
{ "field": "createdAt", "direction": "desc" }
],
"limit": 50,
"skip": 0
}
Variables¶
Variables allow you to create dynamic queries that accept parameters at execution time.
Syntax¶
Use double curly braces to define variables: {{variableName}}
{
"where": {
"userId": { "$eq": "{{currentUserId}}" },
"status": { "$in": ["{{statusFilter}}", "active"] },
"createdAt": { "$gte": "{{startDate}}" }
}
}
Variable Naming Rules¶
- Must start with a letter (a-z, A-Z)
- Can contain letters, numbers, and underscores
- Case-sensitive
- Maximum 64 characters
Valid examples: userId, start_date, filterValue1, CamelCase
Invalid examples: 1user, my-var, user.id
Executing with Variables¶
When executing a query that uses variables, you must provide values for all referenced variables.
API Request (POST):
curl -X POST \
"https://api.centrali.io/workspace/{workspace}/api/v1/smart-queries/slug/{structure}/execute/{queryId}" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"variables": {
"currentUserId": "user_123",
"statusFilter": "pending",
"startDate": "2024-01-01"
}
}'
SDK:
const results = await centrali.smartQueries.execute('orders', 'query-id', {
variables: {
currentUserId: 'user_123',
statusFilter: 'pending',
startDate: '2024-01-01'
}
});
Error Handling¶
If required variables are not provided, the API returns a 400 error:
{
"error": "MISSING_VARIABLE",
"message": "Missing required variable(s): currentUserId, startDate",
"requiredVariables": ["currentUserId", "startDate"],
"providedVariables": ["statusFilter"]
}
Joins¶
Smart Queries support joining data from related structures. Joined fields are returned nested under a _joined object, similar to how _expanded works for reference fields.
Basic Join¶
Join on a field in your structure with a field in another structure:
{
"select": ["title", "status"],
"where": {
"status": { "$eq": "active" }
},
"join": {
"foreignSlug": "categories",
"localField": "categoryCode",
"foreignField": "code",
"select": ["name", "color"]
}
}
Result structure:
{
"title": "My Item",
"status": "active",
"_joined": {
"categories": {
"name": "Electronics",
"color": "blue"
}
}
}
Joining on Record ID¶
Often you'll store a reference to another record's UUID. Use the special _recordId field to join on the actual record ID column instead of a JSONB field.
{
"select": ["title", "description"],
"join": {
"foreignSlug": "authors",
"localField": "authorId",
"foreignField": "_recordId",
"select": ["_recordId", "name", "email"]
}
}
Result structure:
{
"title": "My Article",
"description": "An interesting read",
"_joined": {
"authors": {
"_recordId": "550e8400-e29b-41d4-a716-446655440000",
"name": "John Doe",
"email": "john@example.com"
}
}
}
This translates to SQL like:
SELECT r1.data->>'title', r1.data->>'description', r2.id, r2.data->>'name', r2.data->>'email'
FROM records r1
JOIN records r2 ON r1.data->>'authorId' = r2.id::text
WHERE r2.recordSlug = 'authors'
_recordId Patterns¶
| Pattern | localField | foreignField | Use Case |
|---|---|---|---|
| Data to ID | authorId | _recordId | Your structure stores foreign record UUIDs |
| ID to Data | _recordId | parentId | Foreign structure references your record IDs |
| Selecting ID | - | _recordId | Include the joined record's ID in results |
Filtering on Joined Records (joinWhere)¶
You can filter on fields from the joined table using joinWhere. This applies conditions to the joined records (r2) rather than the main records (r1).
Example: Find product options where the parent product has inventory tracking enabled:
{
"select": ["sku", "price", "inventory"],
"where": {
"merchantId": { "$eq": "{{merchantId}}" }
},
"join": {
"foreignSlug": "products",
"localField": "productId",
"foreignField": "_recordId",
"select": ["title", "inventoryTracking", "lowStockThreshold"]
},
"joinWhere": {
"inventoryTracking": { "$eq": true }
}
}
This translates to SQL like:
SELECT r1.*, r2.title, r2.inventoryTracking, r2.lowStockThreshold
FROM product_options r1
JOIN products r2 ON r1.productId = r2.id
WHERE r1.merchantId = :merchantId
AND r2.inventoryTracking = true
joinWhere supports the same operators as where, including $and and $or:
{
"joinWhere": {
"$and": [
{ "inventoryTracking": { "$eq": true } },
{ "status": { "$eq": "active" } }
]
}
}
Computed Fields¶
⚠️ Important: Smart Queries can ONLY filter on persisted computed fields. Virtual computed fields will NOT work because they are not stored in the database.
Persisted vs Virtual Computed Fields¶
| Type | When Computed | Stored in DB | SmartQuery Support |
|---|---|---|---|
| Persisted | On create/update | ✅ Yes | ✅ Fully supported |
| Virtual | On read | ❌ No | ❌ Not supported |
Why Virtual Computed Fields Don't Work¶
Smart Queries execute SQL directly against the database. Virtual computed fields are only calculated when records are read through the API, so their values don't exist in the database for Smart Queries to filter on.
Using Persisted Computed Fields¶
Required: You MUST set
"computedMode": "persisted"for Smart Queries to work with computed fields.
To filter on a computed value like inventory - reserved, create a persisted computed field in your structure:
{
"name": "availableStock",
"type": "number",
"expression": "inventory - reserved",
"computedMode": "persisted"
}
Then you can filter on it in Smart Queries:
Important Notes¶
-
Set
computedMode: "persisted"- Without this, computed fields default to virtual and won't work with Smart Queries -
Backfill existing records - When adding a new persisted computed field, existing records won't have the computed value until they're updated. You may need to trigger an update on all records to populate the field.
-
Cross-table computed comparisons not supported - You cannot compare a computed field from one table to a field in another table (e.g.,
availableStock <= r2.lowStockThreshold). This would require the computed expression feature (not yet available).
Example: Low Stock Alert Query¶
Structure setup (product-options):
{
"properties": [
{ "name": "inventory", "type": "number" },
{ "name": "reserved", "type": "number" },
{
"name": "availableStock",
"type": "number",
"expression": "inventory - reserved",
"computedMode": "persisted"
}
]
}
Smart Query:
{
"select": ["sku", "inventory", "reserved", "availableStock"],
"where": {
"merchantId": { "$eq": "{{merchantId}}" },
"availableStock": { "$gt": 0 },
"availableStock": { "$lte": 10 }
},
"join": {
"foreignSlug": "products",
"localField": "productId",
"foreignField": "_recordId",
"select": ["title", "inventoryTracking"]
},
"joinWhere": {
"inventoryTracking": { "$eq": true }
},
"sort": [{ "field": "availableStock", "direction": "asc" }]
}
Filter Operators¶
Smart Queries support MongoDB-style operators:
Comparison Operators¶
| Operator | Description | Example |
|---|---|---|
$eq | Equal | { "status": { "$eq": "active" } } |
$ne | Not equal | { "status": { "$ne": "deleted" } } |
$gt | Greater than | { "priority": { "$gt": 5 } } |
$gte | Greater than or equal | { "priority": { "$gte": 5 } } |
$lt | Less than | { "count": { "$lt": 100 } } |
$lte | Less than or equal | { "count": { "$lte": 100 } } |
String Operators¶
| Operator | Description | Example |
|---|---|---|
$startsWith | Starts with | { "name": { "$startsWith": "John" } } |
$endsWith | Ends with | { "email": { "$endsWith": "@company.com" } } |
$contains | Contains substring | { "description": { "$contains": "urgent" } } |
$regex | Regex match | { "code": { "$regex": "^[A-Z]{3}-[0-9]+" } } |
Array Operators¶
| Operator | Description | Example |
|---|---|---|
$in | In array | { "status": { "$in": ["active", "pending"] } } |
$nin | Not in array | { "category": { "$nin": ["archived", "deleted"] } } |
Logical Operators¶
Combine conditions with $and and $or:
{
"where": {
"$and": [
{ "status": { "$eq": "active" } },
{
"$or": [
{ "priority": { "$gte": 8 } },
{ "flagged": { "$eq": true } }
]
}
]
}
}
SDK Reference¶
List Smart Queries¶
// List all in workspace
const all = await centrali.smartQueries.listAll();
// List for a specific structure
const queries = await centrali.smartQueries.list('orders');
Get Smart Query¶
// By ID
const query = await centrali.smartQueries.get('orders', 'query-uuid');
// By name
const query = await centrali.smartQueries.getByName('orders', 'Active Orders');
Execute Smart Query¶
// Simple execution
const results = await centrali.smartQueries.execute('orders', 'query-id');
// With variables
const results = await centrali.smartQueries.execute('orders', 'query-id', {
variables: {
userId: 'user_123',
startDate: '2024-01-01'
}
});
// Accessing joined data
const ordersWithProducts = await centrali.smartQueries.execute('order-items', 'items-with-products');
ordersWithProducts.data.forEach(item => {
console.log('Item:', item.name);
console.log('Product:', item._joined?.product?.title);
});
Best Practices¶
-
Name queries descriptively - Use names like "Active Orders by Customer" instead of "Query 1"
-
Document your variables - Add comments in the query description about what variables are expected
-
Use _recordId for foreign keys - When storing record UUIDs as references, join using
_recordIdfor correct matching -
Type casting happens automatically - Centrali casts values based on your structure's field types (number, boolean, datetime)
-
Test before saving - Use the "Test Query" button in the console to verify your query works before saving
Troubleshooting¶
"Missing required variable" error¶
Ensure you're providing all variables used in the query definition. Check the requiredVariables array in the error response.
Join returns no results¶
- Verify the
localFieldcontains valid UUIDs that match records in the foreign structure - When using
_recordId, ensure the local field stores the exact UUID (not a formatted version) - Check that records exist in the foreign structure with the expected values
Type casting errors¶
Ensure your structure's property types match the values you're querying: - Numbers should be queried as numbers, not strings - Booleans should be true/false - Dates should be in ISO format