Mapping
Mapping defines how ERP data transforms into epilot entities. This page covers the configuration structure and available options.
Mapping Configurationโ
A mapping configuration consists of one or more entity definitions:
{
"entities": [
{
"entity_schema": "contact",
"unique_ids": ["customer_number"],
"enabled": true,
"fields": [...]
}
]
}
Entity Propertiesโ
| Property | Type | Required | Description |
|---|---|---|---|
entity_schema | string | Yes | The epilot entity schema (e.g., contact, contract, meter) |
unique_ids | string[] | Yes | Fields used to find existing entities |
enabled | boolean/string | No | Enable/disable this mapping |
mode | string | No | Operation mode: upsert (default), delete, purge, upsert-prune-scope-purge, or upsert-prune-scope-delete. See Operation Modes. For meter readings, see also upsert-prune-scope in Meter Reading Prune Scope Operations |
scope | object | No | Required for prune-scope modes. Defines which entities to consider for pruning. See Prune Scope Operations |
jsonataExpression | string | No | Pre-process the payload before field mapping |
fields | array | Yes | Field mapping definitions |
Field Mapping Typesโ
Direct Field Mappingโ
Map a source field directly to a target attribute:
{
"attribute": "first_name",
"field": "firstName"
}
Nested Field Access (JSONPath)โ
Access nested data using JSONPath syntax with $ prefix:
{
"attribute": "street",
"field": "$.address.street"
}
JSONata Expressionsโ
Use JSONata for complex transformations:
{
"attribute": "full_name",
"jsonataExpression": "firstName & ' ' & lastName"
}
Common JSONata patterns:
// Concatenation
"firstName & ' ' & lastName"
// Conditional
"$exists(middleName) ? firstName & ' ' & middleName & ' ' & lastName : firstName & ' ' & lastName"
// Date formatting
"$fromMillis($toMillis(dateField), '[Y]-[M01]-[D01]')"
// Array access
"addresses[0].street"
// Filtering
"items[status = 'active']"
Constant Valuesโ
Set a fixed value:
{
"attribute": "source",
"constant": "SAP"
}
Enabled Fieldโ
Use the enabled property to conditionally map fields:
{
"attribute": "phone",
"field": "phoneNumber",
"enabled": "$exists(phoneNumber) and phoneNumber != ''"
}
File Proxy URL Mappingโ
When syncing file entities via inbound use cases, you can use the file_proxy_url field type to auto-construct the file proxy download URL. This avoids manually assembling the URL with boilerplate query parameters โ orgId and integrationId are injected automatically from the processing context.
Configuration (using slug โ portable):
{
"attribute": "custom_download_url",
"file_proxy_url": {
"use_case_slug": "document-download",
"params": {
"documentId": { "field": "documentId" },
"tenantId": { "constant": "ACME" }
}
}
}
Configuration (using UUID โ legacy):
{
"attribute": "custom_download_url",
"file_proxy_url": {
"use_case_id": "uuid-of-file-proxy-use-case",
"params": {
"documentId": { "field": "documentId" },
"tenantId": { "constant": "ACME" }
}
}
}
Exactly one of use_case_id or use_case_slug must be provided. Using use_case_slug is recommended as it makes the configuration portable across environments.
Slug format: ^[a-z0-9][a-z0-9_-]*$ (1-255 chars).
Input:
{
"documentId": "DOC-00034157"
}
Output:
{
"attributes": {
"custom_download_url": "https://erp-file-proxy.sls.epilot.io/download?orgId=123&integrationId=abc&useCaseSlug=document-download&documentId=DOC-00034157&tenantId=ACME"
}
}
The params object maps URL parameter names to values resolved from the payload. Each param value supports three resolution modes:
| Mode | Description | Example |
|---|---|---|
field | Source field name or JSONPath expression (if starts with $) | { "field": "documentId" } |
constant | Fixed value (any type, stringified for URL) | { "constant": "ACME" } |
jsonataExpression | JSONata expression for transformation | { "jsonataExpression": "doc.id" } |
Note: The standard parameters
orgId,integrationId, anduseCaseId/useCaseSlugare always included automatically. You only need to configure additional custom parameters inparams. If nointegrationContextis available (e.g., in mapping simulation mode), thefile_proxy_urlfield is silently skipped.
See also: File Proxy Configuration for details on the proxy URL format and parameter requirements.
Repeatable Fieldsโ
Email and phone fields in epilot are stored as arrays. Use _type to specify the field type:
Email Fieldsโ
{
"attribute": "email",
"field": "emailAddress",
"_type": "email"
}
This transforms the input value into the epilot format:
// Input
{ "emailAddress": "john@example.com" }
// Output in epilot
{ "email": [{ "email": "john@example.com" }] }
Phone Fieldsโ
{
"attribute": "phone",
"field": "phoneNumber",
"_type": "phone"
}
Entity-Level JSONataโ
Pre-process the entire payload before field mapping:
{
"entity_schema": "contact",
"jsonataExpression": "$merge([payload, {'fullAddress': payload.street & ', ' & payload.city}])",
"unique_ids": ["customer_number"],
"fields": [
{ "attribute": "address_display", "field": "fullAddress" }
]
}
Multiple Entitiesโ
Process multiple entities from a single event:
{
"entities": [
{
"entity_schema": "contact",
"unique_ids": ["customer_number"],
"fields": [
{ "attribute": "customer_number", "field": "customerId" },
{ "attribute": "first_name", "field": "firstName" },
{ "attribute": "last_name", "field": "lastName" }
]
},
{
"entity_schema": "account",
"unique_ids": ["account_number"],
"fields": [
{ "attribute": "account_number", "field": "accountId" },
{ "attribute": "name", "field": "companyName" }
]
}
]
}
Dynamic Entity Expansionโ
Create multiple entities from an array in the payload:
{
"entity_schema": "meter",
"jsonataExpression": "meters",
"unique_ids": ["meter_number"],
"fields": [
{ "attribute": "meter_number", "field": "meterId" },
{ "attribute": "type", "field": "meterType" }
]
}
Given this input:
{
"meters": [
{ "meterId": "M001", "meterType": "electricity" },
{ "meterId": "M002", "meterType": "gas" }
]
}
This creates two meter entities.
Conditional Entity Processingโ
Enable or disable entity processing based on payload conditions:
{
"entity_schema": "contact",
"enabled": "customerType = 'individual'",
"unique_ids": ["customer_number"],
"fields": [...]
}
Operation Modesโ
The mode field controls how entities are processed. By default, entities are upserted (created or updated).
Mode Optionsโ
| Mode | Description |
|---|---|
upsert | Create or update the entity (default behavior) |
delete | Soft delete - marks entity as deleted but keeps in Recycle Bin for 30 days |
purge | Hard delete - permanently removes from the system |
upsert-prune-scope-purge | Upsert entities from array, then hard delete entities in scope that weren't upserted |
upsert-prune-scope-delete | Upsert entities from array, then soft delete entities in scope that weren't upserted |
upsert-prune-scope | (Meter readings only) Upsert readings from array, then delete all other readings for the same meter+counter that weren't upserted |
note
The upsert-prune-scope-* modes require a scope configuration. See Prune Scope Operations. The upsert-prune-scope mode is for meter readings only and uses the meter+counter as natural scope. See Meter Reading Prune Scope Operations.
Entity Deletionโ
To delete entities, use mode: "delete" or mode: "purge":
{
"entities": [
{
"entity_schema": "billing_event",
"unique_ids": ["external_id"],
"mode": "purge",
"fields": [
{
"attribute": "external_id",
"field": "billing_event_id"
}
]
}
]
}
For deletion modes, only the unique_ids fields need to be mapped in fields - other field mappings are ignored since no entity attributes are being updated.
Conditional Deletionโ
Combine mode with enabled for conditional deletion based on payload data:
{
"entity_schema": "contract",
"unique_ids": ["contract_number"],
"mode": "delete",
"enabled": "status = 'TERMINATED'",
"fields": [
{ "attribute": "contract_number", "field": "contract_id" }
]
}
Prune Scope Operationsโ
The upsert-prune-scope-purge and upsert-prune-scope-delete modes upsert all entities from an array in the payload, then delete/purge entities within a defined scope that weren't included in the upsert.
This is ideal for synchronizing child entity collections, such as billing events for a billing account.
Scope Configurationโ
Prune-scope modes require a scope configuration that defines which existing entities are eligible for deletion. The scope resolves against the original event payload (not individual array items).
scope_mode Optionsโ
| Mode | Description |
|---|---|
relations | Find scope by looking at all entities related to a specific entity (both direct and reverse relations) |
query | Find scope entities directly via query parameters |
Example: Sync Billing Events for a Billing Accountโ
Sync all billing events for a billing account and remove any that are no longer in the payload:
{
"entities": [
{
"entity_schema": "billing_event",
"unique_ids": ["external_id"],
"jsonataExpression": "$map(billingevents[], function($v) { $merge([$v, { \"billingaccountnumber\": billingaccountnumber }]) })",
"mode": "upsert-prune-scope-purge",
"scope": {
"scope_mode": "relations",
"schema": "billing_account",
"unique_ids": [
{
"attribute": "billing_account_number",
"field": "billingaccountnumber"
}
]
},
"fields": [
{ "attribute": "external_id", "field": "billing_event_number" },
{ "attribute": "billing_account_number", "field": "billingaccountnumber" }
]
}
]
}
Input:
{
"billingaccountnumber": "002800699425",
"billingevents": [
{ "billing_event_number": "002800699425-2025-08-15" },
{ "billing_event_number": "002800699425-2024-08-05" }
]
}
Result:
- Upserts 2 billing_event entities with the specified external_ids
- Finds all billing_event entities related to billing_account with
billing_account_number = "002800699425" - Purges any billing_event entities in that scope that weren't in the upserted list
Example: Query Mode for Direct Matchingโ
Find scope entities directly by query parameters instead of through relations:
{
"entities": [
{
"entity_schema": "billing_event",
"unique_ids": ["external_id"],
"jsonataExpression": "$map(billingevents[], function($v) { $merge([$v, { \"billingaccountnumber\": billingaccountnumber }]) })",
"mode": "upsert-prune-scope-purge",
"scope": {
"scope_mode": "query",
"query": [
{
"attribute": "billing_account_number",
"field": "billingaccountnumber"
},
{
"attribute": "type",
"constant": "INVOICE"
}
]
},
"fields": [
{ "attribute": "external_id", "field": "billing_event_number" },
{ "attribute": "billing_account_number", "field": "billingaccountnumber" }
]
}
]
}
warning
If the array yields zero entities (e.g., billingevents: []), this will result in the deletion of all entities in the scope. Ensure your payload always contains the expected data.
Meter Reading Prune Scope Operationsโ
The upsert-prune-scope mode for meter readings upserts all readings from the payload for a given meter/counter, then permanently deletes all other readings for that meter/counter that weren't part of the upsert.
The scope is naturally defined by meter + counter โ no explicit scope_mode is needed. An optional scope object can restrict pruning to readings from a specific source.
Scope Configuration (optional)โ
| Property | Required | Description |
|---|---|---|
source | No | When set, only readings with this source are eligible for pruning |
Example: Basic Meter Reading Prune Scopeโ
{
"meter_readings": [
{
"jsonataExpression": "$.readings",
"mode": "upsert-prune-scope",
"meter": {
"unique_ids": [{ "attribute": "external_id", "field": "meter_id" }]
},
"meter_counter": {
"unique_ids": [{ "attribute": "external_id", "field": "counter_id" }]
},
"fields": [
{ "attribute": "external_id", "field": "reading_id" },
{ "attribute": "timestamp", "field": "read_at" },
{ "attribute": "source", "constant": "ERP" },
{ "attribute": "value", "field": "meter_value" }
]
}
]
}
Upserts all readings from the readings array, then deletes any other readings for the same meter+counter not in the payload.
Example: With Source Scopeโ
{
"meter_readings": [
{
"jsonataExpression": "$.readings",
"mode": "upsert-prune-scope",
"scope": { "source": "ERP" },
"meter": {
"unique_ids": [{ "attribute": "external_id", "field": "meter_id" }]
},
"meter_counter": {
"unique_ids": [{ "attribute": "external_id", "field": "counter_id" }]
},
"fields": [
{ "attribute": "external_id", "field": "reading_id" },
{ "attribute": "timestamp", "field": "read_at" },
{ "attribute": "source", "constant": "ERP" },
{ "attribute": "value", "field": "meter_value" }
]
}
]
}
With source: "ERP", only readings with source: "ERP" are eligible for pruning. Manually entered or ECP readings remain untouched.
Important Notesโ
- Scope: The scope is always all readings for the meter + counter.
- Permanent Deletion: Meter reading deletion is always permanent (no soft delete/purge distinction).
- Source Filter Recommended: Use
sourceto avoid accidentally deleting readings from other sources (e.g., manually entered readings). - Empty Payload: If the payload yields zero readings, all readings in scope will be deleted (the ERP is treated as source of truth).
- External ID: The
external_idattribute is used to identify which readings to keep during pruning.
Mixed Operationsโ
You can mix upsert and delete operations in the same use case by using multiple entity entries with different modes:
{
"entities": [
{
"entity_schema": "meter",
"unique_ids": ["external_id"],
"mode": "upsert",
"fields": [
{ "attribute": "external_id", "field": "meter_id" },
{ "attribute": "status", "constant": "DECOMMISSIONED" }
]
},
{
"entity_schema": "billing_event",
"unique_ids": ["external_id"],
"mode": "purge",
"jsonataExpression": "billing_events",
"fields": [
{ "attribute": "external_id", "field": "event_id" }
]
}
]
}
Updates the meter status to "DECOMMISSIONED" while purging associated billing events.
Field Mapping Priorityโ
When multiple mapping options are specified, they are evaluated in this order:
info
constant-- Fixed value (highest priority)jsonataExpression-- Computed valuefield-- Direct field mapping (lowest priority)
Array Attribute Operationsโ
Array attributes like _tags support operations to control how values are applied:
Set (_set)โ
Replaces the entire array:
{
"attribute": "_tags",
"constant": ["CUSTOMER", "VIP", "ACTIVE"]
}
Append (_append)โ
Adds new unique values to existing ones (with automatic deduplication):
{
"attribute": "_tags",
"jsonataExpression": "{ \"_append\": [\"NEW_TAG\", \"IMPORTED\"] }"
}
If ["EXISTING", "CUSTOMER"] already exists on the entity, appending ["CUSTOMER", "NEW_TAG"] results in ["EXISTING", "CUSTOMER", "NEW_TAG"] - the duplicate CUSTOMER is not added again.
Append All (_append_all)โ
Adds all values without deduplication:
{
"attribute": "_tags",
"jsonataExpression": "{ \"_append_all\": [\"AUDIT_LOG\", \"PROCESSED\"] }"
}
Best Practicesโ
Use Meaningful Unique IDsโ
Choose stable identifiers that don't change:
// Good - stable business identifier
"unique_ids": ["customer_number"]
// Avoid - may change over time
"unique_ids": ["email"]
Handle Optional Fieldsโ
Use enabled to skip fields when data is missing:
{
"attribute": "secondary_email",
"field": "alternateEmail",
"_type": "email",
"enabled": "$exists(alternateEmail)"
}
Validate Data with JSONataโ
Transform and validate in one expression:
{
"attribute": "status",
"jsonataExpression": "status in ['active', 'inactive', 'pending'] ? status : 'unknown'"
}
Next Stepsโ
- Unique Identifiers - Advanced entity lookup strategies
- Relations - Link entities together
- Pricing - Map ERP line items and calculate prices
- Meter Readings - Handle meter reading data