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:
- Project ID - Found in Project Settings
- Secret Key - Generated in Keys section (starts with
sk_) - 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:
| Mode | Behavior |
|---|---|
strict (default) | Requires a verified entitlement token (offline if provided, otherwise via secret key exchange) |
demo | Skip 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
- Initialize once - Call
initHeadless()once, then run multiple jobs - Use Arrow IPC - Set
exportFormat: "ipc"for large files - Process in parallel - Run multiple
runHeadlessJob()calls concurrently - Explicit WASM bytes - Use
wasmBytes(orwasmPath) in containers/edge to avoid resolution overhead
What's Next?
- Add validation rules - Define complex validation
- Transform expressions - Clean and normalize data
- PII masking - Redact sensitive data
- Error codes reference - Handle errors programmatically