Skip to main content
skill CX Codex

writing-mappings — Claude Code skill: Celigo field mappings

Writes field mappings and transforms for Celigo integrations: Mapper 2.0, Transformation 2.0, lookups and response mapping.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add celigo/ai --skill writing-mappings -a claude-code
Open on GitHub

What writing-mappings does

The skill covers the data reshaping layer in Celigo: how fields from one system become fields for another. Four systems are in scope — Mapper 2.0 on imports, Mapper 1.0 for NetSuite DA, Salesforce and WrapperImport, Transformation 2.0 on exports, and response mapping on pageProcessors[]. Lookups get their own section, split into static, cached and live.

One rule runs through the whole thing: take every source path from a real record. Read the import's sampleData, the export's mockOutput, or the retry data of a failing record before writing an extract. A name the user mentions is not the source field's name, and a path that doesn't exist maps nothing without raising an error. There's a second trap worth knowing. The platform has no single-field update, so every save replaces the whole import, export or flow document — start from the current document and send all of it back.

Before editing an existing import, check which syntax it uses: mappings[] is Mapper 2.0, mapping.fields[] is Mapper 1.0. Plenty of older HTTP and RDBMS imports still run 1.0, and the two must never be mixed. Transformations pay off in two situations: several sources feeding one pipeline, or genuinely messy source data. With a single well-shaped source there's no reason to add one.

What it helps with

  • map import fields through mappings[] in Mapper 2.0
  • migrate an old import from Mapper 1.0 to 2.0 without mixing syntaxes
  • set up a static or cached lookup to translate values
  • carry a lookup response value into later steps with responseMapping
  • build a Transformation 2.0 rule in create or modify mode

Example prompts for the agent

  • read the sampleData for this import and write the mapping for the email field
  • rewrite this import's mapping.fields[] as Mapper 2.0 mappings[]
  • add a cached lookup that translates status codes

How to install writing-mappings

1Skills CLI (Vercel)

npx skills add celigo/ai --skill writing-mappings -a claude-code

2Manually: put the files in place

~/.claude/skills/writing-mappings/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/writing-mappings/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about writing-mappings

How do I install writing-mappings in Claude?

Run npx skills add celigo/ai --skill writing-mappings -a claude-code. The skill lands in ~/.claude/skills/writing-mappings/SKILL.md.

What does writing-mappings do?

It helps you write field mappings and transforms for Celigo integrations: Mapper 2.0, Mapper 1.0, Transformation 2.0, lookups and response mapping.

How is Mapper 2.0 different from Mapper 1.0?

Mapper 2.0 uses mappings[] and handles nested objects, arrays of any depth and conditionals. Mapper 1.0 is flat, on mapping.fields[], and stays on NetSuite DA, Salesforce and WrapperImport.

Source text

SKILL.md · MIT
---
name: writing-mappings
description: Write field mappings and transforms in Celigo integrations. Covers Mapper 2.0 (imports), Transformation 2.0 (exports), lookups, response mapping, and Mapper 1.0 (NetSuite DA/Salesforce). Use when editing mappings[], transform{}, responseMapping, or lookup configurations.
---

<!-- TIER:1 -->

# Writing Mappings and Transforms

Mappings and transforms are the **data reshaping layer** in Celigo integrations. They control how fields from one system translate into fields for another. Mappings are used across flows, APIs, and tools.

**Take every source path from a real record.** Read what the step receives -- the import's `sampleData`, the source export's `mockOutput` or a recent run's records, or a failing record's retry data -- before writing an `extract`. A name the user mentions, or the destination field's name, isn't the source field's name, and a path that doesn't exist maps nothing, with no error ([Understand the source data shape](#2-understand-the-source-data-shape)).

## How to Run the Steps

This skill names each step by what it reads or changes; the exact calls depend on how you reach the account. Read the one reference that matches your tools -- both follow the workflows and sections below, and add how to save and check a change:

- **Celigo MCP tools** (`list_imports`, `upsert_import`, `get_schema`, ...) -- [references/mcp.md](references/mcp.md)
- **The `celigo` CLI** -- [references/cli.md](references/cli.md)

Reading resources, sample records, and metadata is read-only; saving a mapping, transform, or response mapping writes to the account. The platform has no single-field update for any of them, so every save replaces the whole import, export, or flow document -- start from the current document and send all of it back. To check a saved change, test-run the flow: a test run never executes the flow's imports and doesn't advance its delta state, but it still calls the systems behind the steps it runs.

## Mapping Systems

Four systems handle data reshaping:

- **Mapper 2.0** -- modern recursive field mapping on imports (`mappings[]` array). Handles nested objects, arrays of any depth, lookups, conditionals, and date conversions. Default for new imports on all adaptor types except NetSuite DA (`NetSuiteDistributedImport`), Salesforce and `WrapperImport`, which support only Mapper 1.0 -- `NetSuiteHTTPImport` uses Mapper 2.0. The same rows also reshape the arriving record into an HTTP or NetSuite REST lookup's request (`mappings[]` on an `isLookup` `HTTPExport` / `NetSuiteHTTPExport`; no other export takes them), and build a Tool's `output.mappings` and an API response's `mappings`
- **Mapper 1.0** -- legacy flat mapping on Salesforce and `WrapperImport` imports (`mapping.fields[]` / `mapping.lists[]`) and NetSuite DA imports (the same arrays under `netsuite_da.mapping`). Body-level and sublist fields in separate flat arrays. Also present on many older HTTP/FTP/RDBMS imports created before Mapper 2.0 existed
- **Transformation 2.0** -- rule-based data reshaping on exports (`transform.expression.rulesTwoDotZero`). Uses the same Mapper 2.0 schema internally. Two modes: "create" (build new record from scratch) or "modify" (edit fields on existing record, unmapped fields pass through)
- **Response mapping** -- simple extract/generate pairs that carry data from a lookup or import response back into the record (`responseMapping` on flow `pageProcessors[]`). Uses Transformation 1.0 syntax. It's how a step passes a value to later steps; an import's own mapping never reaches them (gotcha 11)

Lookups translate values in every system. They come in three kinds: **static** (an inline `map`), **cached** (`_lookupCacheId`, read from a LookupCache resource), and **live** (imports only: a query to the destination for each record, such as an HTTP request, SOQL, SQL or a NetSuite search). The schemas call cached lookups "dynamic", and some adaptor schemas use the same word for live ones, so say which you mean. NetSuite DA lookups are always live NetSuite searches.

**Direction decides the tool.** Mappings translate data going *out* to a destination -- every import needs them, because the in-flight record almost never matches what the destination expects. Transformations reshape data coming *in* -- on exports, listeners, and API/tool entry stages. Never use an upstream transformation to match a destination's shape; that's the destination import's mapping. Transformations earn their keep in two situations: multiple sources feeding one pipeline (reshape each new source to the canonical record shape the existing steps expect) and genuinely messy source data (flatten deep nesting once at entry instead of fighting it in every downstream mapping). With a single well-shaped source, don't add a transform just because you can -- and skip identity transforms that rename nothing.

## Quick Reference

### Which Mapping System?

| Context | System | Syntax | Read schema |
|---|---|---|---|
| Import field mapping (HTTP, RDBMS, FTP, S3, etc.) | Mapper 2.0 | `mappings[]` | [mappings.yml](references/schemas/mappings.yml) |
| NetSuite DA / Salesforce / Wrapper import | Mapper 1.0 | `netsuite_da.mapping.fields[]` / `mapping.fields[]` | see import schema ([netsuitedistributed.yml](../configuring-imports/references/schemas/netsuitedistributed.yml); the Salesforce and Wrapper `mapping` is in [base.yml](../configuring-imports/references/schemas/base.yml)) |
| Export data reshaping | Transformation 2.0 | `transform{}` | [transform.yml](references/schemas/transform.yml) |
| Response mapping (lookup/import carry-back) | Transformation 1.0 | extract/generate pairs | [response-mapping.yml](references/schemas/response-mapping.yml) |
| Value translation | Lookups | `lookups[]` | [lookups.yml](references/schemas/lookups.yml) |

**Editing existing imports:** Many existing imports use Mapper 1.0 even for HTTP and RDBMS adaptor types (pre-dating Mapper 2.0). Always check whether an import uses `mappings[]` (2.0) or `mapping.fields[]` (1.0) before modifying -- never mix the two.

### Schema Index

| Schema | Contents |
|---|---|
| [mappings.yml](references/schemas/mappings.yml) | Mapper 2.0 field definitions (generate, extract, dataType, buildArrayHelper, conditionals) |
| [lookups.yml](references/schemas/lookups.yml) | Static and cached lookup definitions (the schema calls cached lookups "dynamic") |
| [transform.yml](references/schemas/transform.yml) | Transformation 2.0 envelope (mode, expression, script) |
| [response-mapping.yml](references/schemas/response-mapping.yml) | Transformation 1.0 extract/generate pairs for response carry-back |
| [netsuitedistributed.yml](../configuring-imports/references/schemas/netsuitedistributed.yml) | NetSuite Mapper 1.0 mapping + lookups |
| [base.yml](../configuring-imports/references/schemas/base.yml) | The import-level Mapper 1.0 `mapping` that Salesforce and Wrapper imports use |
| [salesforce.yml](../configuring-imports/references/schemas/salesforce.yml) | Salesforce import settings and lookups |

## Related Skills

- **configuring-imports** ([Quick Reference](../configuring-imports/SKILL.md#quick-reference)) -- import adaptor types, operation logic, hooks
- **configuring-exports** ([Quick Reference](../configuring-exports/SKILL.md#quick-reference)) -- export adaptor types, delta syncs, webhooks
- **writing-handlebars** ([Quick Reference](../writing-handlebars/SKILL.md#quick-reference)) -- Handlebars expressions used inside mapping `extract` fields
- **building-flows** ([How to Build a Flow](../building-flows/SKILL.md#how-to-build-a-flow)) -- wiring exports and imports into a flow pipeline

<!-- TIER:2 -->

## Mapper 2.0 Workflow

The `mappings[]` array is **recursive** -- a mapping can contain nested child mappings of the same structure to any depth. This is the core design principle.

### 1. Check the existing resource

Before modifying mappings, always retrieve the current state of the resource -- search the account for it by name if you don't have its id, then read the complete import. Check whether it uses Mapper 2.0 (`mappings[]`) or Mapper 1.0 (`mapping.fields[]`).

### 2. Understand the source data shape

Map against the record as it reaches this step: the export's records after its transform, output filter and preSavePage hook, plus every field an earlier step's response mapping merged in -- on a grouped source, an array of rows (step 5). Invoking the export shows its records with the transform applied, not what earlier lookups add; a test run's input to this step, or a failed record's retry data, shows the record exactly. For the full field list of the entity type, query the source system's metadata through its connection.

On a one-to-many import (`oneToMany: true`) the mapping sees one child at a time. With `pathToMany`, `$.<field>` is the child's field and `$._PARENT.<field>` the parent's -- `_PARENT` is the parent without the child array, and the parent's own fields aren't at `$`. With a blank `pathToMany` on a grouped record, each row is the record.

### 3. Understand the target data shape

Query the target system's metadata through its connection to discover required fields and types -- list the entity types it offers, then read the fields of the one you're writing to.

### 4. Choose the input context

The **input context** controls what JSONPath extracts start from. On an import it's the import's `inputContext` field; on a Transformation 2.0 transform, `transform.expression.rulesTwoDotZero.inputContext`:

- **`record`** (the default) -- extract paths reference the record directly. `$.user_id` accesses the `user_id` field on the record
- **`envelope`** -- record fields shift to `$.record.user_id`, and the envelope also carries `job`, `testMode` and `settings`. Read settings as `$.settings.<scope>.<field>`, where the scope is `integration`, `flowGrouping`, `flow`, `connection`, `iClient`, or the step's own `import` or `export` (e.g., `$.settings.connection.api_username`); a path outside these resolves to nothing. A webhook listener's transform also gets the incoming request's `headers` and `queryParams`, and file-provider steps get `fileMeta`

An import's envelope carries no request headers. In API and Tool runs, the integration, flow-group and flow settings are empty.

**When to use envelope:** when a mapping needs a setting or job value as a JSON path -- `$.settings.connection.fieldName` instead of a `{{settings.connection.fieldName}}` Handlebars expression.

### 5. Map by data type

Every mapping needs three properties: `generate` (target field name), `dataType` (output type), and `extract` (how to get data from source).

**Extract supports three patterns** (distinguished by syntax):
- **JSON path** -- starts with `$`, the record as it arrives: `$.customer.email` on an object record; `$[0].field` (one row) or `$[*].field` (every row) on a grouped record -- an array of rows from `groupByFields`, file key columns, or a NetSuite saved search (grouped unless `skipGrouping: true`). On a grouped record `$.field` resolves nothing and the mapper still succeeds. Always references the top-level root, even in nested mappings. A key that contains a dot needs bracket notation: `$['P.O. Number']` (or `$["P.O. Number"]`, `$.[P.O. Number]`); `$.P.O. Number` is read as a nested path, maps nothing, and the record still succeeds. Spaces, hyphens and `#` need no brackets (`$.Ship To Name`, `$.Tracking #`). Filter expressions fail the record (gotcha 13).
- **Handlebars** -- contains `{{` (e.g., `{{record.firstName}} {{record.lastName}}`). For computed values
- **Hard-coded** -- plain string literal (e.g., `"Active"`, `"USD"`). Neither starts with `$` nor contains `{{`

**Simple types** (`string`, `number`, `boolean`) -- direct field-to-field mapping. Give each row the type the destination field expects rather than `string` by default: `string` sends a numeric source as text (`"24"`), which an API that checks types refuses. `number` converts it (`"24"` becomes `24`, `"007"` becomes `7`), leaves the field out when the source is `""`, and fails the record with `non_parsable_string` (`<generate>: String contains alpha characters`) when the value holds letters or a thousands separator (`"1,250.00"`). Dates are strings: use `dataType: "string"` with `extractDateFormat` / `generateDateFormat` (plus `extractDateTimezone` / `generateDateTimezone` when the zone changes). There is no `date` dataType; `"date"` is rejected like any unknown type (`Destination datatype is a required field`).

**Objects** -- set `dataType: "object"`, add child mappings in the `mappings[]` array. Never use dot notation in `generate`. A Handlebars `extract` on an `object` row must render a JSON object; anything else fails the record (`You can't map string (source) to object (destination)`).

**Arrays** -- set `dataType` to an array type (`stringarray`, `numberarray`, `booleanarray`, `objectarray`) and configure `buildArrayHelper[]`. The schema also lists `arrayarray`, but the mapper refuses it as an unknown type (`Destination datatype is a required field`), though a save accepts it -- build an array-of-arrays value in a script hook instead. Three entry patterns for object arrays:
- **Extract only** -- pull existing objects from source (`extract: "$.items[*]"`)
- **Mappings only** -- construct objects from individual fields (each `buildArrayHelper` entry creates one array element)
- **Extract + mappings** -- iterate a source array and reshape each element. Uses the **composite object mechanism**: array brackets `[*]` in the extract path collapse to single objects inside the mappings, so `$.orders[*].items[*]` becomes `$.orders.items.fieldName` in child extract paths. Parent context remains accessible (e.g., `$.orders.id`, `$.customerName`)

The entry's own `extract` keeps the `[*]` (without it, `$.line_items` yields one empty element per item), and child extracts drop it but keep the array's path. A child `$.line_items[*].sku` maps nothing, and an element-relative `$.sku` reads the record root, not the line:

```json
{"generate": "lines", "dataType": "objectarray", "status": "Active", "buildArrayHelper": [{
  "extract": "$.line_items[*]",
  "mappings": [
    {"generate": "sku", "dataType": "string", "extract": "$.line_items.sku", "status": "Active"},
    {"generate": "quantity", "dataType": "number", "extract": "$.line_items.quantity", "status": "Active"},
    {"generate": "orderNumber", "dataType": "string", "extract": "$.name", "status": "Active"}
  ]
}]}
```

`dataType: "number"` converts a numeric string (`"2"` becomes `2`).

Entries add their elements in order and can be mixed: an iterating entry (`$.lines[*]`) followed by a constructed one (child `mappings`, no `extract`) gives every line plus one more, such as a fee line. Never index an entry's `extract` (`$[0]`) -- the whole array is silently dropped.

An array row takes its values only from `buildArrayHelper` entries or a `hardCodedValue`, never from its own `extract` -- JSONPath, Handlebars or a literal: an `objectarray` row is refused (`"buildArrayHelper" or "hardCodedValue" must be present for object array datatype.`), and a primitive-array row silently yields nothing. Handlebars works in an entry: an `objectarray` entry that renders a JSON array (`{{{jsonSerialize record.items}}}`) adds one element per item; on a primitive array each entry is one element.

For a constant array or object, set the row's `hardCodedValue` -- a literal entry `extract` is refused (`extract only supports json path or handlebars expression`). A primitive array splits the value on commas exactly as written (`"A, B"` gives `["A", " B"]`; a `numberarray` converts each part to a number), an `object` takes a JSON object string, and an `objectarray` a JSON array string.

### 6. Add lookups for value translation

Define lookups alongside mappings and reference them by name via `lookupName` on any mapping; the row's `extract` is the key the lookup receives. An import whose adaptor keeps its own copy of the lookups (`http.lookups` on an HTTP import; the import schema's [`lookups`](../configuring-imports/references/schemas/base.yml) lists the others) needs each entry there and the same entry in the top-level `lookups`. On an HTTP import the save keeps `http.lookups` and mirrors it to the top level, overwriting a top-level copy that differs; one saved with only the top-level array keeps neither copy, and every record that uses the lookup then fails with `Unable to find lookup in mapping configs - <name>`.

- **Static** -- `map` object with key-value pairs. Best for small, fixed sets (country codes, status values). A key matches only the exact value the row extracts -- case, spacing and any suffix count, so `In Transit` matches neither `in transit` nor `In Transit (Delayed)` -- and a save replaces the whole map, so send every key, not just a new one
- **Cached** -- `_lookupCacheId` referencing a LookupCache resource, with optional `extract` JSON path to pull a specific field from the cached object
- **Live** (imports only) -- a request to the destination for each record (on an HTTP import, `method` + `relativeURI`, with an optional `body` and `extract`), so it adds one call per record. Transformation 2.0 lookups take only static and cached. An HTTP lookup's `extract` is a path into the response written without `$.` -- `results[0].id`, or `ids` for an array of ids -- the opposite of a Mapper 2.0 row's `extract`. A `$.` prefix, `[*]`, or a dot path through an array (`results.id`) resolves nothing, so the lookup misses every time. When the `extract` finds two or more values, see [configuring-imports > Gotchas](../configuring-imports/SKILL.md#gotchas) (gotcha 14)
- A key missing from a static `map` fails the record only when the lookup sets `allowFailures: false` (`Could not find value for static lookup: <name> {Lookup Key: <key>} ...`). With `true`, or with `allowFailures` left out, the `default` is written (`""` writes an empty string), or the field is left out when there's none. A lookup can't pair a `default` with an explicit `false`: the save refuses it (422 `Lookups with default set can not have allowFailures explicitly set to false.`). A live HTTP lookup that matches nothing, or whose `extract` resolves nothing, fails the record only with `false` (`batch_lookup_evaluation_failed`, source `internal`); otherwise the record succeeds, with the `default` when `allowFailures` is `true` and one is set, and without the field when there's none. A cache lookup follows the same `allowFailures` rule; with `false`, a missing key fails the record with `lookupcache_evaluation_failed`. When a miss must stop the record, set `false` explicitly -- left out, the record goes through with the `default`, or without the field

### 7. Add conditionals where needed

`conditional.when` holds one value, and two settings share it: the composite-import gate -- `record_created` (only on insert) or `record_updated` (only on update) -- and the action when the source is empty: `extract_not_empty` (leave the field out) or `fail_record_when_empty` (fail the record with `<generate>: Source field has no value`). Setting one replaces the other. Empty means null, missing or `""`; `0`, `false` and whitespace are values and map through. With no condition, a missing or `""` source leaves the field out, but a null source is written as null. `fail_record_when_empty` can't share a row with a `default`, `lookupName` or `hardCodedValue` (the save refuses it).

A value that depends on another field is a Handlebars `extract` with a block helper: `{{#if (eq record.status "paid")}}closed{{else}}open{{/if}}`, or `{{#compare record.status "==" "paid"}}closed{{else}}open{{/compare}}` (`compare` takes the operator as its second argument). For a fixed set of values, a static lookup with a `default` does the same. Comparisons are exact and case-sensitive, so test the field that holds the value you compare with. `eq` goes inside `#if`: as a block of its own (`{{#eq a b}}...{{/eq}}`) it writes `true` or `false`. A helper the platform doesn't have (`ifEquals`, `ifCond`, `equal`), `compare` without its operator, or an operator inside `{{#if}}` (`===`, `? :`) fails every record with `cannot_evaluate_handlebars`, though a save accepts such a row.

### Schema reference

All Mapper 2.0 field definitions: [mappings.yml](references/schemas/mappings.yml), [lookups.yml](references/schemas/lookups.yml)

## Transformation 2.0 Workflow

Transformation 2.0 reshapes data **on exports** before it enters the pipeline. It wraps Mapper 2.0 syntax in a transform envelope with a mode selector.

### 1. Check the existing resource

Before modifying transforms, retrieve the current export to inspect any existing transform configuration -- search the account for it by name if you don't have its id.

### 2. Choose the mode

- **`create`** -- build a completely new record. Only mapped fields appear in output. Use when the output structure differs significantly from the source
- **`modify`** -- edit specific fields on the existing record. Unmapped fields pass through unchanged when the transform runs; rules you leave out of a save don't survive it (gotcha 9). Use for surgical adjustments (rename, add, remove a few fields)

### 3. Write the mappings

Same Mapper 2.0 syntax: `generate`, `dataType`, `extract`, nested `mappings`, `buildArrayHelper`, lookups. Everything described in the Mapper 2.0 section above applies here, including input context.

**Headers.** An HTTP export's or lookup's transform reads the response headers: `{{{headers.<name>}}}` in either input context, or `$.headers['<name>']` in `envelope` (double braces HTML-escape the value). On a lookup, a field the transform writes reaches later steps only through the step's response mapping (`data[0].<field>`). An API's request transform reads the request's parameters the same way, `{{pathParams.<key>}}`, `{{queryParams.<key>}}` and `{{headers.<key>}}`, but only the ones the API's request declares, in a test run as in a real call; write what later steps need onto the record.

### 4. Configure the transform envelope

Set `transform.type: "expression"`, `expression.version: "2"`, then place `mappings[]` and `lookups[]` under `expression.rulesTwoDotZero` with the chosen `mode`.

**Script alternative:** Set `transform.type: "script"` with `script._scriptId` and `script.function` for programmatic transforms when expression rules aren't sufficient.

### Schema reference

All Transformation 2.0 field definitions: [transform.yml](references/schemas/transform.yml), [mappings.yml](references/schemas/mappings.yml), [lookups.yml](references/schemas/lookups.yml)

## Mapper 1.0 Reference (NetSuite and Salesforce)

NetSuite DA (`NetSuiteDistributedImport`), Salesforce and `WrapperImport` imports use the older flat mapping structure (`NetSuiteHTTPImport` uses Mapper 2.0). Two arrays within the `mapping` object -- the import's own `mapping` on Salesforce and Wrapper imports, `netsuite_da.mapping` on NetSuite DA:

- **`mapping.fields[]`** -- body-level field mappings. Each entry has `extract` (source path) or `hardCodedValue` (static value), `generate` (target field ID), and optional `lookupName`, `dataType`, `internalId`, `immutable`, `discardIfEmpty`, `conditional`. On NetSuite, `internalId: true` marks a reference to another record, so the value must be that record's internal id; a name or code there needs a lookup that returns the id
- **`mapping.lists[]`** -- one entry per destination sublist or child list: `generate` (the NetSuite sublist id, e.g., `"item"`, or the Salesforce child relationship name) and `fields[]` (column mappings with the same properties as body fields, plus `isKey` for matching existing lines). Leave the entry's legacy `jsonPath` unset; on NetSuite DA it breaks iteration, so only the first source element renders. Column extracts start at the record root: `line_items[*].sku` gives one line per element, and a plain path repeats on every line
- **Which existing line a mapped line writes (NetSuite DA).** When the record already exists, is a transform (`Created From`), or is an item fulfillment, item receipt, invoice, cash sale or return authorization, each mapped line updates the one existing line whose `isKey` columns hold the same values. A column is a key only when marked `isKey`: mapping `orderline` without it still matches on the item. With no `isKey` column, those transactions match on the item (`item`, else `itemkey` or `itemname`), a few other sublists on their own default key (a payment's or credit's applied transaction, `refnum`), and the rest -- a sales or purchase order's items among them -- on nothing. Two matching lines fail the record with `MULTIPLE_LINES_WITH_SAME_KEYS`. A line that matches none is added, which an item fulfillment refuses with `SSS_INVALID_SUBLIST_OPERATION`: its lines are its order's. So an update of a sales order whose sublist has no `isKey` column adds every mapped line again; key it on a value each line holds once (a line id the create wrote to a line field), and clean up the records already updated first, since their repeats share that id and fail with `MULTIPLE_LINES_WITH_SAME_KEYS`
- **A key updates lines; it never removes them.** When you add one to stop repeated lines, tell the user that a line deleted at the source stays on the record. Mapping `celigo_replaceAllLines_<sublist id>` (`celigo_replaceAllLines_item`) to `true` removes the lines no mapped line matched; on an item fulfillment or receipt it sets their `itemreceive` to false instead

**Mapper 1.0 is its own dialect.** Extracts are bare dot-paths from the record root (`email`, `customer.first_name`, `line_items[*].sku`), and constants go in `hardCodedValue`, which writes the same value on every record. Dotted and bracket-star `generate` paths build nested destinations (`customer.addresses[*].city`), though NetSuite body fields stay flat. On Salesforce and HTTP imports (not checked on NetSuite DA):

- **Handlebars names fields bare.** An `extract` holding `{{` reads the record's fields without a prefix: `{{email}}`, `{{#if mobile}}{{mobile}}{{else}}{{phone}}{{/if}}`, `{{split contact_name " " 0}}`. `{{record.email}}` and `{{data.email}}` render nothing, so the field is left out even when the record has the value -- unlike Mapper 2.0, which reads `{{record.email}}`.
- **Empty sources.** A row's `default` is written when the source is `""`, null or missing: `{"extract": "phone", "generate": "Phone", "default": "none"}`. Without one, `""` and null are written as they are and a missing source leaves the field out; `discardIfEmpty` leaves the field out in all three cases. A fallback for a field that is sometimes empty is therefore a `default`, never a `hardCodedValue`, which would replace every record's real value.

**Converting a Mapper 1.0 import to 2.0.** Translate every row in one save and remove `mapping`: an import that carries both runs only `mappings[]` and ignores the Mapper 1.0 rows, so a partial conversion silently drops whatever wasn't translated. Bare dot-path extracts become `$.` paths; a dotted or `[*]` `generate` becomes nested `object` / `objectarray` rows, and a `lists[]` entry an `objectarray` row with a `buildArrayHelper`; `discardIfEmpty` becomes `conditional.when: extract_not_empty` (without it, Mapper 2.0 writes a null source as null); `lookupName`, `hardCodedValue`, `default` and the date fields carry over. Mapper 2.0 has no `immutable` and no `lookup_not_empty`, `lookup_empty` or `expression` condition -- tell the user before dropping one. Salesforce, NetSuite DA and Wrapper imports support only Mapper 1.0, so never convert those.

**NetSuite DA `update`, `addupdate` and `delete` need `netsuite_da.internalIdLookup`.** The lookup, not a mapped `internalid`, picks the record (on `update` it overrides a mapped one). When the record already carries the internal id, use the expression form `["internalid","is","{{{id}}}"]` rather than dropping the lookup -- as long as every record carries it: on SuiteApp 2.0 an `addupdate` record whose id is empty doesn't fall through to create, because the empty value is searched as `isempty` and can match an existing record.

**NetSuite lookups are different** -- they query live NetSuite records using `recordType`, `searchField`, `resultField`, and `operator`, unless their static `map` answers first: a source value that is a key of `map` returns its mapped value without a search, so one lookup can be a static table, a search, or both. Defined in `netsuite_da.lookups[]`, referenced by `lookupName` in field mappings. To discover valid field IDs for `searchField` and `resultField`, read the record type's fields from the NetSuite connection's metadata — the returned field IDs are the exact values to use. A lookup's `expression` (which overrides `searchField` and `operator`) renders against the whole source record, without `record.`; a lookup used on a sublist row reaches the current line with `<array>[*].<field>` (e.g., `{{{line_items[*].sku}}}`). A bare line field is undefined at the record root, and a helper that receives one fails every record with `INVALID_HANDLE_BAR_EXPRESSION`. The mapping preview returns an empty sublist for `[*]` inside a block-helper argument, so validate those with a run.

**Salesforce lookups** follow the same Mapper 1.0 pattern but the lookup structure is simpler.

**Sublist field discovery:** For NetSuite `mapping.lists[].generate`, the sublist name (e.g., `"item"`, `"addressbook"`) comes from the record type's field metadata on the NetSuite connection — sublists appear as field groups. For a Salesforce child list, read the sObject type's field metadata on the Salesforce connection for its relationship fields and child objects; the `lists[]` entry's `generate` is the child relationship name. (`distributed.relatedLists` belongs to Salesforce listener exports, not imports.)

### Schema reference

NetSuite Mapper 1.0: see `mapping` and `lookups`...

© celigo · MIT · celigo/ai

Similar skills

All skills for Automation and no-code: n8n, Zapier, Make →

From the same set17

configuring-guardrails A skill for configuring Celigo guardrails: PII detection, content moderation and AI-based evaluation of records. Useful when an integration moves sensitive data. CX Codex +1 2,900installs building-apis Teaches the agent how to build a Celigo API: an external system calls an HTTP endpoint, the integration runs lookups and imports, and a structured response comes back. CX Codex +1 2,900installs building-flows Walks your agent through assembling Celigo flows: schedules, event listeners, record branching and error handling. Useful when the integration is being designed, not fixed. CX Codex +1 3,000installs writing-sql Writes SQL for Celigo RDBMS exports and imports and helps debug errors in rdbms.query. Useful when an integration reads from or writes to Snowflake, Postgres, MySQL, SQL Server, Oracle, BigQuery or Redshift. CX Codex +1 2,900installs configuring-imports A skill for configuring Celigo imports, the destination step that writes records to external systems. Useful when you pick an adaptor type, set up lookups or wire an upsert. CX Codex +1 2,900installs configuring-exports A skill for configuring Celigo exports: connecting to an external system, picking an adaptor, delta exports, webhooks and lookup steps. CX Codex +1 3,000installs managing-stacks A skill that explains how to run Celigo hooks and wrappers on your own server or AWS Lambda, and when that is worth doing. CX Codex +1 2,700installs using-marketplace-templates Walks through finding and installing Celigo Marketplace templates and Integration Apps, and explains when to install the whole thing versus borrow one piece. CX Codex +1 2,700installs

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor