Choose one of the following:
# Create project directory
mkdir salesforce-automation
cd salesforce-automation
# Initialize with interactive setup
npx create-browser-app --example quickstart
# Or clone from existing repo
git clone [your-repo-url]
cd [project-name]
# Install dependencies
npm install cp .env.example .env.env in your editor and fill in: # Browserbase Configuration
BROWSERBASE_API_KEY=your_api_key_here
BROWSERBASE_PROJECT_ID=your_project_id_here
# LLM Configuration (choose one)
OPENAI_API_KEY=your_openai_key_here
# or
ANTHROPIC_API_KEY=your_anthropic_key_here
# Salesforce Credentials (see credential management section)
SALESFORCE_USERNAME=provided_by_admin
SALESFORCE_PASSWORD=provided_by_admin# Run a simple test to verify everything works
npm run test:setup
# If successful, run the main automation
npm start| Issue | Solution |
|---|---|
| "BROWSERBASE_API_KEY not found" | Ensure .env file exists and contains the key |
| "Chrome not found" | Install Chrome or set headless: true in config |
| "Permission denied" | Run with sudo on Mac/Linux or as Administrator on Windows |
| "Module not found" | Delete node_modules and run npm install again |
Never store credentials directly in code or commit them to version control!
Instead of storing actual credentials, store references to secrets managed by Browserbase or external services.
// config/secrets.js
const getSecrets = async () => {
// Use environment variables as references, not actual secrets
const secretRefs = {
salesforce: process.env.SALESFORCE_SECRET_REF || 'salesforce-prod-creds',
apiKeys: process.env.API_KEYS_SECRET_REF || 'api-keys-prod'
};
// Fetch actual secrets at runtime
if (process.env.USE_BROWSERBASE_SECRETS === 'true') {
// Browserbase secrets management (coming soon)
return await fetchBrowserbaseSecrets(secretRefs);
} else if (process.env.USE_AWS_SECRETS === 'true') {
// AWS Secrets Manager
return await fetchAWSSecrets(secretRefs);
} else {
// Local development fallback
return {
salesforceUsername: process.env.SALESFORCE_USERNAME,
salesforcePassword: process.env.SALESFORCE_PASSWORD
};
}
};import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";
const client = new SecretsManagerClient({ region: "us-east-1" });
async function getSecretFromAWS(secretName) {
try {
const response = await client.send(
new GetSecretValueCommand({ SecretId: secretName })
);
return JSON.parse(response.SecretString);
} catch (error) {
console.error("Error fetching secret:", error);
throw error;
}
}
// Usage in Stagehand
const secrets = await getSecretFromAWS("salesforce-automation-secrets");# .github/workflows/automation.yml
name: Run Salesforce Automation
on:
schedule:
- cron: '0 9 * * *' # Daily at 9 AM
jobs:
automate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm install
- run: npm start
env:
BROWSERBASE_API_KEY: ${{ secrets.BROWSERBASE_API_KEY }}
SALESFORCE_USERNAME: ${{ secrets.SALESFORCE_USERNAME }}
SALESFORCE_PASSWORD: ${{ secrets.SALESFORCE_PASSWORD }}For distributing credentials to interns without direct access:
// scripts/setup-intern-env.js
import crypto from 'crypto';
import fs from 'fs';
// Admin runs this to create encrypted credentials
function createEncryptedCredentials(credentials, passphrase) {
const algorithm = 'aes-256-cbc';
const key = crypto.scryptSync(passphrase, 'salt', 32);
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(JSON.stringify(credentials), 'utf8', 'hex');
encrypted += cipher.final('hex');
return {
encrypted,
iv: iv.toString('hex')
};
}
// Intern uses this to decrypt
function decryptCredentials(encryptedData, passphrase) {
const algorithm = 'aes-256-cbc';
const key = crypto.scryptSync(passphrase, 'salt', 32);
const iv = Buffer.from(encryptedData.iv, 'hex');
const decipher = crypto.createDecipheriv(algorithm, key, iv);
let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return JSON.parse(decrypted);
}// utils/testDataGenerator.js
import { faker } from '@faker-js/faker';
import crypto from 'crypto';
class TestDataGenerator {
constructor(options = {}) {
this.seed = options.seed || Date.now();
this.useCache = options.useCache !== false;
this.cache = new Map();
// Set faker seed for reproducible random data
faker.seed(this.seed);
}
// Deterministic data - always the same for a given input
getDeterministicData(type, identifier) {
const cacheKey = `${type}-${identifier}`;
if (this.useCache && this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
let data;
switch (type) {
case 'email':
// Generate consistent email from identifier
data = `test.user.${this.hashIdentifier(identifier)}@example.com`;
break;
case 'company':
// Use a predefined list with consistent selection
const companies = ['Acme Corp', 'TechCo', 'GlobalTech', 'Innovate Inc'];
data = companies[this.hashToIndex(identifier, companies.length)];
break;
case 'phone':
// Generate consistent phone number
const hash = this.hashIdentifier(identifier);
data = `555-${hash.substring(0, 3)}-${hash.substring(3, 7)}`;
break;
}
if (this.useCache) {
this.cache.set(cacheKey, data);
}
return data;
}
// Non-deterministic data - varies each time
getNonDeterministicData(type) {
switch (type) {
case 'firstName':
return faker.person.firstName();
case 'lastName':
return faker.person.lastName();
case 'jobTitle':
return faker.person.jobTitle();
case 'description':
return faker.lorem.paragraph();
case 'address':
return faker.location.streetAddress();
}
}
// Generate complete lead data with mix of both
generateLeadData(options = {}) {
const timestamp = Date.now();
const uniqueId = options.id || crypto.randomUUID();
return {
// Deterministic fields (consistent for testing)
email: this.getDeterministicData('email', uniqueId),
company: this.getDeterministicData('company', uniqueId),
phone: this.getDeterministicData('phone', uniqueId),
leadSource: 'Web', // Always the same
status: 'Open - Not Contacted', // Always the same
// Non-deterministic fields (realistic variation)
firstName: this.getNonDeterministicData('firstName'),
lastName: this.getNonDeterministicData('lastName'),
title: this.getNonDeterministicData('jobTitle'),
description: this.getNonDeterministicData('description'),
street: this.getNonDeterministicData('address'),
// Metadata
testRunId: this.seed,
generatedAt: timestamp,
uniqueId: uniqueId
};
}
// Helper methods
hashIdentifier(identifier) {
return crypto.createHash('md5').update(identifier.toString()).digest('hex');
}
hashToIndex(identifier, max) {
const hash = this.hashIdentifier(identifier);
return parseInt(hash.substring(0, 8), 16) % max;
}
// Save test data for debugging/replay
saveTestData(data, filename) {
const output = {
seed: this.seed,
timestamp: new Date().toISOString(),
data: data
};
fs.writeFileSync(
`test-data/${filename}-${this.seed}.json`,
JSON.stringify(output, null, 2)
);
}
}
// Usage in Stagehand automation
const testDataGen = new TestDataGenerator({
seed: process.env.TEST_SEED || Date.now()
});
// Generate lead data
const leadData = testDataGen.generateLeadData();
// Use in Stagehand
await stagehand.act({
action: `Fill in the Last Name field with "${leadData.lastName}"`,
useVision: true
});// utils/llmTestDataGenerator.js
class LLMTestDataGenerator {
constructor(stagehand) {
this.stagehand = stagehand;
}
async generateContextualData(context, requirements) {
// Extract field requirements from the page
const fieldRequirements = await this.stagehand.extract({
instruction: `Find all required fields and their constraints (field type, max length, format requirements). ${context}`,
schema: z.object({
fields: z.array(z.object({
name: z.string(),
type: z.string(),
required: z.boolean(),
constraints: z.object({
maxLength: z.number().optional(),
pattern: z.string().optional(),
allowedValues: z.array(z.string()).optional()
}).optional()
}))
})
});
// Generate appropriate test data using LLM
const prompt = `
Generate realistic test data for a Salesforce lead with these requirements:
${JSON.stringify(fieldRequirements)}
Requirements:
- Make the data realistic and diverse
- Follow all field constraints
- Use this context: ${requirements.businessContext || 'B2B software company'}
- Industry: ${requirements.industry || 'Technology'}
Return as JSON with field names as keys.
`;
// For demos, you could use a structured approach
const testData = {
lastName: this.generateName(requirements.locale),
company: this.generateCompany(requirements.industry),
email: this.generateEmail(requirements.emailDomain),
// ... other fields based on extracted requirements
};
return testData;
}
// Fallback generators for specific field types
generateName(locale = 'en-US') {
const names = {
'en-US': ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones'],
'es-ES': ['García', 'Rodríguez', 'González', 'Fernández', 'López'],
'fr-FR': ['Martin', 'Bernard', 'Dubois', 'Thomas', 'Robert']
};
const nameList = names[locale] || names['en-US'];
return nameList[Math.floor(Math.random() * nameList.length)];
}
generateCompany(industry) {
const templates = {
'Technology': ['Tech', 'Systems', 'Software', 'Digital', 'Cloud'],
'Healthcare': ['Health', 'Medical', 'Care', 'Wellness', 'Bio'],
'Finance': ['Financial', 'Capital', 'Investment', 'Bank', 'Fund']
};
const keywords = templates[industry] || templates['Technology'];
const prefix = ['Global', 'Premier', 'Advanced', 'Innovative', 'Dynamic'];
return `${prefix[Math.floor(Math.random() * prefix.length)]} ${keywords[Math.floor(Math.random() * keywords.length)]} Inc`;
}
generateEmail(domain) {
const timestamp = Date.now();
const random = Math.random().toString(36).substring(7);
return `test.${random}.${timestamp}@${domain || 'example.com'}`;
}
}// config/stagehandConfig.js
const stagehandConfig = {
// Enable caching for deterministic behavior
enableCaching: true,
// Cache configuration
cacheOptions: {
// Cache LLM responses to disk
cacheDir: '.stagehand-cache',
// TTL for cache entries (in seconds)
ttl: 86400, // 24 hours
// Maximum cache size in MB
maxSize: 100,
// Cache key generation strategy
keyStrategy: 'content-hash' // or 'instruction-based'
},
// Other deterministic settings
viewport: { width: 1920, height: 1080 }, // Fixed viewport
timezone: 'America/New_York', // Fixed timezone
locale: 'en-US', // Fixed locale
// Disable animations for consistent timing
disableAnimations: true,
// Fixed wait times
defaultTimeout: 30000,
actionDelay: 1000 // Delay between actions
};// utils/cacheManager.js
import fs from 'fs/promises';
import path from 'path';
import crypto from 'crypto';
class CacheManager {
constructor(options = {}) {
this.cacheDir = options.cacheDir || '.cache/stagehand';
this.ttl = options.ttl || 3600; // 1 hour default
this.namespace = options.namespace || 'default';
}
async init() {
await fs.mkdir(path.join(this.cacheDir, this.namespace), {
recursive: true
});
}
generateKey(instruction, context = {}) {
const data = {
instruction,
context,
version: '1.0' // Cache version for invalidation
};
return crypto
.createHash('sha256')
.update(JSON.stringify(data))
.digest('hex');
}
async get(key) {
try {
const filePath = path.join(this.cacheDir, this.namespace, `${key}.json`);
const data = await fs.readFile(filePath, 'utf8');
const cached = JSON.parse(data);
// Check if cache is still valid
if (Date.now() - cached.timestamp > this.ttl * 1000) {
await this.delete(key);
return null;
}
return cached.value;
} catch (error) {
return null;
}
}
async set(key, value, options = {}) {
const filePath = path.join(this.cacheDir, this.namespace, `${key}.json`);
const data = {
key,
value,
timestamp: Date.now(),
ttl: options.ttl || this.ttl,
metadata: options.metadata || {}
};
await fs.writeFile(filePath, JSON.stringify(data, null, 2));
}
async delete(key) {
const filePath = path.join(this.cacheDir, this.namespace, `${key}.json`);
try {
await fs.unlink(filePath);
} catch (error) {
// Ignore if file doesn't exist
}
}
async clear() {
const dir = path.join(this.cacheDir, this.namespace);
try {
const files = await fs.readdir(dir);
await Promise.all(
files.map(file => fs.unlink(path.join(dir, file)))
);
} catch (error) {
console.error('Error clearing cache:', error);
}
}
}
// Wrapper for Stagehand with caching
class CachedStagehand {
constructor(stagehand, cacheManager) {
this.stagehand = stagehand;
this.cache = cacheManager;
}
async act(instruction, options = {}) {
if (!options.useCache) {
return this.stagehand.act(instruction, options);
}
const cacheKey = this.cache.generateKey('act', { instruction, options });
const cached = await this.cache.get(cacheKey);
if (cached) {
console.log('Using cached action:', instruction);
return cached;
}
const result = await this.stagehand.act(instruction, options);
await this.cache.set(cacheKey, result);
return result;
}
async extract(instruction, schema, options = {}) {
if (!options.useCache) {
return this.stagehand.extract({ instruction, schema });
}
const cacheKey = this.cache.generateKey('extract', {
instruction,
schema: schema.shape
});
const cached = await this.cache.get(cacheKey);
if (cached) {
console.log('Using cached extraction:', instruction);
return cached;
}
const result = await this.stagehand.extract({ instruction, schema });
await this.cache.set(cacheKey, result);
return result;
}
}// strategies/deterministicStrategies.js
// 1. Use Page State Snapshots
async function waitForStableState(page, options = {}) {
const maxWaitTime = options.maxWaitTime || 10000;
const checkInterval = options.checkInterval || 500;
const stabilityThreshold = options.stabilityThreshold || 3;
let previousState = '';
let stableCount = 0;
const startTime = Date.now();
while (Date.now() - startTime < maxWaitTime) {
// Get current page state
const currentState = await page.evaluate(() => {
return {
url: window.location.href,
readyState: document.readyState,
bodyLength: document.body.innerHTML.length,
activeElement: document.activeElement?.tagName
};
});
const stateString = JSON.stringify(currentState);
if (stateString === previousState) {
stableCount++;
if (stableCount >= stabilityThreshold) {
return true; // Page is stable
}
} else {
stableCount = 0;
}
previousState = stateString;
await new Promise(resolve => setTimeout(resolve, checkInterval));
}
return false; // Timeout reached
}
// 2. Implement Retry Logic with Backoff
async function retryWithBackoff(fn, options = {}) {
const maxRetries = options.maxRetries || 3;
const initialDelay = options.initialDelay || 1000;
const maxDelay = options.maxDelay || 10000;
const factor = options.factor || 2;
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (i === maxRetries - 1) {
throw error;
}
const delay = Math.min(initialDelay * Math.pow(factor, i), maxDelay);
console.log(`Retry ${i + 1}/${maxRetries} after ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
// 3. Normalize Dynamic Content
function normalizeDynamicContent(content, rules = {}) {
let normalized = content;
// Replace timestamps with placeholders
if (rules.replaceTimestamps !== false) {
normalized = normalized.replace(
/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/g,
'[TIMESTAMP]'
);
}
// Replace IDs with placeholders
if (rules.replaceIds !== false) {
normalized = normalized.replace(
/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/gi,
'[UUID]'
);
// Salesforce IDs (15 or 18 characters)
normalized = normalized.replace(
/[a-zA-Z0-9]{15,18}/g,
(match) => {
if (match.length === 15 || match.length === 18) {
return '[SF_ID]';
}
return match;
}
);
}
// Replace numbers that might be dynamic
if (rules.replaceNumbers) {
normalized = normalized.replace(/\d+/g, '[NUMBER]');
}
return normalized;
}
// 4. Create Deterministic Test Scenarios
class DeterministicScenario {
constructor(name, options = {}) {
this.name = name;
this.seed = options.seed || name.hashCode();
this.steps = [];
this.state = {};
}
addStep(action, validation) {
this.steps.push({ action, validation });
return this;
}
async execute(stagehand) {
const results = [];
for (let i = 0; i < this.steps.length; i++) {
const step = this.steps[i];
console.log(`Executing step ${i + 1}/${this.steps.length}`);
try {
// Execute action
const actionResult = await step.action(stagehand, this.state);
// Validate result if validation provided
if (step.validation) {
const isValid = await step.validation(actionResult, this.state);
if (!isValid) {
throw new Error(`Validation failed for step ${i + 1}`);
}
}
results.push({
step: i + 1,
success: true,
result: actionResult
});
} catch (error) {
results.push({
step: i + 1,
success: false,
error: error.message
});
if (!options.continueOnError) {
break;
}
}
}
return {
scenario: this.name,
seed: this.seed,
results
};
}
}// salesforce-lead-automation-complete.js
import { Stagehand } from "@browserbasehq/stagehand";
import dotenv from 'dotenv';
import { z } from 'zod';
import { TestDataGenerator } from './utils/testDataGenerator.js';
import { CacheManager, CachedStagehand } from './utils/cacheManager.js';
import { waitForStableState, retryWithBackoff } from './strategies/deterministicStrategies.js';
// Load environment variables
dotenv.config();
class SalesforceLeadAutomation {
constructor(options = {}) {
this.options = {
useCache: true,
useDeterministicData: true,
retryFailedSteps: true,
saveDebugInfo: true,
...options
};
this.testDataGen = new TestDataGenerator({
seed: options.testSeed || process.env.TEST_SEED
});
this.debugInfo = {
startTime: Date.now(),
steps: []
};
}
async init() {
// Initialize cache manager
if (this.options.useCache) {
this.cacheManager = new CacheManager({
namespace: 'salesforce-leads'
});
await this.cacheManager.init();
}
// Initialize Stagehand
this.stagehandInstance = new Stagehand({
env: process.env.BROWSERBASE_API_KEY ? "BROWSERBASE" : "LOCAL",
apiKey: process.env.BROWSERBASE_API_KEY,
projectId: process.env.BROWSERBASE_PROJECT_ID,
enableCaching: this.options.useCache,
headless: process.env.HEADLESS === 'true',
logger: (logLine) => {
console.log(`[${new Date().toISOString()}] ${logLine.message}`);
this.debugInfo.steps.push({
timestamp: Date.now(),
type: 'log',
message: logLine.message
});
},
modelName: process.env.LLM_MODEL || "gpt-4o",
modelClientOptions: {
apiKey: process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY
}
});
await this.stagehandInstance.init();
// Wrap with cache if enabled
if (this.options.useCache) {
this.stagehand = new CachedStagehand(
this.stagehandInstance,
this.cacheManager
);
} else {
this.stagehand = this.stagehandInstance;
}
this.page = this.stagehandInstance.page;
}
async login() {
console.log("🔐 Logging into Salesforce...");
await this.page.goto("https://login.salesforce.com");
await waitForStableState(this.page);
// Get credentials
const credentials = await this.getCredentials();
// Fill login form
await retryWithBackoff(async () => {
await this.stagehand.act({
action: `Fill username field with "${credentials.username}"`,
useVision: true
});
await this.stagehand.act({
action: `Fill password field with "${credentials.password}"`,
useVision: true
});
await this.stagehand.act({
action: "Click the Log In button",
useVision: true
});
});
// Wait for navigation
await this.page.waitForLoadState('networkidle');
await waitForStableState(this.page);
// Check for MFA or other login challenges
const currentUrl = this.page.url();
if (currentUrl.includes('verification')) {
console.log("⚠️ MFA verification required. Please complete manually.");
// Could implement automated MFA handling here
}
console.log("✅ Login successful");
}
async navigateToLeads() {
console.log("📋 Navigating to Leads...");
await retryWithBackoff(async () => {
await this.stagehand.act({
action: "Click on the Leads tab or navigation item",
useVision: true,
useCache: this.options.useCache
});
});
await this.page.waitForLoadState('networkidle');
await waitForStableState(this.page);
}
async createNewLead(leadData) {
console.log("➕ Creating new lead...");
// Click New button
await this.stagehand.act({
action: "Click the New button to create a new lead",
useVision: true
});
await waitForStableState(this.page);
// Extract required fields
const requiredFields = await this.stagehand.extract({
instruction: "Find all fields with red asterisks (required fields). Include the field label and any visible field identifiers or attributes.",
schema: z.object({
fields: z.array(z.object({
label: z.string(),
identifier: z.string().optional(),
type: z.string().optional()
}))
})
});
console.log(`Found ${requiredFields.fields.length} required fields`);
// Fill each field
for (const field of requiredFields.fields) {
const value = this.getFieldValue(field.label, leadData);
if (value) {
await retryWithBackoff(async () => {
await this.stagehand.act({
action: `Fill the ${field.label} field with "${value}"`,
useVision: true
});
});
// Small delay between fields
await new Promise(resolve => setTimeout(resolve, 500));
} else {
console.warn(`⚠️ No value provided for required field: ${field.label}`);
}
}
// Save the lead
console.log("💾 Saving lead...");
await this.stagehand.act({
action: "Click the Save button",
useVision: true
});
await this.page.waitForLoadState('networkidle');
await waitForStableState(this.page);
return await this.extractLeadId();
}
async extractLeadId() {
const currentUrl = this.page.url();
console.log("Current URL:", currentUrl);
// Try multiple patterns for Salesforce URLs
const patterns = [
/\/Lead\/([a-zA-Z0-9]{15,18})\//, // Lightning
/[?&]id=([a-zA-Z0-9]{15,18})/, // Classic
/\/([a-zA-Z0-9]{15,18})$/ // End of URL
];
for (const pattern of patterns) {
const match = currentUrl.match(pattern);
if (match && match[1]) {
return match[1];
}
}
// Try extracting from page content
try {
const leadInfo = await this.stagehand.extract({
instruction: "Extract the Lead ID from the page. It's usually a 15 or 18 character alphanumeric string.",
schema: z.object({
leadId: z.string()
})
});
if (leadInfo.leadId) {
return leadInfo.leadId;
}
} catch (error) {
console.error("Could not extract Lead ID from page");
}
return null;
}
getFieldValue(fieldLabel, leadData) {
// Map field labels to data properties
const fieldMapping = {
'Last Name': leadData.lastName,
'Company': leadData.company,
'Email': leadData.email,
'Phone': leadData.phone,
'Lead Status': leadData.status,
'Lead Source': leadData.leadSource,
'First Name': leadData.firstName,
'Title': leadData.title,
'Description': leadData.description,
'Street': leadData.street,
// Add more mappings as needed
};
return fieldMapping[fieldLabel] || null;
}
async getCredentials() {
// Implement your credential retrieval logic
return {
username: process.env.SALESFORCE_USERNAME,
password: process.env.SALESFORCE_PASSWORD
};
}
async saveDebugInfo(leadId) {
if (!this.options.saveDebugInfo) return;
const debugData = {
...this.debugInfo,
endTime: Date.now(),
duration: Date.now() - this.debugInfo.startTime,
leadId,
testSeed: this.testDataGen.seed,
success: !!leadId
};
const filename = `debug-${Date.now()}.json`;
await fs.writeFile(
`logs/${filename}`,
JSON.stringify(debugData, null, 2)
);
console.log(`Debug info saved to logs/${filename}`);
}
async close() {
if (this.stagehandInstance) {
await this.stagehandInstance.close();
}
}
async run() {
let leadId = null;
try {
await this.init();
await this.login();
await this.navigateToLeads();
// Generate test data
const leadData = this.testDataGen.generateLeadData({
id: `lead-${Date.now()}`
});
console.log("Generated lead data:", {
lastName: leadData.lastName,
company: leadData.company,
email: leadData.email
});
// Save test data for reference
this.testDataGen.saveTestData(leadData, 'lead-created');
// Create the lead
leadId = await this.createNewLead(leadData);
if (leadId) {
console.log(`
✅ SUCCESS! Lead created with ID: ${leadId}
📊 Test Data Seed: ${this.testDataGen.seed}
⏱️ Duration: ${(Date.now() - this.debugInfo.startTime) / 1000}s
`);
} else {
console.error("❌ Failed to extract Lead ID");
}
} catch (error) {
console.error("❌ Automation failed:", error);
throw error;
} finally {
await this.saveDebugInfo(leadId);
await this.close();
}
return leadId;
}
}
// Run the automation
if (import.meta.url === `file://${process.argv[1]}`) {
const automation = new SalesforceLeadAutomation({
useCache: process.env.USE_CACHE !== 'false',
useDeterministicData: process.env.DETERMINISTIC !== 'false',
testSeed: process.env.TEST_SEED || Date.now()
});
automation.run()
.then(leadId => {
console.log("Automation completed successfully");
process.exit(0);
})
.catch(error => {
console.error("Automation failed:", error);
process.exit(1);
});
}
export { SalesforceLeadAutomation };{
"name": "salesforce-lead-automation",
"version": "2.0.0",
"type": "module",
"scripts": {
"start": "node salesforce-lead-automation-complete.js",
"test": "npm run test:setup && npm run test:cache",
"test:setup": "node scripts/test-setup.js",
"test:cache": "node scripts/test-cache.js",
"debug": "node --inspect salesforce-lead-automation-complete.js",
"clean:cache": "rm -rf .cache .stagehand-cache",
"generate:data": "node scripts/generate-test-data.js",
"encrypt:creds": "node scripts/encrypt-credentials.js",
"decrypt:creds": "node scripts/decrypt-credentials.js"
},
"dependencies": {
"@browserbasehq/stagehand": "^1.0.0",
"@playwright/test": "^1.40.0",
"dotenv": "^16.3.1",
"zod": "^3.22.0",
"@faker-js/faker": "^8.3.1",
"@aws-sdk/client-secrets-manager": "^3.0.0"
}
}# .env.example - Complete configuration
# === BROWSERBASE CONFIGURATION ===
BROWSERBASE_API_KEY=your_browserbase_api_key
BROWSERBASE_PROJECT_ID=your_project_id
# === LLM CONFIGURATION ===
# Choose one:
OPENAI_API_KEY=your_openai_api_key
# ANTHROPIC_API_KEY=your_anthropic_api_key
LLM_MODEL=gpt-4o # or claude-3-5-sonnet-latest
# === SALESFORCE CREDENTIALS ===
# For direct use (development only)
SALESFORCE_USERNAME=your_salesforce_username
SALESFORCE_PASSWORD=your_salesforce_password
# For production (use secret references)
SALESFORCE_SECRET_REF=salesforce-prod-creds
USE_AWS_SECRETS=false
USE_BROWSERBASE_SECRETS=false
# === TEST CONFIGURATION ===
# Deterministic test seed (optional - defaults to timestamp)
TEST_SEED=12345
# Enable/disable features
USE_CACHE=true
DETERMINISTIC=true
HEADLESS=false
SAVE_DEBUG_INFO=true
# === ADVANCED CONFIGURATION ===
# Retry configuration
MAX_RETRIES=3
RETRY_DELAY=1000
# Timeouts (in milliseconds)
DEFAULT_TIMEOUT=30000
PAGE_LOAD_TIMEOUT=60000
# Cache configuration
CACHE_TTL=86400 # 24 hours
CACHE_DIR=.cache/stagehand
# Test data configuration
TEST_EMAIL_DOMAIN=example.com
TEST_LOCALE=en-US
TEST_INDUSTRY=TechnologyThis guide provides a comprehensive solution for:
The implementation is production-ready and includes error handling, retry logic, debugging capabilities, and extensive configuration options. It's designed to be maintainable by teams and scalable for enterprise use.