9.4 Custom Code Best Practices
You can write custom code. You've seen examples. Now let's cover best practices to write code that's fast, reliable, and easy to maintain. These practices will save you time, money, and prevent problems.
Why Best Practices Matter
The difference between good and bad code:
| Bad Code | Good Code |
|---|---|
| Crashes on errors | Handles errors gracefully |
| Wastes money on duplicate API calls | Caches results intelligently |
| Takes hours to debug | Easy to troubleshoot |
| Breaks when data changes | Adapts to edge cases |
| Hard to modify later | Clear and maintainable |
Let's learn how to write good code from the start.
Performance Best Practices
Practice 1: Cache API Results (Save Time & Money)
The Problem: Every time your column runs, it makes an API call—even if nothing has changed. This is slow and expensive.
Example: You're enriching 1,000 leads with company data. If each API call costs $0.01 and takes 2 seconds:
- Cost: $10.00
- Time: 33 minutes
- Problem: Most of that data hasn't changed since yesterday!
Problem: Calling API Every Time
This code calls an external API for EVERY row, even if the data hasn't changed:
// Get email from previous column let email = [Email Address] // Call API (SLOW and EXPENSIVE!) let result = callEnrichmentAPI(email) return result
💸 Cost Impact
How caching works:
- First run: No cached data exists → Make API call → Save result with timestamp
- Second run (1 hour later): Cached data is fresh (< 24 hours) → Return cached data instantly
- Third run (2 days later): Cached data is stale (> 24 hours) → Make new API call → Update cache
When to use caching:
- ✅ Data enrichment (company info, contact details)
- ✅ External API lookups (weather, stock prices, geo data)
- ✅ Expensive calculations that don't change often
- ❌ Real-time data that must always be fresh (current stock price)
- ❌ User-specific data that changes frequently
Practice 2: Use Built-in Methods for Lists
The Problem: Writing manual loops to process lists is slow and error-prone.
Bad approach - Manual loop:
// Get list of 10,000 orders
let orders = [Order List]
let total = 0
// Slow: Manually loop through each item
for (let i = 0; i < orders.length; i++) {
total += orders[i].amount
}
return totalGood approach - Built-in method:
// Get list of orders
let orders = [Order List]
// Fast: Use built-in sum method
let total = orders.sum(order => order.amount) || 0
return totalCommon built-in methods:
| Task | Built-in Method | Example |
|---|---|---|
| Filter list | filter() | orders.filter(o => o.status == 'paid') |
| Transform items | map() | orders.map(o => o.customerName) |
| Count matching | filter().length | orders.filter(o => o.amount > 100).length |
| Sum values | sum() | orders.sum(o => o.amount) |
| Find first match | find() | orders.find(o => o.id == 123) |
| Check if any | some() | orders.some(o => o.urgent) |
| Check if all | every() | orders.every(o => o.paid) |
Performance difference: 10× to 100× faster for large lists!
Practice 3: Return Early (Don't Process Unnecessary Data)
The Problem: You do expensive processing even when you know the result will be useless.
Bad approach:
// Get email and company data
let email = [Email Address]
let company = [Company Data]
// Do expensive processing (even if email is missing!)
let enriched = callExpensiveAPI(company)
let scored = calculateComplexScore(enriched)
let classified = classifyLead(scored)
// Check email at the end (too late!)
if (!email) {
return null // Wasted all that processing!
}
return classifiedGood approach:
// Validate early - exit immediately if invalid
let email = [Email Address]
if (!email) return null // Exit now!
if (!email.includes('@')) {
return { error: 'Invalid email format' }
}
let company = [Company Data]
if (!company) return null // Exit if no company data
// Only do expensive work if all required fields are present
let enriched = callExpensiveAPI(company)
let scored = calculateComplexScore(enriched)
return classifyLead(scored)Benefit: Save CPU time, reduce costs, and speed up execution by 50-90% for invalid rows.
Practice 4: Filter Data Before Processing
The Problem: Processing more data than you actually need.
Bad approach:
// Get ALL orders (100,000 orders!)
let allOrders = [All Orders]
// Process everything (slow!)
let stats = calculateStatistics(allOrders)
return statsGood approach:
// Get all orders
let allOrders = [All Orders]
// Filter FIRST (reduce from 100,000 to 500)
let recentOrders = allOrders.filter(order => {
let orderDate = new Date(order.date)
let daysAgo = daysSince(orderDate)
return daysAgo <= 30 // Only last 30 days
})
// Process filtered data (200× faster!)
let stats = calculateStatistics(recentOrders)
return statsPerformance impact: Processing 500 items instead of 100,000 = 200× faster
Error Handling Best Practices
Practice 5: Wrap Code in Try-Catch Blocks
The Problem: When an error occurs, the cell shows a red "Error" badge with no details. You don't know what went wrong, and the automation stops.
Problem: Crashes on Errors
// Get email and process
let email = [Email Address]
let domain = email.split('@')[1] // CRASH if email is empty!
return domainReal-world example - API call with error handling:
try {
// Get data
let email = [Email Address]
let apiKey = [API Key]
// Validate
if (!email) throw new Error("Email is required")
if (!apiKey) throw new Error("API key is missing")
// Make API call
let result = callAPI(email, apiKey)
return {
success: true,
data: result
}
} catch (error) {
// Return error details instead of crashing
return {
success: false,
error: true,
message: error.message,
email: [Email Address], // Show which email failed
timestamp: now()
}
}Benefits of try-catch:
- ✅ Cell completes (green badge) instead of failing (red badge)
- ✅ You see exactly what went wrong
- ✅ Other columns can check for errors:
if ([Column].error) { ... } - ✅ Automation continues processing other rows
Practice 6: Validate Data Before Processing
The Problem: Assuming data exists and is valid leads to crashes.
Common validation checks:
try {
// Get data
let email = [Email Address]
let company = [Company Data]
let score = [Lead Score]
// Validate email exists
if (!email) {
throw new Error("Email is required")
}
// Validate email format
if (!email.includes('@')) {
throw new Error("Email format is invalid")
}
// Validate company data exists
if (!company) {
throw new Error("Company data is missing")
}
// Validate score is a number
if (typeof score !== 'number') {
throw new Error("Lead score must be a number")
}
// Validate score is in valid range
if (score < 0 || score > 100) {
throw new Error("Lead score must be between 0 and 100")
}
// All valid - process safely
let result = processLead(email, company, score)
return {
success: true,
result: result
}
} catch (error) {
return {
success: false,
error: error.message,
email: email,
timestamp: now()
}
}Validation checklist:
- ✅ Check if required fields exist (
if (!field)) - ✅ Check format (email has @, phone has digits)
- ✅ Check data type (number vs string)
- ✅ Check value ranges (score between 0-100)
- ✅ Check list lengths (not empty, not too large)
Practice 7: Handle Missing/Null Data Gracefully
The Problem: Code crashes when accessing properties of null/undefined data.
Bad approach:
let company = [Company Data]
let employees = company.employeeCount // CRASH if company is null!
return employeesGood approach - Use safe navigation:
let company = [Company Data]
// Safe navigation operator (?.)
// Returns null if company is null, instead of crashing
let employees = company?.employeeCount || 0
return employeesSafe navigation examples:
// Nested data access
let city = [Company Data]?.address?.city || 'Unknown'
// Array access
let firstOrder = [Orders]?.[0]?.amount || 0
// Method calls
let upperName = [Customer Name]?.toUpperCase() || 'NO NAME'
// Multiple levels
let zipCode = [Customer]?.address?.zip?.toString() || 'N/A'Provide defaults with || operator:
let email = [Email Address] || 'no-email@example.com'
let score = [Lead Score] || 0
let status = [Status] || 'unknown'
let tags = [Tags] || []
let metadata = [Metadata] || {}Code Quality Best Practices
Practice 8: Use Clear, Descriptive Variable Names
The Problem: Unclear what variables represent—hard to understand and maintain.
Bad approach:
let d = [Company Data]
let x = d?.size || 0
let y = x > 100 ? 30 : 10
let z = y + (d?.industry == 'Tech' ? 25 : 0)
return zGood approach:
let companyData = [Company Data]
let companySize = companyData?.size || 0
// Calculate size score
let sizeScore = companySize > 100 ? 30 : 10
// Add industry bonus
let industryBonus = companyData?.industry == 'Technology' ? 25 : 0
// Calculate total
let totalScore = sizeScore + industryBonus
return totalScoreVariable naming guidelines:
- ✅ Use full words:
customerEmailnotce - ✅ Be specific:
leadScorenotscore - ✅ Use camelCase:
orderCountnotorder_count - ✅ Make booleans clear:
isValid,hasErrors,canProcess - ✅ Use plural for lists:
orders,customers,emails
Practice 9: Add Comments for Complex Logic
The Problem: Hard to understand what code does months later (or even tomorrow!).
Bad approach:
let s = [Score]
let i = [Industry]
return s > 70 && i in ['A', 'B', 'C'] ? 'P1' : s > 50 ? 'P2' : 'P3'Good approach:
// Classify leads into priority tiers based on score and industry
let score = [Lead Score]
let industry = [Industry]
// Priority 1: High score + target industry
// These leads should be contacted immediately
if (score > 70 && ['Technology', 'Finance', 'Healthcare'].includes(industry)) {
return 'P1 - Immediate Follow-up'
}
// Priority 2: Medium score
// Standard follow-up process
if (score > 50) {
return 'P2 - Standard Follow-up'
}
// Priority 3: Low score
// Add to nurture campaign
return 'P3 - Nurture Campaign'When to add comments:
- ✅ Complex calculations: Explain the formula
- ✅ Business rules: Explain why (not just what)
- ✅ Magic numbers: Explain where 70, 100, etc. come from
- ✅ Edge cases: Explain special handling
- ✅ Workarounds: Explain why you did it this way
Add header comment:
/**
* Lead Scoring Algorithm
*
* Purpose: Calculate lead quality score based on:
* - Company size (0-30 points)
* - Industry fit (0-25 points)
* - Engagement (0-unlimited points)
*
* Returns: Object with total_score, grade, qualified
*
* Dependencies: Company Data, Email Opens, Link Clicks
*
* Last updated: 2024-11-19
*/
let score = 0
// ... rest of code ...Practice 10: Break Complex Code into Steps
The Problem: One giant expression is hard to read, debug, and modify.
Bad approach:
return ([Orders]?.filter(o => o.status == 'completed')?.map(o => o.amount)?.reduce((sum, amt) => sum + amt, 0) || 0) / ([Orders]?.length || 1)Good approach:
// Step 1: Get all orders
let allOrders = [Orders] || []
// Step 2: Filter completed orders
let completedOrders = allOrders.filter(order => order.status == 'completed')
// Step 3: Extract amounts
let amounts = completedOrders.map(order => order.amount)
// Step 4: Calculate total revenue
let totalRevenue = amounts.reduce((sum, amount) => sum + amount, 0)
// Step 5: Calculate average
let orderCount = allOrders.length
let avgOrderValue = orderCount > 0 ? totalRevenue / orderCount : 0
// Return detailed results
return {
total_revenue: totalRevenue,
order_count: orderCount,
completed_count: completedOrders.length,
avg_order_value: avgOrderValue
}Benefits of breaking into steps:
- ✅ Easy to debug: Test each step individually
- ✅ Easy to modify: Change one step without breaking others
- ✅ Easy to understand: Clear progression of logic
- ✅ Easy to reuse: Extract steps into separate columns
Practice 11: Return Consistent Data Structure
The Problem: Sometimes returning strings, sometimes objects—hard to use in other columns.
Bad approach:
let status = [Status]
// Sometimes returns string
if (status == 'active') {
return "User is active"
}
// Sometimes returns object
if (status == 'suspended') {
return { error: true, message: "Account suspended" }
}
// Sometimes returns null
return nullGood approach - Always return same structure:
let status = [Status]
// Always return object with consistent fields
return {
status: status,
is_active: status == 'active',
is_suspended: status == 'suspended',
message: status == 'active'
? 'User is active'
: status == 'suspended'
? 'Account suspended'
: 'Unknown status',
timestamp: now()
}Benefits:
- ✅ Other columns can always access
.status,.message, etc. - ✅ Conditions work reliably:
[Column].is_active == true - ✅ No unexpected crashes from missing fields
- ✅ Easy to document what the column returns
Debugging Best Practices
Practice 12: Test Incrementally (Build Step by Step)
The Problem: Writing all code at once, then trying to debug 100 lines when something breaks.
Bad: Write Everything at Once
⏰ Time wasted: Hours
Good: Build Step by Step
return "test"return [Email]if (!email) return nulllet domain = email.split('@')[1]⚡ Time saved: Hours → Minutes
You know exactly which line caused any error
Pro Tip: Test After Every Change
Every time you add 2-3 lines, click Save and run the column on a test row. If it works, keep building. If it breaks, you know the problem is in the last 2-3 lines you just added.
Incremental testing workflow:
Step 1: Start with simplest possible code
return "test"→ Click Save → Run on test row → ✅ Works!
Step 2: Add data access
let email = [Email Address]
return email→ Click Save → Run on test row → ✅ Works!
Step 3: Add validation
let email = [Email Address]
if (!email) return null
return email→ Click Save → Run on test row → ✅ Works!
Step 4: Add processing
let email = [Email Address]
if (!email) return null
let domain = email.split('@')[1]
return domain→ Click Save → Run on test row → ✅ Works!
Step 5: Keep building gradually, testing after every 2-3 lines
Benefits:
- ✅ Know exactly which line caused any error
- ✅ Catch problems early before they compound
- ✅ Save hours of debugging time
- ✅ Build confidence as you go
Practice 13: Return Debug Information
The Problem: Can't see intermediate values when debugging—hard to understand what went wrong.
Bad approach:
let score = calculateComplexScore()
return score // Can't see how it was calculatedGood approach - Return breakdown:
// Calculate components
let sizeScore = calculateSizeScore()
let industryScore = calculateIndustryScore()
let engagementScore = calculateEngagementScore()
let totalScore = sizeScore + industryScore + engagementScore
// Return score + detailed breakdown
return {
total_score: totalScore,
components: {
size: sizeScore,
industry: industryScore,
engagement: engagementScore
},
inputs: {
company_size: [Company Size],
industry: [Industry],
email_opens: [Email Opens]
},
calculated_at: now()
}What to include in debug info:
- ✅ Final result
- ✅ Intermediate calculations
- ✅ Input values used
- ✅ Timestamps
- ✅ Any warnings or issues encountered
Tip: Once working well, you can simplify the return value. But during development and debugging, more information is better!
Practice 14: Create Test Cases for Edge Cases
The Problem: Only testing with "normal" data—code breaks on edge cases in production.
Create test rows with these scenarios:
Test Case 1: Normal data ✅
Email: john@example.com
Company Size: 500
Expected: Should process normallyTest Case 2: Missing required field ❌
Email: (empty)
Company Size: 500
Expected: Should return error about missing emailTest Case 3: Invalid format ❌
Email: "not-an-email"
Company Size: 500
Expected: Should return format errorTest Case 4: Boundary values ⚠️
Email: john@example.com
Company Size: 100 (exactly at threshold)
Expected: Should handle threshold correctlyTest Case 5: Extreme values 🔥
Email: john@example.com
Company Size: 999999999
Expected: Should not crash on large numbersTest Case 6: Special characters 🔤
Email: test+filter@sub-domain.co.uk
Company Size: 50
Expected: Should handle complex email formatsTest Case 7: Null/undefined data ⚫
Email: john@example.com
Company Size: null
Expected: Should use default value (0)Where to create test rows:
- Create a section in your taible called "Test Cases"
- Add 5-10 test rows covering different scenarios
- Run your column on all test rows
- Verify each behaves correctly
- Keep these rows for future testing when you modify the code
Maintenance Best Practices
Practice 15: Duplicate Column Before Major Changes
The Problem: You make changes to working code, it breaks, and you can't remember how it worked before.
Good practice - Version your code:
Before making major changes:
- Right-click the column header
- Choose "Duplicate Column"
- Rename:
lead_score_backuporlead_score_v2 - Make changes to the duplicate
- Test thoroughly on test rows
- If it works: Delete the old version
- If it breaks: Keep both, revert to backup
When to create backups:
- ✅ Before major algorithm changes
- ✅ Before adding complex new features
- ✅ Before optimizing working code
- ✅ Before customer demo (have a known-working version)
Column naming convention:
lead_score- Current production versionlead_score_v2- New version being developedlead_score_backup- Known working backuplead_score_2024_11_19- Date-stamped backup
Practice 16: Document Column Purpose
The Problem: Six months later, you (or your teammate) look at the code and have no idea what it does or why.
Good practice - Add header comment:
/**
* Lead Scoring Algorithm
*
* Purpose: Calculate lead quality score based on multiple factors
*
* Scoring breakdown:
* - Company size: 0-30 points (larger = higher)
* - Industry fit: 0-25 points (Tech/Finance/Healthcare = bonus)
* - Engagement: 0-unlimited (email opens × 2, clicks × 5)
* - Contact role: 0-30 points (decision-makers = higher)
*
* Returns: Object with:
* - total_score: Number (0-100+)
* - grade: String ('A', 'B', 'C', or 'D')
* - qualified: Boolean (true if score > 50)
*
* Dependencies:
* - Company Data (from enrichment)
* - Email Opens (from email tracking)
* - Link Clicks (from email tracking)
* - Contact Role (from form)
*
* Used by:
* - Route to Sales (condition: qualified == true)
* - Priority Assignment (uses grade)
*
* Last updated: 2024-11-19
* Author: Marketing Team
* Notes: Thresholds based on Q3 2024 conversion analysis
*/
let score = 0
// Company size score (0-30 points)
// Based on historical data: companies > 100 employees convert 3× better
let companySize = [Company Size] || 0
// ... rest of code ...What to document:
- ✅ Purpose: What does this column do?
- ✅ Algorithm: How does it calculate the result?
- ✅ Inputs: What data does it depend on?
- ✅ Outputs: What does it return?
- ✅ Used by: Which other columns use this?
- ✅ Business rules: Why these specific thresholds/logic?
- ✅ Last updated: When was it changed?
- ✅ Author: Who can answer questions?
Practice 17: Don't Repeat Yourself (DRY)
The Problem: Copying same logic to multiple columns—when you need to change it, you must update everywhere.
Bad approach - Duplicate logic:
Column: Lead Score
let score = 0
// Company size logic
let companySize = [Company Size] || 0
if (companySize > 100) score += 20
else if (companySize > 50) score += 10
// ... 50 more lines of scoring ...
return scoreColumn: Contact Score (duplicate logic!)
let score = 0
// Same company size logic (duplicated!)
let companySize = [Company Size] || 0
if (companySize > 100) score += 20
else if (companySize > 50) score += 10
// ... different scoring logic ...
return scoreGood approach - Extract to shared column:
Column: Company Size Score (reusable)
let companySize = [Company Size] || 0
if (companySize > 100) return 20
else if (companySize > 50) return 10
else return 0Column: Lead Score (uses shared logic)
let score = 0
// Use reusable component
score += [Company Size Score]
// Add lead-specific scoring
let industryScore = [Industry] == 'Technology' ? 25 : 0
score += industryScore
return scoreColumn: Contact Score (also uses shared logic)
let score = 0
// Use same reusable component
score += [Company Size Score]
// Add contact-specific scoring
let roleScore = [Role].includes('Director') ? 30 : 0
score += roleScore
return scoreBenefits:
- ✅ Change logic once, applies everywhere
- ✅ Easier to test (test the shared component once)
- ✅ Easier to maintain (one place to update)
- ✅ Easier to understand (clear component names)
Security Best Practices
Practice 18: Never Hardcode API Keys or Passwords
The Problem: API keys and passwords in code can be seen by anyone with access to the taible.
Very bad approach - DON'T DO THIS:
// NEVER DO THIS!
let apiKey = "sk_live_abc123xyz789" // Anyone can see this!
let result = callAPI(apiKey, [Email])
return resultWhy dangerous:
- ❌ Anyone with taible access sees the key
- ❌ Can't rotate keys without changing code everywhere
- ❌ Keys might appear in error messages or logs
- ❌ Security breach if taible is exported or shared
Good approach - Use account system:
- Go to Settings → Integration Accounts
- Create a new account for the service
- Enter your API key there (stored encrypted)
- Use the account in your column configuration
In your column: You'll reference the account, not the key directly. The system handles authentication securely.
For custom HTTP calls: Use dedicated integration columns that support accounts, rather than custom code with hardcoded keys.
Practice 19: Validate Input Data
The Problem: Trusting external data without validation can lead to security issues or data corruption.
Validation example:
try {
// Get user input
let userComment = [User Comment]
// Validate exists
if (!userComment) {
return { error: 'Comment is required' }
}
// Validate length
if (userComment.length > 1000) {
return { error: 'Comment too long (max 1000 characters)' }
}
// Validate format (only alphanumeric and basic punctuation)
let allowedChars = /^[a-zA-Z0-9\s.,!?'-]+$/
if (!allowedChars.test(userComment)) {
return { error: 'Comment contains invalid characters' }
}
// Sanitize - remove potentially dangerous content
let sanitized = userComment
.replace(/[<>]/g, '') // Remove < and >
.trim()
// Process sanitized input
return {
success: true,
comment: sanitized,
length: sanitized.length
}
} catch (error) {
return {
success: false,
error: error.message
}
}What to validate:
- ✅ Length limits (prevent overflow)
- ✅ Format (email, phone, etc.)
- ✅ Allowed characters (prevent injection)
- ✅ Value ranges (numbers within bounds)
- ✅ Required fields (not empty)
Best Practices Summary
Best Practices Quick Checklist
Performance
Check for recent data before calling APIs
Faster than writing manual loops
Don't process data you won't use
Process only what you need
Error Handling
Catch errors and return error details
Check that required fields exist
Explain what went wrong and where
Use safe navigation and defaults
Code Quality
Make code readable for future you
Explain the "why" not the "what"
Easier to debug and modify
Same format every time
Debugging
Include intermediate values
Know exactly what broke
Test with missing, invalid, and extreme values
Maintenance
Keep a backup of working code
Add comments explaining what it does
Extract common logic to reusable columns
Quick Reference: Common Patterns
Pattern 1: Safe Data Access
let value = [Column Name]?.property?.nested || defaultValuePattern 2: Error Handling Wrapper
try {
// Your code here
return { success: true, data: result }
} catch (error) {
return { success: false, error: error.message }
}Pattern 3: Early Return Validation
if (!requiredField) return null
if (!validFormat) return { error: 'Invalid format' }
// Continue with processingPattern 4: Caching Check
let cached = [Cached Data]
if (cached?.timestamp && hoursAgo(cached.timestamp) < 24) {
return cached
}
// Fetch fresh dataPattern 5: List Processing
let items = [Item List] || []
let filtered = items.filter(item => item.condition)
let processed = filtered.map(item => item.property)
let total = processed.reduce((sum, val) => sum + val, 0)Pattern 6: Consistent Return Structure
return {
success: true/false,
data: { /* results */ },
error: null/errorMessage,
timestamp: now()
}Next Steps
You've completed Part 9: Custom Code Columns!
You now know:
- ✅ When to use Custom Code (9.1)
- ✅ How to write Custom Code (9.2)
- ✅ Real-world examples (9.3)
- ✅ Best practices for reliable, efficient code (9.4) ← You are here
What's next?
Apply these best practices to your own automations:
- Review existing columns: Find ones that could benefit from these practices
- Test with edge cases: Create test rows covering different scenarios
- Add error handling: Wrap code in try-catch blocks
- Optimize performance: Add caching where appropriate
- Document your work: Add comments explaining business logic
Remember: Good code is:
- 🎯 Correct: Handles all cases including errors
- ⚡ Fast: Caches results and filters early
- 📖 Readable: Clear names and helpful comments
- 🔧 Maintainable: Easy to modify and debug
- 🛡️ Reliable: Validates input and returns consistent structure
Happy coding! 🚀