← Files Catalyst by ZohoARCHIVED FILE
skills/catalyst-by-zoho/references/functions-and-sdk.md
34.3 KB · Oct 2, 2026 · 00:06 UTC
# Functions & SDK Reference
> **⚠️ PRE-FLIGHT CHECK:** Before using ANY code from this file, confirm that `.catalystrc` and `catalyst.json` already exist in the project directory. If they don't, STOP — do not create files, do not scaffold. Tell the user to run `catalyst init` in their terminal first. These files are auto-generated by the CLI and cannot be created manually. See the Pre-flight Gate in SKILL.md.
## Table of Contents
1. [Function Types Overview](#function-types-overview)
2. [Function Execution Limits](#function-execution-limits)
3. [Basic I/O Functions](#basic-io-functions)
4. [Advanced I/O Functions](#advanced-io-functions)
5. [Event Functions](#event-functions)
6. [Cron Functions](#cron-functions)
7. [Integration Functions](#integration-functions)
8. [Job Functions](#job-functions)
9. [Browser Logic Functions](#browser-logic-functions)
10. [Node.js SDK Setup](#nodejs-sdk-setup)
11. [Python SDK Setup](#python-sdk-setup)
12. [Java SDK Setup](#java-sdk-setup)
13. [Web SDK (Client-Side)](#web-sdk-client-side)
14. [SDK Component Access Patterns](#sdk-component-access-patterns)
15. [Error Handling Patterns](#error-handling-patterns)
16. [Security Rules](#security-rules)
17. [Retry Behavior](#retry-behavior)
18. [Cold Starts](#cold-starts)
19. [Testing](#testing)
---
## Function Types Overview
| Type | Invocation | Use Case | Handler Args (Node.js) | SDK Init |
|------|-----------|----------|----------------------|----------|
| Basic I/O | HTTP GET via API/SDK | Simple request-response | `(context, basicIO)` | `catalyst.initialize(context)` |
| Advanced I/O | HTTP any method | REST APIs, webhooks | `(req, res)` | `catalyst.initialize(req)` |
| Event | Signals/Event Listeners | React to platform events | `(event, context)` | `catalyst.initialize(context)` |
| Cron | Scheduled by Cron jobs | Periodic tasks | `(cronDetails, context)` | `catalyst.initialize(context)` |
| Integration | Zoho service triggers | Zoho ecosystem integration | `(event, context)` | `catalyst.initialize(context)` |
| Job | Job Scheduling service | Background processing | `(jobData, context)` | `catalyst.initialize(context)` |
| Browser Logic | SmartBrowz | Headless browser scripts | `(event, context)` | `catalyst.initialize(context)` |
> Source: https://docs.catalyst.zoho.com/en/sdk/nodejs/v2/overview/ — All function types require
> manual SDK initialization. The SDK is NOT auto-injected as `catalystApp`.
Critical: never copy code between function types. Each type has different modules initialized in the boilerplate. Always start from the correct template.
---
## Function Execution Limits
| Function Type | Timeout | Behavior on Timeout |
|---------------|---------|---------------------|
| Basic I/O | 30 seconds | Returns 504 Gateway Timeout |
| Advanced I/O | 30 seconds | Returns 504 Gateway Timeout |
| Event | 15 minutes | Silently terminated; may auto-retry |
| Cron | 15 minutes | Marked as failed; may auto-retry |
| Integration | 30 seconds | Returns error to calling Zoho service |
| Job | 15 minutes | Marked as failed; may auto-retry |
| Browser Logic | 30 seconds | Browser instance terminated |
For long-running tasks exceeding 30 seconds, use Event, Job, or Cron functions (up to 15 minutes).
For tasks exceeding 15 minutes, migrate to AppSail which has no function-level timeout.
---
## Basic I/O Functions
Simplest function type. Receives a string input, returns a string output. Invoked via GET request.
### Node.js Template
```javascript
// functions/my_basic_io/index.js
'use strict';
const catalyst = require('zcatalyst-sdk-node');
module.exports = (context, basicIO) => {
try {
const catalystApp = catalyst.initialize(context);
// Get input data (sent as query parameter)
const inputData = context.getArgument();
// Access Catalyst components
const dataStore = catalystApp.datastore();
// Your business logic here
const result = `Processed: ${inputData}`;
// Send response (must be a string)
basicIO.write(result);
} catch (error) {
console.error('Error:', error);
basicIO.write(JSON.stringify({ error: error.message }));
}
};
```
### package.json
```json
{
"name": "my_basic_io",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"zcatalyst-sdk-node": "latest"
}
}
```
Invocation: `GET /server/my_basic_io/execute?args=<input_string>`
---
## Advanced I/O Functions
Full HTTP support with raw Node.js request/response objects. Best for REST APIs.
> **node20 runtime — NOT Express.** `req` is a raw `http.IncomingMessage` and `res` is a raw
> `http.ServerResponse`. Do **not** use `res.status()`, `res.json()`, or `req.body` directly —
> these are Express methods and will throw `res.status is not a function`. Use the helpers below.
### Required helpers (always include in node20 Advanced I/O)
```javascript
// Send a JSON response
function sendJson(res, statusCode, data) {
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data));
}
// Read and parse the request body (req.body is NOT auto-parsed in node20)
function getBody(req) {
return new Promise((resolve, reject) => {
if (req.body && typeof req.body === 'object') return resolve(req.body);
if (req.body && typeof req.body === 'string') {
try { return resolve(JSON.parse(req.body)); } catch (e) { return resolve({}); }
}
let data = '';
req.on('data', (chunk) => { data += chunk; });
req.on('end', () => {
try { resolve(data ? JSON.parse(data) : {}); } catch (e) { resolve({}); }
});
req.on('error', reject);
});
}
```
### Node.js Template
```javascript
// functions/my_api/index.js
'use strict';
const catalyst = require('zcatalyst-sdk-node');
function sendJson(res, statusCode, data) {
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data));
}
function getBody(req) {
return new Promise((resolve, reject) => {
if (req.body && typeof req.body === 'object') return resolve(req.body);
if (req.body && typeof req.body === 'string') {
try { return resolve(JSON.parse(req.body)); } catch (e) { return resolve({}); }
}
let data = '';
req.on('data', (chunk) => { data += chunk; });
req.on('end', () => {
try { resolve(data ? JSON.parse(data) : {}); } catch (e) { resolve({}); }
});
req.on('error', reject);
});
}
module.exports = async (req, res) => {
try {
const catalystApp = catalyst.initialize(req);
const method = req.method;
// Parse URL and query params — req.query does NOT exist on http.IncomingMessage
const parsedUrl = new URL(req.url, `https://${req.headers.host}`);
const query = Object.fromEntries(parsedUrl.searchParams);
if (method === 'GET') {
const queryParam = query.id;
sendJson(res, 200, { message: 'GET request', id: queryParam });
} else if (method === 'POST') {
const body = await getBody(req);
sendJson(res, 201, { message: 'Created', data: body });
} else if (method === 'PUT') {
const body = await getBody(req);
sendJson(res, 200, { message: 'Updated', data: body });
} else if (method === 'DELETE') {
sendJson(res, 200, { message: 'Deleted' });
} else {
sendJson(res, 405, { error: 'Method not allowed' });
}
} catch (error) {
console.error('Error:', error);
sendJson(res, 500, { error: error.message });
}
};
```
> **Legacy projects (node14/16/18)** may still use the 4-parameter signature:
> `module.exports = (catalystApp, context, req, res) => { ... }`
> where `catalystApp` is pre-initialized. New CLI-initialized projects (node20+)
> use the 2-parameter format shown above.
Invocation: Any HTTP method to `/server/my_api/execute`
### User-scope vs admin-scope initialization
The SDK supports two initialization scopes. Choose based on what the operation needs:
```javascript
// USER SCOPE (default) — for resolving user identity
const userApp = catalyst.initialize(req);
const currentUser = await userApp.userManagement().getCurrentUser();
// getCurrentUser() makes an internal GET to /project-user/current using the user token.
// Only works for registered app users (signed up via Catalyst auth), NOT collaborators/admins.
// ADMIN SCOPE — for all data operations (DataStore, Stratus, ZCQL, Cache, etc.)
const adminApp = catalyst.initialize(req, { scope: 'admin' });
const dataStore = adminApp.datastore();
const zcql = adminApp.zcql();
const stratus = adminApp.stratus();
```
**Common pattern for apps that need both auth AND data:**
```javascript
module.exports = async (req, res) => {
try {
// 1. Get user identity (user-scope)
const userApp = catalyst.initialize(req);
const currentUser = await userApp.userManagement().getCurrentUser();
// 2. Perform data operations (admin-scope)
const adminApp = catalyst.initialize(req, { scope: 'admin' });
const table = adminApp.datastore().table('MyTable');
// 3. Use currentUser for ownership/filtering
const rows = await adminApp.zcql().executeZCQLQuery(
`SELECT * FROM MyTable WHERE owner_id = '${currentUser.user_id}'`
);
sendJson(res, 200, { user: currentUser, data: rows });
} catch (error) {
sendJson(res, 500, { error: error.message });
}
};
```
> ⚠️ **Do NOT use admin-scope for `getCurrentUser()`** — it throws "no user credentials present".
> Admin scope lacks user identity. Use default (user) scope for identity, admin scope for data.
### CORS handling for Slate → Function cross-domain
When a Slate frontend calls your Advanced I/O function cross-domain, the Catalyst gateway injects
`Access-Control-Allow-Origin` automatically (if the Slate domain is in Authorized Domains in the console).
**Critical rule: do NOT set CORS headers in your function for production origins.** If both the
gateway and your code set `Access-Control-Allow-Origin`, the browser receives duplicate values
and rejects the response.
Only set CORS headers for `localhost` (local dev, where no gateway exists):
```javascript
// Place this BEFORE your route handlers
app.use((req, res, next) => {
const origin = req.headers.origin || '';
if (/^http:\/\/localhost(:\d+)?$/.test(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') return res.status(204).end();
}
next();
});
```
**Setup required in the console:**
Console → Authentication → Whitelisting → Authorized Domains → add your Slate domain → enable CORS toggle.
> ⚠️ **The `Authorization` header your frontend sends is stripped by the gateway.** After
> validation, the gateway replaces it with internal `x-zc-*` headers. `req.headers['authorization']`
> will be `undefined` inside your function. The SDK reads the `x-zc-*` headers internally via
> `catalyst.initialize(req)` — you don't need to handle this manually.
### HTTP payload limits
| Limit | Value |
|-------|-------|
| Request body (JSON/form/multipart) | 250 MB |
| Response body | 250 MB |
Exceeding these limits returns HTTP 413 (Payload Too Large). For very large file transfers,
use Stratus presigned upload URLs instead of passing data through functions.
### Handling request bodies and file uploads in Advanced I/O
> ⚠️ **Advanced I/O functions use raw `http.IncomingMessage` — NOT Express.** There is no `req.body`,
> no `req.files`, no `req.params`, and no built-in body parser of any kind. These are Express/multer
> features that do not exist on a raw Node.js request object.
> - **JSON bodies:** Manually accumulate chunks from the request stream (see `getBody()` helper above).
> - **`multipart/form-data` file uploads:** Install `busboy` (`npm install busboy`) and pipe `req` through it.
> - **Query parameters:** Use `new URL(req.url, ...).searchParams` — `req.query` does not exist.
> - **URL path segments:** Use `new URL(req.url, ...).pathname` — `req.params` does not exist.
#### File upload with busboy
```javascript
'use strict';
const catalyst = require('zcatalyst-sdk-node');
const Busboy = require('busboy');
function sendJson(res, statusCode, data) {
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data));
}
// Parse multipart/form-data from raw http.IncomingMessage
function parseMultipart(req) {
return new Promise((resolve, reject) => {
const bb = Busboy({ headers: req.headers });
const fields = {};
let fileInfo = null;
const chunks = [];
bb.on('field', (name, val) => { fields[name] = val; });
bb.on('file', (name, stream, info) => {
fileInfo = { name: info.filename, mimetype: info.mimeType };
stream.on('data', (chunk) => chunks.push(chunk));
stream.on('end', () => {
fileInfo.data = Buffer.concat(chunks);
fileInfo.size = fileInfo.data.length;
});
});
bb.on('close', () => resolve({ fields, file: fileInfo }));
bb.on('error', reject);
req.pipe(bb);
});
}
module.exports = async (req, res) => {
const catalystApp = catalyst.initialize(req);
// Parse the multipart form data using busboy
const { fields, file } = await parseMultipart(req);
if (!file) {
return sendJson(res, 400, { error: 'No file provided. Send file as multipart/form-data.' });
}
const stratus = catalystApp.stratus();
const bucket = stratus.bucket('my-bucket');
try {
// putObject(key, body) or putObject(key, body, options)
// See: https://docs.catalyst.zoho.com/en/sdk/nodejs/v2/cloud-scale/stratus/upload-object/
await bucket.putObject(file.name, file.data, {
contentType: file.mimetype
});
sendJson(res, 200, { message: 'Uploaded', name: file.name, size: file.size });
} catch (err) {
sendJson(res, 500, { error: err.message });
}
};
```
> **`busboy` must be in your function's `package.json` dependencies.** Run `npm install busboy` inside the function directory before deploying.
---
## Event Functions
Triggered by Signals or Event Listeners. Cannot be invoked directly via HTTP.
```javascript
// functions/my_event_fn/index.js
'use strict';
const catalyst = require('zcatalyst-sdk-node');
module.exports = (event, context) => {
try {
const catalystApp = catalyst.initialize(context);
// Get event data
const eventData = event.getArgument();
const parsedData = JSON.parse(eventData);
console.log('Event received:', parsedData);
// Process the event
// ...
// Must close context when done
context.close();
} catch (error) {
console.error('Event processing error:', error);
context.close();
}
};
```
Event types that can trigger Event Functions:
- **Component Events**: Data Store row insert/update/delete, File Store upload/delete
- **Custom Events**: User-defined events triggered via SDK/API
- **Zoho Events**: Events from Zoho services (CRM, Books, etc.)
---
## Cron Functions
Invoked by Cron jobs on a schedule. Cannot be invoked directly.
```javascript
// functions/my_cron_fn/index.js
'use strict';
const catalyst = require('zcatalyst-sdk-node');
module.exports = async (cronDetails, context) => {
try {
const catalystApp = catalyst.initialize(context);
const cronInfo = cronDetails.getArgument();
console.log('Cron job triggered:', cronInfo);
// Perform scheduled task
// Example: Clean up old records
const zcql = catalystApp.zcql();
const query = "DELETE FROM Reports WHERE CREATEDTIME < '2024-01-01 00:00:00'";
await zcql.executeZCQLQuery(query);
// Signal success
context.closeWithSuccess();
} catch (error) {
console.error('Cron error:', error);
context.closeWithFailure();
}
};
```
---
## Integration Functions
For integrating with other Zoho services. Note: NOT available in EU, AU, IN, or CA data centers.
```javascript
// functions/my_integration_fn/index.js
'use strict';
const catalyst = require('zcatalyst-sdk-node');
module.exports = (event, context) => {
try {
const catalystApp = catalyst.initialize(context);
const integrationData = event.getArgument();
const parsedData = JSON.parse(integrationData);
// Access the Zoho service data
console.log('Integration data:', parsedData);
// Process and respond
context.close();
} catch (error) {
console.error('Integration error:', error);
context.close();
}
};
```
---
## Job Functions
Triggered by the Job Scheduling service for background processing.
```javascript
// functions/my_job_fn/index.js
'use strict';
const catalyst = require('zcatalyst-sdk-node');
module.exports = async (jobData, context) => {
try {
const catalystApp = catalyst.initialize(context);
const jobDetails = jobData.getArgument();
console.log('Job started:', jobDetails);
// Long-running background task
// ...
context.closeWithSuccess();
} catch (error) {
console.error('Job error:', error);
context.closeWithFailure();
}
};
```
---
## Browser Logic Functions
Used with SmartBrowz for headless browser automation.
```javascript
// functions/my_browser_fn/index.js
'use strict';
module.exports = (catalystApp, context, browserData) => {
try {
const input = browserData.getArgument();
// Browser automation logic
context.close();
} catch (error) {
console.error('Browser logic error:', error);
context.close();
}
};
```
---
## Node.js SDK Setup
> **Runtime support:** node20 is the only actively supported runtime — node14, 16, and 18 still work for legacy projects but receive no upstream security patches. Use node20 for all new functions.
### In Functions
The initialization pattern depends on the Node.js runtime version used when the project was created:
```javascript
// NEW (node20+ / CLI-initialized projects) — SDK must be initialized manually:
'use strict';
const catalyst = require('zcatalyst-sdk-node');
module.exports = async (req, res) => {
const catalystApp = catalyst.initialize(req);
const dataStore = catalystApp.datastore();
// ...
};
// LEGACY (node14/16/18) — catalystApp is pre-initialized as first parameter:
// module.exports = (catalystApp, context, req, res) => {
// const dataStore = catalystApp.datastore();
// };
```
### In AppSail (manual initialization)
```javascript
const catalyst = require('zcatalyst-sdk-node');
// Initialize with request context (for user-scoped operations)
app.get('/api/data', (req, res) => {
const catalystApp = catalyst.initialize(req);
const dataStore = catalystApp.datastore();
// ...
});
// Initialize as admin (for system-level operations)
const catalystApp = catalyst.initialize(req, { scope: 'admin' });
```
### NPM Package
```bash
npm install zcatalyst-sdk-node
```
---
## Python SDK Setup
### In Functions
```python
# functions/my_function/main.py
import zcatalyst_sdk
def handler(context, basicIO):
catalyst_app = zcatalyst_sdk.initialize()
datastore = catalyst_app.datastore()
# Business logic
result = "Processed"
basicIO.write(result)
```
### pip Package
```bash
pip install zcatalyst-sdk
```
---
## Java SDK Setup
### In Functions
```java
// Maven dependency: com.zoho.catalyst:zcatalyst-sdk
import com.zoho.catalyst.api.CatalystApp;
import com.zoho.catalyst.api.beans.*;
public class MainFunction implements BasicIO {
@Override
public void runner(CatalystApp catalystApp, Context context, BasicIOObject basicIO) {
try {
String input = context.getArgument();
// Business logic
basicIO.write("Result");
} catch (Exception e) {
basicIO.write("Error: " + e.getMessage());
}
}
}
```
---
## Web SDK (Client-Side)
The Web SDK is used in frontend code to interact with Catalyst backend services.
### Include via script tag
```html
<!-- Always use v4.6.1+ for full feature support (generateAuthToken, isUserAuthenticated, etc.) -->
<script src="https://static.zohocdn.com/catalyst/sdk/js/4.6.1/catalystWebSDK.js"></script>
<script src="/__catalyst/sdk/init.js"></script>
<!-- init.js auto-initializes the SDK with the current project context. Always load it after catalystWebSDK.js. -->
```
### Authentication flow
```javascript
// Sign up a new user
catalyst.auth.signUp({
email_id: "user@example.com",
first_name: "John",
last_name: "Doe"
});
// Login
catalyst.auth.login("user@example.com", "password");
// Check if logged in
const isLoggedIn = catalyst.auth.isUserAuthenticated();
// Logout
catalyst.auth.signOut();
```
### Calling functions from client
```javascript
// Call a Basic I/O function
catalyst.server.callFunction("my_basic_io", { args: "input_data" })
.then(response => console.log(response))
.catch(err => console.error(err));
// Call an Advanced I/O function
catalyst.server.callAdvancedIO("my_api", {
method: "POST",
body: JSON.stringify({ key: "value" }),
headers: { "Content-Type": "application/json" }
})
.then(response => console.log(response))
.catch(err => console.error(err));
```
> **Always use `credentials: 'include'` when using native `fetch()`** to call Catalyst functions
> from a Catalyst-hosted web client. Without it, auth cookies are not forwarded and
> `getCurrentUser()` in the function will throw a 401 — even when both are on the same domain.
>
> ```javascript
> const res = await fetch('/server/my_api/execute', {
> method: 'POST',
> credentials: 'include', // ← required for auth cookies to be sent
> headers: { 'Content-Type': 'application/json' },
> body: JSON.stringify({ key: 'value' })
> });
> ```
>
> The `catalyst.server.callAdvancedIO()` SDK method handles this automatically.
> Use it when possible to avoid this class of issue.
---
## SDK Component Access Patterns
All components are accessed through the initialized `catalystApp` object:
```javascript
// Data Store
const dataStore = catalystApp.datastore();
const table = dataStore.table('TableName'); // by name
const table = dataStore.table(TABLE_ID); // by ID
// File Store
const fileStore = catalystApp.filestore();
const folder = fileStore.folder(FOLDER_ID);
// Cache
const cache = catalystApp.cache();
const segment = cache.segment(SEGMENT_ID);
// ZCQL
const zcql = catalystApp.zcql();
// Email
const email = catalystApp.email();
// Search
const search = catalystApp.search();
// User Management
const userManagement = catalystApp.userManagement();
// Push Notifications
const pushNotification = catalystApp.pushNotification();
// Connections (for third-party auth)
const connection = catalystApp.connection();
```
---
## Error Handling Patterns
### Recommended pattern for Advanced I/O functions
```javascript
'use strict';
const catalyst = require('zcatalyst-sdk-node');
function sendJson(res, statusCode, data) {
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data));
}
function getBody(req) {
return new Promise((resolve, reject) => {
if (req.body && typeof req.body === 'object') return resolve(req.body);
if (req.body && typeof req.body === 'string') {
try { return resolve(JSON.parse(req.body)); } catch (e) { return resolve({}); }
}
let data = '';
req.on('data', (chunk) => { data += chunk; });
req.on('end', () => {
try { resolve(data ? JSON.parse(data) : {}); } catch (e) { resolve({}); }
});
req.on('error', reject);
});
}
module.exports = async (req, res) => {
try {
const catalystApp = catalyst.initialize(req);
const body = await getBody(req);
// Validate input
if (!body.name) {
return sendJson(res, 400, { status: 'error', message: 'Name is required' });
}
// Business logic
const result = await someOperation(catalystApp, body);
sendJson(res, 200, { status: 'success', data: result });
} catch (error) {
console.error('Function error:', error);
if (error.code === 'INVALID_DATA') {
return sendJson(res, 400, { status: 'error', message: error.message });
}
sendJson(res, 500, { status: 'error', message: 'Internal server error' });
}
};
```
---
## Security Rules
Security Rules control who can invoke Basic I/O and Advanced I/O functions.
See: https://docs.catalyst.zoho.com/en/serverless/help/security-rules/key-concepts/
**The only valid values for the `authentication` parameter are:**
- **`optional`** — Anyone can invoke the function without authentication (public access). This is the default.
- **`required`** — Only authenticated users (Catalyst Users Authentication or OAuth) can invoke the function.
⚠️ **Values like `no_auth`, `user_auth`, `admin_auth` do NOT exist** and will throw `"Invalid input value"`.
Security Rules is a binary gate (public vs. authenticated). For admin-only access or per-route
auth control, disable Security Rules and enable **API Gateway** instead.
Security rules are configured in the Catalyst console under Serverless → Security Rules. They define
JSON-based rules that determine HTTP methods and authentication per function.
Default: all functions are set to `optional` (public access). Set to `required` to enforce authentication,
or use the API Gateway for more granular control (API keys, per-route auth, throttling).
---
## Retry Behavior
Background function types (Event, Cron, Job) automatically retry on failure. HTTP-facing
function types (Basic I/O, Advanced I/O) do not retry — the error is returned to the caller.
| Function Type | Auto-retry on failure? | Retry behavior |
|---------------|----------------------|----------------|
| Basic I/O | No | Error returned to caller |
| Advanced I/O | No | Error returned to caller |
| Event | Yes | Retries on failure (developer-configurable) |
| Cron | Yes | Retries on failure (developer-configurable) |
| Job | Yes | Retries on failure (developer-configurable) |
| Integration | No | Error returned to calling Zoho service |
| Browser Logic | No | Error returned to caller |
Retry logic for background functions is controlled by how you write your function and configure
the triggering service. There is no fixed platform retry count — design your handlers to be
**idempotent** (safe to run multiple times with the same input) since retries may occur.
---
## Cold Starts
When a function hasn't been invoked recently, Catalyst provisions a new execution
environment — this adds latency to the first request ("cold start").
**Typical cold-start latency:**
| Runtime | Cold start | Warm invocation |
|---------|-----------|-----------------|
| Node.js | 500ms–2s | 50–200ms |
| Java | 2–8s | 50–200ms |
| Python | 500ms–2s | 50–200ms |
Java has the longest cold starts due to JVM initialization.
**Mitigation strategies:**
- Keep function packages small — fewer dependencies = faster init
- Avoid heavy initialization outside the handler (large file reads, DB pool creation on import)
- Use a scheduled ping (via Job Scheduling) to keep critical functions warm
- For latency-sensitive endpoints, consider AppSail — it runs persistently with no cold starts
---
## Testing
### Unit testing function handlers
Mock the SDK and `res` (raw `http.ServerResponse`) to test handler logic without connecting to Catalyst.
> **Important (node20):** Advanced I/O functions use raw `http.ServerResponse`, NOT Express.
> Your mock must use `writeHead()` and `end()`, not `status()` or `json()`.
```javascript
// test/my_function.test.js
const handler = require('../functions/my_function/index');
const mockReq = {
method: 'POST',
url: '/tasks',
headers: { 'content-type': 'application/json' }
};
// Mock raw http.ServerResponse — NOT Express response
const mockRes = {
writeHead: jest.fn(),
end: jest.fn()
};
jest.mock('zcatalyst-sdk-node', () => ({
initialize: () => ({
datastore: () => ({
table: () => ({
insertRow: jest.fn().mockResolvedValue({ ROWID: '123' })
})
}),
zcql: () => ({
executeZCQLQuery: jest.fn().mockResolvedValue([])
})
})
}));
test('POST returns 201', async () => {
await handler(mockReq, mockRes);
expect(mockRes.writeHead).toHaveBeenCalledWith(201, expect.any(Object));
});
```
### Integration testing with `catalyst serve`
`catalyst serve` runs Basic I/O and Advanced I/O functions locally, connecting to the
remote Development Data Store. Use it for integration tests:
```bash
catalyst serve &
curl -X POST http://localhost:3000/server/my_function/execute -d '{"name":"Test"}'
```
Note: Event, Cron, and Job functions cannot be tested locally via `catalyst serve`.
Deploy to Development and trigger them from the console for integration testing.
---
## Common SDK Mistakes by Language
Agents frequently generate code with these errors. Check this section before finalising any
function code, especially when switching between function types or languages.
### Node.js (`zcatalyst-sdk-node`)
| Mistake | What goes wrong | Correct approach |
|---------|----------------|------------------|
| Calling `catalyst.initialize()` without `req` in Advanced I/O | SDK initialization fails; `catalystApp` is not correctly scoped to the request | Must pass `req`: `catalyst.initialize(req)` |
| Calling `basicIO.write()` more than once | Only the first call is used; subsequent calls are silently ignored or cause errors | `basicIO.write()` can only be called **once** per function execution |
| Expecting JSON output from Basic I/O | Basic I/O only supports STRING output | Basic I/O returns STRING only — use Advanced I/O for JSON responses |
| Setting HTTP response headers in Basic I/O | Basic I/O does not have a response object | Basic I/O does NOT support HTTP headers or status codes — use Advanced I/O |
| Using `res.status()` or `res.json()` in Advanced I/O (node20) | `res` is a raw `http.ServerResponse`, not Express — these methods do not exist | Use `res.writeHead(statusCode, headers)` and `res.end(JSON.stringify(data))` |
| Not handling ZCQL 300-row limit | Queries silently return only 300 rows; data appears missing | Paginate with `LIMIT offset, count` (e.g., `LIMIT 0, 300`, `LIMIT 300, 300`) |
| Using wrong port variable for AppSail | App binds to hard-coded port; Catalyst routes to a different port, causing connection failures | Always use `process.env.X_ZOHO_CATALYST_LISTEN_PORT \|\| 9000` |
| Not adding `credentials: 'include'` to fetch calls from web client | Auth cookies not forwarded; `getCurrentUser()` throws 401 even for authenticated users | Add `credentials: 'include'` to all fetch calls from the web client |
| Parsing `CREATEDTIME` directly with `new Date()` | Catalyst stores CREATEDTIME in the project timezone without an offset marker; `new Date()` treats it as UTC → wrong timestamps | Append the project timezone offset before parsing the date string |
| Using Express `cors()` middleware with Slate → Function cross-domain | Gateway AND Express both inject `Access-Control-Allow-Origin` → duplicate header → browser rejects | Only set CORS headers for localhost (local dev). Remove `cors()` middleware entirely for production origins. The gateway handles it. |
| Using admin-scope for `getCurrentUser()` | Throws "no user credentials present" — admin scope has no user identity | Use default (user) scope: `catalyst.initialize(req)` for `getCurrentUser()`. Use admin scope only for data operations. |
| Not handling `getCurrentUser()` returning `null` | Collaborators/admins are not registered app users → `null` return → `Cannot read properties of null` | Add null check. `getCurrentUser()` only works for users who signed up through Catalyst's auth flow, not console collaborators. |
| Reading `req.headers['authorization']` inside the function | Gateway strips the `Authorization` header after validation and injects `x-zc-*` internal headers instead → `undefined` | Don't read the Authorization header. Use `catalyst.initialize(req)` which reads the `x-zc-*` headers internally. |
### Java
| Mistake | What goes wrong | Correct approach |
|---------|----------------|------------------|
| Uploading compiled function via console without `.class` files | Function fails to execute with a missing class reference | Use `catalyst deploy` from CLI — it auto-compiles and creates missing dependency files |
| Using JDK version other than 8, 11, or 17 | Build or runtime errors; Catalyst only supports these three versions | Only JDK 8, 11, and 17 are supported |
| Not using context timing methods for long operations | Operations may exceed the timeout with no graceful handling | Use `context.getMaxExecutionTimeMs()` and `context.getRemainingExecutionTimeMs()` to manage time-sensitive operations |
### Python
| Mistake | What goes wrong | Correct approach |
|---------|----------------|------------------|
| Not using Flask for Advanced I/O functions | Python Advanced I/O functions require Flask; without it, the function cannot handle HTTP requests | Python Advanced I/O functions require the Flask framework |
| Using wrong handler signature for a function type | Function fails to initialize or throws an error on invocation | Handler signatures differ per function type — always consult the Function Types Overview table above |
| Using `context.getMaxExecutionTimeMs()` (camelCase) in Python | Method not found error | Python uses snake_case: `context.get_max_execution_time_ms()` and `context.get_remaining_execution_time_ms()` |
### General (all languages)
| Mistake | What goes wrong | Correct approach |
|---------|----------------|------------------|
| Creating `ROWID`, `CREATORID`, `CREATEDTIME`, or `MODIFIEDTIME` columns | These system columns are auto-created by Catalyst; attempting to create them causes an error | Never create these columns — Catalyst adds them automatically to every table |
| Hardcoding Catalyst IDs (Table ID, ZAID, Org ID, Project ID) without explanation | Users cannot find the correct values; wrong IDs cause permission errors | Always add an inline comment specifying exactly where to find the ID in the Catalyst console |
| Using Production environment in dev/test code | Production requires separate authorization; mixing environments causes auth failures | Always default to `"Development"` environment; only use production when explicitly requested |
| Checking for `.catalystrc` and `catalyst.json` before init | Re-running init on an already-initialized project overwrites config | Always check for existing `.catalystrc` and `catalyst.json` before scaffolding |
| Not serializing JSON before storing in Cache | Cache values are strings only; storing objects directly causes type errors | Always `JSON.stringify()` before storing in Cache and `JSON.parse()` when reading |
| Inserting emoji or 4-byte UTF-8 into Data Store | Silently stored as `?`; data is corrupted | Store a string key (e.g., `"happy"`) and map to emoji in application code |
SHA-256: 69117c476ea6c3c3bf7a7b749c23193d2a3ed3619e8088021ccd8ee0c9f38f2d