Skip to content

AI Agent Troubleshooting Guide

This page is designed to be read by AI agents and developers using AI tools (Copilot, Cursor, Claude, Codex, MCP clients) to interact with Centrali. It documents the most common failure modes observed in real AI agent sessions and provides the correct patterns to follow.

If you are an AI agent, read this page before generating any Centrali API calls, SDK code, or compute functions.


Host Routing

The single most common AI mistake is sending requests to the wrong host. Centrali uses separate hosts for authentication and data.

Operation Correct Host Wrong Host
Token request (OAuth) https://auth.centrali.io/oidc/token https://api.centrali.io/oidc/token
Service account CRUD https://auth.centrali.io/workspace/{slug}/api/v1/service-accounts https://api.centrali.io/...
IAM (groups, roles, policies) https://auth.centrali.io/workspace/{slug}/api/v1/... https://api.centrali.io/...
Collections, records, functions https://api.centrali.io/data/workspace/{slug}/api/v1/... https://auth.centrali.io/...
Storage (files, folders) https://api.centrali.io/storage/workspace/{slug}/api/v1/... N/A
Search https://api.centrali.io/search/workspace/{slug}/api/v1/... N/A

Rule: Authentication and IAM operations go to auth.centrali.io. Everything else goes to api.centrali.io with a service path prefix (/data/, /storage/, /search/).


Workspace Slug Mismatches

The workspace slug must be consistent across all requests. A token obtained for workspace acme-corp will not work for requests scoped to workspace acme or acme-corp-staging.

Check these:

  • The workspace claim in your JWT matches the workspace slug in your API URL
  • The service account was created in the same workspace you are targeting
  • You are not mixing production and staging workspace slugs
# Decode the workspace from your token (base64 decode the payload)
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | python3 -m json.tool | grep workspace

Service Account Identifiers

Service accounts have two identifiers that are used in different contexts. Confusing them causes silent failures.

Identifier Format Where Used
id (serviceAccountId) Numeric integer (e.g., 123) Service account management endpoints (update, rotate, delete, revoke)
clientId String with prefix (e.g., ci_a1b2c3d4...) OAuth token requests, group assignment, JWT sub claim
# Token request uses clientId (string)
curl -X POST "https://auth.centrali.io/oidc/token" \
  -d "client_id=ci_a1b2c3d4e5f6g7h8i9j0" \
  -d "client_secret=sk_..." \
  -d "grant_type=client_credentials" \
  -d "scope=openid"

# Management endpoints use numeric id
curl -X GET "https://auth.centrali.io/workspace/acme-corp/api/v1/service-accounts/123" \
  -H "Authorization: Bearer $USER_TOKEN"

IAM Actions

Centrali uses retrieve for read operations, not read. Using read in policy definitions will silently fail to grant access.

Valid IAM actions:

Action Meaning
create Create new resources
retrieve Read a single resource
list List multiple resources
update Modify existing resources
delete Remove resources
manage Full administrative control
validate Run validation checks
execute Execute functions, triggers, queries
// Wrong - "read" is not a valid action
{ action: 'read', resource: 'records', effect: 'allow' }

// Correct
{ action: 'retrieve', resource: 'records', effect: 'allow' }

Collection Name vs Structure ID

Centrali has two ways to reference a collection. Using the wrong one in the wrong context causes 404 errors.

Identifier Format Where Used
recordSlug (collection name) Kebab-case string (e.g., my-products) SDK methods, record CRUD, query endpoints
structureId UUID (e.g., f47ac10b-58cc-...) Internal references, some API responses
// SDK uses the collection name (recordSlug), not the structureId
const records = await centrali.queryRecords('my-products', {
  'data.status': 'active'
});

// Wrong - structureId does not work in SDK methods
const records = await centrali.queryRecords('f47ac10b-58cc-4372-a567-0e02b2c3d479', {
  'data.status': 'active'
});

SDK Query Filter Syntax

This is one of the most frequent AI mistakes. The Client SDK and the Compute SDK use different filter syntax.

Client SDK (your application code)

Filters are top-level query parameters with a data. prefix. Do not nest them under a filter key.

// Correct - top-level filters with data. prefix
const products = await centrali.queryRecords('Product', {
  'data.inStock': true,
  'data.price[gte]': 100,
  sort: '-createdAt',
  pageSize: 20
});

// Wrong - nested filter object does not work in Client SDK
const products = await centrali.queryRecords('Product', {
  filter: { inStock: true, price: { gte: 100 } }
});

Operator syntax (Client SDK)

Operation Syntax Example
Equals 'data.field': value 'data.status': 'active'
Greater than 'data.field[gt]': value 'data.price[gt]': 100
Greater or equal 'data.field[gte]': value 'data.price[gte]': 100
Less than 'data.field[lt]': value 'data.price[lt]': 50
Less or equal 'data.field[lte]': value 'data.price[lte]': 50
Not equal 'data.field[ne]': value 'data.status[ne]': 'archived'
In (multiple values) 'data.field[in]': 'a,b,c' 'data.status[in]': 'pending,processing'
Contains (string) 'data.field[contains]': value 'data.email[contains]': '@gmail.com'

Compute SDK (inside compute functions)

Inside a compute function, api.queryRecords supports both the top-level syntax and nested filter: {}. This page documents the Client SDK only.


Compute Function Format

The current compute runtime requires async function run(). Older documentation and SDK examples may show exports.handler or module.exports patterns. Those are not valid in the current runtime.

// Correct - current runtime
async function run() {
  const input = executionParams;
  const config = triggerParams;

  const result = await api.queryRecords('my-collection', { limit: 10 });
  api.log({ found: result.data.length });

  return { success: true, data: result };
}

// Wrong - legacy pattern, will not execute
exports.handler = async function(event, context) {
  // ...
};

// Wrong - also legacy
module.exports = async function(event, context) {
  // ...
};

Available globals inside compute functions

Global Purpose
api SDK-like client for Centrali APIs (api.queryRecords, api.createRecord, api.log, etc.)
triggerParams Parameters configured on the trigger definition
executionParams Parameters passed at invocation time (e.g., from centrali.triggers.invoke(...))

Compute function return contract

// Success
return { success: true, data: { /* your result */ } };

// Failure (business logic error)
return { success: false, error: 'Description of what went wrong' };

// If you need to signal a real failure to orchestrations, throw:
throw new Error('Unrecoverable error');

Returning success: true on provider errors

If your function calls an external API and the provider returns an error (e.g., 403), do not return { success: true }. The orchestration runtime treats a structured return as success. Either throw or return { success: false, error: ... } so the step is marked as failed.


Plan-Gated Features

Some features return 403 or 402 errors because they require a specific workspace plan tier. If you receive a 403 that mentions plan limits or feature access, check whether the workspace plan supports the feature.

Common plan-gated features include:

  • Compute functions (execution quotas)
  • Automations/orchestrations
  • AI features (validation, anomaly insights, schema discovery)
  • Pages
  • Storage quotas
  • Search index limits

A 403 that says "plan" or "quota" is not a permissions problem. It is a billing/plan problem.


Common Error Patterns

Error Likely Cause Fix
Invalid client credentials Wrong client_id or client_secret, or extra whitespace Verify credentials, check for trailing newlines
Token expired JWT tokens expire after 7 hours (25,200 seconds) Fetch a new token; SDK handles this automatically
[FORBIDDEN] Resource not found Service account lacks permissions, or wrong workspace slug Verify workspace slug and group membership
Missing authorization header No Authorization: Bearer ... header Add the header to all API requests
Collection not found Wrong collection name, wrong workspace, or collection deleted List collections to verify: GET /data/workspace/{slug}/api/v1/collections
403 mentioning plan/quota Feature not available on current plan Check workspace plan tier
Unauthorized after token request Sent token request to api.centrali.io instead of auth.centrali.io Use https://auth.centrali.io/oidc/token
404 on service account endpoint Used api.centrali.io for IAM Use https://auth.centrali.io/workspace/{slug}/api/v1/...

Debugging Checklist

Use this checklist before reporting an issue or retrying a failed operation.

1. Verify credentials and token

  • client_id starts with ci_
  • client_secret starts with sk_
  • No trailing whitespace or newlines in credential values
  • Token request goes to https://auth.centrali.io/oidc/token
  • Token response contains access_token and expires_in
  • Token has not expired (7-hour lifetime)

2. Verify workspace

  • Workspace slug in API URL matches the workspace claim in the JWT
  • Service account belongs to a group in this workspace
  • Group has policies granting the required actions

3. Verify request shape

  • Auth/IAM requests go to auth.centrali.io
  • Data/storage/search requests go to api.centrali.io
  • Authorization: Bearer <token> header is present
  • Request body matches the expected schema (check API reference)
  • SDK query filters use top-level data. prefix, not nested filter: {}

4. Verify compute function

  • Function uses async function run(), not exports.handler
  • External API calls use api.fetch() not fetch()
  • External domains are added to the allowed domains list
  • Function returns { success: true, data: ... } or { success: false, error: ... }
  • Provider errors are not masked as success: true

5. Verify IAM

  • Actions use retrieve, not read
  • Resource names match the system resources table (e.g., records, structures, compute-functions)
  • Policies are attached to a group, and the service account is a member of that group

Quick Reference: Correct API Patterns

Obtain a token

curl -X POST "https://auth.centrali.io/oidc/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=$CENTRALI_CLIENT_ID" \
  -d "client_secret=$CENTRALI_CLIENT_SECRET" \
  -d "scope=openid"

List collections

curl "https://api.centrali.io/data/workspace/$WORKSPACE/api/v1/collections" \
  -H "Authorization: Bearer $TOKEN"

Query records (SDK)

const results = await centrali.queryRecords('my-collection', {
  'data.status': 'active',
  'data.priority[in]': 'high,critical',
  sort: '-createdAt',
  pageSize: 50
});

Create a record (SDK)

const record = await centrali.createRecord('my-collection', {
  title: 'New item',
  status: 'todo',
  priority: 'high'
});

Invoke a trigger (SDK)

await centrali.triggers.invoke('my-trigger-name', {
  payload: { key: 'value' }
});
// Payload lands in executionParams inside the compute function

Compute function template

async function run() {
  const input = executionParams;
  api.log({ receivedInput: input });

  try {
    const records = await api.queryRecords('my-collection', {
      limit: 100,
      filter: { status: 'active' }
    });

    return { success: true, data: { count: records.data.length } };
  } catch (error) {
    api.log({ error: error.message });
    return { success: false, error: error.message };
  }
}