Published at: 2025-10-30

4.2 [Function Component] Custom Function Description


Note: 1. The pre-sync, mid-sync, and post-sync custom functions must use a single input parameter named syncArg of type Map. The function return value must also be a Map. 2. In post-sync functions, avoid performing Fx.object.update and modifying any mapped fields (including fields used by data scope) at the same time. Doing both may create circular synchronization. The Integration Platform validates this when enabling an integration flow. If you are certain no loop will be triggered but still require such behavior, contact the Integration Platform development team to request a bypass.

  1. syncArg Properties

syncDataId — sync data ID sourceTenantId — source tenant ID sourceObjectApiName — source Object API name sourceEventType — source event type: 1 = “create”, 2 = “update” destTenantId — destination tenant ID destObjectApiName — destination Object API name destEventType — destination event type: 1 = “create”, 2 = “update” objectData — Primary Object data. Field values can be accessed by API name. details — Sub-object data. Field values can be accessed by API name.

Notes: - In pre-sync functions, objectData and details carry source system data. Pre-sync functions must not modify destination data. - In mid-sync functions, objectData and details carry source system data, and you may modify destination data. - In post-sync functions, objectData and details carry destination system data. - The mid-sync function runs before calling the destination system write API; the post-sync function runs after the write call completes. - Sub-object data exists only for create events.

  1. Pre-sync Function

Use pre-sync custom functions to filter or short-circuit synchronization.

  • If the returned Map includes “isExec”: false, the synchronization stops and the destination will not be updated.
  • If the returned Map includes “isCover”: false, then values returned by the custom function will not overwrite the original data.
  • objectData may represent either Primary Object or Sub-object data. Check object_describe_api_name to determine which type is being processed by the pre-sync function.

Test example: returning a Map with “isExec”: false or “isExec”:”false” prevents any destination changes.

Example code: Map map = [“details”:syncArg.details, “objectData”:syncArg.objectData, “isExec”:false, “isCover”:false]; return map;

  1. Mid-sync Function

In mid-sync custom functions, modify data first and then write the modified data to the destination system.

Example code: log.info(“Event type: “ + syncArg.destEventType); log.info(syncArg.objectData); log.info(syncArg.details); Map objectData = syncArg.objectData as Map;

objectData.name = “Field modified by custom function”

Map map = [“details”:syncArg.details, “objectData”:syncArg.objectData]; log.info(map); return map;

  1. Post-sync Function

Example scenario: After a ShareCRM SalesOrder is synchronized to K3Cloud ERP, write back the ERP order ID and Code to the CRM record.

Use case: write back ERP IDs to the originating CRM SalesOrder.

Example function: // Target source object (Sales Order) def sourceObjectApiName = ‘SalesOrderObj’; // Field to write back ERP ID def erpIdF = “field_86Xfn__c”; // Field to write back ERP Code def erpNoF = “field_K4xdf__c”; def destEventType = syncArg.destEventType def completeDataWriteResult = syncArg.completeDataWriteResult as Map if( syncArg.sourceObjectApiName != sourceObjectApiName || completeDataWriteResult.destEventType != 1 || !completeDataWriteResult.success ){ log.info(“No action”) // Return in following cases: // - Not the SalesOrder object // - Not a create event // - Destination write not successful return syncArg; } log.info(“Start writing back ID”) String sourceDataId = syncArg.sourceDataId def writeResult = completeDataWriteResult.writeResult as Map String destDataId = writeResult.destDataId def split = destDataId.split(“#”, 2) def upArg = [(erpIdF):split[0],(erpNoF):split[1]] log.info(upArg) def upR = Fx.object.update(sourceObjectApiName, sourceDataId, upArg ) log.info(“Write-back result: “ + upR)

  1. Escaping (Adapter) Custom Function — Calling ERP APIs to Transform Data

When an ERP exposes only nonstandard or custom APIs that are not pre-integrated in the Integration Platform, use an adapter custom function to call the ERP API and convert its payloads to the Integration Platform standard API format.

Images: image image image

Guidance: - Conditional filtering should not be inside adapter functions. Use Data Scope where possible; otherwise, use pre-sync functions. - The function parameter must be a Map named syncArg. Inspect parameters with log.info(“Request parameters:” + syncArg).

Example: queryMasterBatch receives system-passed parameters

// Invocation example: log.info(“Request parameters:” + syncArg); Integer offset = syncArg[“objectData”] == null ? 0 : (Integer)syncArg[“objectData”][“offset”]; Integer limit = syncArg[“objectData”] == null ? 10 : (Integer)syncArg[“objectData”][“limit”]; Long startTime = syncArg[“objectData”] == null ? 0 : (Long)syncArg[“objectData”][“startTime”]; Long endTime = syncArg[“objectData”] == null ? 0 : (Long)syncArg[“objectData”][“endTime”];

Example: queryMasterById receives system-passed parameters

// Invocation example: log.info(“Request parameters:” + syncArg); String dataId = syncArg[“objectData”][“dataId”];

  1. Asynchronous Push API via Adapter Function

Endpoint URL: https://www.fxiaoke.com/erp/syncdata/open/objdata/asyncpush

Use case: When client push payloads do not match the platform format, use a custom function to transform inbound payloads into the platform-required format.

Flow: - External systems POST to the Integration Platform push endpoint, which stores data in a cache table. - The platform asynchronously pulls data from the cache and processes it. If writing to the cache fails, the API returns an error directly. If processing fails after cache ingestion, check the Integration Platform data maintenance page for error details.

Request method: POST

Request Header parameters: | Field | Description | | token | Authentication token (obtain from ShareCRM dev team) | | tenantId | Provided by ShareCRM dev team | | dataCenterId | Required in multi-account setups; not required for single-account setups | | objectApiName | The ERP object’s actual API name; find in Integration Platform → ERP Object Settings → ERP Object Code | | version | v1 | | operationType | Use 3 for invalidation. Otherwise omit. |

Push steps: 1. Create the ERP object in Integration Platform UI. image 2. Create a sync policy. image 3. Add a push adapter custom function. (Any push—standard or nonstandard—that uses the push API requires this function.)

Example adapter function: log.info(“Request parameters:” + Fx.json.toJson(syncArg)) String dataStr = syncArg[“objectData”][“pushData”]; Map pushDataMap = Fx.json.parse(dataStr);

List<Map> pushDatas = []; if (pushDataMap[“data”] instanceof List) { /* Single-payload example: { “data” : { “number” : “A2001”, “lrap_days” : “2”, “name” : “Product 41” } } / pushDatas = pushDataMap[“data”] as List } else if (pushDataMap[“data”] instanceof Map) { / Multiple-payload example: { “data” : [ { “number” : “A2001”, “lrap_days” : “2”, “name” : “Product 41” } ] } */ pushDatas.add(pushDataMap[“data”] as Map) }

List resultList = [] pushDatas.each{ map -> Map dataMap = map as Map; String id = map[“code”]; // choose a unique identifier field from primary object payload dataMap.put(“id”, id); // Mandatory: must include this key and value Map data = [“masterFieldVal”: dataMap] // Convert to platform format resultList.add(data) } return [“dataList”: resultList]; // Must return a list type

Push failure examples: image image

Push success examples: image image

  1. Scenario Functions

Trigger synchronization from source system to destination by source data ID (requires getById interface on source).

Use case: Trigger sync for a specified source data ID.

Example function: Map header = [:] Map params1 = [:] params1.put(“erpObjectApiName”, “xxObj”); // Target ERP object API name (parameter name historical) params1.put(“crmDataId”, “id”); // Source CRM data ID params1.put(“crmObjectApiName”, “xxxobj”); // Source CRM object API name Map commonParam = [“type”:”manualSyncCrm2Erp”, “params”:Fx.json.toJson(params1)]; def result1 = Fx.proxy.callAPI(“erp.syncData.executeCustomFunction”, header, commonParam); log.info(result1)

Trigger ERP→CRM synchronization using a CRM data ID (requires existing mapping):

// Required: CRM object API name def crmObjApiName = context.data.object_describe_api_name; // Required: CRM data ID def crmDataId = context.data._id // Required: ERP intermediate object API name def erpObjApiName = “BD_Customer.BillHead”; // Type used to trigger ERP->CRM sync by CRM data ID def type = “manualSyncErpDataByCrmDataId” def params = [“crmObjectApiName”:crmObjApiName, “crmDataId”:crmDataId, “erpObjectApiName”:erpObjApiName] def arg = [“type”:type, “params”:Fx.json.toJson(params)] def ret = Fx.proxy.callAPI(“erp.syncData.executeCustomFunction”, [:], arg) // Handle sync result: log.info(ret)

Calling Kingdee K3Cloud web API from ShareCRM

image

Example snippet to call K3Cloud WebApi from ShareCRM: // K3Cloud base URL String baseUrl = “http://172.31.100.60/K3Cloud/” // API paths String auth = “Kingdee.BOS.WebApi.ServicesStub.AuthService.ValidateUser.common.kdsvc” String query = “Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.ExecuteBillQuery.common.kdsvc” String save = “Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.Save.common.kdsvc” String submit = “Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.Submit.common.kdsvc” String audit = “Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.Audit.common.kdsvc” // Login payload List param = [“5ec229fad54306”, “ces”, “888888”, 2052] // Request body Map body = [“parameters”: param] Map headers = [“Content-Type”: “application/json; charset=utf-8”] Map content = [:] // Login call def (Boolean error, HttpResult data, String errorMessage) = Fx.http.post(baseUrl + auth, headers, body) Map cookieHeader = [:] if (!error) { if (data.statusCode == 200) { content = json.parse(data.content as String) String logInType = content.”LoginResultType” if (logInType == “1” || logInType == “-5”) { log.info(“Login success, content:” + content) } else { log.info(“Login failed, content:” + content) }

} else {
    log.info("Request failed, HttpResult:" + data)
}

} else { log.info(“Request error, errorMessage:” + errorMessage) }

// Bill query example (Inventory query) body.put(“parameters”, [ [ “FormId” : “STK_Inventory”, “FieldKeys” : “FID,FStockId.FNumber,FMaterialId.FNumber,FMaterialId.FName,FLot.FNumber,FLot,FStockLocId,FBaseQty,FBaseAVBQty,FUpdateTIme”, “FilterString” : “FMaterialId.FNumber=’CH4453’”, “OrderString” : “”, “TopRowCount” : “0”, “StartRow” : “0”, “Limit” : “0” ] ]) (error, data, errorMessage) = Fx.http.post(baseUrl + query, headers, body) log.info(data)

/————————— Create Sales Order example —————————————/ // Get sales order from CRM context Map salesOrderMap = context.data log.info(“Order:” + json.toJson(salesOrderMap)) // Get first sales order product Map salesProductMap = ((List)context.details.SalesOrderProductObj)[0] log.info(“Order product:” + salesProductMap) // Get product by ID Map productData (error, productData, errorMessage) = object.findById(“ProductObj”, salesProductMap.product_id as String) log.info(“product:” + productData) // Create order model Map model = [:] // Required fields model.put(“FBillTypeID”, [“FNumber”: “XSDD01_SYS”]) model.put(“FSaleOrgId”, [“FNumber”: “003”]) model.put(“FCustId”, [“FNumber”: “4326rtyu”]) model.put(“FDate”, “2020-05-22 00:00:00”) model.put(“FSalerId”, [“FNumber”: “88888”]) model.put(“FSaleDeptId”, [“FNumber”: “”]) model.put(“FBillNo “, salesOrderMap.name) // Order number // Order detail Map material = [:] material.put(“FMaterialId”, [“FNumber”: productData.product_code]) material.put(“FUnitID”, [“FNumber”: “Pcs”]) material.put(“FQty”, salesProductMap.quantity) material.put(“FTaxPrice”, salesProductMap.sales_price) model.put(“FSaleOrderEntry”, [material]) // Finance info Map finance = [:] finance.put(“FSettleCurrId”, [“FNumber”: “PRE001”]) finance.put(“FExchangeTypeId”, [“FNumber”: “HLTX01_SYS”]) finance.put(“FExchangeRate”, 1) model.put(“FSaleOrderFinance”, finance) body.put(“parameters”,[“SAL_SaleOrder”, json.toJson([“Model”:model])]) log.info(json.toJson(model)) (error, data, errorMessage) = Fx.http.post(baseUrl + save, headers, body) log.info(data) // Submit body.put(“parameters”,[“SAL_SaleOrder”, [“Numbers”: [salesOrderMap.name]]]) (error, data, errorMessage) = Fx.http.post(baseUrl + submit, headers, body) log.info(data) // Audit (error, data, errorMessage) = Fx.http.post(baseUrl + audit, headers, body) log.info(data) // This is an example. Return value must match the custom function’s required return type. return “111”;

Operate intermediate sync mapping table from CRM custom function

Example snippets: Map header = [:] // Create sync mapping Map param1 = [“ployDetailId”:”155bd981457343f291e0edc13776217f”, “sourceObjectApiName”:”AccountObj”, “destObjectApiName”:”BD_Customer.BillHead”, “sourceDataId”:”sourceDataId123”, “destDataId”:”destDataId123666”, “sourceDataName”:”sourceDataName3666”, “destDataName”:”destDataName66”, “remark”:”remark1341”]; def result1 = Fx.proxy.callAPI(“erp.syncData.createSyncDataMapping”, header, param1); // Success returns errCode s106240000 log.info(result1)

// Update mapping by source data ID Map param2 = [“sourceObjectApiName”:”AccountObj”, “destObjectApiName”:”BD_Customer.BillHead”, “sourceDataId”:”sourceDataId123”, “destDataId”:”destDataId123666”] def result2 = Fx.proxy.callAPI(“erp.syncData.updateSyncDataMapping”, header, param2); log.info(result2)

// Query mapping by source data IDs Map param3 = [“sourceObjectApiName”:”AccountObj”, “destObjectApiName”:”BD_Customer.BillHead”, “sourceDataId”:[“sourceDataId123”]] def result3 = Fx.proxy.callAPI(“erp.syncData.getSyncDataMappingBySourceDataId”, header, param3); log.info(result3) // The returned data is a Map keyed by sourceDataId with mapping details.

Capture ployDetailId (policy detail ID) from the sync policy UI: image

OA pending notifications sync uses CRM personnel to create mapping tables:

// channel values: ERP_K3CLOUD, ERP_SAP, ERP_U8, OA, STANDARD_CHANNEL // “dataType”:”employee” indicates employee mapping // dataCenterId is the data center ID // fsDataId is CRM-side data ID. For employees, use employee ID (not CRM user object ID). // erpDataId is ERP-side data ID // fsDataName is CRM data name // erpDataName is ERP data name

Map data = [ “dataCenterId”: “701916***319168”, “channel”: “OA”, “dataType”: “employee_oa”, “fsDataId”: fsDataId, “fsDataName”: fsDataName, “erpDataId”: erpDataId, “erpDataName”: erpDataName ];

def ret = Fx.proxy.callAPI(“erp.syncData.createErpfieldmapping”, [:], data); Fx.log.info(“ret is : “ + ret)

  1. Standard API Interface Specification

Standard API functions have strict input and output requirements: - The input parameter must be named syncArg and be of type Map. - The function must return a Map. - Returned Map fields must strictly follow the Integration Platform’s “Connector Management → Connect Object → Generate API” HTTP response format. - For K3Cloud channel, code = 0 means success; nonzero means failure. When code = 0, data must include values for masterDataId and, if present, detail object IDs. Return detail IDs in the specific format and order required by the Integration Platform. - For Standard and SAP channels, the names of code/message/data fields and the success code can be customized in Integration Platform → Connector Management → Connector Configuration. Once configured, custom functions must return responses using those names and success code.

Reference: Standard API response example (see UI screenshot) image image

Attachment: DSS platform custom function adaptation.docx (download) [File metadata preserved externally.]

8.1 asyncpush — Push Data API

asyncpush is asynchronous: a successful API response only indicates the payload reached the Integration Platform cache. Downstream processing is asynchronous and batched. Unless strict real-time behavior is required, prefer using the queryMasterBatch pull API.

Benefits of queryMasterBatch: - Better fault tolerance - Mature tooling exists to reprocess historical data or re-sync, avoiding manual intervention from customer IT.

Push API developer reference: http://help.fxiaoke.com/2615/9bfa/b0a6/70d7

8.2 create

Used when CRM -> ERP creating data.

Requirements: - The response must include ERP primary keys. ERP primary keys cannot be returned asynchronously. - If the ERP cannot return within 30s, ERP must implement de-duplication logic so repeated retries from CRM return the previous successful creation result.

Request payload: { “objAPIName”:”ERP object API name”, “masterFieldVal”:”master data”, “detailFieldVals”:”list of sub-object data” }

Response payload: { “code”: “error code”, “message”: “message”, “data”: { “masterDataId”: “master primary key”, “detailDataIds”: { “each detail object’s primary key list” } } }

8.3 update

Used when CRM -> ERP updating master data. Master and sub-objects overwrite destination data.

Request: { “objAPIName”:”ERP object API name”, “masterFieldVal”:”master data”, “detailFieldVals”:”list of sub-object data” }

Response: { “code”: “error code”, “message”: “message”, “data”: { “masterDataId”: “master primary key”, “detailDataIds”: { “each detail object’s primary key list” } } }

8.4 queryMasterBatch

ERP -> CRM incremental query API. The Integration Platform polls this endpoint on a timer to obtain incremental changes.

Parameters: - objAPIName: ERP object API name - startTime: change start time (Unix timestamp in milliseconds) - endTime: change end time (Unix timestamp in milliseconds) - includeDetail: whether to include sub-object data - offset: record offset - limit: number of records to return

Response: { “code”: “error code”, “message”: “message”, “data”: { “totalNum”: “total record count”, “dataList”: [{ “objAPIName”: “primary object name”, “masterFieldVal”: {master object data}, “detailFieldVals”: { “subObj1”: [{subObj1 data list}], “subObj2”: [{subObj2 data list}] } }, …] } }

Polling termination rules for a time window: 1) If an intermediate page returns 0 records (e.g., a 6-minute window spread over 10 pages, and page 2 returns 0 records), the platform will stop paging that time slice and move to the next time slice. 2) If the ERP endpoint returns an error for the time slice, skip the time slice and continue to the next.

Example response: image

8.5 queryMasterById

ERP -> CRM fetch by primary ID. Called during re-sync to obtain the latest data.

Parameters: - objAPIName: ERP object API name - dataId: data primary ID - includeDetail: whether to include sub-object data

Response: { “code”: “error code”, “message”: “message”, “data”: { “objAPIName”: “ERP object API name”, “masterFieldVal”: {master data}, “detailFieldVals”: { “subObjectAPIName”: [{sub-object data list}] } } }

Example: image

8.6 invalid

CRM -> ERP invalidation (void/cancel) of master data.

Response examples: { “errCode” : “error code”, “errMsg” : “error message” }

Example: image

8.7 Example: XML-format List Query Function

/* List query example */

// Invocation parameters: log.info(“Request parameters:” + syncArg); Integer offset = syncArg[“objectData”] == null ? 0 : (Integer)syncArg[“objectData”][“offset”]; Integer limit = syncArg[“objectData”] == null ? 50 : (Integer)syncArg[“objectData”][“limit”]; Long startTime = syncArg[“objectData”] == null ? 0 : (Long)syncArg[“objectData”][“startTime”]; Long endTime = syncArg[“objectData”] == null ? 0 : (Long)syncArg[“objectData”][“endTime”];

// Optionally modify request parameters here for testing/debugging

// Convert offset/limit to page format Integer page = ((offset + limit) / limit).toInteger(); Integer pageSize = limit;

// Convert timestamps to dates DateTime startDt = DateTime.of(startTime); DateTime endDt = DateTime.of(endTime); Integer startmonth = startDt.month; Integer startday = startDt.day; Integer starthour = startDt.hour; Integer startminute = startDt.minute;

Integer endmonth = endDt.month; Integer endday = endDt.day;

String startDateStr = startDt.year + “-“ + (startmonth < 10 ? “0” + startmonth : startmonth) + “-“ + (startday < 10 ? “0” + startday : startday); String endDateStr = endDt.year + “-“ + (endmonth < 10 ? “0” + endmonth : endmonth) + “-“ + (endday < 10 ? “0” + endday : endday); String startTimeStr = “” + (starthour < 10 ? “0” + starthour : starthour) + “:” + (startminute < 10 ? “0” + startminute : startminute) + “:00”;

// Request URL obtained via a controller function (urlName parameter) Map urlParams = [“urlName”: “”]; def (error, result, errorMessage) = Fx.function.executeFunc(“func_Yd9n2__c”, urlParams) String requestUrl = result[“url”];

// Request headers (authentication can be placed in controller) Map header = [“Authorization”:”Basic “ + Fx.crypto.base64.encode(Fx.utils.toUTF8Bytes(“username:password”)) ,”Content-Type”:”text/xml; charset=utf-8”];

// Request XML payload String requestXml = “<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:urn="urn:sap-com:document:sap:soap:functions:mc-style">\n” + “ \n” + “ \n" + " \n" + " \n" + " " + endDateStr + "\n" + " \n" + " 23:59:59\n" + " \n" + " " + page + "\n" + " \n" + " " + pageSize + "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " " + startDateStr + "\n" + " \n" + " " + startTimeStr + "\n" + " \n" + " \n" + " \n" + " \n” + “</soapenv:Envelope>”; log.info(“Request URL:” + requestUrl); log.info(“Request XML:” + requestXml); def (Boolean error2, HttpResult result2, String errorMessage2) = Fx.http.post(requestUrl, header, requestXml, 30000, false, 0) result2[“bytes”] = null;

if (result2 == null) { Fx.message.throwErrorMessage(“API timeout. Edit the fields to sync and re-trigger sync”); }

if (error2) { log.info(“Response:” + result2); Fx.message.throwErrorMessage(errorMessage2); } else if (result2[“statusCode”] != 200) { log.info(“Response:” + result2); Fx.message.throwErrorMessage(“Please check request parameters”); }

// Parse XML response def xmlSlurper = new XmlSlurper(); String content = result2[“content”]; def responseMap = xmlSlurper.parseText(content) def masterArrays = responseMap.Body[“Zcrmfu0003Response”][“Rtmaster”] as NodeChildren; def detailArrays = responseMap.Body[“Zcrmfu0003Response”][“Rtdetail”] as NodeChildren;

List<Map> dataList = []; // Optionally choose return fields. Alternatively return full payload as Map and map ERP fields in Integration Platform. List returnMasterFields = [“Vbeln”,”Lfart”,”Vstel”,”Kunnr”,”Vkorg”,……..] masterArrays.children().each{ String masterId = it[“Vbeln”]; if ((masterId != null && “” != masterId)) { Map dataMap = [:] returnMasterFields.each{ field -> // NodeChildren must be converted to string to avoid serialization issues String fieldV = (it[field] as NodeChildren).text() as String; if (fieldV != null && “” != fieldV) { dataMap.put(field, fieldV) } } dataMap[“masterId”] = masterId; dataMap[“Vstel”] = “Plant#” + dataMap[“Vstel”]; // example special field handling dataList.add([“masterFieldVal”: dataMap]) } }

// If details exist, handle them as: [“masterFieldVal”:dataMap, “detailFieldVals”:[[“detailAPIName1”: []], [“detailAPIName2”: []]]]

Map resultMap = [:]; resultMap.put(“dataList”, dataList); return resultMap;

Submit Feedback