writing-mappings — скилл для Claude Code: маппинги Celigo
Помогает писать маппинги полей и трансформации в интеграциях Celigo: Mapper 2.0, Transformation 2.0, лукапы и response mapping.
npx skills add celigo/ai --skill writing-mappings -a claude-codeЧто делает writing-mappings
Скилл разбирает, как устроен слой перекладки данных в Celigo: как поля одной системы превращаются в поля другой. Он покрывает четыре системы — Mapper 2.0 для импортов, Mapper 1.0 для NetSuite DA, Salesforce и WrapperImport, Transformation 2.0 для экспортов и response mapping на pageProcessors[]. Отдельно описаны лукапы трёх видов: статические, кэшированные и живые.
Главное правило, которое скилл вбивает в голову: путь к источнику берётся из реальной записи. Смотрите sampleData импорта, mockOutput экспорта или данные упавшей записи, прежде чем писать extract. Имя, которое назвал пользователь, и имя поля назначения — не имя поля источника, а несуществующий путь просто ничего не смапит и не выдаст ошибку. Ещё один подводный камень: у платформы нет обновления отдельного поля, поэтому каждое сохранение перезаписывает весь документ импорта, экспорта или флоу — начинайте с текущего документа и отправляйте его целиком.
Перед правкой существующего импорта проверьте, что там: mappings[] это Mapper 2.0, mapping.fields[] — Mapper 1.0. Многие старые HTTP и RDBMS импорты до сих пор на 1.0, и смешивать два синтаксиса нельзя. Трансформация нужна в двух случаях: несколько источников сходятся в один пайплайн или исходные данные совсем кривые. При одном аккуратном источнике добавлять её незачем.
Какие задачи решает
- сопоставить поля импорта через
mappings[]в Mapper 2.0 - переписать старый импорт с Mapper 1.0 на 2.0 без смешивания синтаксисов
- настроить статический или кэшированный лукап для перевода значений
- пробросить значение из ответа лукапа в следующие шаги через
responseMapping - собрать правило Transformation 2.0 в режиме create или modify
Примеры запросов агенту
- посмотри
sampleDataэтого импорта и напиши маппинг для поляemail - перепиши
mapping.fields[]этого импорта наmappings[]Mapper 2.0 - добавь кэшированный лукап для перевода кодов статуса
Как установить writing-mappings
1Через Skills CLI (Vercel)
npx skills add celigo/ai --skill writing-mappings -a claude-code2Вручную: положить файлы
~/.claude/skills/writing-mappings/SKILL.mdПапка скилла целиком; для одного проекта — .claude/skills/writing-mappings/
1Через Skills CLI (Vercel)
npx skills add celigo/ai --skill writing-mappings -a codex2Вручную: положить файлы
.agents/skills/writing-mappings/SKILL.mdДля всех проектов — ~/.agents/skills/
1Через Skills CLI (Vercel)
npx skills add celigo/ai --skill writing-mappings -a cursor2Вручную: положить файлы
.cursor/skills/writing-mappings/SKILL.mdCursor читает и .agents/skills/, и .claude/skills/
1GitHub CLI
gh skill install celigo/ai writing-mappings2Вручную: положить файлы
.github/skills/writing-mappings/SKILL.md1Через Skills CLI (Vercel)
npx skills add celigo/ai --skill writing-mappings -a gemini-cli2Командой платформы
gemini skills install https://github.com/celigo/ai --path skills/writing-mappingsВручную: положить файлы
.devin/skills/writing-mappings/SKILL.mdСтарый путь .windsurf/skills/ тоже читается
Через Skills CLI (Vercel)
npx skills add celigo/ai --skill writing-mappings -a opencodeClaude.ai
Упакуйте папку в zip и загрузите в Settings → Capabilities → Skills
По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов
Вопросы о writing-mappings
Как установить writing-mappings в Claude?
Выполните npx skills add celigo/ai --skill writing-mappings -a claude-code. Скилл встанет в ~/.claude/skills/writing-mappings/SKILL.md.
Что делает writing-mappings?
Помогает писать маппинги полей и трансформации в интеграциях Celigo: Mapper 2.0, Mapper 1.0, Transformation 2.0, лукапы и response mapping.
Чем Mapper 2.0 отличается от Mapper 1.0?
Mapper 2.0 работает с mappings[] и умеет вложенные объекты, массивы любой глубины и условия. Mapper 1.0 — плоский, на mapping.fields[], и остаётся на NetSuite DA, Salesforce и WrapperImport.
Исходный текст
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