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