Skip to main content

Data Transformation

The Transform Data node is one of the most powerful nodes in Expedify. It lets you write JavaScript to process, format, combine, and transform data between workflow steps.

Time to complete: 20 minutes


When to Use Transform Data​

Use the Transform Data node when you need to:

  • Format data (names, dates, currencies)
  • Combine data from multiple sources
  • Extract specific fields from complex objects
  • Calculate values (totals, averages, scores)
  • Validate data before processing
  • Convert between formats (arrays, objects, strings)

Basic Structure​

The Transform Data node uses JavaScript with a specific pattern:

// Access input data from previous nodes
const data = input.webhooktrigger_1.payload;

// Process the data
const result = {
formatted_name: data.name.toUpperCase(),
email: data.email.toLowerCase()
};

// Return the output
return result;

The Input Object​

All upstream node outputs are available in input:

input.webhooktrigger_1.payload      // Webhook data
input.transformdata_1.result // Previous transform
input.llmintegration_1.output // AI output
input.contact // From contact lookup

The Return Statement​

Whatever you return becomes the node's output:

return {
field1: "value1",
field2: "value2"
};

Access it downstream as:

{{transformdata_1.field1}}
{{transformdata_1.field2}}

Common Transformations​

String Formatting​

const name = input.contact.name;

return {
uppercase: name.toUpperCase(), // "JOHN SMITH"
lowercase: name.toLowerCase(), // "john smith"
trimmed: name.trim(), // Remove whitespace
first_word: name.split(' ')[0], // "John"
initials: name.split(' ').map(n => n[0]).join(''), // "JS"
};

Name Handling​

const fullName = input.payload.name || "";
const parts = fullName.trim().split(' ');

return {
first_name: parts[0] || "",
last_name: parts.slice(1).join(' ') || "",
full_name: fullName,
display_name: parts[0] || "Friend"
};

Email Formatting​

const email = input.payload.email || "";

return {
email: email.toLowerCase().trim(),
domain: email.split('@')[1] || "",
is_valid: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
};

Date Formatting​

const dateStr = input.payload.date;
const date = new Date(dateStr);

return {
iso: date.toISOString(), // "2024-03-15T10:30:00.000Z"
date_only: date.toISOString().split('T')[0], // "2024-03-15"
formatted: date.toLocaleDateString('en-US'), // "3/15/2024"
time: date.toLocaleTimeString('en-US'), // "10:30:00 AM"
timestamp: date.getTime() // 1710498600000
};

Number Formatting​

const amount = input.order.total;

return {
rounded: Math.round(amount * 100) / 100, // 19.99
currency: `$${amount.toFixed(2)}`, // "$19.99"
percentage: `${(amount * 100).toFixed(1)}%`, // "19.9%"
with_commas: amount.toLocaleString() // "1,234.56"
};

Combining Data​

Merge Objects​

const contact = input.contact;
const company = input.company;
const deal = input.deal;

return {
contact_name: contact.name,
contact_email: contact.email,
company_name: company.name,
deal_value: deal.value,
summary: `${contact.name} from ${company.name} - $${deal.value}`
};

Build Email Content​

const contact = input.contact;
const order = input.order;

const itemList = order.items.map(item =>
`- ${item.name}: $${item.price}`
).join('\n');

return {
subject: `Order Confirmation #${order.id}`,
body: `
Hello ${contact.first_name},

Thank you for your order!

Order Details:
${itemList}

Total: $${order.total}

We'll notify you when it ships.

Best regards,
The Team
`.trim()
};

Create Structured Response​

const results = input.kbsearch_1.results;
const question = input.usermessagetrigger_1.message;

return {
question: question,
answer_count: results.length,
top_result: results[0]?.content || "No results found",
sources: results.map(r => ({
title: r.metadata?.title || "Untitled",
relevance: Math.round(r.similarity * 100) + "%"
}))
};

Array Operations​

Filtering​

const contacts = input.data.contacts;

return {
active: contacts.filter(c => c.is_active),
high_value: contacts.filter(c => c.deal_value > 10000),
with_email: contacts.filter(c => c.email && c.email.length > 0)
};

Mapping (Transform Each Item)​

const items = input.order.items;

return {
formatted_items: items.map(item => ({
name: item.name.toUpperCase(),
price: `$${item.price.toFixed(2)}`,
quantity: item.qty
}))
};

Reducing (Aggregate)​

const items = input.order.items;

return {
total: items.reduce((sum, item) => sum + item.price, 0),
count: items.length,
average: items.reduce((sum, item) => sum + item.price, 0) / items.length
};

Sorting​

const contacts = input.data.contacts;

return {
by_name: [...contacts].sort((a, b) => a.name.localeCompare(b.name)),
by_score_desc: [...contacts].sort((a, b) => b.score - a.score),
by_date: [...contacts].sort((a, b) => new Date(b.date) - new Date(a.date))
};

Finding​

const contacts = input.data.contacts;
const searchEmail = input.search.email;

return {
found: contacts.find(c => c.email === searchEmail) || null,
exists: contacts.some(c => c.email === searchEmail)
};

Grouping​

const deals = input.data.deals;

const grouped = deals.reduce((acc, deal) => {
const stage = deal.stage;
if (!acc[stage]) acc[stage] = [];
acc[stage].push(deal);
return acc;
}, {});

return {
by_stage: grouped,
stages: Object.keys(grouped),
counts: Object.fromEntries(
Object.entries(grouped).map(([stage, items]) => [stage, items.length])
)
};

Conditional Logic​

Simple Conditions​

const score = input.contact.lead_score;

let priority;
if (score >= 80) {
priority = "high";
} else if (score >= 50) {
priority = "medium";
} else {
priority = "low";
}

return { priority };

Ternary Operator​

const contact = input.contact;

return {
greeting: contact.first_name ? `Hello ${contact.first_name}` : "Hello there",
status: contact.is_active ? "Active" : "Inactive",
tier: contact.spend > 1000 ? "Premium" : "Standard"
};

Null Handling​

const data = input.payload;

return {
// Using || for falsy fallback
name: data.name || "Unknown",

// Using ?? for null/undefined only
count: data.count ?? 0,

// Using optional chaining
city: data.address?.city || "Not provided",

// Checking existence
has_email: Boolean(data.email),
has_phone: data.phone !== null && data.phone !== undefined
};

Validation Patterns​

Required Field Validation​

const data = input.payload;
const errors = [];

if (!data.email) errors.push("Email is required");
if (!data.name) errors.push("Name is required");
if (data.phone && !/^\+?[\d\s-]+$/.test(data.phone)) {
errors.push("Invalid phone format");
}

return {
valid: errors.length === 0,
errors: errors,
data: errors.length === 0 ? data : null
};

Email Validation​

const email = input.payload.email;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

return {
email: email,
is_valid: emailRegex.test(email),
domain: email ? email.split('@')[1] : null
};

Schema Validation​

const data = input.payload;

function validate(data) {
const errors = [];

// Type checks
if (typeof data.email !== 'string') errors.push("email must be string");
if (typeof data.age !== 'number') errors.push("age must be number");

// Range checks
if (data.age < 0 || data.age > 120) errors.push("age out of range");

// Format checks
if (data.email && !data.email.includes('@')) errors.push("invalid email");

return errors;
}

const errors = validate(data);

return {
valid: errors.length === 0,
errors,
sanitized_data: errors.length === 0 ? {
email: data.email.toLowerCase().trim(),
age: parseInt(data.age)
} : null
};

Working with JSON​

Parse JSON String​

const jsonString = input.payload.data;

try {
const parsed = JSON.parse(jsonString);
return { success: true, data: parsed };
} catch (e) {
return { success: false, error: "Invalid JSON" };
}

Stringify Object​

const data = input.contact;

return {
json_string: JSON.stringify(data),
pretty_json: JSON.stringify(data, null, 2)
};

Extract from Nested JSON​

const response = input.apiresponse_1.data;

return {
// Safely access nested properties
user_id: response?.user?.id,
user_name: response?.user?.profile?.name,
items: response?.data?.items || []
};

API Request Preparation​

Build Query Parameters​

const filters = input.filters;

const params = new URLSearchParams();
if (filters.status) params.append('status', filters.status);
if (filters.limit) params.append('limit', filters.limit);
if (filters.offset) params.append('offset', filters.offset);

return {
query_string: params.toString(),
url: `https://api.example.com/users?${params.toString()}`
};

Build Request Body​

const contact = input.contact;
const action = input.action;

return {
request_body: {
action: action,
timestamp: new Date().toISOString(),
data: {
contact_id: contact.id,
email: contact.email,
metadata: {
source: "workflow",
workflow_id: "{{workflow.id}}"
}
}
}
};

Debugging Tips​

Log Values​

const data = input.payload;

// Log for debugging (visible in execution logs)
console.log("Input data:", data);
console.log("Email:", data.email);

return { processed: true };

Return All Input (Debug)​

// Useful for debugging - see everything available
return {
debug: true,
all_input: input
};

Check Data Types​

const value = input.payload.amount;

return {
value: value,
type: typeof value,
is_number: typeof value === 'number',
is_string: typeof value === 'string',
parsed: parseFloat(value) || 0
};

Error Handling​

Try-Catch​

try {
const data = input.payload;
const result = JSON.parse(data.json_string);

return {
success: true,
data: result
};
} catch (error) {
return {
success: false,
error: error.message
};
}

Safe Property Access​

const data = input.payload || {};

return {
// Won't error if nested properties don't exist
email: data?.user?.email || "no-email@example.com",
name: data?.user?.profile?.name || "Unknown",
items: data?.order?.items || []
};

Best Practices​

1. Always Handle Missing Data​

// Bad
const name = input.contact.name;

// Good
const name = input.contact?.name || "Unknown";

2. Use Descriptive Variable Names​

// Bad
const x = input.a.b;

// Good
const customerEmail = input.contact.email;

3. Keep Transforms Focused​

// Bad: Does too many things
// Good: One transform per concern, chain them

4. Return Clean Objects​

return {
customer_name: name, // snake_case for consistency
order_total: total,
items_count: items.length
};

5. Document Complex Logic​

// Calculate lead score based on:
// - Email opens (20 points each)
// - Website visits (5 points each)
// - Form submissions (50 points each)
const score = (opens * 20) + (visits * 5) + (forms * 50);

Quick Reference​

String Methods​

str.toUpperCase()      // "HELLO"
str.toLowerCase() // "hello"
str.trim() // Remove whitespace
str.split(',') // ["a", "b", "c"]
str.replace('a', 'b') // Replace first
str.includes('x') // true/false

Array Methods​

arr.filter(fn)         // Filter items
arr.map(fn) // Transform items
arr.reduce(fn, init) // Aggregate
arr.find(fn) // Find first match
arr.some(fn) // Any match?
arr.every(fn) // All match?
arr.sort(fn) // Sort items
arr.slice(0, 5) // First 5 items

Object Methods​

Object.keys(obj)       // ["key1", "key2"]
Object.values(obj) // [val1, val2]
Object.entries(obj) // [["key1", val1], ...]
{ ...obj1, ...obj2 } // Merge objects

Safe Access​

obj?.prop              // Optional chaining
obj ?? default // Nullish coalescing
obj || default // Falsy fallback

Next Steps​

Continue with:

  1. Using Conditions & Branches - Use transformed data in logic
  2. Testing Workflows - Test your transforms
  3. Error Handling - Handle transform errors