Skip to main content

Node.js / Headless Quickstart

Run RowOps without a browser ideal for CLI tools, CI/CD pipelines, scheduled jobs, and server-side processing.

Time required: 10 minutes

Prerequisites:

  • Node.js 18+
  • An RowOps account with a Secret Key or a signed Entitlement Token

Step 1: Install the Package

npm install @rowops/headless

Step 2: Get Your Credentials

You need:

  1. Project ID - Found in Project Settings
  2. Secret Key - Generated in Keys section (starts with sk_)
  3. Entitlement Token - Signed token for offline/air-gapped runs (optional)

Secret keys are for server-side use only. Never expose them in client code.


Step 3: Basic Usage

import { initHeadless, runHeadlessJob, clearHeadlessState } from "@rowops/headless";

async function main() {
// 1. Initialize the headless runtime
await initHeadless({
projectId: "proj_your_project_id",
secretKey: "sk_your_secret_key",
});

try {
// 2. Run a validation job
const result = await runHeadlessJob({
inputPath: "./input.csv",
outputPath: "./output.csv",
schema: {
id: "employees",
name: "Employee Schema",
fields: [
{ key: "first_name", type: "string", required: true },
{ key: "last_name", type: "string", required: true },
{ key: "email", type: "string", required: true },
{ key: "department", type: "string", required: false },
{ key: "salary", type: "number", required: false },
],
},
});

// 3. Check results
console.log(`Parsed: ${result.manifest.totals.parsedRows} rows`);
console.log(`Valid: ${result.manifest.totals.validRows} rows`);
console.log(`Invalid: ${result.manifest.totals.invalidRows} rows`);
console.log(`Output: ${result.outputPath}`);

} finally {
// 4. Clean up
clearHeadlessState();
}
}

main().catch(console.error);

For offline/air-gapped CI, pass a signed entitlement token instead of a secret key:

await initHeadless({
projectId: "proj_your_project_id",
entitlementToken: process.env.ROWOPS_ENTITLEMENT_TOKEN!,
});

Step 4: Run It

npx tsx validate.ts

Or with Node.js directly:

node --loader ts-node/esm validate.ts

Configuration Options

initHeadless Options

interface HeadlessOptions {
// Required
projectId: string;
secretKey?: string;
entitlementToken?: string;

// Optional
endpoint?: string; // Custom API endpoint
licenseMode?: LicenseMode; // "strict" | "demo"
wasmPath?: string; // Explicit path to WASM binary
wasmBytes?: Uint8Array; // Explicit WASM bytes (edge/serverless)
fileAdapter?: HeadlessFileAdapter; // Custom file adapter for cache/wasm resolution
}

Provide either secretKey (online verification) or entitlementToken (offline verification).

For edge/serverless runtimes, prefer wasmBytes and a custom fileAdapter instead of filesystem path resolution. Import the adapter type with import type { HeadlessFileAdapter } from "@rowops/headless";.

License modes:

ModeBehavior
strict (default)Requires a verified entitlement token (offline if provided, otherwise via secret key exchange)
demoSkip validation, 100 row limit, adds watermark

runHeadlessJob Options

interface HeadlessJobInput {
// Required
inputPath: string; // Path to input file
outputPath: string; // Path for output file
schema: ValidationSchema;

// Optional
schemaId?: string; // Schema ID for tracking
sheetName?: string; // Sheet name for XLSX
exportFormat?: "csv" | "ipc"; // Output format (default: csv)
maskConfig?: MaskConfig; // PII masking rules
transformConfig?: TransformConfig; // Transform pipeline
manifestPath?: string; // Path for manifest JSON
}

Complete Examples

Validate and Report Errors

import { initHeadless, runHeadlessJob, clearHeadlessState } from "@rowops/headless";

async function validateWithReport() {
await initHeadless({
projectId: process.env.ROWOPS_PROJECT_ID!,
secretKey: process.env.ROWOPS_SECRET_KEY!,
});

try {
const result = await runHeadlessJob({
inputPath: "./employees.csv",
outputPath: "./employees-valid.csv",
manifestPath: "./employees-manifest.json",
schema: {
id: "employees",
name: "Employees",
fields: [
{
key: "email",
type: "string",
required: true,
regex: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
},
{
key: "name",
type: "string",
required: true,
},
{
key: "department",
type: "string",
required: true,
enumValues: ["Engineering", "Sales", "Marketing", "HR", "Finance"],
},
],
},
});

// Check validation results
const validation = result.snapshot.validation;

if (validation?.invalidRowCount === 0) {
console.log("All rows valid!");
} else {
console.log(`Found ${validation?.invalidRowCount} invalid rows`);

// Display error summary
for (const group of validation?.errorGroups ?? []) {
console.log(` ${group.count} rows: ${group.field} - ${group.code}`);
}
}

console.log(`Output written to: ${result.outputPath}`);
console.log(`Manifest written to: ${result.manifestPath}`);

} finally {
clearHeadlessState();
}
}

validateWithReport();

Transform Data

import { initHeadless, runHeadlessJob, clearHeadlessState } from "@rowops/headless";

async function transformData() {
await initHeadless({
projectId: process.env.ROWOPS_PROJECT_ID!,
secretKey: process.env.ROWOPS_SECRET_KEY!,
});

try {
const result = await runHeadlessJob({
inputPath: "./raw-contacts.csv",
outputPath: "./cleaned-contacts.csv",
schema: {
id: "contacts",
name: "Contacts",
fields: [
{ key: "email", type: "string", required: true },
{ key: "name", type: "string", required: true },
{ key: "country_code", type: "string", required: false },
],
},
transformConfig: {
version: 1,
operations: [
// Lowercase and trim email
{
kind: "derive",
name: "email",
expression: {
kind: "lower",
operand: {
kind: "trim",
operand: { kind: "column", name: "email" },
},
},
},
// Trim name
{
kind: "derive",
name: "name",
expression: {
kind: "trim",
operand: { kind: "column", name: "name" },
},
},
// Uppercase country code
{
kind: "derive",
name: "country_code",
expression: {
kind: "upper",
operand: {
kind: "trim",
operand: { kind: "column", name: "country_code" },
},
},
},
],
},
});

console.log(`Transformed ${result.manifest.totals.validRows} rows`);

} finally {
clearHeadlessState();
}
}

transformData();

Batch Processing Multiple Files

import { initHeadless, runHeadlessJob, clearHeadlessState } from "@rowops/headless";
import fs from "fs";
import path from "path";

interface BatchConfig {
inputDir: string;
outputDir: string;
archiveDir?: string;
}

async function batchProcess(config: BatchConfig) {
// Initialize once for all files
await initHeadless({
projectId: process.env.ROWOPS_PROJECT_ID!,
secretKey: process.env.ROWOPS_SECRET_KEY!,
});

const schema = {
id: "employees",
name: "Employees",
fields: [
{ key: "email", type: "string", required: true },
{ key: "name", type: "string", required: true },
],
};

// Find all CSV/XLSX files
const files = fs.readdirSync(config.inputDir)
.filter(f => /\.(csv|xlsx|xls)$/i.test(f))
.map(f => path.join(config.inputDir, f));

console.log(`Found ${files.length} files to process`);

for (const filePath of files) {
const baseName = path.basename(filePath, path.extname(filePath));
const outputPath = path.join(config.outputDir, `${baseName}-validated.csv`);
const manifestPath = path.join(config.outputDir, `${baseName}-manifest.json`);

try {
const result = await runHeadlessJob({
inputPath: filePath,
outputPath,
manifestPath,
schema,
});

console.log(
`${path.basename(filePath)}: ` +
`${result.manifest.totals.parsedRows}${result.manifest.totals.validRows} rows`
);

// Archive processed file
if (config.archiveDir) {
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const archivePath = path.join(
config.archiveDir,
`${timestamp}_${path.basename(filePath)}`
);
fs.renameSync(filePath, archivePath);
}

} catch (error) {
console.error(`${path.basename(filePath)}: ${(error as Error).message}`);
}
}

clearHeadlessState();
}

batchProcess({
inputDir: "./incoming",
outputDir: "./processed",
archiveDir: "./archive",
});

CI/CD Integration

GitHub Actions

name: Validate CSV Data

on:
push:
paths:
- "data/**/*.csv"

jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "20"

- run: npm install @rowops/headless

- name: Validate CSV files
env:
ROWOPS_PROJECT_ID: ${{ secrets.ROWOPS_PROJECT_ID }}
ROWOPS_SECRET_KEY: ${{ secrets.ROWOPS_SECRET_KEY }}
run: npx tsx scripts/validate-data.ts

- name: Upload validation results
uses: actions/upload-artifact@v4
with:
name: validation-results
path: output/*.json

Docker

FROM node:20-slim

WORKDIR /app

# Install dependencies
COPY package*.json ./
RUN npm install

# Copy validation script
COPY validate.ts ./

# Run validation
CMD ["npx", "tsx", "validate.ts"]

Environment Variables

# Required
ROWOPS_PROJECT_ID=proj_xxx
ROWOPS_SECRET_KEY=sk_xxx

# Optional
ROWOPS_LICENSE_MODE=demo # For air-gapped environments
ROWOPS_WASM_PATH=/app/wasm/rowops_wasm_bg.wasm # Custom WASM path

Output Format

Manifest JSON

The manifestPath option generates a JSON file with import metadata:

{
"version": 1,
"datasetNonce": 3172493810,
"schemaId": "contacts",
"createdAt": 1715866400000,
"totals": {
"parsedRows": 1500,
"validRows": 1450,
"invalidRows": 50
},
"receipts": {
"errorGroups": [],
"receiptSummary": {
"totalGroups": 0,
"totalErrors": 0,
"uniqueErrorRows": 0,
"byCode": {},
"byField": {}
}
},
"metrics": {
"wallTimeByPhaseMs": { "parsing": 120, "validating": 85, "exporting": 45 },
"bytesReadByPhase": { "parsing": 1048576 },
"bytesWrittenByPhase": { "exporting": 524288 },
"chunksByPhase": { "parsing": 3, "exporting": 3 },
"rowsValidByPhase": { "validating": 1450 },
"rowsInvalidByPhase": { "validating": 50 }
}
}

Arrow IPC Output

For maximum performance and compatibility with data tools:

const result = await runHeadlessJob({
inputPath: "./data.csv",
outputPath: "./data.arrow",
exportFormat: "ipc", // Arrow IPC format
schema,
});

// Result can be read with Apache Arrow libraries

Error Handling

import { initHeadless, runHeadlessJob, clearHeadlessState } from "@rowops/headless";

async function main() {
try {
await initHeadless({
projectId: process.env.ROWOPS_PROJECT_ID!,
secretKey: process.env.ROWOPS_SECRET_KEY!,
});
} catch (error) {
console.error("Failed to initialize:", error);
console.error("Check your project ID and secret key");
process.exit(1);
}

try {
const result = await runHeadlessJob({
inputPath: "./data.csv",
outputPath: "./output.csv",
schema: { /* ... */ },
});

if (result.manifest.totals.invalidRows > 0) {
console.error("Validation errors found");
process.exit(2);
}

console.log("Success!");

} catch (error) {
// Errors include code and details
console.error("Job failed:", error);
process.exit(1);

} finally {
clearHeadlessState();
}
}

Performance Tips

  1. Initialize once - Call initHeadless() once, then run multiple jobs
  2. Use Arrow IPC - Set exportFormat: "ipc" for large files
  3. Process in parallel - Run multiple runHeadlessJob() calls concurrently
  4. Explicit WASM bytes - Use wasmBytes (or wasmPath) in containers/edge to avoid resolution overhead

What's Next?