Importer tips
Here are some tips and tricks when using Auto Importer Processors. The examples illustrate the tip using importer XML specifications.
See Programming idioms for JavaScript best practices, a reference of JavaScript functions in Experience Cloud, and for more tips about how to use the editor.
Document comments
Include extensive comments to document and facilitate the transfer of knowledge, and to track changes.
-
Use this template for the beginning of the spec to provide context and capture a log of changes:
<!-- NOTES: <Include general description and notes about special mappings, special logic, edge cases or considerations> TODOS: <Include outstanding TODOs> UPDATE LOG: [yyyy-mm-dd] <name>: <message> [2015-12-16] Lily Chan: Created base template -->For example:
-
Include comments in-line for complex mappings.
-
Include comments in JavaScript code.
Use helper functions
Before writing a complex transform, check the XML documentation to see if there is a helper function available. The XML Documentation link is available below the Definition property.
Additionally, access a list of helper functions while working in your code. After clicking Edit JavaScript, press Ctrl+Space to view a list of helper functions that can be used in a JavaScript transformation.
Always include a duplicate check
Always have a duplicate check in the processor, whether it uses one field or a combination of multiple fields. This ensures that duplicate records in the feed do not create duplicate information in the database.
Always include unique record ID in invitations
e_bp_uniquerecordid_txt (the BPP unique record ID field) is required for survey invitations. Use it as one of your duplicate checks.
This value should define a unique identifier for the record based on data in the import file provided by the client. Most commonly, the unique record ID is set to a unique transaction or interaction ID already used by client IT systems as their own duplicate check. If there is no existing unique identifier in the client systems, define one in the Auto Importer specification by concatenating values from the file itself.
Correctly configuring the unique record ID adds a new level to duplicate checking. While the Experience Cloud built-in duplicate check can see rows with identical data in a single file, this field can be used to check if a record is being duplicated across multiple importers, or if the same file is rerun in a specific importer. This means better data hygiene, as using the Unique Record ID as a duplicate check will prevent additional records from even being created in the system, excluding them from all reporting.
Additionally, the unique record ID can serve as a shared key across Experience Cloud and multiple client systems. This is useful when appending records with additional data with some shared data values. It is also very valuable in troubleshooting, enabling tracking the entire journey of a record across systems.
Considerations
If the unique record ID is a client's unique transaction ID, ensure that it will actually be unique. For example, if invitation data comes from multiple systems on the client side, confirm that the identifier is unique across systems and there will be no overlap. If there is risk of overlap, append an additional value to ensure it will be unique.
When generating a unique record ID by concatenating values, consider the client and their data. Carefully assess the chances that multiple records could share the same values in the selected fields. For example: “store ID + transaction date” is unlikely to be unique, ”store ID + transaction date/time” is more likely to be unique, but could be duplicated, depending on store transaction volume or how accurate the timestamp is. “Store ID + transaction date/time to the millisecond + associate ID” is probably unique, as it is unlikely that an associate would check out 2 people at the exact same millisecond in the same store.
Be careful when you use dates to generate unique identifiers on either the client-side or within Experience Cloud. Appending a date tied not to the record creation, but instead to file creation or import date, can result in duplicate records appearing to be unique. For example, if the client is generating a unique identifier based on the time an export is run, then reruns the same export the next day, the data could be exactly the same, but the different dates would make for different unique record IDs.
When creating the unique record ID in an Auto Importer specification using concatenation, the same guidance applies. Avoid using "now" or the time an import was run as a value in the identifier, and use values actually in the record itself. Using import time could allow records from multiple imports of the same file to appear to be different.
Also consider the behavior desired across different importers or programs as you are choosing the ID. To stop duplicate records across multiple importers, each should generate the unique record ID the same way, either concatenating the same fields or using the same client identifier. The same record data should create matching unique record IDs in each importer, otherwise that record will not be identified as a duplicate.
Duplicating records across multiple survey programs
It is possible to allow reuse of effectively the same invite record data across multiple programs, and still use unique record ID to prevent creating duplicate records within a single program. Simply append the program name to the unique record ID for those programs that will be reusing data.
For example, if an EX client wants to use the same HR employee export for multiple survey programs (quarterly, pulse), unique record ID could be mapped to “quarterly_Q2_employeeID12345” and “janpulse_employeeID112345”. This concatenation will keep duplicate records from being created within the same program but will allow data to be duplicated in different programs.
Fields to use to build a Unique Record ID
While your best options for creating a truly unique identifier will depend on client specific factors — e.g. industry, data accuracy, data availability etc — consider these factors when selecting your options:
-
Use stable, required fields. Do not use fields with values that change over time or that are frequently missing. Consider: “if this file was rerun 4 days later, will this Identifier be the same?”
-
Use a combination of fields from a record until you can be confident that it is unique, but do be mindful of the size of the end result. Consider “will this final value be unmanageably long?”
-
Do not use any fields that are PII or data that should be masked. Consider “am I exposing inappropriate client or customer information with this combination of values?”
Using an "employee" example, it would be appropriate to use "employee ID" in the unique record ID, but not an unmasked email address or an extremely dynamic value like "days employed".
Final considerations
The unique record ID field (e_bp_uniquerecordid_txt) correctly configured and used as a duplicate record check can prevent importing the same record across different importers or different runs of a single importer. Generally, the unique record ID is set to a unique transaction or interaction ID already used by client IT systems as their own duplicate check. If there is no client unique identifier available, one can be defined in the Auto Importer by concatenating values from the file itself.
Always specify a primaryUnitFieldId in invitations
For survey invitations, always specify a primaryUnitFieldId because every survey must be associated with a unit. Some companies have more than one unit field in the record. This option identifies the field that identifies the Unit associated with the record.
primaryUnitFieldId isn't available, map the surveys to unit pending. For information about "pending" units, see Units.Define transforms globally
Define transforms globally to be used in multiple output-column mappings so that the same transforms are not duplicated. Do this by including an ID in the mapping or javascript-transform. This allows for:
-
Easier maintenance because changes only need to be made in one place.
-
Less risk of error from replicating the same code in multiple places.
Use mapping transforms instead of if-else statements
To save processing time, use mapping transforms instead of a series of very simple if-else statements in a JavaScript transform.
Use a record-validation tag
To save processing time, use a record-validation tag to discard records before any processing of mappings for that record.
Discard unchanged records
For survey invitations, use discardUnchangedRecords set to true to improve performance by discarding records when the input data is the same as the data already present in Experience Cloud.
Organize mappings
Move output-column mappings for required fields and mappings that discard records first in the list of mappings. This allows appropriate records to be discarded immediately, avoiding additional processing of field mappings for a record that will eventually be rejected.
Omit unnecessary input-column elements
If the input field value is not being used, omit the input-column element in the output-column mapping. The following example code fills the target field with the result of a calculation that does not use the input field:
Sort records before they are processed
Use the sort tag to provide criteria to arrange and order records before they are processed. Each sort criterion specifies what to sort and in which order, either in ascending order (which is the default) or in descending order.
-
ASCENDING— Sorts from smaller to bigger numbers, from older dates to more recent dates, or from "A" to "Z". -
DESCENDING— Sorts from bigger to smaller numbers, from more recent to older dates, from "Z" to "A".
When you pass multiple sort criteria, they are used in order. That is, Auto Importer sorts records using the first sort criterion and checks the second criterion only in the case of ties. If all criteria yield the same result for 2 records, Auto Importer respects their original relative order in the input.
A sort node specifies an input column from which to extract the values used for sorting. Auto Importer expects a type, but it cannot use every type for sorting: integers, fractionals, strings, dates, and timestamps make good sort type candidates. On the other hand, enumerations, units, unit groups or permissions cannot be compared.
| Attribute name | Description | Required | Notes |
|---|---|---|---|
| direction | Whether to sort smaller first (ASCENDING) or bigger first (DESCENDING). | Optional | The default value is "ASCENDING". The accepted values are:
|
| type | Type of data to be used for sorting. | Required | The accepted types are:
|
Optionally, use transforms to sort on a value derived from the input instead of sorting from the input itself. For instance, you may sort on how close the input record check in date is to New Year's Eve. You can use reference transforms to reuse JavaScript fragments defined to be used throughout the spec.
| Children element type | Description | Notes |
|---|---|---|
| input-column Input | Specifies from which input-column to take the values for sorting. | Can be omitted if a JS transform is used to fetch values from multiple columns. |
| Field Parse Options | Parse options that indicate how to interpret the incoming value to the specific sort type. | |
| Transform | A series of transforms to apply to the incoming value before it is parsed to be used for sorting. | The accepted transforms are:
|
Use the record variable to omit providing an input column and work with JavaScript transforms referring values in various columns.
This example shows how to sort incoming records by ascending check in date (oldest records first) before processing.
<sort type="DATE">
<input-column heading="checkin date" />
<date-field-parse-options format="yyyy-MM-dd" />
</sort>
The next example shows how to sort incoming records by descending full name before processing.
<sort direction="DESCENDING" type="STRING">
<javascript-transform><![CDATA[
return record['guest first name'] + ' ' + record['guest last name'];
]]></javascript-transform>
</sort>
Use the discardRecord() function
Discard records and provide an error message by using the discardRecord() function in JavaScript transforms.
Use the keepOriginal() function
Keep the original value of a field for an existing record by using the keepOriginal() function in a JavaScript transform, or reference a specific field value from the original record using the original.<field_name> syntax. You can reference the sequence number of an enumerated field using original.<field_name>.getSequenceNumber().
Use special input types
Use special input types, including ones for a constant value (constant), the processing date (today), and the processing date and time (now).
Use "convert invalid to null" or default values
Look for opportunities to use "convert invalid to null" or to use default values instead of using JavaScript transforms.
Use the hasContent() function
Use the hasContent() function in a JavaScript transform instead of checking for null, undefined, and empty strings. See Testing for null or empty values for more information.
Use the recordUpdateMode function
When using the recordUpdateMode function, invitations importers should always use CREATE (not CREATE_AND_UPDATE), and append file importers should always use UPDATE.
Use the parse option when updating roles
When updating roles, use the following parse option to ensure the previous roles are removed:
<output-column><input-column heading="USER_ROLE" />
<target-field fieldId="roles" fieldName="Roles" requiredness="OPTIONAL" type="ENUMERATED">
<enumerated-field-parse-options mappingKey="NAME" delimiter="|" />
</target-field>
</output-column>
Use underscores instead of brackets in field identifiers
For target fields whose identifier contains brackets ([]), use underscores (_)in the fieldId. For example, when the Unit Group Identifier is named [ABC]region:
<target-field fieldId="[ABC]region" ... /> // ERROR: This fails
Instead, use:
<target-field fieldId="_ABC_region" ... /> // Use brackets
Use the random() function to seed a random number
You might need to generate a random number for logic within Auto Importer code. A common case is running an A/B test, where some percentage of customers should get survey version A and the remainder should get survey version B.
Normally, the code to accomplish this might look something like the following example, where the JavaScript Math.random() function generates a random number between 0 and 1. The code then compares that value to 0.3 to determine which survey type to return.
The problem with that approach is that, every time Math.random() is run, it produces a different value (non-deterministic), even if you are processing the same record. This means you cannot recreate the same result by processing the same record, which can cause challenges when troubleshooting.
To avoid this problem we use a seed.
With a seed, the code passes the random function an argument and, for a specific value of that argument, it always returns the same random number (deterministic). The JavaScript Math.random() function does not support a seed, but Medallia includes the random() helper function to solve this problem. The function returns a random number, between 0.0 (inclusive) and 1.0 (exclusive), based on the argument value. The argument is typically a stable identifier, such as an email address or customer ID. Consider using the same field for the seed argument as is used for duplicate checking.
Using a seed, importer code might appear as in the following example, where UNIQUE_RECORD_ID is the argument.
<output-column>
<input-column heading="UNIQUE_RECORD_ID" />
<target-field fieldId="e_bp_abtesting_survey_version_alt" fieldName="Medallia Survey Version" requiredness="REQUIRED" type="ENUMERATED">
<enumerated-field-parse-options mappingKey="SEQUENCE_NUMBER" />
<javascript-transform><![CDATA[
var Version = {
SIX : "0",
SEVEN : "1"
};
if (hasContent(record.UNIQUE_RECORD_ID)) {
var rand = random(record.UNIQUE_RECORD_ID);
if (rand < 0.1) {
return Version.SEVEN;
}
else {
return Version.SIX;
}
} else {
setToNull('The seed used for generating a random number does not have content');
}
]]></javascript-transform>
</target-field>
</output-column>
<output-column>
<input-column heading="UNIQUE_RECORD_ID" />
<target-field fieldId="e_bp_abtesting_survey_randomnum_txt" fieldName="A/B Testing Medallia Surveys v7 - Randomized Number for Survey Version" requiredness="REQUIRED" type="STRING">
<javascript-transform><![CDATA[
return random(record.UNIQUE_RECORD_ID).toString();
]]></javascript-transform>
</target-field>
</output-column>
The random() function generates a random number between 0 and 1. In the mapping to field e_bp_abtesting_survey_version_alt, the code then compares that value to 0.1 to determine which survey type to return: 0.1 means 10% of records will receive survey V7 and the remaining surveys will receive survey V6. The code checks that the seed has content. If it does not, it sets the value to null and generates a processing report message: 'The seed used for generating a random number does not have content'.
Note that, in the mapping to field e_bp_abtesting_survey_randomnum_txt, the random() function returns the same result if given the same UNIQUE_RECORD_ID as input.
Use the permission() and permissions() functions
Use the permission() function to create a permission entry with the specified role name, permission context, access, survey field ID, and addRoleToUserIfMissing flag.
permission(roleName, permissionContextName, access, addRoleToUserIfMissing, surveyFieldId, rootIdentifier)
roleName and access are both required. If you don't specify a permission context, the one defined for the company will be used. access must contain one or more unit groups or segments (using the declared delimiter) depending on the type of permission you want to create.
If you are setting up a segment permission, you must specify a surveyFieldId.
If you set addRoleToUserIfMissing to ADD_ROLE_TO_USER_IF_MISSING, the role will be added to the user if it is missing. If you specify CHECK_USER_HAS_ROLE, and the user does not have the role, the record is discarded and the report shows an error description. If you do not provide a value for addRoleToUserIfMissing, then CHECK_USER_HAS_ROLE is set by default. See the table below for examples:
| Flag | Description |
|---|---|
| ADD_ROLE_TO_USER_IF_MISSING | Use this flag only in the permission() function. It indicates that if the user does not have the role used in the permission, it should be added automatically.
|
| CHECK_USER_HAS_ROLE | Use this flag only in the permission() function. It indicates that, if the user does not have the role used in the permission, the record should be discarded.
|
If you provide the rootIdentifier value, it will be used to find Unit Groups under the hierarchy for that identifier.
// add permission for current user using default permission context.
permission('Corporate',null,'Addison');
// add permission for a specific permission context.
permission('Corporate','MyPermissionContext', 'Addison');
// add the role to user if missing.
permission('Corporate',null, 'Addison', ADD_ROLE_TO_USER_IF_MISSING);
// add permission for segment.
permission('Corporate',null, '1|2|3|4', CHECK_USER_HAS_ROLE, 'q_bp_b2b_ltr_scale');
After you finish creating all permissions, use the permissions() function to get the list of created permissions. Use this function in a JavaScript transform of a permission list field.
permission('Corporate',null,'Addison');
permission('Corporate',null,'Atlanta');
return permissions();
Access information about the request with requestInfo
Access information about the request — such as the type and name of the feed account that was used to fetch or receive the file being processed — with the requestInfo JavaScript object.
requestInfo object will only bring very limited information.You have access to request information using requestInfo. Each of the objects below returns a different map.
-
source— Fetch information about the source of the request. -
filename— Fetch the name of the file. -
feedPull— Fetch information about the feed pull, such as:-
Name -
Host -
Username -
Directory
-
-
emailFeed— Fetch information about the email feed, such as:-
UniqueName -
To -
From -
Subject
-
-
ftpFeed— Fetch information about the FTP feed, such as:-
Login
-
-
sftpFeed— Fetch information about the SFTP feed, such as:-
Login
-
-
webFeed— Fetch information about the web feed, such as:-
name -
username -
httpHeaders -
httpParameters
-
-
setupUpload— Fetch information about manual uploads, such as:-
name
-
So you can do requestInfo.webFeed.username, for instance.
The following example shows how to use the file name as a variable to assign to a field within an Auto Importer, setting up a troubleshooting text field to record a file name when a data append file updates a record.
<output-column>
<target-field fieldId="e_bp_invitation_file_name_debug_text" fieldName="Invitation File Name Debug" requiredness="OPTIONAL" type="STRING">
<javascript-transform><![CDATA[
var fileNameToStore=requestInfo.filename;
if(fileNameToStore == null){
return null;
}else{
return fileNameToStore;
}
]]></javascript-transform>
</target-field>
</output-column>
Check if a unit exists and is active
Use findUnit() to check whether a unit exists, and findUnit().isActive() to check if it is active in Experience Cloud.
Find a unit with the given identifier with findUnit(identifier).
if (findUnit('unit1') == null)
discardRecord('Failed to find unit with identifier unit1');
This example shows how to use findUnit(unit).isActive() to check if a unit is active. If the unit is not active, the Auto Importer rejects (discards) the record:
<output-column>
<input-column heading="SALESREP_SMTP_ADDR" />
<target-field fieldId="active" fieldName="Active" requiredness="OPTIONAL" type="ENUMERATED">
<enumerated-field-parse-options mappingKey="NAME" />
<javascript-transform><![CDATA[
if(hasContent(value)) {
var sr_unit = idMap('sales_rep', value);
if(findUnit(sr_unit).isActive()) {
return "ACTIVE";
} else {
discardRecord('Unit ' + sr_unit + ' is inactive. Please activate it manually or request an access in AccessIT.');
}
}
else {
return keepOriginal();
}
]]></javascript-transform>
</target-field>
</output-column>
The next example shows how to use findUnit() and findUnit(unit).isActive() to check if a unit exists and is active in Experience Cloud. If it isn't, the Auto Importer rejects the invitation:
<output-column>
<input-column heading="SALESREP_SMTP_ADDR" />
<target-field fieldId="e_unitid" fieldName="Unit" requiredness="REQUIRED" type="UNIT">
<enumerated-field-parse-options mappingKey="IDENTIFIER" caseSensitive="true" />
<javascript-transform><![CDATA[
var unit = idMap('sales_rep', value);
if(!findUnit(unit)){
discardRecord('Unit ' + unit + ' does not exist in the system. Please request an access in AccessIT.');
}
else if(!findUnit(unit).isActive()) {
discardRecord('Unit ' + unit + ' is inactive. Please activate it manually or request an access in AccessIT.');
}
else {
return unit;
}
]]></javascript-transform>
</target-field>
</output-column>
Enabling asynchronous sampling
For survey invitations, there are three sampling modes currently available on Auto Importer:
-
Synchronous sampling — Processes sampling during the Auto Importer ingestion process.
-
Delayed sampling — Sampling is configured to run on a schedule (e.g.: once a day).
-
Asynchronous delayed sampling — The calling code signals when to start sampling, which is determined systematically. On Auto Importer, the
asyncSamplingattribute provides this option for survey invitations.
The mode that runs depends on the sampling service status, the web feed mode and the asyncSampling attribute in the survey-processor-options XML element of the Auto Importer specification. For information about Web Feed modes, see Web Feeds mode.
| Sampling service status | Web Feed mode | AsyncSampling attribute | Sampling mode |
|---|---|---|---|
| Disabled | Feed file | false | Synchronous sampling |
| true | Delayed sampling | ||
| Concurrent synchronous | false | ||
| true | |||
| Enabled | Feed file | false | Synchronous sampling |
| true | Asynchronous delayed sampling | ||
| Concurrent synchronous | false | ||
| true |
Consider enabling asynchronous sampling for concurrent Web Feeds: it is not efficient to run the sampling process on small data sets on every HTTP POST call. Instead, run sampling periodically on the batch of surveys received, adding asyncSampling="true" to the Auto Importer that handles each Web Feed to make the processing run asynchronously and allow sampling to run in batches of 5,000 records.
Bulk delete accounts without using the synchronous mode
Generally, an Auto Importer with synchronous sampling mode is required for clients to automate the deletion of users. In specific scenarios where a full active user file is unavailable, preventing the use of the synchronous mode, Experience Cloud offers alternative solutions.
To remove users from an instance without using the synchronous mode, change the "active" field to "deleted" using an import specification like the one below:
<?xml version="1.0" encoding="UTF-8"?>
<!-- This AI spec is used to bulk delete accounts without using SYNC mode -->
<import-spec inputType="CSV" name="User Deletion">
<input-column heading="username" />
<input-column heading="active" />
<output-column-group pluginName="Account" recordUpdateMode="UPDATE">
<output-column>
<input-column heading="username" />
<target-field fieldId="username" fieldName="Username" requiredness="REQUIRED_USED_FOR_DUPLICATE_CHECK" type="STRING" />
</output-column>
<output-column>
<input-column heading="active" />
<target-field fieldId="active" fieldName="Active" requiredness="REQUIRED" type="ENUMERATED">
<enumerated-field-parse-options mappingKey="NAME" />
</target-field>
</output-column>
<account-processor-options />
</output-column-group>
</import-spec>