npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

npm version

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

Quick 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: workspaceId is 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, and searchFields is 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:

  1. Fetch current records first
  2. Subscribe to realtime
  3. 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 use module.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: pendingrunningcompleted | 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