All skills
catalystbyzoho avatar

/catalyst-functions

@4a64353

Catalyst serverless functions — all 7 types (Basic I/O, Advanced I/O, Event, Cron, Job, Integration, Browser Logic), handler signatures, catalyst-config.json, Security Rules, API Gateway routing, file uploads, busboy, Express middleware, environment variables, function URL, and function testing. Requires MCP connection — check for CatalystbyZoho_* tools before any operation. Trigger on 'write a function', 'catalyst function', 'API Gateway', 'Security Rules', 'function not found', 'function returns 401', 'busboy', 'middleware', 'function URL', 'environment variable in function', 'duplicate CORS headers', 'CORS error in browser', 'Access-Control-Allow-Origin multiple values', 'function URL 404', 'execute suffix', 'function timeout', 'function hangs', or any function type question. Do NOT use for persistent servers, long-running processes, or Docker deployments — use catalyst-appsail instead.

Use this Skill: https://skilld.dev/gh/catalystbyzoho/agent-skills/catalyst-functions

This session only. Nothing lands on disk.

referencesfunctions-advanced.md

≈4.4k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Template Types: Express vs Raw-http

Advanced I/O functions support two templates. The template is chosen at function creation and determines the handler API surface.

Express template Raw-http template
Request req.query, req.params, req.body (Express) new URL(req.url, base).searchParams
Response res.status(200).json({}) res.writeHead(200); res.end(...)
Middleware app.use(...) works No middleware — plain http.IncomingMessage
Use when Familiar Express API Minimal footprint, raw stream control

The examples in this file are labelled with their template type.


File Upload via Advanced I/O Function (busboy)

<!-- Express template -->

Parse multipart/form-data file uploads using busboy. Install: npm install busboy.

// Express template
const Busboy = require('busboy');
const catalyst = require('zcatalyst-sdk-node');

module.exports = async (catalystApp, context, req, res) => {
  const busboy = Busboy({ headers: req.headers });
  const chunks = [];
  let fileName = '';
  let mimeType = '';

  await new Promise((resolve, reject) => {
    busboy.on('file', (fieldname, file, info) => {
      fileName = info.filename;
      mimeType = info.mimeType;
      file.on('data', chunk => chunks.push(chunk));
      file.on('end', resolve);
      file.on('error', reject);
    });
    busboy.on('error', reject);
    req.pipe(busboy);
  });

  const fileBuffer = Buffer.concat(chunks);

  // Upload to Stratus
  const { Readable } = require('stream');
  const stream = Readable.from(fileBuffer);
  await catalystApp.stratus().bucket('myapp-files-70699')
    .putObject(`uploads/${fileName}`, stream);

  res.status(200).json({ status: 'uploaded', fileName });
};

Use "advancedio" (lowercase, no space) as the type value in catalyst-config.json.

{
  "deployment": {
    "name": "file_upload",
    "type": "advancedio",
    "stack": "node20",
    "env_variables": {}
  },
  "execution": {
    "main": "index.js"
  }
}

authentication, memory, and max_time are not catalyst-config.json fields — configure them in the Catalyst console under Functions → Security Rules / Configuration.


Stream a File from Stratus to Response

// Express template
module.exports = async (catalystApp, context, req, res) => {
  const key = req.query.file;
  if (!key) return res.status(400).json({ error: 'file param required' });

  const bucket = catalystApp.stratus().bucket('myapp-files-70699');

  // HEAD check first
  const head = await bucket.headObject(key, { throwErr: false });
  if (!head) return res.status(404).json({ error: 'File not found' });

  res.setHeader('Content-Type', head.content_type || 'application/octet-stream');
  res.setHeader('Content-Disposition', `attachment; filename="${key.split('/').pop()}"`);

  const stream = await bucket.getObject(key);
  stream.pipe(res);
};

Error Handling Pattern

Standard error response pattern for Advanced I/O functions:

// Express template
module.exports = async (catalystApp, context, req, res) => {
  try {
    // ... your logic
    const result = await someOperation();
    res.status(200).json({ data: result });
  } catch (err) {
    console.error(JSON.stringify({
      action: 'myFunction',
      error: err.message,
      stack: err.stack
    }));

    // Classify error type
    if (err.name === 'ValidationError') {
      return res.status(400).json({ error: err.message });
    }
    if (err.status === 404 || err.message?.includes('not found')) {
      return res.status(404).json({ error: 'Resource not found' });
    }

    res.status(500).json({ error: 'Internal server error' });
  }
};

CORS for Local Dev

This pattern is for the Express template only.

Do NOT add CORS headers in function code for production origins — the Catalyst gateway handles this. Only set CORS for localhost (local dev where no gateway exists):

// Express template
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();
});

Testing Functions Locally (Mock Pattern)

Mock http.ServerResponse for unit testing Basic I/O functions:

// test/myFunction.test.js
const handler = require('../functions/my_function/index.js');

async function runTest() {
  // Mock context (getArgument() lives on context, not basicIO)
  const context = {
    closeWithSuccess: () => console.log('SUCCESS'),
    closeWithFailure: (msg) => console.error('FAILURE:', msg),
    getArgument: () => JSON.stringify({ userId: '12345', action: 'test' })
  };

  // Mock basicIO (write() — not setOutput())
  const basicIO = {
    write: (result) => console.log('OUTPUT:', result)
  };

  await handler(context, basicIO);
}

runTest().catch(console.error);

For Advanced I/O (Express template), test against the actual local dev server:

catalyst serve
curl -X POST http://localhost:3000/server/my_function/execute \
  -H "Content-Type: application/json" \
  -d '{"key": "value"}'

Chaining Functions (Call One Function from Another)

Anti-pattern: Never call Advanced I/O functions via HTTP from other functions in production.

Preferred patterns:

  1. Shared module: Extract common logic into functions/utils/ and import it
  2. Circuits: For multi-step orchestration
  3. Job Scheduling: For async fan-out
// functions/utils/dataHelper.js
async function getUserById(catalystApp, userId) {
  const rows = await catalystApp.zcql().executeZCQLQuery(
    `SELECT * FROM Users WHERE ROWID = '${userId}'`
  );
  return rows[0]?.Users || null;
}

module.exports = { getUserById };
// functions/my_function/index.js
const { getUserById } = require('../utils/dataHelper');

module.exports = async (catalystApp, context, req, res) => {
  const user = await getUserById(catalystApp, req.params.id);
  if (!user) return res.status(404).json({ error: 'User not found' });
  res.json({ user });
};

Result Unwrapping (ZCQL)

ZCQL result rows are wrapped — always unwrap before accessing column values:

const rows = await catalystApp.zcql().executeZCQLQuery('SELECT * FROM Tasks');

// Each row is: { Tasks: { ROWID: '...', Title: '...', ... } }
const tasks = rows.map(row => row.Tasks);  // ← Unwrap the table name wrapper

// For JOINs
const joinRows = await catalystApp.zcql().executeZCQLQuery(
  'SELECT * FROM Tasks JOIN Users ON Tasks.UserId = Users.ROWID'
);
const items = joinRows.map(row => ({ task: row.Tasks, user: row.Users }));

HTTP Payload Limits

Function Type Max Request Body Max Response Body
Advanced I/O 250 MB 250 MB
Basic I/O 250 MB 250 MB
AppSail No explicit limit (configurable) No explicit limit

For large uploads, consider using pre-signed Stratus URLs for direct browser → Stratus upload (bypasses the function entirely).

Common Errors

Error Cause Fix
busboy never emits finish event Pipe not set up before response end Ensure req.pipe(bb) and finish listener registered before piping
File upload silently truncated Function memory limit exceeded mid-stream Use pre-signed Stratus URL for files > 50 MB
Chained function call times out Inner function cold start exceeds outer timeout Use invokeType: 'async' for fire-and-forget; Job functions for long pipelines
Cannot read properties of undefined (reading 'files') express-fileupload not added as middleware before route Add app.use(fileUpload()) before route definitions
'use strict';
const catalyst = require('zcatalyst-sdk-node');

module.exports = (event, context) => {
  try {
    const catalystApp = catalyst.initialize(context);
    const eventData = JSON.parse(event.getArgument());
    console.log('Event received:', eventData);
    context.close();
  } catch (error) {
    console.error('Event processing error:', error);
    context.close();
  }
};

Cron Function Template

'use strict';
const catalyst = require('zcatalyst-sdk-node');

module.exports = async (cronDetails, context) => {
  try {
    const catalystApp = catalyst.initialize(context);
    context.closeWithSuccess();
  } catch (error) {
    context.closeWithFailure();
  }
};

Job Function Template

'use strict';
const catalyst = require('zcatalyst-sdk-node');

module.exports = async (jobData, context) => {
  try {
    // catalyst.initialize(context) defaults to Admin scope in non-HTTP functions.
    // Passing { scope: 'admin' } is explicit but equivalent to the default.
    const catalystApp = catalyst.initialize(context, { scope: 'admin' });
    const jobDetails = jobData.getAllJobParams();
    const maxMs = context.getMaxExecutionTimeMs(); // 15 minutes

    const zcql = catalystApp.zcql();
    const rows = await zcql.executeZCQLQuery('SELECT * FROM MyTable LIMIT 0, 300');

    context.closeWithSuccess();
  } catch (error) {
    context.closeWithFailure();
  }
};

Integration Function Template

'use strict';
const catalyst = require('zcatalyst-sdk-node');

module.exports = (event, context) => {
  try {
    const catalystApp = catalyst.initialize(context);
    const integrationData = JSON.parse(event.getArgument());
    context.close();
  } catch (error) {
    context.close();
  }
};

Integration functions are NOT available in EU, AU, IN, JP, SA, or CA data centers.


SDK Component Reference

npm install zcatalyst-sdk-node
const dataStore = catalystApp.datastore();
const table = dataStore.table('TableName');

const inserted = await table.insertRow({ ColumnName: value });
const insertedRows = await table.insertRows([{ ColumnName: value1 }, { ColumnName: value2 }]);
const updated = await table.updateRow({ ROWID: rowId, ColumnName: value });
await table.deleteRow(rowId);

const row = result.TableName; // unwrap ZCQL table wrapper, e.g. result.Orders

const zcql = catalystApp.zcql();
const cache = catalystApp.cache();
const stratus = catalystApp.stratus();
const email = catalystApp.email();
const userManagement = catalystApp.userManagement();
const pushNotification = catalystApp.pushNotification();
const search = catalystApp.search();
const nosql = catalystApp.nosql();
const connection = catalystApp.connection();

Data Store tables must exist before SDK operations target them. Functions cannot create tables programmatically — use Zoho MCP for table creation and schema updates.


Retry Behavior

Function Type Auto-retry on failure?
Basic I/O No
Advanced I/O No
Event Yes
Cron No — cron failures trigger Application Alerts only; the JOBS a cron submits retry per their job_config
Job Configurable — job_config: { number_of_retries: 0–10, retry_interval: 60–86400 /* SECONDS */ } at submit time. Each retry is a NEW job record linked by parent_job_id; the original job's record stays FAILURE (runtime-confirmed)
Integration No
Browser Logic No

For full retry semantics, see skills/catalyst-job-scheduling/references/job-scheduling-advanced.md.

Design background function handlers to be idempotent (safe to run multiple times).


SDK Auth Scope in Job / Cron / Event Functions

Default Behavior (Official)

catalyst.initialize(context) — used in Job, Cron, and Event function boilerplates — defaults to Admin scope. From the official docs:

"It is not mandatory to initialize with a scope. By default, a project that is initialized will have Admin privileges."

Explicitly passing { scope: 'admin' } is equivalent to the default. It is not required but is acceptable for clarity.

Scope only applies to: DataStore, FileStore, and ZCQL. Other services (Cache, Circuits, Zia, Push Notifications) always require admin regardless of the scope flag.

SDK Operation → Required Scope (Official Table)

DataStore Operation Scope Required
Get rows, Update rows, Delete rows, ZCQL query User or Admin
Bulk Read, Bulk Write, Bulk Delete Admin only
Other Component Scope Required
Cache Admin only
Circuits Admin only
Zia Services Admin only
File Store (upload, download, delete) User or Admin
File Store (other operations) Admin only
Email, Search User or Admin
Push Notifications Admin only

When to Use User Scope

User scope is only relevant in HTTP functions (Basic I/O / Advanced I/O) where a real user token is present in the request:

// HTTP function — user scope applies the caller's Data Store permissions
const userApp = catalyst.initialize(req, { scope: 'user' });

// HTTP function — admin scope bypasses per-user table permissions
const adminApp = catalyst.initialize(req, { scope: 'admin' });

In Job/Cron/Event functions there is no user in the request — the first argument is always context, not req. Passing { scope: 'user' } in a non-HTTP function will fail because there is no user token to resolve.

Bulk Read for Large DataStore Reads

When a Job function needs to read more than 300 rows, ZCQL pagination inside a 15-minute limit is risky for very large tables. Use the Bulk Read REST API instead:

  • Requires Admin scope (confirmed in SDK scope table)
  • Triggers an async job; use callback URL or poll the Check Bulk Read Status API
  • Returns a CSV download URL on completion
  • Can read up to 200,000 records per page

Runtime Cold start Warm invocation
Node.js 500ms–2s 50–200ms
Java 2–8s 50–200ms
Python 500ms–2s 50–200ms

Mitigation: Keep packages small, avoid heavy initialization outside the handler, use Job Scheduling to ping critical functions warm.


Common Errors

Error Cause Fix
res.status() / res.json() in node20 Raw-http template — no Express methods Use res.writeHead() + res.end()
req.body undefined Raw-http template — no body parser Manually parse with getBody() helper
req.query undefined Raw-http template — no query parser Use new URL(req.url, ...).searchParams
basicIO.write() called twice Can only be called once per execution Call basicIO.write() exactly once
Admin-scope for getCurrentUser() Admin scope has no user token Use user-scope: catalyst.initialize(req)
req.headers['authorization'] undefined Gateway strips it before function receives it Use catalyst.initialize(req) to identify the user
Using cors() middleware with Slate Gateway owns CORS for production origins Only set CORS headers for localhost
new Date(row.CREATEDTIME) wrong timezone Stored timestamp lacks timezone offset Append timezone offset before parsing
Inserting emoji into Data Store Unsupported character in column type Store a string key instead
Not paginating ZCQL Max 300 rows per query Use LIMIT offset, count
is_deployed: false in API responses All functions return this value regardless of live status Verify deployment status in the Console
Need to read >300 rows in a Job function ZCQL cap is 300 rows; paginating inside 15-min limit is risky Use the Bulk Read REST API for large-volume reads
busboy never emits finish event Pipe not set up before response end or req not passed correctly Ensure req.pipe(bb) and finish listener registered before piping
File upload silently truncated Function memory limit exceeded mid-stream Use pre-signed Stratus URL for files > 50 MB
Chained function call times out Inner function cold start adds latency beyond outer timeout Use invokeType: 'async' for fire-and-forget; Job functions for long pipelines
Cannot read properties of undefined (reading 'files') express-fileupload not added as middleware before route Add app.use(fileUpload()) before route definitions
Python Job: calling dir(context) before initialize() to "fix" SDK failures FALSE. dir(context) has no effect on SDK init — tested on Python 3.13 runtime. Both paths produce identical DataStore/ZCQL access. If Python Job SDK calls fail, check scope, table permissions, and column names — not dir() ordering
table.updateRow() hangs in Job functions with admin scope FALSE. table.updateRow() works reliably in Node.js Job functions with { scope: 'admin' } — tested, completes in ~425ms with no hang If updates hang, check that ROWID is present in the payload and that admin scope is used; user-scope in a Job function (no user token) will fail
Immediate jobs (job.submitJob()) have a shorter timeout than scheduled jobs FALSE. Immediate jobs have the same 15-minute timeout as scheduled Job functions — runtime-confirmed (130s sleep completed successfully) No special handling needed for immediate vs scheduled jobs

Source: SKILL.md on GitHub

No alerts16d3 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The skill provides templates and instructions for developing serverless functions on the Zoho Catalyst platform. It includes guidance on authentication, CORS, and deployment. The identified concerns are primarily related to standard data processing patterns in serverless development.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

Signed by skilld at 4a64353. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 3 weeks ago.

Activeupdated 3 weeks ago
metadata
{
  "version": "2.0.2"
}
Other metadata
compatibility
Requires Catalyst CLI (`npm install -g zcatalyst-cli`) and Node.js v20 (recommended; v14–v18 also supported). Java functions also require JDK 8, 11, or 17. Python functions require Python 3.9.

README badge

README badge for catalystbyzoho/agent-skills/catalyst-functions