@centrali-io/centrali-sdk
v4.5.2
Published
Centrali Node SDK
Readme
Centrali JavaScript/TypeScript SDK
Official Node.js SDK for Centrali - Build modern web applications without managing infrastructure.
Full documentation: docs.centrali.io — SDK guide, API reference, compute functions, orchestrations, and more.
Try it now! Explore the SDK in our Live Playground on StackBlitz - no setup required.
Installation
npm install @centrali-io/centrali-sdkQuick Start
import { CentraliSDK } from '@centrali-io/centrali-sdk';
// Initialize the SDK
const centrali = new CentraliSDK({
baseUrl: 'https://centrali.io',
workspaceId: 'your-workspace-slug', // This is the workspace slug, not a UUID
token: 'your-api-key'
});
// Create a record
const product = await centrali.createRecord('Product', {
name: 'Awesome Product',
price: 99.99,
inStock: true
});
console.log('Created product:', product.data);
// Search for records
const results = await centrali.search('Awesome');
console.log('Found:', results.data.totalHits, 'results');Authentication
The SDK supports three authentication methods. See the full authentication guide for details.
Recommended Auth by Context
| Context | Method | Why |
|---------|--------|-----|
| Browser / frontend app | Publishable key | Safe to expose in client code, scoped to specific resources |
| Server app / API route | Service account | Server-to-server, automatic token refresh |
| MCP agent | Service account (env vars) | CENTRALI_CLIENT_ID + CENTRALI_CLIENT_SECRET + CENTRALI_WORKSPACE_ID |
| App with its own auth (Clerk, Auth0) | External token / BYOT | Dynamic token callback |
Publishable Key (Frontend Apps)
Safe to use in browser code. Scoped to specific resources.
const centrali = new CentraliSDK({
baseUrl: 'https://centrali.io',
workspaceId: 'your-workspace-slug',
publishableKey: 'pk_live_your_key_here'
});External Token / BYOT (Clerk, Auth0, etc.)
Dynamic token callback for apps with their own auth.
const centrali = new CentraliSDK({
baseUrl: 'https://centrali.io',
workspaceId: 'your-workspace-slug',
getToken: async () => await clerk.session.getToken()
});Service Account (Server-to-Server)
Never use in browser code. Client secret must stay on the server.
const centrali = new CentraliSDK({
baseUrl: 'https://centrali.io',
workspaceId: 'your-workspace-slug',
clientId: process.env.CENTRALI_CLIENT_ID,
clientSecret: process.env.CENTRALI_CLIENT_SECRET
});Note:
workspaceIdis always the workspace slug (e.g.,"acme"), not a UUID.
Features
- ✅ Type-safe API with full TypeScript support
- ✅ Automatic authentication and token management
- ✅ Records management - Create, read, update, delete records
- ✅ Query operations - Powerful data querying with filters and sorting
- ✅ Smart Queries - Execute reusable, predefined queries
- ✅ Full-text search - Search records across structures with Meilisearch
- ✅ Realtime events - Subscribe to record changes via SSE
- ✅ Compute functions - Execute serverless functions
- ✅ File uploads - Upload files to Centrali storage
- ✅ Data Validation - AI-powered typo detection, format validation, duplicate detection
- ✅ Anomaly Insights - AI-powered anomaly detection and data quality insights
- ✅ Orchestrations - Multi-step workflows with compute functions, decision logic, and delays
- ✅ Publishable keys - Scoped, browser-safe keys for frontend apps
- ✅ External auth (BYOT) - Dynamic token callback for Clerk, Auth0, etc.
- ✅ Service accounts - Automatic token refresh for server-to-server auth
Core Operations
Records
// Create a record
const record = await centrali.createRecord('StructureName', {
field1: 'value1',
field2: 123
});
// Get a record
const record = await centrali.getRecord('StructureName', 'record-id');
// Update a record
await centrali.updateRecord('StructureName', 'record-id', {
field1: 'new value'
});
// Delete a record (soft delete - can be restored)
await centrali.deleteRecord('StructureName', 'record-id');
// Hard delete a record (permanent - cannot be restored)
await centrali.deleteRecord('StructureName', 'record-id', { hard: true });
// Restore a soft-deleted record
await centrali.restoreRecord('StructureName', 'record-id');
// Query archived (soft-deleted) records
const archived = await centrali.queryRecords('StructureName', {
includeArchived: true
});
// Query records with top-level filters
const products = await centrali.queryRecords('Product', {
'data.inStock': true,
'data.price[lte]': 100,
sort: '-createdAt',
limit: 10
});
// Same query using nested filter object (compute function style)
const products2 = await centrali.queryRecords('Product', {
filter: { 'data.inStock': true, 'data.price': { lte: 100 } },
sort: '-createdAt',
limit: 10
});Filter Syntax
The SDK supports two filter styles — use whichever you prefer:
Style 1 — Top-level filters (SDK-native):
const results = await centrali.queryRecords('Order', {
'data.status': 'active', // Exact match
'data.total[gte]': 100, // Operator with bracket notation
'data.tags[hasAny]': 'vip,premium', // Comma-separated for array ops
sort: '-createdAt',
pageSize: 25
});Style 2 — Nested filter object (same syntax as compute functions):
const results = await centrali.queryRecords('Order', {
filter: {
'data.status': 'active',
'data.total': { gte: 100 },
'data.tags': { hasAny: ['vip', 'premium'] }
},
sort: '-createdAt',
pageSize: 25
});Both styles work because the data service merges them. Use data. prefix for custom fields. System fields (id, createdAt, updatedAt, status) don't need the prefix.
Compute functions use
api.queryRecords()which has a slightly different interface — sort is an array of objects[{ field, direction }]instead of a string, andsearchFieldsis an array instead of comma-separated. See the compute function docs for details.
Filter Operators
| Operator | SDK (top-level) | SDK (nested) / Compute |
|----------|----------------|----------------------|
| Equal | 'data.status': 'active' | 'data.status': 'active' or { eq: 'active' } |
| Not equal | 'data.status[ne]': 'deleted' | 'data.status': { ne: 'deleted' } |
| Greater than | 'data.age[gt]': 18 | 'data.age': { gt: 18 } |
| Greater/equal | 'data.age[gte]': 18 | 'data.age': { gte: 18 } |
| Less than | 'data.age[lt]': 65 | 'data.age': { lt: 65 } |
| Less/equal | 'data.age[lte]': 65 | 'data.age': { lte: 65 } |
| In list | 'data.status[in]': 'a,b,c' | 'data.status': { in: ['a', 'b', 'c'] } |
| Not in list | 'data.status[nin]': 'a,b' | 'data.status': { nin: ['a', 'b'] } |
| Contains | 'data.email[contains]': '@gmail' | 'data.email': { contains: '@gmail' } |
| Starts with | 'data.name[startswith]': 'John' | 'data.name': { startswith: 'John' } |
| Ends with | 'data.email[endswith]': '.com' | 'data.email': { endswith: '.com' } |
| Has any (array) | 'data.tags[hasAny]': 'a,b' | 'data.tags': { hasAny: ['a', 'b'] } |
| Has all (array) | 'data.tags[hasAll]': 'a,b' | 'data.tags': { hasAll: ['a', 'b'] } |
Date Range Filtering
Use dateWindow to filter records by a date field:
// Records created in the last 30 days
const recent = await centrali.queryRecords('Order', {
dateWindow: {
field: 'createdAt',
from: new Date(Date.now() - 30 * 86400000).toISOString()
}
});
// Records updated in a specific range
const q1 = await centrali.queryRecords('Order', {
dateWindow: {
field: 'updatedAt',
from: '2024-01-01T00:00:00Z',
to: '2024-03-31T23:59:59Z'
}
});The dateWindow object has three properties:
| Property | Type | Description |
|----------|------|-------------|
| field | string | Date field to filter on (e.g., 'createdAt', 'updatedAt', or a custom date field) |
| from | string? | ISO 8601 lower bound (inclusive) |
| to | string? | ISO 8601 upper bound (inclusive) |
Reserved Query Parameters
When using queryRecords, the following parameter names are reserved and cannot be used as filter field names. Any other key you pass is treated as a record data filter.
| Parameter | Purpose |
|-----------|---------|
| page, pageSize, limit | Pagination |
| sort | Sorting (string with optional - prefix for descending) |
| search, searchField, searchFields | Full-text search |
| filter | Structured filter object (alternative to top-level filters) |
| fields, select | Field selection |
| expand | Reference expansion |
| includeDeleted, includeArchived, all | Include soft-deleted records |
| includeTotal | Include total count in response |
| dateWindow | Date range filtering (see above) |
To filter by a record field that shares a name with a reserved parameter, use the data. prefix:
// Filter by a field called "sort" in your record data
const results = await centrali.queryRecords('MyStructure', {
'data.sort': 'alphabetical'
});Expanding References
When querying records with reference fields, use the expand parameter to include the full referenced record data:
// Expand a single reference field
const orders = await centrali.queryRecords('Order', {
expand: 'customer'
});
// Expand multiple reference fields
const orders = await centrali.queryRecords('Order', {
expand: 'customer,product'
});
// Nested expansion (up to 3 levels deep)
const orders = await centrali.queryRecords('Order', {
expand: 'customer,customer.address'
});
// Get a single record with expanded references
const order = await centrali.getRecord('Order', 'order-id', {
expand: 'customer,items'
});Expanded data is placed in the _expanded object within the record's data:
// Response structure
{
"id": "order-123",
"data": {
"customerId": "cust-456",
"total": 99.99,
"_expanded": {
"customerId": {
"id": "cust-456",
"data": {
"name": "John Doe",
"email": "[email protected]"
}
}
}
}
}
// Access expanded data
const customerName = order.data.data._expanded.customerId.data.name;For many-to-many relationships, the expanded field contains an array:
// Many-to-many expansion
const post = await centrali.getRecord('Post', 'post-id', {
expand: 'tags'
});
// _expanded.tags will be an array of tag records
const tagNames = post.data.data._expanded.tags.map(tag => tag.data.name);Smart Queries
Smart queries are reusable, predefined queries that are created in the Centrali console and can be executed programmatically via the SDK. Pagination (limit/skip) is defined in the query definition itself.
// List all smart queries in the workspace
const allQueries = await centrali.smartQueries.listAll();
// List smart queries for a specific structure
const employeeQueries = await centrali.smartQueries.list('employee');
// Get a smart query by name
const query = await centrali.smartQueries.getByName('employee', 'Active Employees');
console.log('Query ID:', query.data.id);
// Execute a smart query
const results = await centrali.smartQueries.execute('employee', query.data.id);
console.log('Found:', results.data.length, 'employees');
// Execute with variables
// Query definition uses {{variableName}} syntax: { where: { status: { $eq: "{{statusFilter}}" } } }
const filteredResults = await centrali.smartQueries.execute('orders', query.data.id, {
variables: {
statusFilter: 'active',
startDate: '2024-01-01'
}
});Search
Perform full-text search across workspace records using Meilisearch:
// Basic search
const results = await centrali.search('customer email');
console.log('Found:', results.data.totalHits, 'results');
results.data.hits.forEach(hit => {
console.log(hit.id, hit.structureSlug);
});
// Search with structure filter
const userResults = await centrali.search('john', {
structures: 'users'
});
// Search multiple structures with limit
const multiResults = await centrali.search('active', {
structures: ['users', 'orders'],
limit: 50
});Search Response
| Field | Type | Description |
|-------|------|-------------|
| hits | SearchHit[] | Array of matching records |
| totalHits | number | Estimated total matches |
| processingTimeMs | number | Search time in milliseconds |
| query | string | The original search query |
Realtime Events
Subscribe to record changes in real-time using Server-Sent Events (SSE):
// Subscribe to realtime events
const subscription = centrali.realtime.subscribe({
structures: ['Order'], // Filter by structure (optional)
events: ['record_created', 'record_updated'], // Filter by event type (optional)
filter: 'status = "pending"', // CFL filter expression (optional)
onEvent: (event) => {
console.log('Event received:', event.event);
console.log('Record:', event.recordSlug, event.recordId);
console.log('Data:', event.data);
},
onConnected: () => {
console.log('Connected to realtime service');
},
onDisconnected: (reason) => {
console.log('Disconnected:', reason);
},
onError: (error) => {
console.error('Realtime error:', error.code, error.message);
}
});
// Later: cleanup subscription
subscription.unsubscribe();Initial Sync Pattern
Realtime delivers only new events after connection. For dashboards and lists, always:
- Fetch current records first
- Subscribe to realtime
- Apply diffs while UI shows the snapshot
// 1. Fetch initial data
const orders = await centrali.queryRecords('Order', {
filter: 'status = "pending"'
});
renderOrders(orders.data);
// 2. Subscribe to realtime updates
const subscription = centrali.realtime.subscribe({
structures: ['Order'],
events: ['record_created', 'record_updated', 'record_deleted'],
filter: 'status = "pending"',
onEvent: (event) => {
// 3. Apply updates to UI
switch (event.event) {
case 'record_created':
addOrderToUI(event.data);
break;
case 'record_updated':
updateOrderInUI(event.recordId, event.data);
break;
case 'record_deleted':
removeOrderFromUI(event.recordId);
break;
}
}
});Event Types
| Event | Description |
|-------|-------------|
| record_created | A new record was created |
| record_updated | An existing record was updated |
| record_deleted | A record was deleted |
| validation_suggestion_created | AI detected a data quality issue |
| validation_batch_completed | Batch validation scan finished |
| anomaly_insight_created | AI detected a data anomaly |
| anomaly_detection_completed | Anomaly detection scan finished |
Subscribing to Validation Events
// Subscribe to validation events for a structure
const subscription = centrali.realtime.subscribe({
structures: ['orders'], // Filter by structure
events: ['validation_suggestion_created', 'validation_batch_completed'],
onEvent: (event) => {
if (event.event === 'validation_suggestion_created') {
console.log('New suggestion:', event.data.field, event.data.issueType);
console.log('Confidence:', event.data.confidence);
} else if (event.event === 'validation_batch_completed') {
console.log('Scan complete:', event.data.issuesFound, 'issues found');
}
}
});Subscribing to Anomaly Events
// Subscribe to anomaly detection events for a structure
const subscription = centrali.realtime.subscribe({
structures: ['orders'],
events: ['anomaly_insight_created', 'anomaly_detection_completed'],
onEvent: (event) => {
if (event.event === 'anomaly_insight_created') {
console.log('New anomaly:', event.data.title);
console.log('Severity:', event.data.severity);
console.log('Type:', event.data.insightType);
} else if (event.event === 'anomaly_detection_completed') {
console.log('Analysis complete:', event.data.insightsCreated, 'anomalies found');
console.log('Critical:', event.data.criticalCount);
}
}
});Reconnection
The SDK automatically reconnects with exponential backoff when connections drop. Configure reconnection behavior:
import { RealtimeManager } from '@centrali-io/centrali-sdk';
const realtime = new RealtimeManager(
'https://centrali.io',
'your-workspace',
() => centrali.getToken(),
{
maxReconnectAttempts: 10, // Default: 10
initialReconnectDelayMs: 1000, // Default: 1000ms
maxReconnectDelayMs: 30000 // Default: 30000ms
}
);Compute Functions
Compute functions are JavaScript code blocks that execute in a sandboxed environment. Every function uses the same signature:
async function run() {
// Three variables are available:
// - api: SDK for records, HTTP, files, crypto, logging
// - triggerParams: static config from the trigger definition
// - executionParams: dynamic data from the current invocation
const result = await api.httpRequest({
url: 'https://api.example.com/data',
method: 'POST',
body: JSON.stringify(executionParams.payload)
});
return { success: true, data: result };
}Important: Use
async function run() { ... }. Do NOT usemodule.exports.
Compute Input Contract
What triggerParams and executionParams contain depends on how the function is invoked:
| Trigger type | triggerParams | executionParams |
|--------------|-----------------|-------------------|
| HTTP trigger | Static params from trigger definition | { payload } — parsed request body |
| Endpoint trigger | Static params from trigger definition | { payload, method, headers, query } — full HTTP context |
| Scheduled trigger | Static params from trigger definition (max 64KB) | {} — empty |
| Pages action | { input, token } — from page action invocation | {} — empty |
| Orchestration step | Orchestration input + previous step outputs + decrypted encrypted params | Not used (all input arrives via triggerParams) |
The api Object
Every compute function has access to api, which provides:
| Category | Methods |
|----------|---------|
| Records | queryRecords, fetchRecord, createRecord, updateRecord, deleteRecord, bulkCreateRecords, bulkUpdateRecords, bulkDeleteRecords |
| HTTP | httpRequest, httpFetch (outbound calls to allowed domains) |
| Files | storeFile, storeAsCSV, storeAsJSON |
| Crypto | crypto.sha256, crypto.hmacSha256, crypto.rsaSign, crypto.signJwt |
| Utilities | uuid, formatDate, chunk, merge, math, evaluate, renderTemplate, log, logError |
| Conversion | toCSV, toJSON |
Secrets in Compute Functions
Compute functions have no built-in environment variables or secrets field. To pass secrets (API keys, credentials) to a compute function:
- Recommended: Wrap the function in an orchestration and use encrypted params on the compute step. Encrypted params are stored with AES-256-GCM at rest and decrypted at execution time — they arrive in
triggerParams. - Alternative: Pass secrets in the trigger invocation payload via
executionParams. This works but means the calling app is the courier for the secret.
See Orchestrations for encrypted params usage.
Invoking Triggers
Trigger invocation is asynchronous — it returns a job ID, not the execution result. Use the function runs API to poll for the result.
// Invoke an on-demand trigger
const job = await centrali.triggers.invoke('trigger-id', {
payload: { data: 'your-input-data' }
});
console.log('Job queued:', job.data);
// Poll for the result using function runs
// (The job ID maps to a function run — query by trigger to find it)
const runs = await centrali.runs.listByTrigger('trigger-id', { limit: 1 });
const latestRun = runs.data.data[0];
console.log('Status:', latestRun.status); // pending | running | completed | failure | timeout
console.log('Result:', latestRun.runData); // execution output (once completed)
console.log('Error:', latestRun.errorMessage); // error details (if failed)Function Runs (Execution History)
Query execution history for debugging and observability:
// Get a specific run by ID
const run = await centrali.runs.get('run-uuid');
console.log('Status:', run.data.status);
console.log('Output:', run.data.runData);
console.log('Duration:', run.data.startedAt, '→', run.data.endedAt);
// List runs for a trigger (most recent first)
const triggerRuns = await centrali.runs.listByTrigger('trigger-uuid', {
status: 'failure', // Filter by status
limit: 10
});
// List runs for a function
const functionRuns = await centrali.runs.listByFunction('function-uuid');Run statuses: pending → running → completed | failure | timeout
File Uploads
// Upload a file (defaults to /root/shared)
const result = await centrali.uploadFile(file);
const renderId = result.data; // e.g., "kvHJ4ipZ3Q6EAoguKrWmU7KYyDHcU03C"
// Upload to a specific folder (must exist, use full path)
const result = await centrali.uploadFile(file, '/root/shared/images');
// Upload as public file
const result = await centrali.uploadFile(file, '/root/shared/public', true);
// Get URLs for the uploaded file
const renderUrl = centrali.getFileRenderUrl(renderId); // For inline display
const downloadUrl = centrali.getFileDownloadUrl(renderId); // For download
// Get URL with image transformations
const thumbnailUrl = centrali.getFileRenderUrl(renderId, {
width: 200,
quality: 80,
format: 'webp'
});Audio & video files are supported up to 500 MB. The storage service supports HTTP range requests (Accept-Ranges: bytes), so render URLs work with standard <audio> and <video> elements — seeking and progressive playback work out of the box:
<video src={centrali.getFileRenderUrl(renderId)} controls />
<audio src={centrali.getFileRenderUrl(renderId)} controls />Data Validation
AI-powered data quality validation to detect typos, format issues, duplicates, and semantic errors:
// Trigger a batch validation scan on a structure
const batch = await centrali.validation.triggerScan('orders');
console.log('Scan started:', batch.data.batchId);
console.log('Records to scan:', batch.data.total);
// Wait for the scan to complete (with polling)
const result = await centrali.validation.waitForScan(batch.data.batchId, {
pollInterval: 5000, // Check every 5 seconds
timeout: 300000 // Timeout after 5 minutes
});
console.log('Issues found:', result.data.issuesFound);
// List pending validation suggestions
const suggestions = await centrali.validation.listSuggestions({
status: 'pending',
issueType: 'typo' // 'format' | 'typo' | 'duplicate' | 'semantic'
});
// Review a suggestion
for (const suggestion of suggestions.data) {
console.log(`Field: ${suggestion.field}`);
console.log(`Original: ${suggestion.originalValue}`);
console.log(`Suggested: ${suggestion.suggestedValue}`);
console.log(`Confidence: ${suggestion.confidence}`);
}
// Accept a suggestion (applies the fix to the record)
const accepted = await centrali.validation.accept('suggestion-id');
if (accepted.data.recordUpdated) {
console.log('Fix applied successfully');
}
// Reject a suggestion
await centrali.validation.reject('suggestion-id');
// Bulk accept high-confidence suggestions
const highConfidence = suggestions.data.filter(s => s.confidence >= 0.95);
await centrali.validation.bulkAccept(highConfidence.map(s => s.id));
// Get validation summary
const summary = await centrali.validation.getSummary();
console.log('Pending:', summary.data.pending);
console.log('By type:', summary.data.byIssueType);
// Get pending count for a structure
const count = await centrali.validation.getPendingCount('structure-uuid');
console.log('Pending issues:', count.data.pendingCount);Validation Issue Types
| Type | Description |
|------|-------------|
| format | Format validation (email, phone, URL patterns) |
| typo | Spelling mistakes in text fields |
| duplicate | Duplicate records detected |
| semantic | Logical inconsistencies (e.g., end date before start date) |
Anomaly Insights
AI-powered anomaly detection to identify unusual patterns in your data:
// Trigger anomaly analysis for a structure
const result = await centrali.anomalyInsights.triggerAnalysis('orders');
if (result.data.success) {
console.log('Analysis started:', result.data.batchId);
}
// List all active insights
const insights = await centrali.anomalyInsights.list({
status: 'active',
severity: 'critical' // 'info' | 'warning' | 'critical'
});
// List insights for a specific structure
const orderInsights = await centrali.anomalyInsights.listByStructure('orders');
// Get a single insight
const insight = await centrali.anomalyInsights.get('insight-id');
console.log('Title:', insight.data.title);
console.log('Description:', insight.data.description);
console.log('Affected records:', insight.data.affectedRecordIds);
// Acknowledge an insight (mark as reviewed)
await centrali.anomalyInsights.acknowledge('insight-id');
// Dismiss an insight (mark as not relevant)
await centrali.anomalyInsights.dismiss('insight-id');
// Bulk acknowledge multiple insights
await centrali.anomalyInsights.bulkAcknowledge(['id1', 'id2', 'id3']);
// Get insights summary
const summary = await centrali.anomalyInsights.getSummary();
console.log('Active:', summary.data.active);
console.log('By severity:', summary.data.bySeverity);Insight Types
| Type | Description |
|------|-------------|
| time_series_anomaly | Unusual patterns in time-series data |
| statistical_outlier | Values significantly outside normal ranges |
| pattern_deviation | Deviations from expected data patterns |
| volume_spike | Unusual spikes or drops in data volume |
| missing_data | Missing or null values in required fields |
| orphaned_reference | References to deleted or non-existent records |
| reference_integrity | Broken relationships between records |
Orchestrations
Multi-step workflows that chain compute functions together with conditional logic, delays, and decision branches:
// List all orchestrations
const orchestrations = await centrali.orchestrations.list();
// Get an orchestration by ID
const orch = await centrali.orchestrations.get('orch-id');
console.log('Name:', orch.data.name);
console.log('Status:', orch.data.status);
console.log('Steps:', orch.data.steps.length);
// Create a new orchestration
const newOrch = await centrali.orchestrations.create({
slug: 'order-processing',
name: 'Order Processing Workflow',
trigger: { type: 'on-demand' },
steps: [
{
id: 'validate',
type: 'compute',
functionId: 'func_validate_order',
onSuccess: { nextStepId: 'check-result' }
},
{
id: 'check-result',
type: 'decision',
cases: [
{
conditions: [{ path: 'steps.validate.output.isValid', op: 'eq', value: true }],
nextStepId: 'fulfill'
}
],
defaultNextStepId: 'reject'
},
{
id: 'fulfill',
type: 'compute',
functionId: 'func_fulfill_order'
},
{
id: 'reject',
type: 'compute',
functionId: 'func_reject_order'
}
]
});
// Trigger an orchestration run
const run = await centrali.orchestrations.trigger('orch-id', {
input: {
orderId: '12345',
customerId: 'cust_abc'
}
});
console.log('Run started:', run.data.id);
console.log('Status:', run.data.status);
// Get run status
const runStatus = await centrali.orchestrations.getRun('orch-id', run.data.id);
console.log('Current step:', runStatus.data.currentStepId);
console.log('Has errors:', runStatus.data.hasErrors);
// Get run with step history
const runWithSteps = await centrali.orchestrations.getRun('orch-id', run.data.id, true);
for (const step of runWithSteps.data.steps || []) {
console.log(`${step.stepId}: ${step.status}`);
}
// List runs for an orchestration
const runs = await centrali.orchestrations.listRuns('orch-id', {
status: 'completed', // Filter by status
limit: 10
});
// Activate an orchestration (enables scheduled/event triggers)
await centrali.orchestrations.activate('orch-id');
// Pause an orchestration (disables all triggers)
await centrali.orchestrations.pause('orch-id');
// Update an orchestration
await centrali.orchestrations.update('orch-id', {
name: 'Updated Name',
status: 'active'
});
// Delete an orchestration
await centrali.orchestrations.delete('orch-id');Trigger Types
| Type | Description |
|------|-------------|
| on-demand | Manual invocation via API |
| event-driven | React to record events (created, updated, deleted) |
| scheduled | Run on a schedule (cron, interval, or once) |
| http-trigger | External webhook endpoint |
Step Types
| Type | Description |
|------|-------------|
| compute | Execute a compute function |
| decision | Branch based on conditions |
| delay | Wait for a duration |
Run Statuses
| Status | Description |
|--------|-------------|
| pending | Run is queued |
| running | Run is executing |
| waiting | Run is waiting (delay step) |
| completed | Run finished successfully |
| failed | Run failed with error |
TypeScript Support
The SDK includes full TypeScript definitions for type-safe development:
interface Product {
name: string;
price: number;
inStock: boolean;
}
const product = await centrali.createRecord<Product>('Product', {
name: 'Laptop',
price: 999.99,
inStock: true
});
// TypeScript knows product.data is of type Product
console.log(product.data.price);Error Handling
try {
const record = await centrali.createRecord('Product', productData);
console.log('Success:', record);
} catch (error) {
if (error.response?.status === 400) {
console.error('Validation error:', error.response.data);
} else if (error.response?.status === 401) {
console.error('Authentication failed');
} else {
console.error('Error:', error.message);
}
}Documentation
📚 Full documentation available at: docs.centrali.io
Examples
Check out complete example applications:
Support
License
ISC © Blueinit
Built with ❤️ by the Centrali team
