How to Implement Custom Data Lookups with Architect GetExternalContactAction
What You Will Build
- You will build a Node.js Express microservice that acts as a custom data provider for Genesys Cloud CX Architect.
- This service receives a POST request from the
GetExternalContactActionnode, extracts the caller ID, and queries a local database or external API to retrieve customer context. - The tutorial covers the exact JSON schema required by the
GetExternalContactActionnode and demonstrates how to handle the asynchronous nature of the lookup.
Prerequisites
- Platform: Genesys Cloud CX.
- Feature: Architect Flow using
GetExternalContactActionnode. - Language: Node.js (v18+).
- Dependencies:
express,cors,dotenv. - Genesys Configuration:
- An Architect Flow containing a
GetExternalContactActionnode. - The node must be configured with a valid URL pointing to your service.
- The node must be configured to pass the
CallerID(or other relevant attributes) in the request body.
- An Architect Flow containing a
Authentication Setup
The GetExternalContactAction node does not use standard OAuth2 client credentials for the HTTP POST it sends to your external service. Instead, it relies on IP Allowlisting or Basic Auth configured within the Architect node properties. For this tutorial, we assume IP Allowlisting is the security model, as it is the most common pattern for high-throughput data lookups.
If you require authentication, you can configure the Architect node to send a custom header (e.g., Authorization: Bearer <token>) or use Basic Auth. The code below includes a simple middleware to validate a shared secret header, which is a robust pattern for securing these endpoints without the overhead of OAuth token rotation on every call.
Environment Variables
Create a .env file:
PORT=3000
SHARED_SECRET=your_super_secret_key_here
DB_CONNECTION_STRING=your_database_url
Implementation
Step 1: Configure the Architect GetExternalContactAction Node
Before writing code, you must understand the contract. The GetExternalContactAction node sends a POST request to your URL. The body of this request is defined by the Input Attributes you select in the Architect UI.
- Open your Architect Flow.
- Add a
GetExternalContactActionnode. - Set the URL to your endpoint (e.g.,
https://api.yourcompany.com/lookup). - In the Input Attributes section, add the attribute containing the phone number.
- Common source:
system.call.caller.idorsystem.call.caller.displayName. - Let us assume you map
system.call.caller.idto a variable namedcallerPhoneNumber.
- Common source:
- In the Output Attributes section, define what you expect back.
- The node expects a JSON object. The top-level keys of your JSON response will become variables in the flow.
- Example: If you return
{ "customerId": "123", "tier": "gold" }, the flow will have variablescustomerIdandtieravailable downstream.
Critical Note: The GetExternalContactAction node has a timeout. By default, it waits for 5 seconds. If your database query takes longer, the node fails. Ensure your lookup is optimized or increase the timeout in the node settings if necessary.
Step 2: Build the Express Server
Initialize your project:
mkdir genesys-lookup-service
cd genesys-lookup-service
npm init -y
npm install express cors dotenv
Create server.js. This service will parse the incoming request, validate the security header, perform the lookup, and return the JSON payload.
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(cors());
app.use(express.json());
// Simple security middleware
// In production, use a robust HMAC verification or API Key management system.
const authenticateRequest = (req, res, next) => {
const providedSecret = req.headers['x-api-key'];
if (providedSecret !== process.env.SHARED_SECRET) {
return res.status(401).json({ error: 'Unauthorized: Invalid API Key' });
}
next();
};
// Mock Database Service
// Replace this with your actual database call (e.g., MongoDB, PostgreSQL, Redis)
const mockCustomerDatabase = {
'+15551234567': {
customerId: 'CUST-9012',
firstName: 'John',
lastName: 'Doe',
tier: 'Platinum',
recentOrderTotal: 150.00
},
'+15559876543': {
customerId: 'CUST-3321',
firstName: 'Jane',
lastName: 'Smith',
tier: 'Silver',
recentOrderTotal: 45.50
}
};
/**
* GET /health
* Health check endpoint for load balancers
*/
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok' });
});
/**
* POST /lookup
* The endpoint consumed by Genesys GetExternalContactAction
*/
app.post('/lookup', authenticateRequest, async (req, res) => {
try {
// 1. Parse Input Attributes
// The body structure depends on how you configured the Input Attributes in Architect.
// Typically, Genesys sends the attributes in the root of the JSON body.
const { callerPhoneNumber } = req.body;
// 2. Validate Input
if (!callerPhoneNumber) {
return res.status(400).json({
error: 'Bad Request',
message: 'callerPhoneNumber is required in the request body.'
});
}
console.log(`Received lookup request for: ${callerPhoneNumber}`);
// 3. Perform Data Lookup
// Simulate an async database call
const customerData = await performCustomerLookup(callerPhoneNumber);
// 4. Construct Response
// If customer not found, we still return a 200 OK with null/empty values.
// Returning a 404 or 500 will cause the GetExternalContactAction node to fail/error out.
const responsePayload = customerData ? {
...customerData,
lookupSuccess: true
} : {
customerId: null,
firstName: null,
lastName: null,
tier: null,
recentOrderTotal: 0,
lookupSuccess: false
};
// 5. Send Response
// Important: Set Content-Type to application/json
res.status(200).json(responsePayload);
} catch (error) {
console.error('Lookup failed:', error);
// Always return 200 OK with error details in the body if possible,
// or log internally. Genesys expects a JSON body.
res.status(200).json({
error: 'Internal Server Error',
message: error.message,
lookupSuccess: false
});
}
});
/**
* Simulates an asynchronous database query
*/
async function performCustomerLookup(phoneNumber) {
// Simulate network/database latency
await new Promise(resolve => setTimeout(resolve, 50));
// Normalize phone number if necessary
const normalizedPhone = phoneNumber.trim();
return mockCustomerDatabase[normalizedPhone] || null;
}
// Start Server
if (require.main === module) {
app.listen(PORT, () => {
console.log(`Genesys Lookup Service running on port ${PORT}`);
});
}
module.exports = app;
Step 3: Handling Edge Cases and Performance
The GetExternalContactAction is synchronous from the flow’s perspective. The flow stops at the node until the HTTP request completes. This introduces strict latency requirements.
1. Caching
If you have a high call volume, hitting your primary database for every call is inefficient and risky for timeouts. Implement a caching layer (e.g., Redis or In-Memory LRU cache).
Add this logic to server.js:
// Simple In-Memory Cache with TTL
const cache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
async function performCustomerLookupWithCache(phoneNumber) {
const cacheKey = `customer_${phoneNumber}`;
const cachedItem = cache.get(cacheKey);
if (cachedItem && Date.now() < cachedItem.expiry) {
console.log(`Cache hit for ${phoneNumber}`);
return cachedItem.data;
}
console.log(`Cache miss for ${phoneNumber}, querying DB...`);
const data = await performCustomerLookup(phoneNumber); // Original DB call
// Store in cache
cache.set(cacheKey, {
data: data,
expiry: Date.now() + CACHE_TTL
});
return data;
}
Replace the call in your route handler:
// Inside app.post('/lookup', ...)
const customerData = await performCustomerLookupWithCache(callerPhoneNumber);
2. Handling Missing Data
Always return a consistent JSON structure. If the customer is not found, do not return an empty object {}. Return the expected keys with null or default values. This prevents “Undefined” errors in downstream Architect nodes that expect specific variable types.
3. Timeout Management
If your lookup involves multiple microservices, chain them with Promise.all or use a timeout wrapper to ensure the response is sent before Genesys cuts the connection.
// Utility to enforce timeout
function withTimeout(promise, ms) {
let timer;
return Promise.race([
promise,
new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error('Lookup timeout')), ms);
})
]).finally(() => clearTimeout(timer));
}
// Usage in route
try {
const customerData = await withTimeout(performCustomerLookupWithCache(callerPhoneNumber), 3000); // 3s timeout
// ... rest of logic
} catch (error) {
if (error.message === 'Lookup timeout') {
console.warn('Lookup timed out for', callerPhoneNumber);
// Return fallback data
}
// ...
}
Complete Working Example
Here is the full server.js file combining security, caching, and robust error handling.
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const app = express();
const PORT = process.env.PORT || 3000;
// Configuration
const SHARED_SECRET = process.env.SHARED_SECRET || 'default_secret_change_me';
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
const LOOKUP_TIMEOUT_MS = 3000; // 3 seconds max for DB call
// Middleware
app.use(cors());
app.use(express.json());
// Security Middleware
const authenticateRequest = (req, res, next) => {
const providedSecret = req.headers['x-api-key'];
if (providedSecret !== SHARED_SECRET) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
};
// In-Memory Cache
const cache = new Map();
// Mock Database
const mockDB = {
'+15551234567': { customerId: 'CUST-9012', firstName: 'John', lastName: 'Doe', tier: 'Platinum' },
'+15559876543': { customerId: 'CUST-3321', firstName: 'Jane', lastName: 'Smith', tier: 'Silver' }
};
// Database Service
async function queryDatabase(phoneNumber) {
// Simulate DB latency
await new Promise(resolve => setTimeout(resolve, 50));
return mockDB[phoneNumber.trim()] || null;
}
// Cache Service
async function getCachedData(phoneNumber) {
const key = `cust_${phoneNumber}`;
const item = cache.get(key);
if (item && Date.now() < item.expiry) {
return item.data;
}
const data = await queryDatabase(phoneNumber);
cache.set(key, { data, expiry: Date.now() + CACHE_TTL });
return data;
}
// Timeout Wrapper
function withTimeout(promise, ms) {
let timer;
return Promise.race([
promise,
new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error('Timeout')), ms);
})
]).finally(() => clearTimeout(timer));
}
// Main Lookup Endpoint
app.post('/lookup', authenticateRequest, async (req, res) => {
try {
const { callerPhoneNumber } = req.body;
if (!callerPhoneNumber) {
return res.status(400).json({ error: 'Missing callerPhoneNumber' });
}
let customerData;
try {
customerData = await withTimeout(getCachedData(callerPhoneNumber), LOOKUP_TIMEOUT_MS);
} catch (timeoutError) {
console.error(`Timeout for ${callerPhoneNumber}`);
customerData = null;
}
// Standardize Output
const response = customerData ? {
...customerData,
lookupSuccess: true
} : {
customerId: null,
firstName: null,
lastName: null,
tier: null,
lookupSuccess: false
};
res.status(200).json(response);
} catch (error) {
console.error('Server Error:', error);
// Genesys expects JSON. Return a safe fallback.
res.status(200).json({
lookupSuccess: false,
error: 'Internal Error'
});
}
});
// Health Check
app.get('/health', (req, res) => res.status(200).json({ status: 'ok' }));
if (require.main === module) {
app.listen(PORT, () => {
console.log(`Service running on port ${PORT}`);
});
}
module.exports = app;
Common Errors & Debugging
Error: GetExternalContactAction Node Fails with “Timeout”
- Cause: Your service took longer than the node’s timeout setting (default 5s) to respond.
- Fix:
- Check your server logs for slow queries.
- Implement the caching strategy shown above.
- In Architect, open the
GetExternalContactActionnode and increase the Timeout value (e.g., to 10s). - Ensure your database connection pool is not exhausted.
Error: Node Fails with “HTTP 500/400”
- Cause: Your service returned a non-200 status code.
- Fix: The
GetExternalContactActionnode is strict. It expects a200 OKwith a valid JSON body. If your service crashes or returns404 Not Found, the node fails.- Update your catch blocks to always return
res.status(200).json(...). - Use the
lookupSuccess: falseflag in your JSON to handle “not found” logic in Architect downstream, rather than relying on HTTP status codes.
- Update your catch blocks to always return
Error: Variables are Undefined in Downstream Nodes
- Cause: The JSON keys returned by your service do not match the Output Attributes defined in the Architect node, or the casing is different.
- Fix:
- In Architect, ensure the Output Attributes are defined.
- In your code, ensure the JSON keys match exactly.
- Example: If Architect expects
customerId, your JSON must contain"customerId": "...". It will not map"CustomerID"or"customer_id"automatically.
Error: CORS Policy Blocked the Request
- Cause: If you are testing locally and the Architect node is trying to hit a local IP without proper headers, or if you are using a proxy.
- Fix: Ensure
cors()middleware is active. For production, restrict CORS to only allow requests from Genesys Cloud domains if necessary, though IP allowlisting is preferred for backend-to-backend communication.