npx skills add ...
npx skills add celigo/ai --skill writing-scripts
Write Celigo JavaScript hook scripts -- preSavePage, preMap, postMap, postSubmit, postResponseMap, filter, transform, branching, handleRequest. Use when creating or editing scripts, choosing the right hook point, understanding input/output data shapes, or debugging script behavior.
npx skills add celigo/ai --skill writing-scripts
A script is a JavaScript function that runs at a specific hook point in the Celigo data pipeline. Scripts handle logic that expressions, filters, and visual mappings cannot -- complex conditionals, cross-record calculations, API calls within the pipeline, and custom routing.
Concerns when writing a script:
options contains and what the function must return (array length rules are strict)import three built-in modules: integrator-api (call Celigo APIs), dayjs (date/time manipulation), and sjcl (Stanford JavaScript Crypto Library for hashing/encryption)Used across flows, APIs, and tools.
Every script function runs at a specific point in the pipeline. Choose based on when you need to act and what data you need access to.
| Hook | Runs on | When | Input | Must return |
|---|---|---|---|---|
preSavePage | Export | After retrieval, before pipeline | options.data[], errors[], files[], retryData{} | { data[], errors[], abort, newErrorsAndRetryData[] } |
preMap | Import | Before field mapping | options.data[] (unmapped records) | Array matching data.length: { data }, { errors }, or {} to skip |
postMap | Import | After field mapping, before submit | options.preMapData[], postMapData[] | Array matching postMapData.length: { data }, { errors }, or {} to skip |
postSubmit | Import | After destination submission | options.preMapData[], postMapData[], responseData[] | responseData[] (same length, modified) |
postAggregate | Import | After file aggregation upload | options.postAggregateData: { success, _json, code, message } | void |
| Hook | When | Input | Must return |
|---|---|---|---|
filter | Per-record, before processing | options.record | boolean (true = process) |
input_filter | Per-record on lookup exports | options.record | boolean (true = include) |
transform | Per-record, reshaping before mapping | options.record | Transformed record |
filter and transform have expression-based alternatives. Only use a script when the logic is too complex for an expression (multi-field conditionals, date math, external lookups).
| Hook | Runs on | When | Input | Must return |
|---|---|---|---|---|
postResponseMap | Page processor (flow/API/tool) | After response mapping merges results | options.postResponseMapData[], responseData[] | postResponseMapData[] (same length) |
Configured on the flow's pageProcessors[] entry, not on the export/import. Plan this hook when building the resource, but wire it at the flow level.
| Hook | Runs on | When | Input | Must return |
|---|---|---|---|---|
branching | Router | Per-record routing decision | options.record, settings | number[] (branch indices, e.g., [0, 2]) |
handleRequest | API resource | Incoming HTTP request (script-mode API) | options.method, headers, queryString, body, rawBody | { statusCode, headers?, body } |
contentBasedFlowRouter | AS2 connection | EDI message routing | options.httpHeaders, mimeHeaders, rawMessageBody | { _flowId, _exportId } |
| When you need to... | Use hook | Configured on | Input / Output |
|---|---|---|---|
| Transform or filter a batch after retrieval | preSavePage | Export | Receives pages of records, returns pages (with optional errors) |
| Filter individual records before processing | filter | Export or import | Receives single record, returns boolean (true = keep) |
| Filter records entering a lookup export | input_filter | Export (lookup) | Receives single record, returns boolean (true = include) |
| Reshape records before mapping | transform | Export or import | Receives single record, returns transformed record |
| Transform records before field mapping | preMap | Import | Receives unmapped records array, returns array (same length) |
| Transform records after field mapping | postMap | Import | Receives pre-map + post-map arrays, returns array (same length) |
| Process API responses after submission | postSubmit | Import | Receives pre-map, post-map, and response arrays, returns response array |
| Handle results after file aggregation | postAggregate | Import (file) | Receives aggregation result, returns void |
| Post-response processing (merge lookup/import results) | postResponseMap | Flow pageProcessors[] entry | Receives merged records + response data, returns merged records (same length) |
| Route records to branches | branching | Router in flow/tool | Receives single record + settings, returns branch indices array |
| Handle incoming HTTP requests (script-mode API) | handleRequest | API resource | Receives method, headers, query, body; returns { statusCode, headers?, body } |
| Route EDI messages to flows | contentBasedFlowRouter | AS2 connection | Receives HTTP/MIME headers + raw body, returns { _flowId, _exportId } |
A script resource needs only two fields:
name -- descriptive name (convention: <System> - <step> - <hookType>, e.g., "Salesforce - getBatchRecords - postResponseMap")content -- the JavaScript source code as a stringSee references/schemas/request.yml for the full create/update schema.
preSavePage, filter, transform, and input_filter hooks are wiredpreMap, postMap, postSubmit, and postAggregate hooks are wiredpostResponseMap and branching hooks are wiredMost hooks receive these context fields in options:
_flowId, _integrationId, _apiId, _parentIntegrationId -- execution context IDs_exportId or _importId -- the step's resource ID_connectionId -- the connection in usesettings -- custom settings in scope for the resourcetestMode -- boolean, whether running in test/preview modejob -- the current job objectScripts run at twelve function points, grouped into four categories. The Hook Points tables above give each one's input/output contract; this is the mental model for which kind of point you're wiring and whether a non-script alternative exists.
preSavePage, preMap, postMap, postSubmit, postAggregatepageProcessors[] entry, not the step) -- postResponseMapfilter, input_filter, transform, branchingcontentBasedFlowRouter (on an AS2 connection) and handleRequest (on a script-mode API)Script-only points have no declarative equivalent: postSubmit, postResponseMap, postAggregate, contentBasedFlowRouter, and handleRequest. On those slots a script is the only option. The four script-mode slots (filter, input_filter, transform, branching) each hold either a declarative rule tree or a script -- never both -- so prefer the declarative path there unless the logic genuinely can't be expressed as rules (see Declarative vs Script Mode).
The four mode-switchable slots -- filter, input_filter, transform, and branching -- hold a declarative rule tree or a script reference at any one moment, not both. Because the slot's contents change, switching modes is a two-part operation.
From script mode to declarative mode (the common direction -- prototype with a script, then clean up):
From declarative mode to script mode (rarer -- the rules engine couldn't express what you need):
Wiring a script and clearing it are mirror operations on the same slot. Recognize the mode-swap in phrasing like "switch the filter to rules", "convert this transform back to expressions", or "use a script for this filter instead of rules".
Map your goal to the right hook point using the Hook Point Decision Matrix above.
Filter, transform, and output filter all have expression-based alternatives. Expressions are simpler to maintain and don't require a script resource. Use a script only when you need:
integrator-apipreMapData alongside postMapDataBuild the script with the correct function name matching the hook point. A single script can contain multiple functions.
See references/schemas/request.yml for the create/update schema and references/schemas/response.yml for the response shape.
Key fields:
name -- descriptive name (convention: <System> - <step> - <hookType>, e.g., "Salesforce - getBatchRecords - postResponseMap")content -- the JavaScript source codeWiring depends on the hook type:
| Hook | Wiring pattern | Where |
|---|---|---|
preSavePage, preMap, postMap, postSubmit, postAggregate | hooks.{hookType}: { _scriptId, function } | Export or import resource |
filter, input_filter, transform | {field}: { type: "script", script: { _scriptId, function } } | Export or import resource |
postResponseMap | hooks.postResponseMap: { _scriptId, function } | Flow pageProcessors[] entry |
branching | routeRecordsUsing: "script" + script reference | Router in flow |
handleRequest | script: { _scriptId, function } + type: "script" | API resource |
contentBasedFlowRouter | as2.contentBasedFlowRouter: { _scriptId, function } | AS2 connection |
Hook-based attachment (preSavePage, preMap, etc.) is additive -- adding a hook doesn't remove existing config. Replace-based attachment (filter, transform) replaces the existing filter/transform expression.
Scripts can import three built-in modules:
Call Celigo APIs from within the script -- run exports, read connections, trigger imports.
Useful in preSavePage for enrichment, handleRequest for orchestration, and postSubmit for triggering downstream processes.
Date and time manipulation. Handles parsing, formatting, diffing, and timezone conversions without manual date math.
Stanford JavaScript Crypto Library for hashing, encryption, and HMAC generation.
Script logic is runtime-dependent -- it only works against the specific shape of data it handles -- so a script is written and validated against a sample input. The sample comes from the step's recent runs, a test/run capture on the parent flow, or a JSON example you supply. A script written without sample data is written blind.
Treat authoring as a loop, not a one-shot:
A script that fails on the first pass isn't a failure; it's the first turn of the loop -- the runtime error and the code are both visible, so the next pass is informed by what went wrong. When no sample is available (the step has never run and no parent provided records), supply a JSON example before writing the script; a user-supplied sample plays the same validation role as captured runtime data.
Scripts write to a per-script execution log using standard console methods. What gets captured depends on the level:
console.error(), console.warn(), console.info(), and console.log() are always captured -- no setup, no toggle (info and log are equivalent).console.debug() is gated: its output is persisted only while a time-bounded debug window is open on the script. When the window is closed, console.debug() still runs but its output is dropped."Debugging a script" here means exactly this -- turning on console.debug() capture for a window. It is not breakpoint-style debugging; there is no pausing or stepping through code. Open a window only when you need console.debug() output; for "why did this fail?" / "what errors happened?", the always-captured error/warn/info/log entries are usually enough.
The debug window is time-bounded and expires automatically -- it defaults to a short window (15 minutes) and is opened with celigo scripts enable-debug <id> [--duration <minutes>]. There's no need to close it manually, though celigo scripts disable-debug <id> ends it early.
Each log entry records its time, level (INFO / WARN / ERROR / DEBUG), the message, and two locating fields:
functionType -- which hook produced the entry (preMap, postSubmit, etc.)_resourceId -- the export or import that ran the hookBecause one script can carry many functions across many hook sites, an unfiltered log stream interleaves entries from every consumer. Filter aggressively when reading -- by flow (--flow-id), by time (--since / --start-date / --end-date), and by level (--level). The practical query is "logs for this script, in this flow, on this step, during this window."
Before creating or updating a script, verify:
preSavePage function for a hooks.preSavePage reference)preMap, postMap, postSubmit, postResponseMap) return arrays that match the input array length exactly{ errors: [...] } return values, not thrown exceptions (which fail the entire page)content field is included on PUT -- omitting content on update erases the code; always GET first, modify, then PUTceligo scripts disable-debug <id> to avoid log noise in productionpreMap, postMap, and postResponseMap return arrays MUST match the input array length. Returning fewer or more elements fails the entire page silently or with cryptic errors.abort: true stops pagination, not the flow. In preSavePage, setting abort: true tells the export to stop generating new pages. It does NOT stop the flow or cancel processing of the current page's records.content is not returned in list responses. celigo scripts list shows metadata only. You must celigo scripts get <id> to see the actual JavaScript code.content if omitted. Always GET the script first, modify, then PUT the complete object. The set command handles this automatically.preSavePage and preMap functions can be wired to different resources by specifying the function name in each hook reference.{ errors: [...] }) for per-record errors.postResponseMap lives on the flow, not the resource. The hook is configured on the pageProcessors[] entry in the flow/API/tool, even though it processes export or import response data.console.log() output goes to script logs, not stdout. Use celigo scripts debug-logs to see output. Logs require debug mode to be enabled for debug-level messages.console.debug() needs the debug window. error / warn / info / log are always captured; debug output is persisted only while a time-bounded debug window is open (celigo scripts enable-debug). A closed window silently drops console.debug() output.functionType and _resourceId identify where it came from.filter, input_filter, transform, branching) hold a rule tree or a script, never both -- removing the script drops the slot back to rules, and wiring a script replaces the rules.| Error / Symptom | Cause | Fix |
|---|---|---|
| "The number of elements in the return value must match the input" | Batch hook return array length differs from input | Ensure return array has exactly data.length (preMap) or postMapData.length (postMap) elements; use {} for skipped records |
| All records on a page fail with no per-record detail | Unhandled exception thrown in batch hook | Wrap logic in try/catch; return { errors: [...] } per record instead of throwing |
| Script content is empty after update | PUT omitted the content field | Always GET first, modify, then PUT the complete object (or use celigo scripts set) |
abort: true set but flow keeps running | abort only stops pagination; current page still processes | This is expected behavior; use error returns or filter to skip individual records |
| Script not executing / no logs | Script not wired to any resource, or debug mode not enabled | Verify _scriptId + function reference on the export/import/flow; enable debug with celigo scripts enable-debug |
| "Function not found" or similar | function name in hook reference doesn't match an exported function in the script | Check the function name matches exactly (case-sensitive) between the hook config and the script's export |
| Filter always returns all/no records | Filter function returns truthy/falsy value instead of strict boolean | Return explicit true or false; avoid returning objects or undefined |
postResponseMap not firing | Hook wired on the import/export instead of the flow's pageProcessors[] entry | Move the hook config to the pageProcessors[] entry in the flow, not the resource |
console.debug() lines missing from logs | No debug window was open while the script ran | Open a window first (celigo scripts enable-debug <id>), then reproduce; error/warn/info/log don't require it |
| Log stream is a confusing mix of unrelated entries | Script is shared across many hooks/flows and the query is unfiltered | Filter by --flow-id, --level, and date range; use each entry's functionType / _resourceId to identify the origin |
import { exports, imports, connections } from 'integrator-api'
const result = exports.run({ _id: 'exportId' })
const conn = connections.get({ _id: 'connectionId' })import dayjs from 'dayjs'
const formatted = dayjs(record.createdAt).format('YYYY-MM-DD')
const isRecent = dayjs().diff(dayjs(record.updatedAt), 'day') < 7import sjcl from 'sjcl'
const hash = sjcl.hash.sha256.hash(payload)
const hexDigest = sjcl.codec.hex.fromBits(hash)celigo scripts list
celigo scripts get <id>
celigo scripts create < script.json
celigo scripts update <id> < script.json
celigo scripts set <id> name="New Name"
celigo scripts delete <id>celigo scripts debug-logs <id> [--limit N] [--offset N] [--level error|warn|info|debug] [--start-date ISO] [--end-date ISO]
celigo scripts enable-debug <id> [--duration <minutes>]
celigo scripts disable-debug <id>
celigo scripts debug-logs <id> [--since <minutes>] [--flow-id <id>]