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.
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:
Choosing the right hook point -- which function type matches what you're trying to accomplish
Input/output contracts -- what options contains and what the function must return (array length rules are strict)
Expression alternative -- filter, transform, and output filter have expression-based alternatives that don't require a script; prefer expressions when possible
Available modules -- scripts can import three built-in modules: integrator-api (call Celigo APIs), dayjs (date/time manipulation), and sjcl (Stanford JavaScript Crypto Library for hashing/encryption)
One script, many functions -- a single script resource can contain multiple exported functions, each wired independently to different hook points
Used across flows, APIs, and tools.
Hook Points
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.
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).
Flow-Level Hook
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.
settings -- custom settings in scope for the resource
testMode -- boolean, whether running in test/preview mode
job -- the current job object
Function Point Categories
Scripts 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.
Step-level pipeline hooks (on the export or import) -- preSavePage, preMap, postMap, postSubmit, postAggregate
Parent-level response hook (on the flow/API/tool pageProcessors[] entry, not the step) -- postResponseMap
Script-mode replacements for declarative slots -- filter, input_filter, transform, branching
Resource-specific function points -- contentBasedFlowRouter (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).
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):
Clear the script from the slot. The slot reverts to declarative mode by default.
Author the declarative rule for that slot (rules-engine filter, Mapper 2.0 transform, or router input-filter rule).
From declarative mode to script mode (rarer -- the rules engine couldn't express what you need):
Wire a script into the slot. The declarative rules already there are replaced by the script reference automatically.
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".
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:
Multi-step logic or loops
Cross-record calculations (totals, deduplication)
External API calls via integrator-api
Error handling with retry data
Access to preMapData alongside postMapData
3. Check for existing scripts in the account
celigo scripts list
celigo scripts get <id> # content is only returned on individual GET
4. Create the script resource
Build 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")
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:
Generate or edit the function against the sample input.
Run it against that sample.
Check the output for errors or obviously-wrong results.
Iterate -- refine and re-run until it passes.
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.
Execution Logs and the Debug Window
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 hook
Because 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."
Pre-Submit Checklist
Before creating or updating a script, verify:
Hook point is correct -- the function name matches the hook type being wired (e.g., preSavePage function for a hooks.preSavePage reference)
Return value matches contract -- batch hooks (preMap, postMap, postSubmit, postResponseMap) return arrays that match the input array length exactly
Error handling uses return pattern, not throw -- per-record errors use { errors: [...] } return values, not thrown exceptions (which fail the entire page)
Expression alternative considered -- filter, transform, and output filter can use expressions; only use a script when expressions cannot handle the logic
content field is included on PUT -- omitting content on update erases the code; always GET first, modify, then PUT
Debug mode is disabled after testing -- celigo scripts disable-debug <id> to avoid log noise in production
Gotchas
Array length contracts are strict.preMap, 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.
Script 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.
PUT erases content if omitted. Always GET the script first, modify, then PUT the complete object. The set command handles this automatically.
One script resource can contain multiple functions. A single script with both preSavePage and preMap functions can be wired to different resources by specifying the function name in each hook reference.
Throwing an exception fails the entire page. In batch hooks (preSavePage, preMap, postMap, postSubmit), an unhandled exception fails ALL records on that page, not just one. Use the error return pattern ({ 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.
filter/transform scripts replace expression-based alternatives. Wiring a script filter replaces any existing expression filter. They cannot coexist on the same resource.
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.
Only 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.
Shared-script logs interleave across hook sites. One script can hold many functions used by many exports/imports, so its log stream mixes entries from every consumer. Filter by flow, level, and time when reading; each entry's functionType and _resourceId identify where it came from.
Clearing a script-mode filter/transform reverts the slot to declarative mode. The four mode-switchable slots (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.
Common Errors
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
1---2name: writing-scripts3description: 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.4---56<!-- TIER:1 -->78# Writing Scripts910A 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.1112Concerns when writing a script:1314- **Choosing the right hook point** -- which function type matches what you're trying to accomplish15- **Input/output contracts** -- what `options` contains and what the function must return (array length rules are strict)16- **Expression alternative** -- filter, transform, and output filter have expression-based alternatives that don't require a script; prefer expressions when possible17- **Available modules** -- scripts can `import` three built-in modules: `integrator-api` (call Celigo APIs), `dayjs` (date/time manipulation), and `sjcl` (Stanford JavaScript Crypto Library for hashing/encryption)18- **One script, many functions** -- a single script resource can contain multiple exported functions, each wired independently to different hook points1920Used across flows, APIs, and tools.2122## Hook Points2324Every 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.2526### Data Pipeline Hooks2728| Hook | Runs on | When | Input | Must return |29|------|---------|------|-------|-------------|30| `preSavePage` | Export | After retrieval, before pipeline | `options.data[]`, `errors[]`, `files[]`, `retryData{}` | `{ data[], errors[], abort, newErrorsAndRetryData[] }` |31| `preMap` | Import | Before field mapping | `options.data[]` (unmapped records) | Array matching `data.length`: `{ data }`, `{ errors }`, or `{}` to skip |32| `postMap` | Import | After field mapping, before submit | `options.preMapData[]`, `postMapData[]` | Array matching `postMapData.length`: `{ data }`, `{ errors }`, or `{}` to skip |33| `postSubmit` | Import | After destination submission | `options.preMapData[]`, `postMapData[]`, `responseData[]` | `responseData[]` (same length, modified) |34| `postAggregate` | Import | After file aggregation upload | `options.postAggregateData: { success, _json, code, message }` | void |3536### Record-Level Processors (on export or import)3738| Hook | When | Input | Must return |39|------|------|-------|-------------|40| `filter` | Per-record, before processing | `options.record` | `boolean` (true = process) |41| `input_filter` | Per-record on lookup exports | `options.record` | `boolean` (true = include) |42| `transform` | Per-record, reshaping before mapping | `options.record` | Transformed record |4344**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).4546### Flow-Level Hook4748| Hook | Runs on | When | Input | Must return |49|------|---------|------|-------|-------------|50| `postResponseMap` | Page processor (flow/API/tool) | After response mapping merges results | `options.postResponseMapData[]`, `responseData[]` | `postResponseMapData[]` (same length) |5152Configured 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.5354### Routing and Handlers5556| Hook | Runs on | When | Input | Must return |57|------|---------|------|-------|-------------|58| `branching` | Router | Per-record routing decision | `options.record`, `settings` | `number[]` (branch indices, e.g., `[0, 2]`) |59| `handleRequest` | API resource | Incoming HTTP request (script-mode API) | `options.method`, `headers`, `queryString`, `body`, `rawBody` | `{ statusCode, headers?, body }` |60| `contentBasedFlowRouter` | AS2 connection | EDI message routing | `options.httpHeaders`, `mimeHeaders`, `rawMessageBody` | `{ _flowId, _exportId }` |6162## Quick Reference6364### Hook Point Decision Matrix6566| When you need to... | Use hook | Configured on | Input / Output |67|---|---|---|---|68| Transform or filter a batch after retrieval | `preSavePage` | Export | Receives pages of records, returns pages (with optional errors) |69| Filter individual records before processing | `filter` | Export or import | Receives single record, returns boolean (true = keep) |70| Filter records entering a lookup export | `input_filter` | Export (lookup) | Receives single record, returns boolean (true = include) |71| Reshape records before mapping | `transform` | Export or import | Receives single record, returns transformed record |72| Transform records before field mapping | `preMap` | Import | Receives unmapped records array, returns array (same length) |73| Transform records after field mapping | `postMap` | Import | Receives pre-map + post-map arrays, returns array (same length) |74| Process API responses after submission | `postSubmit` | Import | Receives pre-map, post-map, and response arrays, returns response array |75| Handle results after file aggregation | `postAggregate` | Import (file) | Receives aggregation result, returns void |76| Post-response processing (merge lookup/import results) | `postResponseMap` | Flow `pageProcessors[]` entry | Receives merged records + response data, returns merged records (same length) |77| Route records to branches | `branching` | Router in flow/tool | Receives single record + settings, returns branch indices array |78| Handle incoming HTTP requests (script-mode API) | `handleRequest` | API resource | Receives method, headers, query, body; returns `{ statusCode, headers?, body }` |79| Route EDI messages to flows | `contentBasedFlowRouter` | AS2 connection | Receives HTTP/MIME headers + raw body, returns `{ _flowId, _exportId }` |8081### Minimum Required Fields8283A script resource needs only two fields:8485- `name` -- descriptive name (convention: `<System> - <step> - <hookType>`, e.g., `"Salesforce - getBatchRecords - postResponseMap"`)86- `content` -- the JavaScript source code as a string8788See [references/schemas/request.yml](references/schemas/request.yml) for the full create/update schema.8990## Related Skills9192- [configuring-exports > Quick Reference](../configuring-exports/SKILL.md#quick-reference) -- export configuration, where `preSavePage`, `filter`, `transform`, and `input_filter` hooks are wired93- [configuring-imports > Quick Reference](../configuring-imports/SKILL.md#quick-reference) -- import configuration, where `preMap`, `postMap`, `postSubmit`, and `postAggregate` hooks are wired94- [building-flows > How to Build a Flow](../building-flows/SKILL.md#how-to-build-a-flow) -- flow construction, where `postResponseMap` and `branching` hooks are wired95- [writing-handlebars > Quick Reference](../writing-handlebars/SKILL.md#quick-reference) -- Handlebars expressions for dynamic values in scripts and hook configurations9697<!-- TIER:2 -->9899## Common Options Available to All Hooks100101Most hooks receive these context fields in `options`:102103- `_flowId`, `_integrationId`, `_apiId`, `_parentIntegrationId` -- execution context IDs104- `_exportId` or `_importId` -- the step's resource ID105- `_connectionId` -- the connection in use106- `settings` -- custom settings in scope for the resource107- `testMode` -- boolean, whether running in test/preview mode108- `job` -- the current job object109110## Function Point Categories111112Scripts run at twelve function points, grouped into four categories. The [Hook Points](#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.113114- **Step-level pipeline hooks** (on the export or import) -- `preSavePage`, `preMap`, `postMap`, `postSubmit`, `postAggregate`115- **Parent-level response hook** (on the flow/API/tool `pageProcessors[]` entry, not the step) -- `postResponseMap`116- **Script-mode replacements for declarative slots** -- `filter`, `input_filter`, `transform`, `branching`117- **Resource-specific function points** -- `contentBasedFlowRouter` (on an AS2 connection) and `handleRequest` (on a script-mode API)118119**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](#declarative-vs-script-mode)).120121## Declarative vs Script Mode122123The 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.124125**From script mode to declarative mode** (the common direction -- prototype with a script, then clean up):1261271. Clear the script from the slot. The slot reverts to declarative mode by default.1282. Author the declarative rule for that slot (rules-engine filter, Mapper 2.0 transform, or router input-filter rule).129130**From declarative mode to script mode** (rarer -- the rules engine couldn't express what you need):1311321. Wire a script into the slot. The declarative rules already there are replaced by the script reference automatically.133134Wiring 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"*.135136## How to Write a Script137138### 1. Determine what you need to accomplish139140Map your goal to the right hook point using the [Hook Point Decision Matrix](#hook-point-decision-matrix) above.141142### 2. Check if an expression can handle it143144Filter, 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:145146- Multi-step logic or loops147- Cross-record calculations (totals, deduplication)148- External API calls via `integrator-api`149- Error handling with retry data150- Access to `preMapData` alongside `postMapData`151152### 3. Check for existing scripts in the account153154```bash155celigo scripts list156celigo scripts get <id> # content is only returned on individual GET157```158159### 4. Create the script resource160161Build the script with the correct function name matching the hook point. A single script can contain multiple functions.162163See [references/schemas/request.yml](references/schemas/request.yml) for the create/update schema and [references/schemas/response.yml](references/schemas/response.yml) for the response shape.164165Key fields:166- `name` -- descriptive name (convention: `<System> - <step> - <hookType>`, e.g., `"Salesforce - getBatchRecords - postResponseMap"`)167- `content` -- the JavaScript source code168169### 5. Wire the script to the resource170171Wiring depends on the hook type:172173| Hook | Wiring pattern | Where |174|------|---------------|-------|175| `preSavePage`, `preMap`, `postMap`, `postSubmit`, `postAggregate` | `hooks.{hookType}: { _scriptId, function }` | Export or import resource |176| `filter`, `input_filter`, `transform` | `{field}: { type: "script", script: { _scriptId, function } }` | Export or import resource |177| `postResponseMap` | `hooks.postResponseMap: { _scriptId, function }` | Flow `pageProcessors[]` entry |178| `branching` | `routeRecordsUsing: "script"` + script reference | Router in flow |179| `handleRequest` | `script: { _scriptId, function }` + `type: "script"` | API resource |180| `contentBasedFlowRouter` | `as2.contentBasedFlowRouter: { _scriptId, function }` | AS2 connection |181182**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.183184### 6. Test and iterate185186```bash187# Enable debug logging on the script188celigo scripts enable-debug <script-id>189190# Run the flow or API that triggers the script191celigo flows run <flow-id> -y192193# Check debug logs194celigo scripts debug-logs <script-id> --since 30195196# Check execution logs197celigo scripts debug-logs <script-id> --level error --limit 20198199# Disable debug when done200celigo scripts disable-debug <script-id>201```202203## Available Modules204205Scripts can import three built-in modules:206207### integrator-api208209Call Celigo APIs from within the script -- run exports, read connections, trigger imports.210211```javascript212import { exports, imports, connections } from 'integrator-api'213214const result = exports.run({ _id: 'exportId' })215const conn = connections.get({ _id: 'connectionId' })216```217218Useful in `preSavePage` for enrichment, `handleRequest` for orchestration, and `postSubmit` for triggering downstream processes.219220### dayjs221222Date and time manipulation. Handles parsing, formatting, diffing, and timezone conversions without manual date math.223224```javascript225import dayjs from 'dayjs'226227const formatted = dayjs(record.createdAt).format('YYYY-MM-DD')228const isRecent = dayjs().diff(dayjs(record.updatedAt), 'day') < 7229```230231### sjcl232233Stanford JavaScript Crypto Library for hashing, encryption, and HMAC generation.234235```javascript236import sjcl from 'sjcl'237238const hash = sjcl.hash.sha256.hash(payload)239const hexDigest = sjcl.codec.hex.fromBits(hash)240```241242## CLI Commands243244### CRUD245246```bash247celigo scripts list248celigo scripts get <id>249celigo scripts create < script.json250celigo scripts update <id> < script.json251celigo scripts set <id> name="New Name"252celigo scripts delete <id>253```254255### Logs and Debugging256257```bash258celigo scripts debug-logs <id> [--limit N] [--offset N] [--level error|warn|info|debug] [--start-date ISO] [--end-date ISO]259celigo scripts enable-debug <id> [--duration <minutes>]260celigo scripts disable-debug <id>261celigo scripts debug-logs <id> [--since <minutes>] [--flow-id <id>]262```263264## Authoring Against Sample Data265266Script 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.267268Treat authoring as a loop, not a one-shot:2692701. **Generate or edit** the function against the sample input.2712. **Run** it against that sample.2723. **Check** the output for errors or obviously-wrong results.2734. **Iterate** -- refine and re-run until it passes.274275A 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.276277## Execution Logs and the Debug Window278279Scripts write to a per-script **execution log** using standard `console` methods. What gets captured depends on the level:280281- `console.error()`, `console.warn()`, `console.info()`, and `console.log()` are **always captured** -- no setup, no toggle (`info` and `log` are equivalent).282- `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.283284"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.285286The 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.287288Each log entry records its time, level (`INFO` / `WARN` / `ERROR` / `DEBUG`), the message, and two locating fields:289290- `functionType` -- which hook produced the entry (`preMap`, `postSubmit`, etc.)291- `_resourceId` -- the export or import that ran the hook292293Because 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."294295<!-- TIER:3 -->296297## Pre-Submit Checklist298299Before creating or updating a script, verify:300301- [ ] **Hook point is correct** -- the function name matches the hook type being wired (e.g., `preSavePage` function for a `hooks.preSavePage` reference)302- [ ] **Return value matches contract** -- batch hooks (`preMap`, `postMap`, `postSubmit`, `postResponseMap`) return arrays that match the input array length exactly303- [ ] **Error handling uses return pattern, not throw** -- per-record errors use `{ errors: [...] }` return values, not thrown exceptions (which fail the entire page)304- [ ] **Expression alternative considered** -- filter, transform, and output filter can use expressions; only use a script when expressions cannot handle the logic305- [ ] **`content` field is included on PUT** -- omitting `content` on update erases the code; always GET first, modify, then PUT306- [ ] **Debug mode is disabled after testing** -- `celigo scripts disable-debug <id>` to avoid log noise in production307308## Gotchas3093101. **Array length contracts are strict.** `preMap`, `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.3112. **`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.3123. **Script `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.3134. **PUT erases `content` if omitted.** Always GET the script first, modify, then PUT the complete object. The `set` command handles this automatically.3145. **One script resource can contain multiple functions.** A single script with both `preSavePage` and `preMap` functions can be wired to different resources by specifying the `function` name in each hook reference.3156. **Throwing an exception fails the entire page.** In batch hooks (preSavePage, preMap, postMap, postSubmit), an unhandled exception fails ALL records on that page, not just one. Use the error return pattern (`{ errors: [...] }`) for per-record errors.3167. **`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.3178. **filter/transform scripts replace expression-based alternatives.** Wiring a script filter replaces any existing expression filter. They cannot coexist on the same resource.3189. **`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.31910. **Only `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.32011. **Shared-script logs interleave across hook sites.** One script can hold many functions used by many exports/imports, so its log stream mixes entries from every consumer. Filter by flow, level, and time when reading; each entry's `functionType` and `_resourceId` identify where it came from.32112. **Clearing a script-mode filter/transform reverts the slot to declarative mode.** The four mode-switchable slots (`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.322323## Common Errors324325| Error / Symptom | Cause | Fix |326|---|---|---|327| "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 |328| 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 |329| 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`) |330| `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 |331| 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` |332| "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` |333| 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 |334| `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 |335| `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 |336| 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 |
Run npx skillmds@latest add celigo/writing-scripts in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
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. It is listed under Coding & Dev Tools on SkillMD.
This skill has not completed SkillMD's automated safety review yet. Independent scanners report: SkillSpector: PASS, Skill Scanner: PASS. Capability flags: docs only. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free, and the skill stays under its author's original license.
celigo (@celigo) published this skill. Their other Agent Skills are listed on their SkillMD profile.