Push Validation Script
Upload a validation script and register it on a model or deployment in one call
/api/v1/extraction-schema/{extractionSchemaId}/validation-scriptsUploads the script source to storage and registers (or updates) its database record. This is the only step needed to add a custom validation — no separate storage or database credentials are involved.
The endpoint is idempotent on key and content-addressed on source. The action field in the response tells you what happened:
action | Meaning |
|---|---|
created | No script with this key existed; stored as version 1. Responds 201. |
new-version | The source changed; stored under a new version and the record now points at it. |
metadata-updated | The source is byte-identical but name, description, behavior, enabled or timeoutMs differed. No new version. |
unchanged | Source and metadata already match. Nothing was written. |
Each version is stored under its own key, so earlier versions are never overwritten.
Requires the EDIT_MODELS permission (OWNER or DEVELOPER).
Parameters
| Name | Type | In | Required | Description |
|---|---|---|---|---|
extractionSchemaId | string | path | Yes | The model or deployment's extraction schema ID, returned when listing a project's deployments. |
key | string | body | Yes | Stable identifier for the script — lowercase letters, digits and hyphens only |
name | string | body | Yes | Human-readable name shown in the UI |
description | string | body | No | What the script checks |
behavior | string | body | No | SURFACE (report only) or RESEND (request a re-extraction of failing cells). Defaults to SURFACE |
source | string | body | Yes | The full ES module source, up to 512 KB |
enabled | boolean | body | No | Whether the script runs after extraction. Defaults to true |
timeoutMs | number | body | No | Per-run timeout in milliseconds, 1000–600000. Defaults to 60000 |
Request
import { readFileSync } from "node:fs";
const res = await fetch(
"https://app.limai.io/api/v1/extraction-schema/esch_abc123/validation-scripts",
{
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json",
},
body: JSON.stringify({
key: "vin-checksum",
name: "VIN format (ISO 3779)",
description: "Validates the VIN against the ISO 3779 format",
behavior: "RESEND",
source: readFileSync("./script.mjs", "utf-8"),
}),
}
);
const { action, script } = await res.json();Response
{
"action": "created",
"script": {
"id": "cvs_abc123",
"extractionSchemaId": "esch_abc123",
"key": "vin-checksum",
"name": "VIN format (ISO 3779)",
"description": "Validates the VIN against the ISO 3779 format",
"behavior": "RESEND",
"s3Key": "custom-validations/esch_abc123/vin-checksum/v1-9f2b7c1d4e05.mjs",
"version": 1,
"contentHash": "9f2b...",
"enabled": true,
"timeoutMs": 60000,
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
}
}Script contract
The script is a standalone ES module executed with node script.mjs input.json. It reads the extracted data from the path in process.argv[2] and writes a JSON { findings, breakdown? } object to stdout.
import { readFileSync } from 'node:fs'
import { pathToFileURL } from 'node:url'
function validate(input) {
const findings = []
for (const row of input.rows) {
// inspect row.cells[columnId].value against input.schema.tables
}
return { findings }
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const input = JSON.parse(readFileSync(process.argv[2], 'utf8'))
process.stdout.write(JSON.stringify(validate(input)))
}Each finding has the shape:
{
"ruleKey": "vin-length",
"scope": "ROW",
"tableId": "tbl_abc",
"rowId": "row_abc",
"columnIds": ["col_vin"],
"passed": false,
"message": "VIN has 16 characters, expected exactly 17",
"expected": "17 characters",
"actual": "WVWZZZ1KZAW12345",
"resend": true,
"resendHint": "Re-read field E and extract all 17 characters"
}