A comprehensive collection of custom processor examples and practical guide for business analysts and JavaScript developers to create custom data transformations in Chain.io flows.
This repository contains:
- Real-world examples - Production-ready custom processors (see
.jsfiles in this repo) - Complete documentation - Everything you need to get started
- Best practices - Proven patterns from the Chain.io community
- EXECUTION_SEARCH.md - Detailed guide for the
executionSearchByPartner()function - XML_LIBRARY.md - XML parsing and manipulation reference
alphabetically_sort_xml.js- Sort XML elements alphabetically to support testingexcel_to_csv.js- Convert Excel files to CSV formatfilter_shipments_with_update_or_delete_action_type.js- Filter shipments by action typeoverwrite_output_field_with_mapping.js- Map field values using lookup tableserror_edi_810_cancel_files.js- Handle EDI cancellation filesnonstandard_edi_value_replace.js- Replace non-standard EDI valuesport_of_discharge_to_port_of_destination.js- Port mapping logiclog_another_flow_with_search.js- Search for previous flow executions using executionSearchByPartner
- Quick Start
- What Are Custom Processors?
- When to Use Custom Processors
- Setting Up Your First Custom Processor
- Available Tools and Libraries
- Common Use Cases with Examples
- Best Practices
- Troubleshooting
- Advanced Examples
- Using Async Operations
- File Compatibility
- Limitations to Remember
- Getting Help
- Contributing
What you need to know:
- Basic JavaScript knowledge (variables, functions, arrays, objects)
- Understanding of JSON and CSV data formats
- 5 minutes to read this guide
What you can accomplish:
- Transform data between different formats
- Add calculated fields to your data
- Filter and validate data
- Enrich data with custom business logic
Custom processors are JavaScript scripts that run within your Chain.io flows to transform data. Think of them as your personal data transformation assistants that can:
- Pre-processors: Modify data before it goes through Chain.io's main processing
- Post-processors: Modify data after Chain.io processes it but before final delivery
- ✅ No server setup required - runs in Chain.io's cloud
- ✅ Secure sandboxed environment
- ✅ Built-in logging and error handling
- ✅ Access to powerful libraries (Excel, XML, date handling)
- Data Enrichment: Adding calculated fields, lookups, or business rules
- Format Conversion: Converting between JSON, CSV, XML formats
- Data Validation: Checking data quality and completeness
- Field Mapping: Renaming or restructuring data fields
- Conditional Logic: Processing data based on business rules
- ❌ External API calls (not allowed)
- ❌ Database connections (not supported)
- ❌ File system operations (sandboxed environment)
- ❌ Long-running processes (60-second timeout)
- Open your Chain.io flow
- Navigate to the "Edit Flow" screen
- Find the "Custom Processors" section
- Choose either "Pre-processor" or "Post-processor"
Here's a simple example that adds a timestamp to every record:
// Add current timestamp to each file
const processedFiles = sourceFiles.map(file => {
// Parse the JSON data
const data = JSON.parse(file.body)
// Add timestamp
data.processed_at = DateTime.now().toISO()
// Return the modified file
return {
...file,
body: JSON.stringify(data)
}
})
// Log what we did
userLog.info(`Added timestamps to ${processedFiles.length} files`)
// Return the processed files
returnSuccess(processedFiles)- Save your script
- Deploy your flow
- Monitor the execution logs for any issues
Your custom processors have access to these powerful tools:
sourceFiles(pre-processors): Array of incoming filesdestinationFiles(post-processors): Array of processed filesuserLog: For logging messages (userLog.info(),userLog.warning(),userLog.error())
Every file in Chain.io custom processors is represented as a JavaScript object with the following properties:
{
uuid: "550e8400-e29b-41d4-a716-446655440000", // Unique identifier for the file
type: "file", // Always "file" for file objects
file_name: "example.json", // Original filename with extension
format: "json", // File format (json, csv, xml, etc.)
mime_type: "application/json", // MIME type of the file
body: "{ \"key\": \"value\" }" // File content as string
}body(string): The actual file content. Always a string regardless of original formatfile_name(string): The filename including extension (e.g., "orders.json", "data.csv")type(string): Always set to "file"uuid(string): Unique identifier. Generate withuuid()when creating new files
format(string): File format identifier (json, csv, xml, xlsx, etc.)mime_type(string): Standard MIME type (application/json, text/csv, text/xml, etc.)
Binary Files (Excel, Images, etc.)
// Excel files come as base64-encoded strings
const workbook = XLSX.read(file.body, { type: 'base64' })Text Files (JSON, CSV, XML)
// Text files are plain strings
const data = JSON.parse(file.body) // For JSON
const lines = file.body.split('\n') // For CSVWhen creating new files in your processor, use this structure:
const newFile = {
uuid: uuid(), // Generate unique ID
type: 'file', // Always "file"
file_name: 'output.json', // Set appropriate filename
format: 'json', // Set format
mime_type: 'application/json', // Set MIME type
body: JSON.stringify(data) // Convert data to string
}When modifying files, preserve the original structure:
const modifiedFile = {
...originalFile, // Keep all original properties
body: newContent, // Update only the content
file_name: newFileName // Optionally update filename
}lodash(v4.17.21): Utility functions for arrays and objectsDateTime(Luxon v3.3.0): Date and time manipulationuuid(): Generate unique identifiers (Node.js built-in)XLSX(SheetJS v0.20.3): Read and write Excel filesxml(Chain.io v0.3.0): XML parsing and manipulation - see XML_LIBRARY.md for detailed documentationxmldom(v0.8.8) &xpath(v0.0.32): Advanced XML processing
returnSuccess(files): Process completed successfullyreturnError(files): Process failed with errorreturnSkipped(files): Skip this execution
Search for flow execution records by trading partner. This function provides programmatic access to the same execution data you see in the Flow Execution Search screen in the Chain.io portal.
Quick Example:
(async () => {
// Search for recent executions from a partner
const results = await executionSearchByPartner('partner-uuid-here', {
startDateAfter: '2024-01-01T00:00:00Z',
dataTag: 'ORDER_BATCH_123'
})
userLog.info(`Found ${results.data.length} executions`)
// Use the results in your processing
const processedFiles = sourceFiles.map(file => {
// Your logic here
return file
})
return returnSuccess(processedFiles)
})()Key Points:
⚠️ Requires async wrapper: Must wrap entire script in(async () => { ... })()⚠️ Rate limited: Maximum 10 searches per execution⚠️ Returns Promise: Useawaitandreturn returnSuccess()- 📖 Complete Documentation: See detailed guide for all parameters, options, and use cases
Common Use Cases:
- Check for duplicate processing
- Enrich data with execution history
- Track batch processing status
- Analyze execution patterns
💡 See also:
- EXECUTION_SEARCH.md - Complete documentation with all parameters and examples
log_another_flow_with_search.js- Working code example
Scenario: Add total price calculation to order data
const processedFiles = sourceFiles.map(file => {
const orders = JSON.parse(file.body)
// Add total calculation to each order
orders.forEach(order => {
order.total = order.quantity * order.unit_price
order.total_with_tax = order.total * 1.08 // 8% tax
})
return {
...file,
body: JSON.stringify(orders)
}
})
userLog.info(`Calculated totals for ${processedFiles.length} order files`)
returnSuccess(processedFiles)Scenario: Only process orders above a certain value
💡 See also:
filter_shipments_with_update_or_delete_action_type.jsfor filtering shipments by action type
const processedFiles = sourceFiles.map(file => {
const orders = JSON.parse(file.body)
// Filter orders above $100
const validOrders = orders.filter(order => {
const total = order.quantity * order.unit_price
if (total < 100) {
userLog.warning(`Skipping order ${order.id}: total $${total} below minimum`)
return false
}
return true
})
return {
...file,
body: JSON.stringify(validOrders)
}
})
userLog.info(`Filtered to high-value orders only`)
returnSuccess(processedFiles)Scenario: Convert CSV data to structured JSON
const processedFiles = sourceFiles.map(file => {
// Assuming CSV content in file.body
const lines = file.body.split('\n')
const headers = lines[0].split(',')
const jsonData = lines.slice(1).map(line => {
const values = line.split(',')
const record = {}
headers.forEach((header, index) => {
record[header.trim()] = values[index]?.trim()
})
return record
})
return {
...file,
body: JSON.stringify(jsonData),
file_name: file.file_name.replace('.csv', '.json')
}
})
userLog.info(`Converted ${processedFiles.length} CSV files to JSON`)
returnSuccess(processedFiles)Scenario: Extract data from Excel spreadsheet
💡 See also:
excel_to_csv.jsin this repository for a production example
const processedFiles = sourceFiles.map(file => {
// Skip non-Excel files (like the example in this repo)
if (!file.file_name?.match(/\.xls[xbm]$/)) return null
// Parse Excel file - note: use 'base64' type for binary data
const workbook = XLSX.read(file.body, { type: 'base64' })
const sheetName = workbook.SheetNames[0]
const worksheet = workbook.Sheets[sheetName]
// Convert to JSON
const jsonData = XLSX.utils.sheet_to_json(worksheet)
// Add processing metadata
jsonData.forEach(row => {
row.source_sheet = sheetName
row.processed_date = DateTime.now().toISODate()
})
return {
uuid: uuid(),
type: 'file',
body: JSON.stringify(jsonData),
file_name: file.file_name.replace(/\.xls[xbm]$/, '.json'),
format: 'json',
mime_type: 'application/json'
}
}).filter(x => x) // Remove null entries
userLog.info(`Processed ${processedFiles.length} Excel files`)
returnSuccess(processedFiles)Scenario: Extract specific data from XML files
const processedFiles = sourceFiles.map(file => {
try {
// Parse XML
const xmlDoc = xml.XmlParser.parseFromString(file.body)
// Extract order information
const orders = xml.elements(xmlDoc, '//order').map(orderElement => ({
id: xml.text(orderElement, 'id'),
customer: xml.text(orderElement, 'customer'),
amount: parseFloat(xml.text(orderElement, 'amount')),
date: xml.text(orderElement, 'date')
}))
return {
...file,
body: JSON.stringify(orders),
file_name: file.file_name.replace('.xml', '.json')
}
} catch (error) {
userLog.error(`Failed to process XML file ${file.file_name}: ${error.message}`)
return file // Return original file if processing fails
}
})
userLog.info(`Processed ${processedFiles.length} XML files`)
returnSuccess(processedFiles)Scenario: Add customer information based on customer ID
💡 See also:
overwrite_output_field_with_mapping.jsfor XML field mapping andport_of_discharge_to_port_of_destination.jsfor port lookups
// Sample customer lookup data
const customerLookup = {
'CUST001': { name: 'Acme Corp', tier: 'Premium' },
'CUST002': { name: 'Beta Inc', tier: 'Standard' },
'CUST003': { name: 'Gamma LLC', tier: 'Premium' }
}
const processedFiles = sourceFiles.map(file => {
const orders = JSON.parse(file.body)
// Enrich each order with customer data
orders.forEach(order => {
const customer = customerLookup[order.customer_id]
if (customer) {
order.customer_name = customer.name
order.customer_tier = customer.tier
// Apply tier-based discount
if (customer.tier === 'Premium') {
order.discount_percent = 10
order.discounted_total = order.total * 0.9
}
} else {
userLog.warning(`Unknown customer ID: ${order.customer_id}`)
}
})
return {
...file,
body: JSON.stringify(orders)
}
})
userLog.info(`Enriched orders with customer data`)
returnSuccess(processedFiles)try {
// Your processing logic here
const data = JSON.parse(file.body)
// ... process data
} catch (error) {
userLog.error(`Processing failed: ${error.message}`)
return file // Return original file or handle appropriately
}userLog.info(`Starting to process ${sourceFiles.length} files`)
userLog.info(`Successfully processed ${validRecords.length} records`)
userLog.warning(`Skipped ${skippedRecords.length} invalid records`)// Check if required fields exist
if (!order.customer_id || !order.amount) {
userLog.warning(`Invalid order missing required fields: ${JSON.stringify(order)}`)
return null // Skip this record
}- Break complex logic into smaller functions
- Use meaningful variable names
- Comment your code for future reference
- Start with a few records to test your logic
- Gradually increase data volume once working
Issue: "Invalid processor output"
// ❌ Wrong - not returning files properly
return data
// ✅ Correct - always return file objects
returnSuccess(processedFiles)Issue: "Timeout after 60 seconds"
// ❌ Avoid complex loops on large datasets
sourceFiles.forEach(file => {
// Expensive operation on every record
})
// ✅ Use efficient processing
const processedFiles = sourceFiles.map(file => {
// Quick transformation
return transformFile(file)
})Issue: "Cannot parse JSON"
// ❌ Assuming data is always valid JSON
const data = JSON.parse(file.body)
// ✅ Handle parsing errors
try {
const data = JSON.parse(file.body)
// Process data
} catch (error) {
userLog.error(`Invalid JSON in file ${file.file_name}`)
return file // Return original or skip
}- Use console logging:
userLog.info(JSON.stringify(data, null, 2)) - Check file structure: Log the file object to understand its properties
- Validate step by step: Add logging at each major step
- Test with minimal data: Use simple test cases first
const processedFiles = sourceFiles.map(file => {
// Handle different file types
if (file.file_name.endsWith('.json')) {
return processJsonFile(file)
} else if (file.file_name.endsWith('.csv')) {
return processCsvFile(file)
} else if (file.file_name.endsWith('.xml')) {
return processXmlFile(file)
} else {
userLog.warning(`Unsupported file type: ${file.file_name}`)
return file
}
})
function processJsonFile(file) {
const data = JSON.parse(file.body)
// JSON-specific processing
return { ...file, body: JSON.stringify(data) }
}
function processCsvFile(file) {
// CSV processing logic
return file
}
function processXmlFile(file) {
// XML processing logic
return file
}
returnSuccess(processedFiles)Data tags appear in the Flow Execution Screen for tracking and monitoring:
// Process your data
const processedFiles = sourceFiles.map(file => {
const orders = JSON.parse(file.body)
// Calculate summary statistics
const totalOrders = orders.length
const totalValue = orders.reduce((sum, order) => sum + order.total, 0)
// Publish data tags for monitoring
publishDataTags([
{ label: 'Total Orders', value: totalOrders.toString() },
{ label: 'Total Value', value: `$${totalValue.toFixed(2)}` },
{ label: 'Processing Date', value: DateTime.now().toISODate() }
])
return { ...file, body: JSON.stringify(orders) }
})
returnSuccess(processedFiles)Note: Data tag labels and values are automatically truncated to 255 bytes each to ensure compatibility with the Chain.io platform. If your data contains labels or values longer than 255 bytes, they will be truncated when published. (A unicode character is 1-4 bytes. If you need to see how many bytes a particular string is, there are online tools available. Here is an example: UTF-8 String Length & Byte Counter )
const processedFiles = sourceFiles.map(file => {
const data = JSON.parse(file.body)
// Check if data meets processing criteria
if (data.length === 0) {
userLog.warning('No data to process')
return null
}
if (data.some(record => !record.required_field)) {
userLog.error('Data missing required fields')
return null
}
// Process valid data
return { ...file, body: JSON.stringify(data) }
}).filter(file => file !== null) // Remove null entries
if (processedFiles.length === 0) {
userLog.warning('No valid files to process')
returnSkipped([])
} else {
returnSuccess(processedFiles)
}Custom processors support asynchronous operations, but they require a specific wrapper pattern.
You need to use the async wrapper when:
- Using
executionSearchByPartner()to search for previous executions - Using
awaitwith any Promise-based operation - Calling any function that returns a Promise
❌ This will NOT work:
// Without async wrapper - will cause errors
const results = await executionSearchByPartner('partner-uuid')
returnSuccess(sourceFiles)✅ This WILL work:
// With async wrapper - correct pattern
(async () => {
const results = await executionSearchByPartner('partner-uuid')
return returnSuccess(sourceFiles) // Note: use "return"
})()- Wrap everything in
(async () => { ... })() - Use
returnbefore yourreturnSuccess(),returnError(), orreturnSkipped()calls - Keep it simple - the wrapper goes around your entire script
- Remember the timeout - all async operations must complete within the 60 second total runtime alloted to custom processors
(async () => {
// You can now use await anywhere in your code
const results = await executionSearchByPartner('partner-uuid-here', {
startDateAfter: '2024-01-01T00:00:00Z'
})
userLog.info(`Found ${results.data.length} previous executions`)
// Process your files normally
const processedFiles = sourceFiles.map(file => {
const data = JSON.parse(file.body)
// Use the search results to enrich your data
data.previousExecutionCount = results.data.length
return {
...file,
body: JSON.stringify(data)
}
})
// Don't forget to use "return" before returnSuccess
return returnSuccess(processedFiles)
})()(async () => {
// Search for previous executions
const results = await executionSearchByPartner('partner-uuid')
// Use conditional logic as normal
if (results.data.length === 0) {
userLog.warning('No previous executions found')
return returnSkipped([])
}
return returnSuccess(processedFiles)
})()- ✅ JSON: Full read/write support
- ✅ CSV: Parse and generate CSV data
- ✅ XML: Parse, query, and generate XML
- ✅ Excel: Read .xlsx files, extract data
- ❌ Cannot be processed in custom processors (though you can make minor edits to individual fields like changing charge codes)
- ✅ Use Chain.io's Custom Mapping Tool instead
- ✅ Process EDI after conversion to JSON
- No external libraries: Cannot
require()orimportadditional packages beyond the built-in libraries listed above - 60-second timeout: Total execution time (including any async operations) must complete within 60 seconds
- 10,000 character limit: Per processor (pre and post can each be 10,000 characters)
- No Symbol object access: Security restriction
- Async operations require wrapper: If using
awaitor async functions likeexecutionSearchByPartner, you must wrap your entire script in(async () => { ... })()and usereturnstatements
- Check the execution logs in Chain.io for error messages
- Start simple - test with minimal data first
- Use logging extensively to understand data flow
- Review the examples in this guide for similar use cases
Q: Can I call external APIs?
A: No, custom processors run in a sandboxed environment without external network access. However, you can use executionSearchByPartner() to search for previous execution data within Chain.io.
Q: How do I use async/await in my processor?
A: Wrap your entire script in (async () => { ... })() and use return before your returnSuccess(), returnError(), or returnSkipped() calls. See the "Advanced Functions" section above for complete examples.
Q: Why do I get "await is only valid in async function" error?
A: You need to wrap your entire script in the async wrapper: (async () => { /* your code here */ })(). Don't forget to add return before your return function calls.
Q: How do I handle large files? A: Process data in chunks and use efficient algorithms. Consider splitting large files before processing.
Q: Can I save state between executions?
A: No, each execution is independent. However, you can use executionSearchByPartner() to look up data from previous executions.
Q: What happens if my code has errors? A: The flow will fail with an error status, and details will appear in the execution logs.
We welcome contributions from the Chain.io community!
- Fork this repository - Click the "Fork" button at the top of this page
- Add your example - Create a new
.jsfile with a descriptive name - Follow the pattern - Look at existing examples for structure and commenting style
- Test your code - Make sure it works in your Chain.io environment
- Submit a pull request - Create a pull request with your changes
- Use descriptive filenames -
convert_xml_to_json.jsinstead ofprocessor.js - Add comments - Explain what your processor does and any assumptions
- Include error handling - Show how to handle edge cases
- Keep it focused - One clear use case per example
- Test with real data - Ensure your example works in production
- Review existing examples in this repository
- Ask questions in your pull request - the community is here to help!
This repository contains practical examples and comprehensive documentation for Chain.io custom processors. For specific business requirements or complex scenarios, consider consulting with your development team or Chain.io support.