sf-integration: Salesforce Integration Patterns Expert Use this skill when the user needs integration architecture and runtime plumbing : Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, CDC, and event-driven integration design. When This Skill Owns the Task Use when the work involves: - or External Credential metadata - outbound REST/SOAP callouts - External Service registration from OpenAPI specs - Platform Events, CDC, and event-driven architecture - choosing sync vs async integration patterns Delegate elsewhere when the user is: - co…

, re.IGNORECASE),\n 'external_credential': re.compile(r'\\.externalCredential-meta\\.xml

sf-integration: Salesforce Integration Patterns Expert Use this skill when the user needs integration architecture and runtime plumbing : Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, CDC, and event-driven integration design. When This Skill Owns the Task Use when the work involves: - or External Credential metadata - outbound REST/SOAP callouts - External Service registration from OpenAPI specs - Platform Events, CDC, and event-driven architecture - choosing sync vs async integration patterns Delegate elsewhere when the user is: - co…

, re.IGNORECASE),\n 'csp_trusted_site': re.compile(r'\\.cspTrustedSite-meta\\.xml

sf-integration: Salesforce Integration Patterns Expert Use this skill when the user needs integration architecture and runtime plumbing : Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, CDC, and event-driven integration design. When This Skill Owns the Task Use when the work involves: - or External Credential metadata - outbound REST/SOAP callouts - External Service registration from OpenAPI specs - Platform Events, CDC, and event-driven architecture - choosing sync vs async integration patterns Delegate elsewhere when the user is: - co…

, re.IGNORECASE),\n 'remote_site': re.compile(r'\\.remoteSiteSetting-meta\\.xml$|\\.remoteSite-meta\\.xml

sf-integration: Salesforce Integration Patterns Expert Use this skill when the user needs integration architecture and runtime plumbing : Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, CDC, and event-driven integration design. When This Skill Owns the Task Use when the work involves: - or External Credential metadata - outbound REST/SOAP callouts - External Service registration from OpenAPI specs - Platform Events, CDC, and event-driven architecture - choosing sync vs async integration patterns Delegate elsewhere when the user is: - co…

, re.IGNORECASE),\n 'external_service': re.compile(r'\\.externalServiceRegistration-meta\\.xml

sf-integration: Salesforce Integration Patterns Expert Use this skill when the user needs integration architecture and runtime plumbing : Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, CDC, and event-driven integration design. When This Skill Owns the Task Use when the work involves: - or External Credential metadata - outbound REST/SOAP callouts - External Service registration from OpenAPI specs - Platform Events, CDC, and event-driven architecture - choosing sync vs async integration patterns Delegate elsewhere when the user is: - co…

, re.IGNORECASE),\n}\n\n# Script recommendations per file type\nSCRIPT_RECOMMENDATIONS = {\n 'named_credential': {\n 'script': 'configure-named-credential.sh',\n 'description': 'Set API key securely via ConnectApi (Enhanced Named Credentials)',\n 'usage': './scripts/configure-named-credential.sh \u003corg-alias>',\n 'next_steps': [\n 'Deploy metadata: sf project deploy start --metadata NamedCredential:\u003cname>',\n 'Run script to configure API key securely',\n 'Test connection in Setup → Named Credentials'\n ]\n },\n 'external_credential': {\n 'script': 'configure-named-credential.sh',\n 'description': 'Configure External Credential with ConnectApi',\n 'usage': './scripts/configure-named-credential.sh \u003corg-alias>',\n 'next_steps': [\n 'Deploy External Credential first',\n 'Deploy associated Named Credential',\n 'Run script to set authentication parameters'\n ]\n },\n 'csp_trusted_site': {\n 'script': None,\n 'description': 'CSP Trusted Site created for endpoint security',\n 'usage': None,\n 'next_steps': [\n 'Deploy: sf project deploy start --metadata CspTrustedSite:\u003cname>',\n 'Verify in Setup → CSP Trusted Sites'\n ]\n },\n 'remote_site': {\n 'script': None,\n 'description': 'Remote Site Setting created (legacy endpoint security)',\n 'usage': None,\n 'next_steps': [\n 'Deploy: sf project deploy start --metadata RemoteSiteSetting:\u003cname>',\n 'Consider migrating to CSP Trusted Sites for modern approach'\n ]\n },\n 'external_service': {\n 'script': None,\n 'description': 'External Service registration created',\n 'usage': None,\n 'next_steps': [\n 'Ensure Named Credential is configured first',\n 'Deploy: sf project deploy start --metadata ExternalServiceRegistration:\u003cname>',\n 'Apex classes will be auto-generated from OpenAPI spec'\n ]\n }\n}\n\n\ndef detect_file_type(file_path: str) -> str | None:\n \"\"\"Detect the credential file type from the file path.\"\"\"\n filename = os.path.basename(file_path)\n\n for file_type, pattern in PATTERNS.items():\n if pattern.search(filename):\n return file_type\n\n return None\n\n\ndef extract_credential_name(file_path: str, file_type: str) -> str:\n \"\"\"Extract the credential name from the file path.\"\"\"\n filename = os.path.basename(file_path)\n\n # Remove the metadata suffix to get the credential name\n patterns = {\n 'named_credential': r'(.+)\\.namedCredential-meta\\.xml

sf-integration: Salesforce Integration Patterns Expert Use this skill when the user needs integration architecture and runtime plumbing : Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, CDC, and event-driven integration design. When This Skill Owns the Task Use when the work involves: - or External Credential metadata - outbound REST/SOAP callouts - External Service registration from OpenAPI specs - Platform Events, CDC, and event-driven architecture - choosing sync vs async integration patterns Delegate elsewhere when the user is: - co…

,\n 'external_credential': r'(.+)\\.externalCredential-meta\\.xml

sf-integration: Salesforce Integration Patterns Expert Use this skill when the user needs integration architecture and runtime plumbing : Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, CDC, and event-driven integration design. When This Skill Owns the Task Use when the work involves: - or External Credential metadata - outbound REST/SOAP callouts - External Service registration from OpenAPI specs - Platform Events, CDC, and event-driven architecture - choosing sync vs async integration patterns Delegate elsewhere when the user is: - co…

,\n 'csp_trusted_site': r'(.+)\\.cspTrustedSite-meta\\.xml

sf-integration: Salesforce Integration Patterns Expert Use this skill when the user needs integration architecture and runtime plumbing : Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, CDC, and event-driven integration design. When This Skill Owns the Task Use when the work involves: - or External Credential metadata - outbound REST/SOAP callouts - External Service registration from OpenAPI specs - Platform Events, CDC, and event-driven architecture - choosing sync vs async integration patterns Delegate elsewhere when the user is: - co…

,\n 'remote_site': r'(.+)\\.(?:remoteSiteSetting|remoteSite)-meta\\.xml

sf-integration: Salesforce Integration Patterns Expert Use this skill when the user needs integration architecture and runtime plumbing : Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, CDC, and event-driven integration design. When This Skill Owns the Task Use when the work involves: - or External Credential metadata - outbound REST/SOAP callouts - External Service registration from OpenAPI specs - Platform Events, CDC, and event-driven architecture - choosing sync vs async integration patterns Delegate elsewhere when the user is: - co…

,\n 'external_service': r'(.+)\\.externalServiceRegistration-meta\\.xml

sf-integration: Salesforce Integration Patterns Expert Use this skill when the user needs integration architecture and runtime plumbing : Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, CDC, and event-driven integration design. When This Skill Owns the Task Use when the work involves: - or External Credential metadata - outbound REST/SOAP callouts - External Service registration from OpenAPI specs - Platform Events, CDC, and event-driven architecture - choosing sync vs async integration patterns Delegate elsewhere when the user is: - co…

,\n }\n\n pattern = patterns.get(file_type)\n if pattern:\n match = re.match(pattern, filename, re.IGNORECASE)\n if match:\n return match.group(1)\n\n return filename\n\n\ndef analyze_file_content(file_path: str) -> dict:\n \"\"\"Analyze the file content for additional context.\"\"\"\n context = {\n 'auth_protocol': None,\n 'endpoint_url': None,\n 'has_oauth': False,\n 'has_certificate': False\n }\n\n try:\n with open(file_path, 'r', encoding='utf-8') as f:\n content = f.read()\n\n # Detect authentication protocol\n if '\u003cauthProtocol>OAuth\u003c/authProtocol>' in content:\n context['auth_protocol'] = 'OAuth 2.0'\n context['has_oauth'] = True\n elif '\u003cauthProtocol>Jwt\u003c/authProtocol>' in content:\n context['auth_protocol'] = 'JWT Bearer'\n elif '\u003cauthProtocol>Custom\u003c/authProtocol>' in content:\n context['auth_protocol'] = 'Custom (API Key)'\n elif '\u003cauthProtocol>Certificate\u003c/authProtocol>' in content:\n context['auth_protocol'] = 'Certificate'\n context['has_certificate'] = True\n\n # Extract endpoint URL\n url_match = re.search(r'\u003cendpoint>([^\u003c]+)\u003c/endpoint>', content)\n if url_match:\n context['endpoint_url'] = url_match.group(1)\n\n # Check for Named Credential URL pattern\n url_match = re.search(r'\u003curl>([^\u003c]+)\u003c/url>', content)\n if url_match:\n context['endpoint_url'] = url_match.group(1)\n\n except Exception:\n pass # File analysis is optional\n\n return context\n\n\ndef generate_suggestion_message(file_type: str, cred_name: str, file_context: dict) -> str:\n \"\"\"Generate the suggestion message for Claude.\"\"\"\n recommendation = SCRIPT_RECOMMENDATIONS.get(file_type, {})\n\n lines = [\n '',\n '═' * 60,\n '🔐 CREDENTIAL CONFIGURATION DETECTED',\n '═' * 60,\n '',\n f'📄 File Type: {file_type.replace(\"_\", \" \").title()}',\n f'📛 Name: {cred_name}',\n ]\n\n if file_context.get('auth_protocol'):\n lines.append(f'🔑 Auth Protocol: {file_context[\"auth_protocol\"]}')\n\n if file_context.get('endpoint_url'):\n lines.append(f'🌐 Endpoint: {file_context[\"endpoint_url\"]}')\n\n lines.append('')\n\n if recommendation.get('script'):\n lines.extend([\n '┌─────────────────────────────────────────────────────────┐',\n '│ 🚀 AUTOMATION SCRIPT AVAILABLE │',\n '├─────────────────────────────────────────────────────────┤',\n f'│ Script: {recommendation[\"script\"]:\u003c46} │',\n f'│ Purpose: {recommendation[\"description\"][:44]:\u003c44} │',\n '├─────────────────────────────────────────────────────────┤',\n '│ 💡 OFFER TO RUN: │',\n f'│ {recommendation[\"usage\"]:\u003c55} │',\n '└─────────────────────────────────────────────────────────┘',\n '',\n ])\n\n lines.extend([\n '📋 NEXT STEPS:',\n '─' * 60,\n ])\n\n for i, step in enumerate(recommendation.get('next_steps', []), 1):\n lines.append(f' {i}. {step}')\n\n # Add OAuth-specific suggestion\n if file_context.get('has_oauth'):\n lines.extend([\n '',\n '⚠️ OAuth detected: Consider using /sf-connected-apps to',\n ' create the Connected App for this credential.',\n ])\n\n lines.extend([\n '',\n '═' * 60,\n ])\n\n return '\\n'.join(lines)\n\n\ndef main():\n \"\"\"Main entry point for the hook.\"\"\"\n # Get file path from command line or stdin\n file_path = None\n\n if len(sys.argv) > 1:\n file_path = sys.argv[1]\n else:\n # Try to read from stdin (hook input)\n try:\n hook_input = json.load(sys.stdin)\n tool_input = hook_input.get('tool_input', {})\n file_path = tool_input.get('file_path', '')\n except (json.JSONDecodeError, IOError):\n pass\n\n if not file_path:\n # No file path, exit silently\n print(json.dumps({'continue': True}))\n return 0\n\n # Detect file type\n file_type = detect_file_type(file_path)\n\n if not file_type:\n # Not a credential file, exit silently\n print(json.dumps({'continue': True}))\n return 0\n\n # Extract credential name\n cred_name = extract_credential_name(file_path, file_type)\n\n # Analyze file content\n file_context = analyze_file_content(file_path)\n\n # Generate suggestion message\n message = generate_suggestion_message(file_type, cred_name, file_context)\n\n # Output hook result\n result = {\n 'continue': True,\n 'hookSpecificOutput': {\n 'hookEventName': 'PostToolUse',\n 'additionalContext': message\n }\n }\n\n print(json.dumps(result))\n return 0\n\n\nif __name__ == '__main__':\n sys.exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":9466,"content_sha256":"fd3fd470d2d3e1b6ab683c16aa3ee7f9129b9ee58e1b458a1c7112ff46c787dc"},{"filename":"hooks/scripts/validate_integration.py","content":"#!/usr/bin/env python3\n\"\"\"\nsf-integration Validation Script\n\nValidates integration-related files against best practices.\nScoring: 120 points across 6 categories.\n\nCategories:\n- Security (30 points)\n- Error Handling (25 points)\n- Bulkification (20 points)\n- Architecture (20 points)\n- Best Practices (15 points)\n- Documentation (10 points)\n\"\"\"\n\nimport sys\nimport re\nimport os\nfrom pathlib import Path\n\n# Scoring configuration\nMAX_SCORE = 120\nCATEGORIES = {\n 'security': {'max': 30, 'score': 0, 'issues': []},\n 'error_handling': {'max': 25, 'score': 0, 'issues': []},\n 'bulkification': {'max': 20, 'score': 0, 'issues': []},\n 'architecture': {'max': 20, 'score': 0, 'issues': []},\n 'best_practices': {'max': 15, 'score': 0, 'issues': []},\n 'documentation': {'max': 10, 'score': 0, 'issues': []}\n}\n\n# File type patterns\nAPEX_PATTERN = re.compile(r'\\.cls$|\\.trigger

sf-integration: Salesforce Integration Patterns Expert Use this skill when the user needs integration architecture and runtime plumbing : Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, CDC, and event-driven integration design. When This Skill Owns the Task Use when the work involves: - or External Credential metadata - outbound REST/SOAP callouts - External Service registration from OpenAPI specs - Platform Events, CDC, and event-driven architecture - choosing sync vs async integration patterns Delegate elsewhere when the user is: - co…

)\nXML_PATTERN = re.compile(r'\\.xml

sf-integration: Salesforce Integration Patterns Expert Use this skill when the user needs integration architecture and runtime plumbing : Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, CDC, and event-driven integration design. When This Skill Owns the Task Use when the work involves: - or External Credential metadata - outbound REST/SOAP callouts - External Service registration from OpenAPI specs - Platform Events, CDC, and event-driven architecture - choosing sync vs async integration patterns Delegate elsewhere when the user is: - co…

)\nNAMED_CRED_PATTERN = re.compile(r'namedCredential.*\\.xml

sf-integration: Salesforce Integration Patterns Expert Use this skill when the user needs integration architecture and runtime plumbing : Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, CDC, and event-driven integration design. When This Skill Owns the Task Use when the work involves: - or External Credential metadata - outbound REST/SOAP callouts - External Service registration from OpenAPI specs - Platform Events, CDC, and event-driven architecture - choosing sync vs async integration patterns Delegate elsewhere when the user is: - co…

, re.IGNORECASE)\n\n\ndef validate_apex_file(content: str, filename: str) -> None:\n \"\"\"Validate Apex class/trigger for integration patterns.\"\"\"\n\n # Security checks (30 points)\n security_score = 30\n\n # Check for hardcoded credentials\n if re.search(r'Authorization.*Bearer\\s+[a-zA-Z0-9_\\-]{20,}', content):\n security_score -= 15\n CATEGORIES['security']['issues'].append('❌ Hardcoded Bearer token detected')\n\n if re.search(r'api[_-]?key\\s*=\\s*[\\'\"][a-zA-Z0-9]{10,}', content, re.IGNORECASE):\n security_score -= 15\n CATEGORIES['security']['issues'].append('❌ Hardcoded API key detected')\n\n if re.search(r'password\\s*=\\s*[\\'\"][^\\'\"]{5,}', content, re.IGNORECASE):\n security_score -= 15\n CATEGORIES['security']['issues'].append('❌ Hardcoded password detected')\n\n # Check for Named Credential usage\n if 'HttpRequest' in content:\n if 'callout:' in content:\n if not CATEGORIES['security']['issues']:\n CATEGORIES['security']['issues'].append('✅ Named Credential used')\n else:\n security_score -= 10\n CATEGORIES['security']['issues'].append('⚠️ HttpRequest without Named Credential')\n\n CATEGORIES['security']['score'] = max(0, security_score)\n\n # Error Handling checks (25 points)\n error_score = 25\n\n if 'HttpRequest' in content or 'Http().send' in content:\n # Check for try-catch\n if 'try' not in content or 'catch' not in content:\n error_score -= 10\n CATEGORIES['error_handling']['issues'].append('❌ Missing try-catch for callout')\n else:\n CATEGORIES['error_handling']['issues'].append('✅ Try-catch present')\n\n # Check for CalloutException handling\n if 'CalloutException' in content:\n CATEGORIES['error_handling']['issues'].append('✅ CalloutException handled')\n else:\n error_score -= 5\n CATEGORIES['error_handling']['issues'].append('⚠️ CalloutException not explicitly caught')\n\n # Check for status code handling\n if 'getStatusCode()' in content:\n CATEGORIES['error_handling']['issues'].append('✅ Status code checked')\n else:\n error_score -= 5\n CATEGORIES['error_handling']['issues'].append('⚠️ Status code not checked')\n\n # Check for timeout setting\n if 'setTimeout' in content:\n CATEGORIES['error_handling']['issues'].append('✅ Timeout configured')\n else:\n error_score -= 5\n CATEGORIES['error_handling']['issues'].append('⚠️ No timeout set (default may be too short)')\n\n CATEGORIES['error_handling']['score'] = max(0, error_score)\n\n # Bulkification checks (20 points)\n bulk_score = 20\n\n # Check for SOQL in loops\n if re.search(r'for\\s*\\([^)]+\\)\\s*\\{[^}]*\\[SELECT', content, re.DOTALL | re.IGNORECASE):\n bulk_score -= 10\n CATEGORIES['bulkification']['issues'].append('❌ SOQL in loop')\n\n # Check for DML in loops\n if re.search(r'for\\s*\\([^)]+\\)\\s*\\{[^}]*(insert|update|delete)\\s+', content, re.DOTALL | re.IGNORECASE):\n bulk_score -= 10\n CATEGORIES['bulkification']['issues'].append('❌ DML in loop')\n\n # Check for HTTP callout in loops (expensive)\n if re.search(r'for\\s*\\([^)]+\\)\\s*\\{[^}]*\\.send\\(', content, re.DOTALL):\n bulk_score -= 5\n CATEGORIES['bulkification']['issues'].append('⚠️ HTTP callout in loop (consider batching)')\n\n if bulk_score == 20:\n CATEGORIES['bulkification']['issues'].append('✅ No obvious bulkification issues')\n\n CATEGORIES['bulkification']['score'] = max(0, bulk_score)\n\n # Architecture checks (20 points)\n arch_score = 20\n\n # Check if class implements proper interfaces for callouts\n if 'implements Queueable' in content and 'Database.AllowsCallouts' in content:\n CATEGORIES['architecture']['issues'].append('✅ Proper Queueable + AllowsCallouts pattern')\n elif 'Queueable' in content and 'AllowsCallouts' not in content:\n if 'HttpRequest' in content or 'Http(' in content:\n arch_score -= 10\n CATEGORIES['architecture']['issues'].append('❌ Queueable with callout missing AllowsCallouts')\n\n # Check for trigger context callout (should be async)\n if '.trigger' in filename.lower():\n if 'Http(' in content or 'HttpRequest' in content:\n arch_score -= 15\n CATEGORIES['architecture']['issues'].append('❌ Synchronous callout in trigger (must use async)')\n\n CATEGORIES['architecture']['score'] = max(0, arch_score)\n\n # Best Practices checks (15 points)\n bp_score = 15\n\n # Check for logging\n if 'System.debug' in content:\n CATEGORIES['best_practices']['issues'].append('✅ Debug logging present')\n else:\n bp_score -= 5\n CATEGORIES['best_practices']['issues'].append('⚠️ No debug logging')\n\n # Check for proper HTTP methods\n if re.search(r'setMethod\\s*\\(\\s*[\\'\"](?:GET|POST|PUT|PATCH|DELETE)[\\'\"]\\s*\\)', content):\n CATEGORIES['best_practices']['issues'].append('✅ Standard HTTP method used')\n\n CATEGORIES['best_practices']['score'] = max(0, bp_score)\n\n # Documentation checks (10 points)\n doc_score = 10\n\n # Check for ApexDoc\n if '/**' in content and '@description' in content:\n CATEGORIES['documentation']['issues'].append('✅ ApexDoc present')\n else:\n doc_score -= 5\n CATEGORIES['documentation']['issues'].append('⚠️ Missing ApexDoc comments')\n\n # Check for class-level documentation\n if re.search(r'/\\*\\*[\\s\\S]*?\\*/\\s*public\\s+(with sharing\\s+)?class', content):\n CATEGORIES['documentation']['issues'].append('✅ Class-level documentation')\n\n CATEGORIES['documentation']['score'] = max(0, doc_score)\n\n\ndef validate_named_credential(content: str) -> None:\n \"\"\"Validate Named Credential XML.\"\"\"\n\n # Security checks\n security_score = 30\n\n # Check for hardcoded password in XML (should never be there)\n if '\u003cpassword>' in content and re.search(r'\u003cpassword>[^\u003c]+\u003c/password>', content):\n password_value = re.search(r'\u003cpassword>([^\u003c]+)\u003c/password>', content)\n if password_value and len(password_value.group(1)) > 0:\n security_score -= 15\n CATEGORIES['security']['issues'].append('⚠️ Password value in metadata (should be empty, set via UI)')\n\n # Check for protocol\n if '\u003cprotocol>Oauth\u003c/protocol>' in content:\n CATEGORIES['security']['issues'].append('✅ OAuth authentication configured')\n elif '\u003cprotocol>Password\u003c/protocol>' in content:\n CATEGORIES['security']['issues'].append('✅ Password authentication configured')\n security_score -= 5 # OAuth preferred over password\n elif '\u003cprotocol>NoAuthentication\u003c/protocol>' in content:\n CATEGORIES['security']['issues'].append('⚠️ No authentication (verify this is intentional)')\n security_score -= 10\n\n CATEGORIES['security']['score'] = max(0, security_score)\n\n # Best practices\n bp_score = 15\n\n if '\u003callowMergeFieldsInBody>true\u003c/allowMergeFieldsInBody>' in content:\n CATEGORIES['best_practices']['issues'].append('✅ Merge fields in body enabled')\n\n if '\u003callowMergeFieldsInHeader>true\u003c/allowMergeFieldsInHeader>' in content:\n CATEGORIES['best_practices']['issues'].append('✅ Merge fields in header enabled')\n\n CATEGORIES['best_practices']['score'] = bp_score\n\n # Named Credential XML files are configuration-only — Error Handling,\n # Bulkification, Architecture, and Documentation categories are not\n # applicable. Grant full marks for non-applicable categories so that\n # valid XML files are not penalized by the Apex-oriented rubric.\n CATEGORIES['error_handling']['score'] = CATEGORIES['error_handling']['max']\n CATEGORIES['bulkification']['score'] = CATEGORIES['bulkification']['max']\n CATEGORIES['architecture']['score'] = CATEGORIES['architecture']['max']\n CATEGORIES['documentation']['score'] = CATEGORIES['documentation']['max']\n\n\ndef validate_platform_event(content: str) -> None:\n \"\"\"Validate Platform Event definition.\"\"\"\n\n # Check event type\n if '\u003ceventType>HighVolume\u003c/eventType>' in content:\n CATEGORIES['best_practices']['issues'].append('✅ High Volume event type')\n CATEGORIES['best_practices']['score'] = 15\n elif '\u003ceventType>StandardVolume\u003c/eventType>' in content:\n CATEGORIES['best_practices']['issues'].append('✅ Standard Volume event type')\n CATEGORIES['best_practices']['score'] = 15\n\n # Check publish behavior\n if '\u003cpublishBehavior>PublishAfterCommit\u003c/publishBehavior>' in content:\n CATEGORIES['architecture']['issues'].append('✅ PublishAfterCommit (recommended)')\n CATEGORIES['architecture']['score'] = 20\n elif '\u003cpublishBehavior>PublishImmediately\u003c/publishBehavior>' in content:\n CATEGORIES['architecture']['issues'].append('⚠️ PublishImmediately (verify this is intentional)')\n CATEGORIES['architecture']['score'] = 15\n\n\ndef calculate_total_score() -> int:\n \"\"\"Calculate total score from all categories.\"\"\"\n return sum(cat['score'] for cat in CATEGORIES.values())\n\n\ndef get_rating(score: int) -> str:\n \"\"\"Get star rating based on score.\"\"\"\n percentage = (score / MAX_SCORE) * 100\n if percentage >= 90:\n return '⭐⭐⭐⭐⭐ Excellent'\n elif percentage >= 80:\n return '⭐⭐⭐⭐ Very Good'\n elif percentage >= 70:\n return '⭐⭐⭐ Good'\n elif percentage >= 60:\n return '⭐⭐ Needs Work'\n else:\n return '⭐ Critical'\n\n\ndef print_score_report(filename: str) -> None:\n \"\"\"Print formatted score report.\"\"\"\n total = calculate_total_score()\n rating = get_rating(total)\n\n print(f'\\n📊 INTEGRATION SCORE: {total}/{MAX_SCORE} {rating}')\n print('═' * 50)\n\n category_icons = {\n 'security': '🔐',\n 'error_handling': '⚠️',\n 'bulkification': '📦',\n 'architecture': '🏗️',\n 'best_practices': '✅',\n 'documentation': '📝'\n }\n\n for cat_name, cat_data in CATEGORIES.items():\n icon = category_icons.get(cat_name, '•')\n max_score = cat_data['max']\n score = cat_data['score']\n pct = (score / max_score * 100) if max_score > 0 else 0\n bar = '█' * int(pct / 10) + '░' * (10 - int(pct / 10))\n\n print(f'\\n{icon} {cat_name.replace(\"_\", \" \").title():18} {score:2}/{max_score:2} {bar} {pct:.0f}%')\n\n for issue in cat_data['issues']:\n print(f' {issue}')\n\n print('\\n' + '═' * 50)\n\n if total \u003c 54:\n print('🚫 DEPLOYMENT BLOCKED - Score below 45% threshold')\n elif total \u003c 72:\n print('⚠️ WARNING - Review issues before deployment')\n else:\n print('✅ PASSED - Ready for deployment')\n\n\ndef main():\n import json\n\n file_path = None\n\n # Mode 1: Hook mode - read from stdin JSON (PostToolUse hooks)\n if not sys.stdin.isatty():\n try:\n hook_input = json.load(sys.stdin)\n tool_input = hook_input.get(\"tool_input\", {})\n file_path = tool_input.get(\"file_path\", \"\")\n except (json.JSONDecodeError, EOFError):\n pass\n\n # Mode 2: CLI mode - read from command-line argument\n if not file_path and len(sys.argv) >= 2:\n file_path = sys.argv[1]\n\n # Skip if no file path\n if not file_path:\n print('Usage: validate_integration.py \u003cfile_path>')\n sys.exit(1)\n\n filename = os.path.basename(file_path)\n\n # Determine file type and validate\n try:\n with open(file_path, 'r', encoding='utf-8') as f:\n content = f.read()\n except Exception as e:\n print(f'Error reading file: {e}')\n sys.exit(1)\n\n # Skip if file is too small (likely not a real integration file)\n if len(content) \u003c 50:\n sys.exit(0)\n\n # Skip template files (contain placeholders)\n if '{{' in content and '}}' in content:\n print(f'ℹ️ Skipping template file: {filename}')\n sys.exit(0)\n\n # Validate based on file type\n if APEX_PATTERN.search(filename):\n # Only validate if it looks like integration code\n if any(keyword in content for keyword in ['HttpRequest', 'Http(', 'callout:', 'EventBus', 'ChangeEvent']):\n validate_apex_file(content, filename)\n print_score_report(filename)\n elif NAMED_CRED_PATTERN.search(filename):\n validate_named_credential(content)\n print_score_report(filename)\n elif '__e.object-meta.xml' in filename:\n validate_platform_event(content)\n print_score_report(filename)\n else:\n # Not an integration file we validate\n sys.exit(0)\n\n\nif __name__ == '__main__':\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":13738,"content_sha256":"c4774545553a418f3df9c9a68586aad735a7a9e290f0b38a39a93560b858bb96"},{"filename":"README.md","content":"# sf-integration\n\nCreates comprehensive Salesforce integrations with 120-point scoring. Build secure Named Credentials, External Services, REST/SOAP callouts, Platform Events, and Change Data Capture patterns.\n\n## Features\n\n- **Named Credentials**: OAuth 2.0, JWT Bearer, Certificate, API Key authentication patterns\n- **External Credentials**: Modern API 61+ credential architecture with Named Principals\n- **External Services**: Auto-generate Apex from OpenAPI/Swagger specs\n- **Callout Patterns**: Synchronous and asynchronous REST/SOAP implementations\n- **Event-Driven**: Platform Events and Change Data Capture subscribers\n- **120-Point Scoring**: Automated validation across 6 categories\n- **Automation Scripts**: Configure credentials without UI (ConnectApi)\n\n## Installation\n\n```bash\n# Install as part of sf-skills\nnpx skills add Jaganpro/sf-skills\n\n# Or install just this skill\nnpx skills add Jaganpro/sf-skills --skill sf-integration\n```\n\n## Quick Start\n\n### 1. Invoke the skill\n\n```\nSkill: sf-integration\nRequest: \"Create a Named Credential for Stripe API with OAuth\"\n```\n\n### 2. Answer requirements questions\n\nThe skill will ask about:\n- Integration type (REST, SOAP, Event-driven)\n- Authentication method (OAuth, JWT, Certificate, API Key)\n- External system details (URL, rate limits)\n- Sync vs Async requirements\n\n### 3. Deploy and configure\n\n```bash\n# Deploy credential metadata\nsf project deploy start --metadata NamedCredential:StripeAPI --target-org MyOrg\n\n# Run automation script to set API key\n./scripts/configure-named-credential.sh MyOrg\n```\n\n## Scoring System (120 Points)\n\n| Category | Points | Focus |\n|----------|--------|-------|\n| Security | 30 | Named Credentials, no hardcoded secrets, OAuth scopes |\n| Error Handling | 25 | Retry logic, timeouts, specific exceptions |\n| Bulkification | 20 | Batch callouts, event batching |\n| Architecture | 20 | Async patterns, service separation |\n| Best Practices | 15 | Governor limits, idempotency |\n| Documentation | 10 | Clear intent, API versioning |\n\n**Thresholds**: ⭐⭐⭐⭐⭐ 90+ | ⭐⭐⭐⭐ 80-89 | ⭐⭐⭐ 70-79 | Block: \u003c45%\n\n## Helper Scripts\n\nAutomate credential configuration without manual UI steps:\n\n| Script | Purpose |\n|--------|---------|\n| `configure-named-credential.sh` | Set API keys via ConnectApi (Enhanced NC) |\n| `set-api-credential.sh` | Store keys in Custom Settings (dev/test) |\n\n**Auto-run**: When you create credential files, Claude suggests running these scripts.\n\n```bash\n# After deploying Named Credential metadata\n./scripts/configure-named-credential.sh MyOrg\n# Enter API key when prompted (secure, hidden input)\n```\n\n## Prerequisites\n\n- **Salesforce CLI v2+**: `sf` command\n- **Authenticated org**: `sf org login web -a \u003calias>`\n- **API Version**: 62.0+ recommended for External Credentials\n\n## Cross-Skill Integration\n\n| Skill | Integration |\n|-------|-------------|\n| sf-connected-apps | Create OAuth Connected App for Named Credential |\n| sf-apex | Custom callout services beyond templates |\n| sf-flow | HTTP Callout Flow for Agentforce |\n| sf-deploy | Deploy credentials and callout code |\n| sf-ai-agentscript | Agent actions using External Services |\n\n## Documentation\n\n- [SKILL.md](SKILL.md) - Complete skill reference\n- [references/named-credentials-automation.md](references/named-credentials-automation.md) - Script automation guide\n- [references/named-credentials-guide.md](references/named-credentials-guide.md) - Template reference\n- [references/callout-patterns.md](references/callout-patterns.md) - REST/SOAP patterns\n- [references/event-patterns.md](references/event-patterns.md) - Platform Events & CDC\n\n## License\n\nMIT License - See LICENSE file for details.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3691,"content_sha256":"02151d251879745e9ff98a01ae2a51b1f70de16d9f79ba094df655bf94768905"},{"filename":"references/callout-patterns.md","content":"\u003c!-- Parent: sf-integration/SKILL.md -->\n# Callout Patterns Reference\n\nThis document provides detailed implementation patterns for REST and SOAP callouts in Salesforce integrations.\n\n> **Parent Document**: [sf-integration/SKILL.md](../SKILL.md)\n> **Related**: [event-patterns.md](./event-patterns.md)\n\n---\n\n## Table of Contents\n\n- [REST Callout Patterns](#rest-callout-patterns)\n - [Synchronous REST Callout](#synchronous-rest-callout)\n - [Asynchronous REST Callout (Queueable)](#asynchronous-rest-callout-queueable)\n - [Retry Handler with Exponential Backoff](#retry-handler-with-exponential-backoff)\n- [SOAP Callout Patterns](#soap-callout-patterns)\n - [WSDL2Apex Process](#wsdl2apex-process)\n - [SOAP Service Implementation](#soap-service-implementation)\n\n---\n\n## REST Callout Patterns\n\n### Synchronous REST Callout\n\n**Use Case**: Need immediate response, NOT triggered from DML\n\n**Template**: `assets/callouts/rest-sync-callout.cls`\n\n**When to Use**:\n- User-initiated actions requiring immediate feedback\n- API calls from Lightning Web Components\n- Scheduled batch jobs needing sequential processing\n- Any non-trigger context where response is needed\n\n**When NOT to Use**:\n- Triggered from DML operations (triggers, Process Builder, flows)\n- Long-running operations (>10 seconds expected)\n- High-volume batch operations\n\n#### Implementation\n\n```apex\npublic with sharing class {{ServiceName}}Callout {\n\n private static final String NAMED_CREDENTIAL = 'callout:{{NamedCredentialName}}';\n\n public static HttpResponse makeRequest(String method, String endpoint, String body) {\n HttpRequest req = new HttpRequest();\n req.setEndpoint(NAMED_CREDENTIAL + endpoint);\n req.setMethod(method);\n req.setHeader('Content-Type', 'application/json');\n req.setTimeout(120000); // 120 seconds max\n\n if (String.isNotBlank(body)) {\n req.setBody(body);\n }\n\n Http http = new Http();\n return http.send(req);\n }\n\n public static Map\u003cString, Object> get(String endpoint) {\n HttpResponse res = makeRequest('GET', endpoint, null);\n return handleResponse(res);\n }\n\n public static Map\u003cString, Object> post(String endpoint, Map\u003cString, Object> payload) {\n HttpResponse res = makeRequest('POST', endpoint, JSON.serialize(payload));\n return handleResponse(res);\n }\n\n public static Map\u003cString, Object> put(String endpoint, Map\u003cString, Object> payload) {\n HttpResponse res = makeRequest('PUT', endpoint, JSON.serialize(payload));\n return handleResponse(res);\n }\n\n public static Map\u003cString, Object> patch(String endpoint, Map\u003cString, Object> payload) {\n HttpResponse res = makeRequest('PATCH', endpoint, JSON.serialize(payload));\n return handleResponse(res);\n }\n\n public static void deleteRequest(String endpoint) {\n makeRequest('DELETE', endpoint, null);\n }\n\n private static Map\u003cString, Object> handleResponse(HttpResponse res) {\n Integer statusCode = res.getStatusCode();\n\n if (statusCode >= 200 && statusCode \u003c 300) {\n return (Map\u003cString, Object>) JSON.deserializeUntyped(res.getBody());\n } else if (statusCode >= 400 && statusCode \u003c 500) {\n throw new CalloutException('Client Error: ' + statusCode + ' - ' + res.getBody());\n } else if (statusCode >= 500) {\n throw new CalloutException('Server Error: ' + statusCode + ' - ' + res.getBody());\n }\n\n return null;\n }\n}\n```\n\n#### Key Features\n\n- **Named Credential Integration**: Uses `callout:` syntax for secure authentication\n- **Timeout Management**: 120-second max timeout (governor limit)\n- **HTTP Method Support**: GET, POST, PUT, PATCH, DELETE\n- **Status Code Handling**: Differentiates between client (4xx) and server (5xx) errors\n- **JSON Serialization**: Automatic JSON handling for request/response bodies\n\n#### Usage Example\n\n```apex\n// GET request\ntry {\n Map\u003cString, Object> data = StripeCallout.get('/v1/customers/cus_123');\n String email = (String) data.get('email');\n} catch (CalloutException e) {\n // Handle error\n System.debug(LoggingLevel.ERROR, 'Callout failed: ' + e.getMessage());\n}\n\n// POST request\nMap\u003cString, Object> payload = new Map\u003cString, Object>{\n 'email' => '[email protected]',\n 'name' => 'John Doe'\n};\nMap\u003cString, Object> response = StripeCallout.post('/v1/customers', payload);\n```\n\n---\n\n### Asynchronous REST Callout (Queueable)\n\n**Use Case**: Callouts triggered from DML (triggers, Process Builder)\n\n**Template**: `assets/callouts/rest-queueable-callout.cls`\n\n**When to Use**:\n- Callouts from triggers (REQUIRED - sync callouts fail in triggers)\n- Fire-and-forget operations (no immediate response needed)\n- Bulk operations processing multiple records\n- Long-running API calls (>10 seconds)\n\n**Governor Limit Considerations**:\n- Max 50 queueable jobs per transaction\n- Queueable can chain to another queueable (max depth varies by org)\n- Callout timeout: 120 seconds\n\n#### Implementation\n\n```apex\npublic with sharing class {{ServiceName}}QueueableCallout implements Queueable, Database.AllowsCallouts {\n\n private List\u003cId> recordIds;\n private String operation;\n\n public {{ServiceName}}QueueableCallout(List\u003cId> recordIds, String operation) {\n this.recordIds = recordIds;\n this.operation = operation;\n }\n\n public void execute(QueueableContext context) {\n if (recordIds == null || recordIds.isEmpty()) {\n return;\n }\n\n try {\n // Query records\n List\u003c{{ObjectName}}> records = [\n SELECT Id, Name, {{FieldsToSend}}\n FROM {{ObjectName}}\n WHERE Id IN :recordIds\n WITH USER_MODE\n ];\n\n // Make callout for each record (consider batching)\n for ({{ObjectName}} record : records) {\n makeCallout(record);\n }\n\n } catch (CalloutException e) {\n // Log callout errors\n System.debug(LoggingLevel.ERROR, 'Callout failed: ' + e.getMessage());\n // Consider: Create error log record, retry logic, notification\n } catch (Exception e) {\n System.debug(LoggingLevel.ERROR, 'Error: ' + e.getMessage());\n }\n }\n\n private void makeCallout({{ObjectName}} record) {\n HttpRequest req = new HttpRequest();\n req.setEndpoint('callout:{{NamedCredentialName}}/{{Endpoint}}');\n req.setMethod('POST');\n req.setHeader('Content-Type', 'application/json');\n req.setTimeout(120000);\n\n Map\u003cString, Object> payload = new Map\u003cString, Object>{\n 'id' => record.Id,\n 'name' => record.Name\n // Add more fields\n };\n req.setBody(JSON.serialize(payload));\n\n Http http = new Http();\n HttpResponse res = http.send(req);\n\n if (res.getStatusCode() >= 200 && res.getStatusCode() \u003c 300) {\n // Success - update record status if needed\n } else {\n // Handle error\n throw new CalloutException('API Error: ' + res.getStatusCode());\n }\n }\n}\n```\n\n#### Trigger Integration\n\n```apex\ntrigger OpportunityTrigger on Opportunity (after insert, after update) {\n List\u003cId> opportunityIds = new List\u003cId>();\n\n for (Opportunity opp : Trigger.new) {\n // Only sync closed-won opportunities\n if (opp.StageName == 'Closed Won') {\n opportunityIds.add(opp.Id);\n }\n }\n\n if (!opportunityIds.isEmpty()) {\n // Enqueue async callout\n System.enqueueJob(new SalesforceQueueableCallout(opportunityIds, 'SYNC'));\n }\n}\n```\n\n#### Bulkification Pattern\n\nFor high-volume scenarios, batch multiple callouts:\n\n```apex\nprivate void makeCallouts(List\u003c{{ObjectName}}> records) {\n // Batch up to 10 records per callout\n Integer BATCH_SIZE = 10;\n List\u003cMap\u003cString, Object>> batch = new List\u003cMap\u003cString, Object>>();\n\n for (Integer i = 0; i \u003c records.size(); i++) {\n batch.add(buildPayload(records[i]));\n\n if (batch.size() == BATCH_SIZE || i == records.size() - 1) {\n // Make single callout with batch\n sendBatch(batch);\n batch.clear();\n }\n }\n}\n\nprivate void sendBatch(List\u003cMap\u003cString, Object>> batch) {\n HttpRequest req = new HttpRequest();\n req.setEndpoint('callout:{{NamedCredentialName}}/batch');\n req.setMethod('POST');\n req.setHeader('Content-Type', 'application/json');\n req.setTimeout(120000);\n req.setBody(JSON.serialize(new Map\u003cString, Object>{'records' => batch}));\n\n Http http = new Http();\n HttpResponse res = http.send(req);\n // Handle response\n}\n```\n\n---\n\n### Retry Handler with Exponential Backoff\n\n**Use Case**: Handle transient failures with intelligent retry logic\n\n**Template**: `assets/callouts/callout-retry-handler.cls`\n\n**Retry Strategy**:\n- **Max Retries**: 3 attempts\n- **Backoff**: Exponential (1s, 2s, 4s)\n- **Retry on**: 5xx server errors, network timeouts\n- **Don't Retry**: 4xx client errors (bad request, auth failure)\n\n#### Implementation\n\n```apex\npublic with sharing class CalloutRetryHandler {\n\n private static final Integer MAX_RETRIES = 3;\n private static final Integer BASE_DELAY_MS = 1000; // 1 second\n\n public static HttpResponse executeWithRetry(HttpRequest request) {\n Integer retryCount = 0;\n HttpResponse response;\n\n while (retryCount \u003c MAX_RETRIES) {\n try {\n Http http = new Http();\n response = http.send(request);\n\n // Success or client error (4xx) - don't retry\n if (response.getStatusCode() \u003c 500) {\n return response;\n }\n\n // Server error (5xx) - retry with backoff\n retryCount++;\n if (retryCount \u003c MAX_RETRIES) {\n // Exponential backoff: 1s, 2s, 4s\n Integer delayMs = BASE_DELAY_MS * (Integer) Math.pow(2, retryCount - 1);\n // Note: Apex doesn't have sleep(), so we schedule retry via Queueable\n throw new RetryableException('Server error, retry ' + retryCount);\n }\n\n } catch (CalloutException e) {\n retryCount++;\n if (retryCount >= MAX_RETRIES) {\n throw e;\n }\n }\n }\n\n return response;\n }\n\n public class RetryableException extends Exception {}\n}\n```\n\n#### Queueable Retry Pattern\n\nSince Apex doesn't support `Thread.sleep()`, implement retry delays using Queueable chaining:\n\n```apex\npublic with sharing class CalloutWithRetryQueueable implements Queueable, Database.AllowsCallouts {\n\n private HttpRequest request;\n private Integer retryCount;\n private static final Integer MAX_RETRIES = 3;\n\n public CalloutWithRetryQueueable(HttpRequest req) {\n this(req, 0);\n }\n\n private CalloutWithRetryQueueable(HttpRequest req, Integer retries) {\n this.request = req;\n this.retryCount = retries;\n }\n\n public void execute(QueueableContext context) {\n try {\n Http http = new Http();\n HttpResponse res = http.send(request);\n\n if (res.getStatusCode() >= 500 && retryCount \u003c MAX_RETRIES) {\n // Server error - retry\n System.debug(LoggingLevel.WARN, 'Retry ' + (retryCount + 1) + ' for ' + request.getEndpoint());\n System.enqueueJob(new CalloutWithRetryQueueable(request, retryCount + 1));\n } else if (res.getStatusCode() >= 200 && res.getStatusCode() \u003c 300) {\n // Success\n handleSuccess(res);\n } else {\n // Client error - don't retry\n handleError(res);\n }\n\n } catch (CalloutException e) {\n if (retryCount \u003c MAX_RETRIES) {\n System.enqueueJob(new CalloutWithRetryQueueable(request, retryCount + 1));\n } else {\n throw e;\n }\n }\n }\n\n private void handleSuccess(HttpResponse res) {\n // Process successful response\n System.debug('Callout succeeded: ' + res.getBody());\n }\n\n private void handleError(HttpResponse res) {\n // Log error\n System.debug(LoggingLevel.ERROR, 'Callout error: ' + res.getStatusCode() + ' - ' + res.getBody());\n }\n}\n```\n\n#### Idempotency Considerations\n\nWhen implementing retries, ensure API operations are idempotent:\n\n```apex\n// BAD: Non-idempotent (creates new record on each retry)\nPOST /api/orders { \"item\": \"Widget\", \"quantity\": 1 }\n\n// GOOD: Idempotent (uses idempotency key)\nPOST /api/orders\nHeaders: Idempotency-Key: {{recordId}}-{{timestamp}}\n{ \"item\": \"Widget\", \"quantity\": 1 }\n```\n\n---\n\n## SOAP Callout Patterns\n\n### WSDL2Apex Process\n\nSOAP integrations in Salesforce use WSDL2Apex to auto-generate Apex classes from WSDL files.\n\n#### Step-by-Step Process\n\n**Step 1: Generate Apex from WSDL**\n\n1. Navigate to **Setup** → **Apex Classes** → **Generate from WSDL**\n2. Upload WSDL file or provide URL\n3. Salesforce parses WSDL and generates:\n - Stub class (contains service endpoint and operations)\n - Request/Response classes (for each operation)\n - Type classes (for complex data types)\n\n**Step 2: Configure Named Credential**\n\nCreate a Named Credential for the SOAP endpoint:\n\n```xml\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\u003cNamedCredential xmlns=\"http://soap.sforce.com/2006/04/metadata\">\n \u003clabel>{{ServiceName}} SOAP\u003c/label>\n \u003cendpoint>https://api.example.com/soap/v1\u003c/endpoint>\n \u003cprincipalType>NamedUser\u003c/principalType>\n \u003cprotocol>Password\u003c/protocol>\n \u003cusername>{{Username}}\u003c/username>\n \u003cpassword>{{Password}}\u003c/password>\n\u003c/NamedCredential>\n```\n\n**Step 3: Use Generated Classes**\n\nGenerated classes follow this naming pattern:\n- **Stub Class**: `{{WsdlNamespace}}.{{ServiceName}}`\n- **Port Type**: `{{WsdlNamespace}}.{{PortTypeName}}`\n- **Operations**: Methods on the port type class\n\n---\n\n### SOAP Service Implementation\n\n**Template**: `assets/soap/soap-callout-service.cls`\n\n#### Basic SOAP Callout\n\n```apex\npublic with sharing class {{ServiceName}}SoapService {\n\n public static {{ResponseType}} callService({{RequestType}} request) {\n try {\n // Generated stub class\n {{WsdlGeneratedClass}}.{{PortType}} stub = new {{WsdlGeneratedClass}}.{{PortType}}();\n\n // Set endpoint (use Named Credential if possible)\n stub.endpoint_x = 'callout:{{NamedCredentialName}}';\n\n // Set timeout\n stub.timeout_x = 120000;\n\n // Make the call\n return stub.{{OperationName}}(request);\n\n } catch (Exception e) {\n System.debug(LoggingLevel.ERROR, 'SOAP Callout Error: ' + e.getMessage());\n throw new CalloutException('SOAP service error: ' + e.getMessage());\n }\n }\n}\n```\n\n#### Example: Weather Service SOAP Callout\n\n**WSDL**: `http://www.webservicex.net/globalweather.asmx?WSDL`\n\n**Generated Classes**:\n- `GlobalWeatherSoap`\n- `GetWeatherRequest`\n- `GetWeatherResponse`\n\n**Service Implementation**:\n\n```apex\npublic with sharing class WeatherService {\n\n public static String getWeather(String city, String country) {\n try {\n // Initialize SOAP stub\n GlobalWeatherSoap.GlobalWeatherSoap stub =\n new GlobalWeatherSoap.GlobalWeatherSoap();\n\n // Configure endpoint and timeout\n stub.endpoint_x = 'callout:GlobalWeather_NC';\n stub.timeout_x = 120000;\n\n // Build request\n GlobalWeatherSoap.GetWeatherRequest req =\n new GlobalWeatherSoap.GetWeatherRequest();\n req.CityName = city;\n req.CountryName = country;\n\n // Make callout\n GlobalWeatherSoap.GetWeatherResponse res = stub.GetWeather(req);\n\n return res.GetWeatherResult;\n\n } catch (System.CalloutException e) {\n System.debug(LoggingLevel.ERROR, 'Weather API callout failed: ' + e.getMessage());\n return null;\n }\n }\n}\n```\n\n#### SOAP Headers and Authentication\n\nFor SOAP services requiring custom headers (e.g., WS-Security):\n\n```apex\npublic with sharing class SecureSoapService {\n\n public static void callServiceWithAuth(String username, String password) {\n // Generated stub\n MyService.MyServiceSoap stub = new MyService.MyServiceSoap();\n\n // Set endpoint\n stub.endpoint_x = 'callout:MyService_NC';\n stub.timeout_x = 120000;\n\n // Set SOAP headers for authentication\n stub.inputHttpHeaders_x = new Map\u003cString, String>{\n 'SOAPAction' => 'http://tempuri.org/IMyService/MyOperation',\n 'Authorization' => 'Basic ' + EncodingUtil.base64Encode(\n Blob.valueOf(username + ':' + password)\n )\n };\n\n // Make request\n MyService.MyRequest req = new MyService.MyRequest();\n MyService.MyResponse res = stub.MyOperation(req);\n }\n}\n```\n\n#### SOAP Fault Handling\n\n```apex\npublic with sharing class RobustSoapService {\n\n public static Object callWithFaultHandling() {\n try {\n MyService.MyServiceSoap stub = new MyService.MyServiceSoap();\n stub.endpoint_x = 'callout:MyService_NC';\n stub.timeout_x = 120000;\n\n MyService.MyRequest req = new MyService.MyRequest();\n return stub.MyOperation(req);\n\n } catch (System.CalloutException e) {\n // Parse SOAP fault\n String errorMessage = e.getMessage();\n\n if (errorMessage.contains('faultcode')) {\n // SOAP Fault occurred\n System.debug(LoggingLevel.ERROR, 'SOAP Fault: ' + errorMessage);\n // Extract fault details using XML parsing if needed\n } else {\n // Network/HTTP error\n System.debug(LoggingLevel.ERROR, 'Callout error: ' + errorMessage);\n }\n\n throw e;\n }\n }\n}\n```\n\n#### Async SOAP Callout (Queueable)\n\nFor SOAP callouts triggered from DML:\n\n```apex\npublic with sharing class SoapQueueableCallout implements Queueable, Database.AllowsCallouts {\n\n private List\u003cId> recordIds;\n\n public SoapQueueableCallout(List\u003cId> recordIds) {\n this.recordIds = recordIds;\n }\n\n public void execute(QueueableContext context) {\n try {\n // Query records\n List\u003cAccount> accounts = [\n SELECT Id, Name, BillingCity, BillingCountry\n FROM Account\n WHERE Id IN :recordIds\n WITH USER_MODE\n ];\n\n // Initialize SOAP stub\n GlobalWeatherSoap.GlobalWeatherSoap stub =\n new GlobalWeatherSoap.GlobalWeatherSoap();\n stub.endpoint_x = 'callout:GlobalWeather_NC';\n stub.timeout_x = 120000;\n\n // Process each record\n for (Account acc : accounts) {\n GlobalWeatherSoap.GetWeatherRequest req =\n new GlobalWeatherSoap.GetWeatherRequest();\n req.CityName = acc.BillingCity;\n req.CountryName = acc.BillingCountry;\n\n GlobalWeatherSoap.GetWeatherResponse res = stub.GetWeather(req);\n\n // Update account with weather data\n acc.Weather_Data__c = res.GetWeatherResult;\n }\n\n // Update records\n update as user accounts;\n\n } catch (Exception e) {\n System.debug(LoggingLevel.ERROR, 'SOAP Queueable error: ' + e.getMessage());\n }\n }\n}\n```\n\n---\n\n## Best Practices\n\n### Callout Governor Limits\n\n| Limit | Value | Notes |\n|-------|-------|-------|\n| Max callouts per transaction | 100 | Batch multiple requests if possible |\n| Max timeout per callout | 120 seconds | Set explicitly with `setTimeout()` |\n| Max total timeout per transaction | 120 seconds | All callouts combined |\n| Max heap size | 6 MB (sync) / 12 MB (async) | Large responses consume heap |\n\n### Security Checklist\n\n- Use Named Credentials for authentication (NEVER hardcode credentials)\n- Minimize OAuth scopes to least privilege\n- Use Certificate-based auth for high-security integrations\n- Validate SSL certificates (don't disable SSL verification)\n- Sanitize user input before including in callout payloads\n- Log callout errors without exposing sensitive data\n\n### Error Handling Patterns\n\n```apex\ntry {\n HttpResponse res = makeCallout();\n handleResponse(res);\n} catch (System.CalloutException e) {\n // Network error, timeout, SSL error\n logError('Callout failed', e);\n} catch (JSONException e) {\n // Malformed JSON response\n logError('JSON parsing failed', e);\n} catch (Exception e) {\n // Unexpected error\n logError('Unexpected error', e);\n}\n```\n\n### Testing Callouts\n\nUse `Test.setMock()` to mock HTTP responses:\n\n```apex\n@isTest\nprivate class MyCalloutTest {\n\n @isTest\n static void testSuccessfulCallout() {\n // Set mock response\n Test.setMock(HttpCalloutMock.class, new MockHttpResponseGenerator());\n\n Test.startTest();\n Map\u003cString, Object> result = MyService.get('/customers/123');\n Test.stopTest();\n\n System.assertEquals('[email protected]', result.get('email'));\n }\n\n // Mock class\n private class MockHttpResponseGenerator implements HttpCalloutMock {\n public HttpResponse respond(HttpRequest req) {\n HttpResponse res = new HttpResponse();\n res.setHeader('Content-Type', 'application/json');\n res.setBody('{\"email\":\"[email protected]\"}');\n res.setStatusCode(200);\n return res;\n }\n }\n}\n```\n\n---\n\n## Related Resources\n\n- [Event Patterns](./event-patterns.md) - Platform Events and Change Data Capture\n- [Main Skill Documentation](../SKILL.md) - sf-integration overview\n- [Named Credentials Templates](../assets/named-credentials/) - Authentication templates\n- [Callout Templates](../assets/callouts/) - Ready-to-use callout patterns\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":22053,"content_sha256":"0f90de0e09372b5d79eb0e8ff9c6aaec9b88916565969aa0a4f3b9c5c5f0857a"},{"filename":"references/cdc-guide.md","content":"\u003c!-- Parent: sf-integration/SKILL.md -->\n# Change Data Capture (CDC) Guide\n\n## Overview\n\nChange Data Capture publishes change events for Salesforce records, enabling near real-time data synchronization with external systems.\n\n## How CDC Works\n\n```\n┌─────────────────────────────────────────────────────────────────┐\n│ CHANGE DATA CAPTURE FLOW │\n├─────────────────────────────────────────────────────────────────┤\n│ │\n│ 1. Record Created/Updated/Deleted in Salesforce │\n│ ↓ │\n│ 2. Salesforce generates Change Event │\n│ ↓ │\n│ 3. Event published to {{Object}}ChangeEvent channel │\n│ ↓ │\n│ 4. Apex Trigger or External Subscriber receives event │\n│ ↓ │\n│ 5. Process event (sync, audit, notify) │\n│ │\n└─────────────────────────────────────────────────────────────────┘\n```\n\n## CDC vs Platform Events\n\n| Feature | CDC | Platform Events |\n|---------|-----|-----------------|\n| Trigger | Automatic on DML | Manual publish |\n| Schema | Predefined (object fields) | Custom defined |\n| Use Case | Data sync/replication | Custom messaging |\n| Control | Limited | Full control |\n\n## Enabling CDC\n\n### Via Setup\n\n1. Go to **Setup → Integrations → Change Data Capture**\n2. Select objects to enable\n3. Save\n\n### Supported Objects\n\n- Most Standard objects (Account, Contact, Opportunity, etc.)\n- Custom objects\n- Some system objects (User, Group, etc.)\n\n## Event Channel Names\n\n| Object Type | Channel Name |\n|-------------|--------------|\n| Standard | `AccountChangeEvent`, `ContactChangeEvent` |\n| Custom | `My_Object__ChangeEvent` (append ChangeEvent) |\n\n## Change Event Structure\n\n### ChangeEventHeader\n\n```apex\nEventBus.ChangeEventHeader header = event.ChangeEventHeader;\n\n// Available methods\nheader.getChangeType(); // CREATE, UPDATE, DELETE, UNDELETE\nheader.getChangedFields(); // List of changed field API names\nheader.getRecordIds(); // Affected record IDs\nheader.getEntityName(); // Object API name\nheader.getCommitNumber(); // Transaction sequence\nheader.getCommitTimestamp(); // When change occurred\nheader.getTransactionKey(); // Unique transaction ID\n```\n\n### Change Types\n\n| Type | Description | Event Contains |\n|------|-------------|----------------|\n| CREATE | New record | All field values |\n| UPDATE | Record modified | Changed field values only |\n| DELETE | Record deleted | Record IDs only |\n| UNDELETE | Restored from bin | All field values |\n| GAP_* | Events missed | Affected record IDs |\n| GAP_OVERFLOW | Too many changes | Entity name |\n\n### Field Values\n\n- **Changed fields**: Contain new values\n- **Unchanged fields**: Null\n- Use `getChangedFields()` to know what changed\n\n## Subscribing to CDC\n\n### Apex Trigger\n\n```apex\ntrigger AccountCDC on AccountChangeEvent (after insert) {\n for (AccountChangeEvent event : Trigger.new) {\n EventBus.ChangeEventHeader header = event.ChangeEventHeader;\n\n String changeType = header.getChangeType();\n List\u003cString> changedFields = header.getChangedFields();\n List\u003cString> recordIds = header.getRecordIds();\n\n switch on changeType {\n when 'CREATE' {\n // Handle new records\n }\n when 'UPDATE' {\n // Handle updates\n }\n when 'DELETE' {\n // Handle deletions\n }\n }\n }\n\n // Set checkpoint\n EventBus.TriggerContext.currentContext().setResumeCheckpoint(\n Trigger.new[Trigger.new.size()-1].ReplayId\n );\n}\n```\n\n### External (CometD)\n\n```\nChannel: /data/AccountChangeEvent\n```\n\n## Handling Specific Changes\n\n### Filter by Changed Fields\n\n```apex\n// Only process if important fields changed\nSet\u003cString> importantFields = new Set\u003cString>{'Status__c', 'Amount__c'};\nList\u003cString> changedFields = header.getChangedFields();\n\nBoolean relevant = false;\nfor (String field : changedFields) {\n if (importantFields.contains(field)) {\n relevant = true;\n break;\n }\n}\n\nif (!relevant) return;\n```\n\n### Get New Values\n\n```apex\n// Access field values from event (UPDATE: only changed fields have values)\nif (changedFields.contains('Status__c')) {\n String newStatus = event.Status__c;\n // Process status change\n}\n```\n\n## Gap Events\n\nGap events indicate missed events:\n\n### Types\n\n| Event | Meaning | Action |\n|-------|---------|--------|\n| GAP_CREATE | Missed creates | Query and sync records |\n| GAP_UPDATE | Missed updates | Query current state |\n| GAP_DELETE | Missed deletes | Reconcile with source |\n| GAP_UNDELETE | Missed restores | Query and sync |\n| GAP_OVERFLOW | Too many changes | Full sync required |\n\n### Handling Gaps\n\n```apex\nwhen 'GAP_CREATE', 'GAP_UPDATE', 'GAP_DELETE', 'GAP_UNDELETE' {\n // Query current state and sync\n List\u003cAccount> records = [\n SELECT Id, Name, Status__c\n FROM Account\n WHERE Id IN :header.getRecordIds()\n ];\n syncToExternalSystem(records);\n}\nwhen 'GAP_OVERFLOW' {\n // Trigger full sync batch job\n Database.executeBatch(new FullSyncBatch());\n}\n```\n\n## Replay and Durability\n\n### Retention\n\nCDC events retained for **3 days** (72 hours).\n\n### Replay ID\n\nEach event has unique ReplayId for tracking:\n\n```apex\nString replayId = event.ReplayId;\n```\n\n### Resume Checkpoint\n\nCritical for durability:\n\n```apex\n// Always set checkpoint at end of trigger\nEventBus.TriggerContext.currentContext().setResumeCheckpoint(lastReplayId);\n```\n\nIf trigger fails after checkpoint, processing resumes from that point.\n\n## External Sync Pattern\n\n```apex\npublic class AccountCDCHandler {\n\n public static void syncToExternal(AccountChangeEvent event) {\n EventBus.ChangeEventHeader header = event.ChangeEventHeader;\n\n Map\u003cString, Object> payload = new Map\u003cString, Object>{\n 'recordIds' => header.getRecordIds(),\n 'operation' => header.getChangeType(),\n 'timestamp' => header.getCommitTimestamp(),\n 'changedFields' => header.getChangedFields()\n };\n\n // Add field values for CREATE/UPDATE\n if (header.getChangeType() != 'DELETE') {\n payload.put('name', event.Name);\n payload.put('accountNumber', event.AccountNumber);\n // Add relevant fields\n }\n\n // Queue async callout\n System.enqueueJob(new ExternalSyncJob(payload));\n }\n}\n```\n\n## Best Practices\n\n### DO\n\n1. **Set resume checkpoint** in every trigger\n2. **Filter by relevant fields** to reduce noise\n3. **Handle all change types** including GAPs\n4. **Process idempotently** (events may replay)\n5. **Use async** for external callouts\n6. **Log changes** for debugging\n\n### DON'T\n\n1. **Don't throw exceptions** - catch and log\n2. **Don't ignore GAP events** - they indicate data loss\n3. **Don't assume single record** - batch DML creates multi-record events\n4. **Don't block on external calls** - use Queueable\n\n## Monitoring\n\n### Event Delivery\n\nCheck for failures in:\n- Setup → Platform Events → Monitor\n- Debug logs for triggers\n\n### Common Issues\n\n| Issue | Cause | Solution |\n|-------|-------|----------|\n| Missing events | No CDC enabled | Enable in Setup |\n| Duplicate processing | No idempotency | Check transactionKey |\n| GAP events | Processing too slow | Optimize trigger, scale out |\n| Timeout | Heavy processing | Move to async |\n\n## Limits\n\n| Limit | Value |\n|-------|-------|\n| Objects per org | 100 |\n| Events per 15 minutes | Varies by edition |\n| Event retention | 3 days (72 hours) |\n| Replay window | 3 days |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":8387,"content_sha256":"d15d0f4a08f9b07d1c135cb362a95da7e1fccbd4907f4515d23e0b3212c3d14a"},{"filename":"references/cli-reference.md","content":"\u003c!-- Parent: sf-integration/SKILL.md -->\n\n# CLI Commands & Helper Scripts\n\n## Named Credentials\n\n```bash\n# List Named Credentials\nsf org list metadata --metadata-type NamedCredential --target-org {{alias}}\n\n# Deploy Named Credential\nsf project deploy start --metadata NamedCredential:{{Name}} --target-org {{alias}}\n\n# Retrieve Named Credential\nsf project retrieve start --metadata NamedCredential:{{Name}} --target-org {{alias}}\n```\n\n## External Services\n\n```bash\n# List External Service Registrations\nsf org list metadata --metadata-type ExternalServiceRegistration --target-org {{alias}}\n\n# Deploy External Service\nsf project deploy start --metadata ExternalServiceRegistration:{{Name}} --target-org {{alias}}\n```\n\n## Platform Events\n\n```bash\n# List Platform Events\nsf org list metadata --metadata-type CustomObject --target-org {{alias}} | grep \"__e\"\n\n# Deploy Platform Event\nsf project deploy start --metadata CustomObject:{{EventName}}__e --target-org {{alias}}\n```\n\n## API Requests (Beta)\n\n```bash\n# REST API request\nsf api request rest /services/data/v66.0/sobjects/Account/describe --target-org {{alias}}\n\n# REST with POST body\nsf api request rest /services/data/v66.0/sobjects/Account --method POST \\\n --body '{\"Name\":\"Test Account\"}' --target-org {{alias}}\n\n# GraphQL query\nsf api request graphql --body '{\"query\":\"{ uiapi { query { Account { edges { node { Name { value } } } } } } }\"}' --target-org {{alias}}\n```\n\n> **[Beta]** These commands simplify API exploration. For production, use Named Credentials and Apex callouts.\n\n---\n\n## Helper Scripts\n\nsf-integration includes automation scripts to configure credentials without manual UI steps.\n\n### Available Scripts\n\n| Script | Purpose | Usage |\n|--------|---------|-------|\n| `configure-named-credential.sh` | Set API keys via ConnectApi (Enhanced NC) | `./scripts/configure-named-credential.sh \u003corg-alias>` |\n| `set-api-credential.sh` | Store keys in Custom Settings (legacy) | `./scripts/set-api-credential.sh \u003cname> - \u003corg-alias>` |\n\n### Auto-Run Behavior\n\n| File Pattern | Suggested Action |\n|--------------|------------------|\n| `*.namedCredential-meta.xml` | Run `configure-named-credential.sh` |\n| `*.externalCredential-meta.xml` | Run `configure-named-credential.sh` |\n| `*.cspTrustedSite-meta.xml` | Deploy endpoint security |\n\n### Example Workflow\n\n```bash\n# 1. Deploy metadata first\nsf project deploy start --metadata ExternalCredential:WeatherAPI \\\n --metadata NamedCredential:WeatherAPI \\\n --target-org MyOrg\n\n# 2. Run automation script\n./scripts/configure-named-credential.sh MyOrg\n# Enter API key when prompted (secure, hidden input)\n```\n\n### Prerequisites\n\n- **Salesforce CLI v2+**: `sf` command available\n- **Authenticated org**: `sf org login web -a \u003calias>`\n- **Deployed metadata**: External Credential and Named Credential deployed\n\nSee [references/named-credentials-automation.md](../references/named-credentials-automation.md) for complete guide.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2936,"content_sha256":"c86e410c06c8d6766f8d59f4ee0617f01d8b577b23a24ecb4d59e707e6ae87fd"},{"filename":"references/event-driven-architecture-guide.md","content":"\u003c!-- Parent: sf-integration/SKILL.md -->\n# Event-Driven Architecture Guide\n\n> **Source**: Salesforce Architect Decision Guides — Event-Driven Architecture, Async Processing\n> **Related**: [event-patterns.md](./event-patterns.md) | [callout-patterns.md](./callout-patterns.md)\n\n---\n\n## Overview\n\nEvent-Driven Architecture (EDA) decouples producers from consumers, enabling scalable, resilient integrations. Salesforce supports multiple event mechanisms — choosing the right one depends on volume, latency, and consumer location.\n\n---\n\n## 5 Core EDA Patterns\n\n### Pattern Comparison Matrix\n\n| Pattern | Description | Salesforce Implementation | Best For |\n|---------|-------------|--------------------------|----------|\n| **Pub/Sub** | Publisher emits, multiple subscribers consume | Platform Events + Pub/Sub API | Multi-consumer notifications, cross-system sync |\n| **Fanout** | One event → multiple independent consumers | Platform Events + multiple subscribers | Parallel processing, diverse downstream systems |\n| **Passed Messages** | Event carries data payload for consumer processing | Platform Events with rich fields | External systems needing full context in message |\n| **Streaming** | Continuous data feed for real-time consumers | CDC + Pub/Sub API | Data replication, real-time dashboards |\n| **Queueing** | Ordered, guaranteed-delivery message processing | Platform Events (High-Volume) with checkpoints | Sequential processing, backpressure handling |\n\n### When to Use Each\n\n**Pub/Sub**: Default choice for most event-driven integrations. Works for both internal (Apex triggers) and external (Pub/Sub API) consumers.\n\n**Fanout**: When a single business event (e.g., \"Order Placed\") needs to notify billing, shipping, analytics, and notifications simultaneously.\n\n**Passed Messages**: When the consumer needs all context in the event itself (no callback to Salesforce). Keep payloads lean — 1 MB limit.\n\n**Streaming**: For data replication to warehouses or lakes. CDC + Pub/Sub API replaces legacy Streaming API.\n\n**Queueing**: When processing order matters and you need backpressure handling. Use High-Volume Platform Events with resume checkpoints.\n\n---\n\n## Pub/Sub API (Recommended External Subscription)\n\nThe Pub/Sub API is the **recommended mechanism for external consumers** subscribing to Platform Events and CDC events. It replaces the legacy Streaming API (CometD).\n\n### Key Characteristics\n\n| Feature | Pub/Sub API | Legacy Streaming API (Deprecated) |\n|---------|-------------|----------------------------------|\n| **Protocol** | gRPC | CometD (long-polling) |\n| **Authentication** | OAuth 2.0 | Session-based |\n| **Event Types** | Platform Events, CDC, Custom Channels | PushTopic, Generic Events, Platform Events |\n| **Performance** | High throughput, binary protocol | Limited by long-polling overhead |\n| **Status** | **Current — use for all new development** | Deprecated — no new investments |\n\n### Subscription Modes\n\n- **Subscribe**: Stream events from a given replay ID forward\n- **PublishStream**: Bi-directional — publish events via gRPC (no Apex needed)\n- **ManagedSubscribe**: Salesforce manages replay state (simplest for external consumers)\n\n### External Consumer Architecture\n\n```\nSalesforce Org\n └── Platform Event / CDC Event\n └── Pub/Sub API (gRPC endpoint)\n └── External Consumer (Java, Python, Node.js, Go)\n ├── Process event\n ├── Commit replay ID\n └── Handle failures with retry\n```\n\n### LWC Subscription (Internal)\n\nFor Lightning Web Components subscribing to Platform Events, use **empApi**:\n\n```javascript\nimport { subscribe, unsubscribe, onError } from 'lightning/empApi';\n\nconst channelName = '/event/Order_Status__e';\nlet subscription = {};\n\nconnectedCallback() {\n subscribe(channelName, -1, (response) => {\n console.log('Event received:', JSON.stringify(response));\n this.handleEvent(response.data.payload);\n }).then((sub) => {\n subscription = sub;\n });\n\n onError((error) => {\n console.error('empApi error:', JSON.stringify(error));\n });\n}\n\ndisconnectedCallback() {\n unsubscribe(subscription);\n}\n```\n\n---\n\n## Event Relays to AWS EventBridge\n\nSalesforce Event Relays forward Platform Events to AWS EventBridge, enabling cloud-native event processing.\n\n### Architecture\n\n```\nSalesforce Platform Event\n └── Event Relay Definition (Metadata)\n └── AWS EventBridge Partner Event Source\n ├── AWS Lambda\n ├── AWS SQS\n ├── AWS Step Functions\n └── Any EventBridge target\n```\n\n### When to Use\n\n- AWS-native architecture needing Salesforce events\n- Complex event processing requiring AWS services (Step Functions, SQS, SNS)\n- Fan-out to multiple AWS consumers from a single Salesforce event\n- Event archival in AWS S3 or data lakes\n\n### Limitations\n\n- One-way only (Salesforce → AWS)\n- Adds latency (~seconds) compared to direct Pub/Sub API\n- Requires AWS account configuration and IAM setup\n- Platform Event limits still apply on the Salesforce side\n\n---\n\n## Apache Kafka on Heroku\n\nFor organizations needing long-retention, high-throughput event streaming beyond Platform Event limits.\n\n### Comparison with Platform Events\n\n| Feature | Platform Events | Kafka on Heroku |\n|---------|----------------|-----------------|\n| **Retention** | 24h (HV) / 72h (SV) | 1-6 weeks (configurable) |\n| **Throughput** | Millions/day (HV) | Millions/second |\n| **Consumer groups** | Limited | Unlimited |\n| **Replay** | ReplayId-based | Offset-based, topic-level |\n| **Cost** | Included / Platform Event add-on | Heroku Kafka add-on |\n\n### When to Choose Kafka\n\n- Retention > 72 hours required\n- Need multiple independent consumer groups\n- Event throughput exceeds Platform Event limits\n- Existing Kafka ecosystem in organization\n- Need topic partitioning for ordered processing\n\n### Integration Pattern\n\n```\nSalesforce → Platform Event → Apex/Flow subscriber → Heroku Kafka producer\n └── Consumer Group A (analytics)\n └── Consumer Group B (data lake)\n └── Consumer Group C (external CRM)\n```\n\n---\n\n## When NOT to Use Events\n\nEvents are not always the right choice. Prefer synchronous patterns when:\n\n| Scenario | Why Not Events | Better Alternative |\n|----------|---------------|-------------------|\n| **Need synchronous response** | Events are async — no return value | REST callout with Named Credential |\n| **Infrequent data changes** | Event infrastructure overhead not justified | Scheduled batch sync |\n| **Target system lacks event support** | Consumer can't subscribe to events | Outbound Messages or REST callout |\n| **Simple record sync** | Over-engineering for basic needs | Salesforce Connect / External Objects |\n| **Data volume \u003c 100 records/day** | Platform Event overhead unnecessary | Scheduled Flow with REST callout |\n\n---\n\n## High-Volume Outbound Pattern\n\nFor scenarios requiring high-volume data push to external systems:\n\n> **Do NOT use async Apex directly for high-volume outbound.** Apex async limits (250K daily Queueable, 250K daily @future) are shared across all org operations. Consuming them for outbound sync starves other automation.\n\n### Recommended Pattern: Middleware + Platform Events\n\n```\nSalesforce Record Change\n └── After-Save Flow / Trigger\n └── Publish Platform Event (lightweight payload)\n └── External Middleware (MuleSoft, Pub/Sub API consumer)\n ├── Enrich data (callback to Salesforce REST API if needed)\n ├── Transform to target format\n ├── Deliver to target system with retry logic\n └── Report status back via Platform Event or REST callback\n```\n\n### Benefits\n\n- **No Apex async limit consumption** — events don't count against daily limits\n- **Middleware handles retries** — exponential backoff, dead letter queues\n- **Scalable** — middleware scales independently of Salesforce\n- **Observable** — middleware provides logging, monitoring, alerting\n\n---\n\n## Monitoring Event-Driven Systems\n\n### AsyncApexJob Monitoring\n\nQuery job status for async Apex that processes events:\n\n```apex\nList\u003cAsyncApexJob> jobs = [\n SELECT Id, JobType, Status, NumberOfErrors, MethodName, CreatedDate\n FROM AsyncApexJob\n WHERE CreatedDate = TODAY\n AND Status IN ('Failed', 'Aborted')\n ORDER BY CreatedDate DESC\n LIMIT 50\n];\n```\n\n> **Polling limit**: AsyncApexJob queries are subject to SOQL limits. Max polling frequency: every 5 minutes.\n\n### Platform Event Metrics\n\n- **Setup → Platform Events → Usage**: View publish/subscribe counts\n- **EventBusSubscriber**: Query for subscriber status and position\n- **Proactive Monitoring**: Set up Flow or Apex to alert on failed event processing\n\n```apex\n// Check subscriber lag\nList\u003cEventBusSubscriber> subs = [\n SELECT Name, Position, Retries, LastError, Status\n FROM EventBusSubscriber\n WHERE Topic = 'Order_Status__e'\n];\n```\n\n### Key Metrics to Monitor\n\n| Metric | Source | Alert Threshold |\n|--------|--------|-----------------|\n| Failed events | `EventBusSubscriber.Retries` | > 3 consecutive retries |\n| Subscriber lag | `EventBusSubscriber.Position` vs latest ReplayId | Lag > 1000 events |\n| Async job failures | `AsyncApexJob.NumberOfErrors` | Any failure |\n| Event publish errors | `Database.SaveResult` in publisher | Any failure |\n| Daily event usage | Setup → Company Information → Platform Event Usage | > 80% of allocation |\n\n---\n\n## Summary: EDA Decision Tree\n\n```\nNeed real-time data sync?\n ├── YES → Is consumer external?\n │ ├── YES → Pub/Sub API + Platform Events (or CDC for record changes)\n │ └── NO → Platform Event trigger subscriber (or empApi for LWC)\n └── NO → Is volume high (>10K records/day)?\n ├── YES → Middleware + Platform Events (high-volume outbound pattern)\n └── NO → Scheduled batch sync (simplest, most maintainable)\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":10275,"content_sha256":"23039fc1fd8efaf29af87d1e581063af1d6f96261406f2bdee08e940d479fbfc"},{"filename":"references/event-patterns.md","content":"\u003c!-- Parent: sf-integration/SKILL.md -->\n# Event-Driven Integration Patterns\n\nThis document provides detailed implementation patterns for Platform Events and Change Data Capture (CDC) in Salesforce integrations.\n\n> **Parent Document**: [sf-integration/SKILL.md](../SKILL.md)\n> **Related**: [callout-patterns.md](./callout-patterns.md)\n\n---\n\n## Table of Contents\n\n- [Platform Events](#platform-events)\n - [Platform Event Definition](#platform-event-definition)\n - [Event Publisher](#event-publisher)\n - [Event Subscriber Trigger](#event-subscriber-trigger)\n - [High-Volume vs Standard-Volume Events](#high-volume-vs-standard-volume-events)\n- [Change Data Capture (CDC)](#change-data-capture-cdc)\n - [CDC Enablement](#cdc-enablement)\n - [CDC Subscriber Trigger](#cdc-subscriber-trigger)\n - [CDC Handler Service](#cdc-handler-service)\n - [Field-Specific Change Detection](#field-specific-change-detection)\n\n---\n\n## Platform Events\n\nPlatform Events enable asynchronous, event-driven communication between applications. They provide a publish-subscribe model where publishers fire events and subscribers listen for them.\n\n### Platform Event Definition\n\n**Use Case**: Asynchronous, event-driven communication\n\n**Template**: `assets/platform-events/platform-event-definition.object-meta.xml`\n\n#### Standard Volume Event\n\nBest for moderate event volumes (~2,000 events/hour):\n\n```xml\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\u003cCustomObject xmlns=\"http://soap.sforce.com/2006/04/metadata\">\n \u003cdeploymentStatus>Deployed\u003c/deploymentStatus>\n \u003ceventType>StandardVolume\u003c/eventType>\n \u003clabel>{{EventLabel}}\u003c/label>\n \u003cpluralLabel>{{EventPluralLabel}}\u003c/pluralLabel>\n \u003cpublishBehavior>PublishAfterCommit\u003c/publishBehavior>\n\n \u003cfields>\n \u003cfullName>{{FieldName}}__c\u003c/fullName>\n \u003clabel>{{FieldLabel}}\u003c/label>\n \u003ctype>Text\u003c/type>\n \u003clength>255\u003c/length>\n \u003c/fields>\n\n \u003cfields>\n \u003cfullName>RecordId__c\u003c/fullName>\n \u003clabel>Record ID\u003c/label>\n \u003ctype>Text\u003c/type>\n \u003clength>18\u003c/length>\n \u003cdescription>Salesforce record ID related to this event\u003c/description>\n \u003c/fields>\n\n \u003cfields>\n \u003cfullName>Timestamp__c\u003c/fullName>\n \u003clabel>Timestamp\u003c/label>\n \u003ctype>DateTime\u003c/type>\n \u003cdescription>When the event was triggered\u003c/description>\n \u003c/fields>\n\u003c/CustomObject>\n```\n\n#### High-Volume Event\n\nBest for millions of events per day:\n\n```xml\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\u003cCustomObject xmlns=\"http://soap.sforce.com/2006/04/metadata\">\n \u003cdeploymentStatus>Deployed\u003c/deploymentStatus>\n \u003ceventType>HighVolume\u003c/eventType>\n \u003clabel>{{EventLabel}}\u003c/label>\n \u003cpluralLabel>{{EventPluralLabel}}\u003c/pluralLabel>\n \u003cpublishBehavior>PublishAfterCommit\u003c/publishBehavior>\n\n \u003cfields>\n \u003cfullName>{{FieldName}}__c\u003c/fullName>\n \u003clabel>{{FieldLabel}}\u003c/label>\n \u003ctype>Text\u003c/type>\n \u003clength>255\u003c/length>\n \u003c/fields>\n\u003c/CustomObject>\n```\n\n#### Key Configuration Options\n\n| Option | Values | Description |\n|--------|--------|-------------|\n| **eventType** | `StandardVolume`, `HighVolume` | Event throughput capacity |\n| **publishBehavior** | `PublishAfterCommit`, `PublishImmediately` | When events are published |\n| **deploymentStatus** | `Deployed`, `InDevelopment` | Deployment status |\n\n**PublishBehavior Details**:\n- `PublishAfterCommit`: Event published only if transaction commits (recommended)\n- `PublishImmediately`: Event published immediately, even if transaction rolls back\n\n---\n\n### Event Publisher\n\n**Template**: `assets/platform-events/event-publisher.cls`\n\n#### Bulk Event Publisher\n\n```apex\npublic with sharing class {{EventName}}Publisher {\n\n public static void publishEvents(List\u003c{{EventName}}__e> events) {\n if (events == null || events.isEmpty()) {\n return;\n }\n\n List\u003cDatabase.SaveResult> results = EventBus.publish(events);\n\n for (Integer i = 0; i \u003c results.size(); i++) {\n Database.SaveResult sr = results[i];\n if (!sr.isSuccess()) {\n for (Database.Error err : sr.getErrors()) {\n System.debug(LoggingLevel.ERROR,\n 'Event publish error: ' + err.getStatusCode() + ' - ' + err.getMessage());\n }\n }\n }\n }\n\n public static void publishSingleEvent(Map\u003cString, Object> eventData) {\n {{EventName}}__e event = new {{EventName}}__e();\n\n // Map fields from eventData\n event.{{FieldName}}__c = (String) eventData.get('{{fieldKey}}');\n event.RecordId__c = (String) eventData.get('recordId');\n event.Timestamp__c = DateTime.now();\n\n Database.SaveResult sr = EventBus.publish(event);\n if (!sr.isSuccess()) {\n throw new EventPublishException('Failed to publish event: ' + sr.getErrors());\n }\n }\n\n public class EventPublishException extends Exception {}\n}\n```\n\n#### Usage Examples\n\n**Single Event**:\n```apex\nMap\u003cString, Object> eventData = new Map\u003cString, Object>{\n 'recordId' => '001xx000003DXXXAAA',\n 'status' => 'Completed',\n 'amount' => 1000.00\n};\nOrderStatusPublisher.publishSingleEvent(eventData);\n```\n\n**Bulk Events**:\n```apex\nList\u003cOrder_Status__e> events = new List\u003cOrder_Status__e>();\n\nfor (Order order : orders) {\n Order_Status__e event = new Order_Status__e();\n event.RecordId__c = order.Id;\n event.Status__c = order.Status;\n event.Timestamp__c = DateTime.now();\n events.add(event);\n}\n\nOrderStatusPublisher.publishEvents(events);\n```\n\n#### Best Practices for Publishing\n\n1. **Batch Events**: Publish up to 2,000 events per transaction (governor limit)\n2. **Error Handling**: Always check `Database.SaveResult` for failures\n3. **Transaction Context**: Use `PublishAfterCommit` to ensure events only fire on successful transactions\n4. **Field Population**: Populate all required fields before publishing\n5. **Logging**: Log failed event publishes for debugging\n\n---\n\n### Event Subscriber Trigger\n\n**Template**: `assets/platform-events/event-subscriber-trigger.trigger`\n\n#### Standard Volume Subscriber\n\n```apex\ntrigger {{EventName}}Subscriber on {{EventName}}__e (after insert) {\n // Get replay ID for resumption\n String lastReplayId = '';\n\n for ({{EventName}}__e event : Trigger.new) {\n // Store replay ID for potential resume\n lastReplayId = event.ReplayId;\n\n try {\n // Process event\n {{EventName}}Handler.processEvent(event);\n } catch (Exception e) {\n // Log error but don't throw - allow other events to process\n System.debug(LoggingLevel.ERROR,\n 'Event processing error: ' + e.getMessage() +\n ' ReplayId: ' + event.ReplayId);\n }\n }\n}\n```\n\n#### High-Volume Subscriber (with Resume Checkpoint)\n\n```apex\ntrigger {{EventName}}Subscriber on {{EventName}}__e (after insert) {\n String lastReplayId = '';\n\n for ({{EventName}}__e event : Trigger.new) {\n lastReplayId = event.ReplayId;\n\n try {\n {{EventName}}Handler.processEvent(event);\n } catch (Exception e) {\n System.debug(LoggingLevel.ERROR,\n 'Event processing error: ' + e.getMessage() +\n ' ReplayId: ' + event.ReplayId);\n }\n }\n\n // Set resume checkpoint for high-volume events\n // Allows resuming from this point if subscriber fails\n EventBus.TriggerContext.currentContext().setResumeCheckpoint(lastReplayId);\n}\n```\n\n#### Event Handler Class\n\n```apex\npublic with sharing class {{EventName}}Handler {\n\n public static void processEvent({{EventName}}__e event) {\n // Extract event data\n String recordId = event.RecordId__c;\n String status = event.Status__c;\n DateTime timestamp = event.Timestamp__c;\n\n System.debug('Processing event - RecordId: ' + recordId +\n ', Status: ' + status +\n ', ReplayId: ' + event.ReplayId);\n\n // Business logic\n updateRelatedRecords(recordId, status);\n syncToExternalSystem(recordId, status);\n }\n\n private static void updateRelatedRecords(String recordId, String status) {\n // Update related records based on event\n List\u003cTask> tasks = [\n SELECT Id, Status\n FROM Task\n WHERE WhatId = :recordId\n WITH USER_MODE\n ];\n\n for (Task t : tasks) {\n t.Status = status;\n }\n\n update as user tasks;\n }\n\n private static void syncToExternalSystem(String recordId, String status) {\n // Queue async callout\n Map\u003cString, Object> payload = new Map\u003cString, Object>{\n 'recordId' => recordId,\n 'status' => status\n };\n System.enqueueJob(new ExternalSyncQueueable(payload));\n }\n}\n```\n\n---\n\n\u003ca id=\"high-volume-vs-standard-volume-events\">\u003c/a>\n\n### High-Volume vs Standard-Volume Events\n\n| Feature | Standard-Volume | High-Volume |\n|---------|----------------|-------------|\n| **Throughput** | ~2,000 events/hour | Millions/day |\n| **Delivery** | Exactly-once | At-least-once (may deliver duplicates) |\n| **Retention** | 3 days (72 hours) | 24 hours |\n| **Replay** | ReplayId from last 3 days | ReplayId from last 24 hours |\n| **Use Case** | Low-volume integrations, workflows | IoT, real-time analytics, high-traffic |\n| **Cost** | Included in platform | Additional licensing |\n\n#### When to Use High-Volume Events\n\n- **IoT Data**: Sensor data, device telemetry\n- **Real-Time Analytics**: Clickstream, user behavior tracking\n- **High-Traffic Systems**: E-commerce order processing, stock updates\n- **Event Sourcing**: Append-only event logs\n\n#### When to Use Standard-Volume Events\n\n- **Business Workflows**: Order status updates, approval processes\n- **Integration Events**: Sync to external CRM, ERP systems\n- **Notifications**: Email triggers, Slack notifications\n- **Audit Trails**: Record-level change notifications\n\n---\n\n## Change Data Capture (CDC)\n\nChange Data Capture publishes change events whenever records are created, updated, deleted, or undeleted in Salesforce. CDC events are published automatically—no custom code needed.\n\n### CDC Enablement\n\n#### Enable via Setup UI\n\n1. Navigate to **Setup** → **Integrations** → **Change Data Capture**\n2. Select objects to enable (Standard or Custom)\n3. Save\n\n#### Enable via Metadata API\n\n**File**: `force-app/main/default/changeDataCaptures/AccountChangeEvent.cdc-meta.xml`\n\n```xml\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\u003cChangeDataCapture xmlns=\"http://soap.sforce.com/2006/04/metadata\">\n \u003centityName>Account\u003c/entityName>\n \u003cisEnabled>true\u003c/isEnabled>\n\u003c/ChangeDataCapture>\n```\n\n#### Channel Naming Convention\n\nCDC channels follow the pattern: `{{ObjectAPIName}}ChangeEvent`\n\n**Examples**:\n- `AccountChangeEvent` (Standard object)\n- `Order__ChangeEvent` (Custom object)\n- `OpportunityChangeEvent`\n- `Contact_Request__ChangeEvent`\n\n---\n\n### CDC Subscriber Trigger\n\n**Template**: `assets/cdc/cdc-subscriber-trigger.trigger`\n\n#### Basic CDC Subscriber\n\n```apex\ntrigger {{ObjectName}}CDCSubscriber on {{ObjectName}}ChangeEvent (after insert) {\n\n for ({{ObjectName}}ChangeEvent event : Trigger.new) {\n // Get change event header\n EventBus.ChangeEventHeader header = event.ChangeEventHeader;\n\n String changeType = header.getChangeType();\n List\u003cString> changedFields = header.getChangedFields();\n String recordId = header.getRecordIds()[0]; // First record ID\n\n System.debug('CDC Event - Type: ' + changeType +\n ', RecordId: ' + recordId +\n ', Changed Fields: ' + changedFields);\n\n // Route based on change type\n switch on changeType {\n when 'CREATE' {\n // Handle new record\n {{ObjectName}}CDCHandler.handleCreate(event);\n }\n when 'UPDATE' {\n // Handle update\n {{ObjectName}}CDCHandler.handleUpdate(event, changedFields);\n }\n when 'DELETE' {\n // Handle delete\n {{ObjectName}}CDCHandler.handleDelete(recordId);\n }\n when 'UNDELETE' {\n // Handle undelete\n {{ObjectName}}CDCHandler.handleUndelete(event);\n }\n }\n }\n}\n```\n\n#### ChangeEventHeader Fields\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `getChangeType()` | String | CREATE, UPDATE, DELETE, UNDELETE |\n| `getRecordIds()` | List\u003cString> | Record IDs affected (usually 1, up to 5 for related changes) |\n| `getChangedFields()` | List\u003cString> | Field API names that changed (UPDATE only) |\n| `getCommitTimestamp()` | Long | Transaction commit timestamp |\n| `getCommitUser()` | String | User ID who made the change |\n| `getCommitNumber()` | Long | Monotonically increasing commit number |\n| `getEntityName()` | String | Object API name |\n\n---\n\n### CDC Handler Service\n\n**Template**: `assets/cdc/cdc-handler.cls`\n\n```apex\npublic with sharing class {{ObjectName}}CDCHandler {\n\n public static void handleCreate({{ObjectName}}ChangeEvent event) {\n // Sync to external system on create\n Map\u003cString, Object> payload = buildPayload(event);\n System.enqueueJob(new ExternalSystemSyncQueueable(payload, 'CREATE'));\n }\n\n public static void handleUpdate({{ObjectName}}ChangeEvent event, List\u003cString> changedFields) {\n // Only sync if relevant fields changed\n Set\u003cString> fieldsToWatch = new Set\u003cString>{'Name', 'Status__c', 'Amount__c'};\n\n Boolean relevantChange = false;\n for (String field : changedFields) {\n if (fieldsToWatch.contains(field)) {\n relevantChange = true;\n break;\n }\n }\n\n if (relevantChange) {\n Map\u003cString, Object> payload = buildPayload(event);\n payload.put('changedFields', changedFields);\n System.enqueueJob(new ExternalSystemSyncQueueable(payload, 'UPDATE'));\n }\n }\n\n public static void handleDelete(String recordId) {\n Map\u003cString, Object> payload = new Map\u003cString, Object>{'recordId' => recordId};\n System.enqueueJob(new ExternalSystemSyncQueueable(payload, 'DELETE'));\n }\n\n public static void handleUndelete({{ObjectName}}ChangeEvent event) {\n handleCreate(event); // Treat undelete like create\n }\n\n private static Map\u003cString, Object> buildPayload({{ObjectName}}ChangeEvent event) {\n return new Map\u003cString, Object>{\n 'recordId' => event.ChangeEventHeader.getRecordIds()[0],\n 'commitTimestamp' => event.ChangeEventHeader.getCommitTimestamp(),\n 'commitUser' => event.ChangeEventHeader.getCommitUser(),\n // Add event field values\n 'name' => event.Name,\n 'status' => event.Status__c\n // Add more fields\n };\n }\n}\n```\n\n---\n\n\u003ca id=\"field-specific-change-detection\">\u003c/a>\n\n### Field-Specific Change Detection\n\n#### Filtering by Changed Fields\n\n```apex\npublic static void handleUpdate(AccountChangeEvent event, List\u003cString> changedFields) {\n // Only process if billing address changed\n Set\u003cString> billingFields = new Set\u003cString>{\n 'BillingStreet',\n 'BillingCity',\n 'BillingState',\n 'BillingPostalCode',\n 'BillingCountry'\n };\n\n Boolean billingChanged = false;\n for (String field : changedFields) {\n if (billingFields.contains(field)) {\n billingChanged = true;\n break;\n }\n }\n\n if (billingChanged) {\n updateShippingPartner(event);\n }\n}\n```\n\n#### Multi-Field Change Logic\n\n```apex\npublic static void handleUpdate(OpportunityChangeEvent event, List\u003cString> changedFields) {\n Set\u003cString> changedFieldSet = new Set\u003cString>(changedFields);\n\n // Check if stage AND amount both changed\n if (changedFieldSet.contains('StageName') && changedFieldSet.contains('Amount')) {\n // Alert sales ops about significant deal change\n sendAlert('Deal stage and amount changed', event);\n }\n\n // Check if close date moved backward\n if (changedFieldSet.contains('CloseDate')) {\n checkCloseDateRegression(event);\n }\n}\n```\n\n---\n\n## CDC vs Platform Events: When to Use Which\n\n| Use Case | Platform Events | Change Data Capture |\n|----------|----------------|---------------------|\n| **Custom business events** | **Preferred** | Not applicable |\n| **Record change notifications** | Requires custom trigger | **Automatic** (no code) |\n| **External system sync** | Both work | **CDC** (lower maintenance) |\n| **Custom event fields** | Fully customizable | Limited to object fields |\n| **Event filtering** | Filter in publisher code | Filter in subscriber code |\n| **Performance overhead** | Manual event creation | Automatic (minimal overhead) |\n\n### Decision Matrix\n\n```\n┌───────────────────────────────────────────────────────────────────────┐\n│ WHEN TO USE PLATFORM EVENTS vs CHANGE DATA CAPTURE │\n├───────────────────────────────────────────────────────────────────────┤\n│ Use PLATFORM EVENTS when: │\n│ • Custom business event (not tied to record changes) │\n│ • Event needs custom fields not on object │\n│ • Need to batch/aggregate data before publishing │\n│ • Publishing from external system to Salesforce │\n│ • Complex event logic (multi-object aggregation) │\n│ │\n│ Use CHANGE DATA CAPTURE when: │\n│ • Syncing record changes to external system │\n│ • Audit trail of all record modifications │\n│ • Real-time replication to data warehouse │\n│ • Event sourcing from Salesforce objects │\n│ • Zero-code event publishing required │\n└───────────────────────────────────────────────────────────────────────┘\n```\n\n---\n\n## Advanced Patterns\n\n### Combining CDC with Callouts\n\n```apex\npublic with sharing class AccountCDCHandler {\n\n public static void handleUpdate(AccountChangeEvent event, List\u003cString> changedFields) {\n // Extract data\n String recordId = event.ChangeEventHeader.getRecordIds()[0];\n String accountName = event.Name;\n\n // Queue async callout to external CRM\n Map\u003cString, Object> payload = new Map\u003cString, Object>{\n 'salesforceId' => recordId,\n 'name' => accountName,\n 'changedFields' => changedFields,\n 'timestamp' => event.ChangeEventHeader.getCommitTimestamp()\n };\n\n System.enqueueJob(new CRMSyncQueueable(payload));\n }\n}\n```\n\n### Event Replay with Stored ReplayId\n\n```apex\npublic with sharing class EventReplayService {\n\n @future(callout=true)\n public static void replayFromLastCheckpoint(String eventChannel) {\n // Get last stored replay ID\n Event_Checkpoint__c checkpoint = [\n SELECT ReplayId__c\n FROM Event_Checkpoint__c\n WHERE Channel__c = :eventChannel\n LIMIT 1\n ];\n\n if (checkpoint != null) {\n // Use stored replay ID to resume from last successful event\n // Note: Replay is typically done via API/streaming API, not Apex\n System.debug('Last replay ID: ' + checkpoint.ReplayId__c);\n }\n }\n\n public static void storeCheckpoint(String channel, String replayId) {\n Event_Checkpoint__c checkpoint = new Event_Checkpoint__c(\n Channel__c = channel,\n ReplayId__c = replayId,\n Last_Updated__c = DateTime.now()\n );\n upsert checkpoint Channel__c;\n }\n}\n```\n\n### Error Handling with Dead Letter Queue\n\n```apex\ntrigger OrderEventSubscriber on Order_Status__e (after insert) {\n for (Order_Status__e event : Trigger.new) {\n try {\n OrderEventHandler.processEvent(event);\n } catch (Exception e) {\n // Log to dead letter queue for manual review\n Event_Error_Log__c errorLog = new Event_Error_Log__c(\n Event_Type__c = 'Order_Status__e',\n Replay_Id__c = event.ReplayId,\n Error_Message__c = e.getMessage(),\n Event_Payload__c = JSON.serialize(event),\n Occurred_At__c = DateTime.now()\n );\n insert as user errorLog;\n }\n }\n}\n```\n\n---\n\n## Best Practices\n\n### Platform Events\n\n1. **Batch Publishing**: Publish events in batches (up to 2,000 per transaction)\n2. **Idempotent Subscribers**: Design subscribers to handle duplicate events (especially for high-volume)\n3. **ReplayId Tracking**: Store ReplayIds for resume capability\n4. **Error Isolation**: Catch exceptions in subscriber loops to process remaining events\n5. **Resume Checkpoints**: Use `setResumeCheckpoint()` for high-volume events\n\n### Change Data Capture\n\n1. **Field Filtering**: Only process relevant field changes to reduce processing overhead\n2. **Async Processing**: Use Queueable/Future for callouts or long-running operations\n3. **ChangeType Routing**: Use switch statements for different change types\n4. **RecordIds Array**: Handle multiple RecordIds (CDC can batch related changes)\n5. **Commit Metadata**: Use `getCommitTimestamp()` and `getCommitUser()` for audit trails\n\n---\n\n## Governor Limits\n\n| Limit | Value | Notes |\n|-------|-------|-------|\n| Platform Events published/transaction | 2,000 | Both Standard and High-Volume |\n| Platform Event delivery | Asynchronous | Delivered to subscribers after commit |\n| CDC events/hour (per object) | 250,000 | Auto-throttled if exceeded |\n| Event message size | 1 MB | Total size of all fields |\n| Event retention (Standard) | 3 days | ReplayId available for 72 hours |\n| Event retention (High-Volume) | 24 hours | ReplayId available for 24 hours |\n\n---\n\n## Testing Event-Driven Integrations\n\n### Platform Event Test\n\n```apex\n@isTest\nprivate class OrderEventTest {\n\n @isTest\n static void testEventPublish() {\n Test.startTest();\n\n Order_Status__e event = new Order_Status__e(\n RecordId__c = '006xx000000XXXAAA',\n Status__c = 'Completed'\n );\n Database.SaveResult sr = EventBus.publish(event);\n\n Test.stopTest();\n\n System.assert(sr.isSuccess(), 'Event should publish successfully');\n }\n\n @isTest\n static void testEventSubscriber() {\n // Subscriber triggers execute synchronously in tests\n Test.startTest();\n\n Order_Status__e event = new Order_Status__e(\n RecordId__c = '006xx000000XXXAAA',\n Status__c = 'Completed'\n );\n EventBus.publish(event);\n\n Test.stopTest();\n\n // Verify subscriber logic executed\n // (check that handler updated related records)\n }\n}\n```\n\n### CDC Test\n\n```apex\n@isTest\nprivate class AccountCDCTest {\n\n @isTest\n static void testAccountUpdate() {\n Account acc = new Account(Name = 'Test Account');\n insert acc;\n\n Test.startTest();\n\n acc.BillingCity = 'San Francisco';\n update acc;\n\n Test.stopTest();\n\n // CDC events don't fire in test context\n // Must test handler methods directly\n AccountChangeEvent mockEvent = new AccountChangeEvent();\n // Note: Can't instantiate ChangeEvent in Apex\n // Test handler logic with mock data instead\n }\n}\n```\n\n---\n\n## Pub/Sub API (Recommended for External Consumers)\n\nThe Pub/Sub API is the **recommended mechanism** for external systems subscribing to Platform Events and CDC events. It replaces the legacy Streaming API (CometD).\n\n### Why Pub/Sub API\n\n| Feature | Pub/Sub API | Legacy Streaming API |\n|---------|-------------|---------------------|\n| **Protocol** | gRPC (binary, high performance) | CometD (long-polling, HTTP overhead) |\n| **Authentication** | OAuth 2.0 | Session-based |\n| **Event Types** | Platform Events, CDC, Custom Channels | PushTopic, Generic Events |\n| **Status** | **Current** | **Deprecated — no new investments** |\n\n### Subscription Modes\n\n- **Subscribe**: Stream events from a given replay ID forward\n- **PublishStream**: Bi-directional — publish events via gRPC\n- **ManagedSubscribe**: Salesforce manages replay state (simplest)\n\n### LWC Internal Subscription\n\nFor Lightning Web Components, use the `empApi` module:\n\n```javascript\nimport { subscribe, unsubscribe, onError } from 'lightning/empApi';\n\nconnectedCallback() {\n subscribe('/event/Order_Status__e', -1, (response) => {\n this.handleEvent(response.data.payload);\n }).then((sub) => { this.subscription = sub; });\n}\n\ndisconnectedCallback() {\n unsubscribe(this.subscription);\n}\n```\n\n> **Note**: `empApi` uses CometD internally but is the supported LWC API. For external consumers, always use the gRPC-based Pub/Sub API.\n\n---\n\n## Platform Event Anti-Patterns\n\n### 1. Publishing from Trigger on Same Event Object → Infinite Loop\n\n```apex\n// ❌ WRONG: Trigger on Order_Status__e publishes Order_Status__e\ntrigger OrderStatusSubscriber on Order_Status__e (after insert) {\n for (Order_Status__e event : Trigger.new) {\n // This creates an infinite loop!\n EventBus.publish(new Order_Status__e(Status__c = 'Processed'));\n }\n}\n```\n\n**Fix**: Never publish the same event type from its own subscriber trigger. Use a different event type or update a record instead.\n\n### 2. PublishImmediately When Data Integrity Matters\n\n```apex\n// ❌ WRONG: Event fires even if the transaction rolls back\nOrder_Status__e event = new Order_Status__e();\nevent.Status__c = 'Completed';\n// publishBehavior = PublishImmediately in event definition\nEventBus.publish(event);\n\n// If subsequent DML fails, the event was already published\n// External system thinks order is \"Completed\" but it's not\n```\n\n**Fix**: Use `PublishAfterCommit` (the default) when the event represents a state change that depends on the transaction succeeding.\n\n### 3. Using Platform Events for Synchronous-Style Flow Orchestration\n\n```\n// ❌ WRONG: Using events to simulate synchronous request/response\nFlow A → Publish \"Request\" Event → Subscriber triggers Flow B → Publish \"Response\" Event → ???\n```\n\n**Fix**: Platform Events are asynchronous and unordered. For synchronous orchestration, use Subflows, @InvocableMethod, or direct Apex calls. Events are for decoupled, fire-and-forget communication.\n\n### 4. Oversized Event Payloads\n\n> **1 MB message size limit.** Balance payload size — smaller = faster delivery, larger = fewer API calls. Include record IDs and essential context; let consumers query Salesforce for full records if needed.\n\n---\n\n## Related Resources\n\n- [Callout Patterns](./callout-patterns.md) - REST and SOAP callout implementations\n- [Event-Driven Architecture Guide](./event-driven-architecture-guide.md) - EDA patterns, Pub/Sub API deep dive, monitoring\n- [Main Skill Documentation](../SKILL.md) - sf-integration overview\n- [Platform Event Templates](../assets/platform-events/) - Event definitions and triggers\n- [CDC Templates](../assets/cdc/) - Change Data Capture triggers\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":27722,"content_sha256":"6aa1926e1fade2bc46f0a687e5f5d0a03101114d0fb81aba5431daa654d1af19"},{"filename":"references/external-services-guide.md","content":"\u003c!-- Parent: sf-integration/SKILL.md -->\n# External Services Guide\n\n## Overview\n\nExternal Services in Salesforce automatically generate Apex classes from OpenAPI (Swagger) specifications, enabling type-safe REST API integrations without writing HTTP code.\n\n## How It Works\n\n```\n┌─────────────────────────────────────────────────────────────────┐\n│ EXTERNAL SERVICE FLOW │\n├─────────────────────────────────────────────────────────────────┤\n│ │\n│ 1. OpenAPI Spec (JSON/YAML) │\n│ ↓ │\n│ 2. External Service Registration (Metadata) │\n│ ↓ │\n│ 3. Auto-generated Apex Classes │\n│ ↓ │\n│ 4. Type-safe API calls from Apex │\n│ │\n└─────────────────────────────────────────────────────────────────┘\n```\n\n## Prerequisites\n\n1. **Named Credential** configured for authentication\n2. **OpenAPI Specification** (2.0 or 3.0) for the API\n3. **API Version 48.0+** (Winter '20)\n\n## Creating External Service\n\n### Via Setup UI\n\n1. Go to **Setup → External Services**\n2. Click **New External Service**\n3. Provide name and description\n4. Select **Named Credential**\n5. Upload or paste OpenAPI spec\n6. Review operations\n7. Save\n\n### Via Metadata API\n\n```xml\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\u003cExternalServiceRegistration xmlns=\"http://soap.sforce.com/2006/04/metadata\">\n \u003clabel>Stripe API\u003c/label>\n \u003cdescription>Stripe payment processing API\u003c/description>\n \u003cnamedCredential>Stripe_API\u003c/namedCredential>\n \u003cschemaType>OpenApi3\u003c/schemaType>\n \u003cschema>\n{\n \"openapi\": \"3.0.0\",\n \"info\": { \"title\": \"Stripe API\", \"version\": \"1.0\" },\n \"paths\": { ... }\n}\n \u003c/schema>\n \u003cstatus>Complete\u003c/status>\n\u003c/ExternalServiceRegistration>\n```\n\n## Generated Classes\n\nFor an External Service named \"StripeAPI\":\n\n| Class | Purpose |\n|-------|---------|\n| `ExternalService.StripeAPI` | Main service class with operation methods |\n| `ExternalService.StripeAPI_createCustomer_Request` | Request wrapper |\n| `ExternalService.StripeAPI_createCustomer_Response` | Response wrapper |\n| `ExternalService.StripeAPI_Customer` | DTO matching schema |\n\n## Usage Patterns\n\n### Basic GET Request\n\n```apex\n// Instantiate service\nExternalService.StripeAPI stripe = new ExternalService.StripeAPI();\n\n// Call GET operation\nExternalService.StripeAPI_getCustomer_Response response =\n stripe.getCustomer('cus_ABC123');\n\n// Access response data\nString email = response.email;\n```\n\n### POST with Request Body\n\n```apex\n// Create request object\nExternalService.StripeAPI_createCustomer_Request request =\n new ExternalService.StripeAPI_createCustomer_Request();\nrequest.email = '[email protected]';\nrequest.name = 'John Doe';\n\n// Make call\nExternalService.StripeAPI_createCustomer_Response response =\n stripe.createCustomer(request);\n\n// Get created resource ID\nString customerId = response.id;\n```\n\n### Handling Nested Objects\n\n```apex\n// Access nested data\nExternalService.StripeAPI_Address address = response.address;\nString city = address.city;\nString postalCode = address.postalCode;\n```\n\n## Error Handling\n\n```apex\ntry {\n ExternalService.StripeAPI stripe = new ExternalService.StripeAPI();\n response = stripe.getCustomer('invalid_id');\n\n} catch (ExternalService.ExternalServiceException e) {\n // API returned error response\n System.debug('Status Code: ' + e.getStatusCode());\n System.debug('Error Body: ' + e.getBody());\n System.debug('Error Message: ' + e.getMessage());\n\n} catch (CalloutException e) {\n // Network/connection error\n System.debug('Connection failed: ' + e.getMessage());\n}\n```\n\n## Async Calls (Queueable)\n\nUse Queueable for calls from triggers:\n\n```apex\npublic class StripeCustomerSync implements Queueable, Database.AllowsCallouts {\n\n private Account account;\n\n public StripeCustomerSync(Account account) {\n this.account = account;\n }\n\n public void execute(QueueableContext context) {\n ExternalService.StripeAPI stripe = new ExternalService.StripeAPI();\n\n ExternalService.StripeAPI_createCustomer_Request req =\n new ExternalService.StripeAPI_createCustomer_Request();\n req.email = account.Email__c;\n req.name = account.Name;\n\n try {\n ExternalService.StripeAPI_createCustomer_Response resp =\n stripe.createCustomer(req);\n\n account.Stripe_Customer_Id__c = resp.id;\n update account;\n } catch (Exception e) {\n System.debug('Sync failed: ' + e.getMessage());\n }\n }\n}\n```\n\n## OpenAPI Schema Tips\n\n### Supported Features\n\n- GET, POST, PUT, PATCH, DELETE methods\n- Path and query parameters\n- Request and response bodies\n- JSON schema types (string, number, boolean, object, array)\n- References ($ref)\n- Basic authentication headers\n\n### Limitations\n\n- **No file uploads** (multipart/form-data limited)\n- **No WebSockets** (HTTP only)\n- **No streaming responses**\n- **Some complex schemas** may not parse\n- **Maximum schema size** limited\n\n### Schema Best Practices\n\n```json\n{\n \"openapi\": \"3.0.0\",\n \"info\": {\n \"title\": \"My API\",\n \"version\": \"1.0.0\"\n },\n \"paths\": {\n \"/customers/{id}\": {\n \"get\": {\n \"operationId\": \"getCustomer\",\n \"parameters\": [\n {\n \"name\": \"id\",\n \"in\": \"path\",\n \"required\": true,\n \"schema\": { \"type\": \"string\" }\n }\n ],\n \"responses\": {\n \"200\": {\n \"description\": \"Success\",\n \"content\": {\n \"application/json\": {\n \"schema\": { \"$ref\": \"#/components/schemas/Customer\" }\n }\n }\n }\n }\n }\n }\n },\n \"components\": {\n \"schemas\": {\n \"Customer\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": { \"type\": \"string\" },\n \"email\": { \"type\": \"string\" }\n }\n }\n }\n }\n}\n```\n\n## Updating External Service\n\nWhen API changes:\n\n1. Get updated OpenAPI spec\n2. Go to Setup → External Services\n3. Edit service\n4. Upload new schema\n5. Review changes to operations\n6. Save and validate\n7. Update calling code if signatures changed\n\n## Testing\n\n```apex\n@isTest\nprivate class StripeIntegrationTest {\n\n @isTest\n static void testCreateCustomer() {\n // Set mock\n Test.setMock(HttpCalloutMock.class, new StripeMock());\n\n Test.startTest();\n\n ExternalService.StripeAPI stripe = new ExternalService.StripeAPI();\n ExternalService.StripeAPI_createCustomer_Request req =\n new ExternalService.StripeAPI_createCustomer_Request();\n req.email = '[email protected]';\n\n ExternalService.StripeAPI_createCustomer_Response resp =\n stripe.createCustomer(req);\n\n Test.stopTest();\n\n System.assertEquals('cus_test123', resp.id);\n }\n\n private class StripeMock implements HttpCalloutMock {\n public HttpResponse respond(HttpRequest request) {\n HttpResponse response = new HttpResponse();\n response.setStatusCode(201);\n response.setBody('{\"id\": \"cus_test123\", \"email\": \"[email protected]\"}');\n return response;\n }\n }\n}\n```\n\n## Use with Agentforce\n\nExternal Services are ideal for Agent Actions:\n\n1. Create External Service from API spec\n2. Create Flow that calls External Service\n3. Reference Flow in Agent Script action\n\n```agentscript\nactions:\n lookup_customer:\n description: \"Looks up customer in payment system\"\n inputs:\n customer_email: string\n outputs:\n customer_id: string\n target: \"flow://Lookup_Stripe_Customer\"\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":8425,"content_sha256":"324c56e1e29d2e76daa4fc404ab9b3d7158fccca876a9d83544df55cabee0598"},{"filename":"references/messaging-api-v2.md","content":"\u003c!-- Parent: sf-integration/SKILL.md -->\n# Messaging API v2 Guide (MIAW)\n\nThis guide covers building custom clients for Messaging for In-App and Web (MIAW), enabling Agentforce and Service Cloud conversations outside of Salesforce.\n\n---\n\n## Overview\n\n```\n┌─────────────────────────────────────────────────────────────────────────┐\n│ MIAW CUSTOM CLIENT ARCHITECTURE │\n├─────────────────────────────────────────────────────────────────────────┤\n│ │\n│ ┌──────────────┐ ┌──────────────────┐ │\n│ │ Custom │ REST API v2 │ Salesforce │ │\n│ │ Client │◀────────────────────────────▶│ MIAW / Agent │ │\n│ │ (React/Vue) │ /iamessage/api/v2/* │ Service Cloud │ │\n│ └──────────────┘ └──────────────────────┘ │\n│ │\n│ Endpoints: │\n│ • POST /authorization JWT token exchange │\n│ • POST /conversation Start conversation │\n│ • POST /conversation/{id}/message Send message │\n│ • GET /conversation/{id}/messages Poll for messages (or SSE) │\n│ • POST /conversation/{id}/end End conversation │\n│ │\n│ Use Cases: │\n│ • Custom chat widgets on external websites │\n│ • Mobile app integrations │\n│ • Kiosk / in-store experiences │\n│ • Third-party platform integrations │\n│ │\n└─────────────────────────────────────────────────────────────────────────┘\n```\n\n---\n\n## Prerequisites\n\n### Salesforce Setup\n\n1. **Messaging for In-App and Web** license\n2. **Embedded Service Deployment** created\n3. **Agentforce** or **Omni-Channel** routing configured\n4. **Connected App** for JWT authentication\n\n### Embedded Service Deployment\n\n```\nSetup → Embedded Service Deployments → New Deployment\n├─ Type: Messaging for In-App and Web\n├─ Channel: Web\n└─ API Name: Your_Deployment_API_Name\n```\n\nGet the **Deployment ID** and **Organization ID** from the deployment settings.\n\n---\n\n## Authentication\n\n### JWT Token Exchange\n\nMIAW uses JWT bearer tokens for API authentication.\n\n```javascript\n// Server-side token generation (Node.js example)\nimport jwt from 'jsonwebtoken';\nimport fetch from 'node-fetch';\n\nasync function getAccessToken(orgId, deploymentId, privateKey) {\n // CRITICAL: OrgId must be 15-character format for JWT\n const orgId15 = orgId.substring(0, 15);\n\n const payload = {\n iss: 'YOUR_CONNECTED_APP_CLIENT_ID',\n sub: `${orgId15}`, // 15-char org ID\n aud: 'https://login.salesforce.com',\n exp: Math.floor(Date.now() / 1000) + 300, // 5 min expiry\n iat: Math.floor(Date.now() / 1000)\n };\n\n const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });\n\n // Exchange JWT for access token\n const response = await fetch(\n 'https://login.salesforce.com/services/oauth2/token',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',\n assertion: token\n })\n }\n );\n\n const data = await response.json();\n return data.access_token;\n}\n```\n\n### Authorization Endpoint\n\nExchange access token for MIAW-specific authorization:\n\n```javascript\nasync function getMessagingAuth(accessToken, orgDomain, deploymentId) {\n const response = await fetch(\n `https://${orgDomain}/iamessage/api/v2/authorization`,\n {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${accessToken}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({\n orgId: 'YOUR_ORG_ID',\n esDeveloperName: deploymentId,\n capabilitiesVersion: '1',\n platform: 'Web'\n })\n }\n );\n\n return await response.json();\n // Returns: { accessToken, context, ... }\n}\n```\n\n---\n\n## Conversation Lifecycle\n\n### Start Conversation\n\n```javascript\nasync function startConversation(messagingAuth, customerName) {\n const { accessToken, context } = messagingAuth;\n\n const response = await fetch(\n `https://${context.url}/iamessage/api/v2/conversation`,\n {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${accessToken}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({\n esDeveloperName: context.esDeveloperName,\n routingAttributes: {\n // Optional: Pre-chat fields\n customerName: customerName\n },\n // Optional: Context for agent\n contextParameters: {\n recordId: '001xx000003ABC',\n caseReason: 'Technical Support'\n }\n })\n }\n );\n\n const data = await response.json();\n return data.conversationId;\n}\n```\n\n### Send Message\n\n```javascript\nasync function sendMessage(messagingAuth, conversationId, text) {\n const { accessToken, context } = messagingAuth;\n\n const response = await fetch(\n `https://${context.url}/iamessage/api/v2/conversation/${conversationId}/message`,\n {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${accessToken}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({\n message: {\n messageType: 'StaticContentMessage',\n staticContent: {\n formatType: 'Text',\n text: text\n }\n }\n })\n }\n );\n\n return await response.json();\n}\n```\n\n### Receive Messages\n\n#### Option 1: Server-Sent Events (SSE)\n\nSSE provides real-time streaming but may not work on serverless platforms.\n\n```javascript\nfunction subscribeToMessages(messagingAuth, conversationId, onMessage) {\n const { accessToken, context } = messagingAuth;\n\n const eventSource = new EventSource(\n `https://${context.url}/iamessage/api/v2/conversation/${conversationId}/messages?stream=true`,\n {\n headers: {\n 'Authorization': `Bearer ${accessToken}`\n }\n }\n );\n\n eventSource.onmessage = (event) => {\n const message = JSON.parse(event.data);\n onMessage(message);\n };\n\n eventSource.onerror = (error) => {\n console.error('SSE error:', error);\n // Fallback to polling\n };\n\n return eventSource;\n}\n```\n\n#### Option 2: Polling (Serverless Compatible)\n\nFor Vercel, AWS Lambda, or other serverless environments:\n\n```javascript\nclass MessagePoller {\n constructor(messagingAuth, conversationId, onMessage) {\n this.messagingAuth = messagingAuth;\n this.conversationId = conversationId;\n this.onMessage = onMessage;\n this.lastMessageId = null;\n this.seenMessageIds = new Set();\n this.intervalId = null;\n }\n\n start(intervalMs = 2000) {\n this.intervalId = setInterval(() => this.poll(), intervalMs);\n this.poll(); // Immediate first poll\n }\n\n stop() {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n this.intervalId = null;\n }\n }\n\n async poll() {\n const { accessToken, context } = this.messagingAuth;\n\n try {\n const url = new URL(\n `https://${context.url}/iamessage/api/v2/conversation/${this.conversationId}/messages`\n );\n if (this.lastMessageId) {\n url.searchParams.set('after', this.lastMessageId);\n }\n\n const response = await fetch(url, {\n headers: { 'Authorization': `Bearer ${accessToken}` }\n });\n\n const data = await response.json();\n\n for (const message of data.messages || []) {\n // CRITICAL: Deduplicate messages\n if (!this.seenMessageIds.has(message.id)) {\n this.seenMessageIds.add(message.id);\n this.lastMessageId = message.id;\n this.onMessage(message);\n }\n }\n } catch (error) {\n console.error('Poll error:', error);\n }\n }\n}\n```\n\n### End Conversation\n\n```javascript\nasync function endConversation(messagingAuth, conversationId) {\n const { accessToken, context } = messagingAuth;\n\n await fetch(\n `https://${context.url}/iamessage/api/v2/conversation/${conversationId}/end`,\n {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${accessToken}`,\n 'Content-Type': 'application/json'\n }\n }\n );\n}\n```\n\n---\n\n## Message Types\n\n### Incoming Message Structure\n\n```javascript\n{\n \"id\": \"msg_abc123\",\n \"conversationId\": \"conv_xyz789\",\n \"messageType\": \"StaticContentMessage\",\n \"sender\": {\n \"role\": \"Agent\", // or \"EndUser\", \"Chatbot\"\n \"displayName\": \"Service Agent\"\n },\n \"staticContent\": {\n \"formatType\": \"Text\",\n \"text\": \"Hello! How can I help you today?\"\n },\n \"timestamp\": \"2026-01-15T10:30:00.000Z\"\n}\n```\n\n### Rich Message Types\n\n| Type | Use Case |\n|------|----------|\n| `StaticContentMessage` | Plain text |\n| `RichLinkMessage` | Clickable cards with images |\n| `ListPickerMessage` | Selection lists |\n| `QuickReplyMessage` | Suggested responses |\n| `AttachmentMessage` | File attachments |\n\n### Handling Rich Messages\n\n```javascript\nfunction renderMessage(message) {\n switch (message.messageType) {\n case 'StaticContentMessage':\n return renderText(message.staticContent.text);\n\n case 'QuickReplyMessage':\n return renderQuickReplies(message.quickReplies);\n\n case 'ListPickerMessage':\n return renderListPicker(message.listPicker);\n\n case 'RichLinkMessage':\n return renderRichLink(message.richLink);\n\n default:\n console.warn('Unknown message type:', message.messageType);\n return null;\n }\n}\n\nfunction renderQuickReplies(quickReplies) {\n return quickReplies.replies.map(reply => ({\n label: reply.title,\n value: reply.itemId,\n onClick: () => sendQuickReplySelection(reply.itemId)\n }));\n}\n```\n\n---\n\n## React Integration Example\n\n```jsx\n// ChatWidget.jsx\nimport { useState, useEffect, useRef } from 'react';\n\nexport function ChatWidget({ orgDomain, deploymentId }) {\n const [messages, setMessages] = useState([]);\n const [inputText, setInputText] = useState('');\n const [conversationId, setConversationId] = useState(null);\n const [isConnecting, setIsConnecting] = useState(false);\n const pollerRef = useRef(null);\n const authRef = useRef(null);\n\n // Initialize conversation\n const startChat = async () => {\n setIsConnecting(true);\n try {\n // Get auth from your backend\n const authResponse = await fetch('/api/messaging/auth', {\n method: 'POST',\n body: JSON.stringify({ deploymentId })\n });\n authRef.current = await authResponse.json();\n\n // Start conversation\n const convId = await startConversation(authRef.current, 'Web User');\n setConversationId(convId);\n\n // Start polling for messages\n pollerRef.current = new MessagePoller(\n authRef.current,\n convId,\n handleNewMessage\n );\n pollerRef.current.start();\n } catch (error) {\n console.error('Failed to start chat:', error);\n } finally {\n setIsConnecting(false);\n }\n };\n\n const handleNewMessage = (message) => {\n setMessages(prev => [...prev, message]);\n };\n\n const handleSend = async () => {\n if (!inputText.trim() || !conversationId) return;\n\n // Optimistic update\n const localMessage = {\n id: `local_${Date.now()}`,\n sender: { role: 'EndUser' },\n staticContent: { text: inputText },\n timestamp: new Date().toISOString()\n };\n setMessages(prev => [...prev, localMessage]);\n setInputText('');\n\n // Send to server\n await sendMessage(authRef.current, conversationId, inputText);\n };\n\n // Cleanup\n useEffect(() => {\n return () => {\n if (pollerRef.current) {\n pollerRef.current.stop();\n }\n };\n }, []);\n\n return (\n \u003cdiv className=\"chat-widget\">\n {!conversationId ? (\n \u003cbutton onClick={startChat} disabled={isConnecting}>\n {isConnecting ? 'Connecting...' : 'Start Chat'}\n \u003c/button>\n ) : (\n \u003c>\n \u003cdiv className=\"messages\">\n {messages.map(msg => (\n \u003cMessageBubble key={msg.id} message={msg} />\n ))}\n \u003c/div>\n \u003cinput\n value={inputText}\n onChange={(e) => setInputText(e.target.value)}\n onKeyPress={(e) => e.key === 'Enter' && handleSend()}\n placeholder=\"Type a message...\"\n />\n \u003cbutton onClick={handleSend}>Send\u003c/button>\n \u003c/>\n )}\n \u003c/div>\n );\n}\n```\n\n---\n\n## Common Gotchas\n\n### 1. OrgId Format\n\n```javascript\n// ❌ WRONG: Using 18-character OrgId in JWT\nconst orgId = '00D5g000004ABCDEFGH'; // 18 chars\n\n// ✅ CORRECT: Use 15-character format for JWT\nconst orgId15 = orgId.substring(0, 15); // '00D5g000004ABCD'\n```\n\n### 2. Message Deduplication\n\nPolling can return the same messages multiple times:\n\n```javascript\n// ❌ WRONG: No deduplication\nmessages.forEach(msg => onMessage(msg));\n\n// ✅ CORRECT: Track seen message IDs\nif (!this.seenMessageIds.has(message.id)) {\n this.seenMessageIds.add(message.id);\n onMessage(message);\n}\n```\n\n### 3. SSE on Serverless\n\nSSE connections won't work on serverless platforms:\n\n```javascript\n// ❌ WRONG: SSE on Vercel/Netlify Functions\nconst eventSource = new EventSource(url); // Connection dies immediately\n\n// ✅ CORRECT: Use polling fallback\nconst poller = new MessagePoller(auth, convId, onMessage);\npoller.start(2000);\n```\n\n### 4. Token Refresh\n\nAccess tokens expire; implement refresh logic:\n\n```javascript\nclass MessagingClient {\n constructor() {\n this.auth = null;\n this.tokenExpiry = null;\n }\n\n async ensureValidToken() {\n const now = Date.now();\n const buffer = 60000; // 1 minute buffer\n\n if (!this.auth || now >= this.tokenExpiry - buffer) {\n this.auth = await this.refreshAuth();\n this.tokenExpiry = now + (this.auth.expiresIn * 1000);\n }\n\n return this.auth;\n }\n\n async sendMessage(conversationId, text) {\n const auth = await this.ensureValidToken();\n // ... send with valid token\n }\n}\n```\n\n---\n\n## Security Best Practices\n\n| Practice | Implementation |\n|----------|----------------|\n| Never expose private keys | Keep JWT signing server-side only |\n| Use short-lived tokens | 5-15 minute expiry for JWTs |\n| Validate conversation ownership | Server tracks user → conversation mapping |\n| Rate limit messages | Prevent spam/abuse |\n| Sanitize message content | XSS prevention on display |\n\n---\n\n## Deployment Platforms\n\n### Vercel\n\n```javascript\n// api/messaging/auth.js\nexport default async function handler(req, res) {\n // Server-side auth - never expose keys to client\n const auth = await getMessagingAuth(\n process.env.SF_ACCESS_TOKEN,\n process.env.SF_ORG_DOMAIN,\n req.body.deploymentId\n );\n\n res.json(auth);\n}\n```\n\n### AWS Lambda\n\n```javascript\n// handler.js\nexports.startConversation = async (event) => {\n const { deploymentId, customerName } = JSON.parse(event.body);\n\n const auth = await getMessagingAuth(/* ... */);\n const conversationId = await startConversation(auth, customerName);\n\n return {\n statusCode: 200,\n body: JSON.stringify({ conversationId })\n };\n};\n```\n\n---\n\n## Cross-Skill References\n\n| Topic | Resource |\n|-------|----------|\n| Connected Apps setup | [sf-connected-apps skill](../../sf-connected-apps/SKILL.md) |\n| Named Credentials | [named-credentials-guide.md](named-credentials-guide.md) |\n| Agentforce agents | [sf-ai-agentforce skill](../../sf-ai-agentforce/SKILL.md) |\n| Platform Events | [platform-events-guide.md](platform-events-guide.md) |\n| REST callout patterns | [rest-callout-patterns.md](rest-callout-patterns.md) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":18249,"content_sha256":"2671c42347ad8ea429a80fbac2f210ffcb798ee55eae5ef4adf99709ab6d1963"},{"filename":"references/named-credentials-automation.md","content":"\u003c!-- Parent: sf-integration/SKILL.md -->\n# Named Credentials Automation Guide\n\nThis guide explains how to automate Named Credential configuration using the sf-integration helper scripts.\n\n> **Key Insight:** Enhanced Named Credentials (API 60+) can be configured programmatically using `ConnectApi.NamedCredentials` - no UI required!\n\n---\n\n## Overview\n\nEnhanced Named Credentials = **External Credential** + **Named Credential** + **Endpoint Security**\n\n```\nExternal Credential (stores the API key securely)\n ↓\nNamed Credential (references the External Credential)\n ↓\nYour HTTP Callout (uses callout:NamedCredentialName)\n```\n\n### Why Enhanced Named Credentials?\n\n| Benefit | Description |\n|---------|-------------|\n| **Security** | AES-256 encryption by Salesforce platform |\n| **Flexibility** | Supports Custom, Basic, OAuth, JWT protocols |\n| **Portability** | Easier to manage across multiple orgs |\n| **Automation** | Configurable via ConnectApi (no UI required) |\n| **Compliance** | Passes PCI-DSS, SOC 2 audits |\n\n---\n\n## Deployment Order\n\n**CRITICAL:** Deploy components in this exact order:\n\n```bash\n# 1. Deploy External Credential (defines the credential structure)\nsf project deploy start \\\n --source-dir force-app/main/default/externalCredentials/YourAPI.externalCredential-meta.xml \\\n --target-org YourOrg\n\n# 2. Deploy Named Credential (references the External Credential)\nsf project deploy start \\\n --source-dir force-app/main/default/namedCredentials/YourAPI.namedCredential-meta.xml \\\n --target-org YourOrg\n\n# 3. Deploy endpoint security (allows outbound HTTP calls)\nsf project deploy start \\\n --source-dir force-app/main/default/cspTrustedSites/YourAPI.cspTrustedSite-meta.xml \\\n --target-org YourOrg\n\n# 4. Set the API key using our automation script\n./scripts/configure-named-credential.sh YourOrg\n```\n\n---\n\n## Automation Scripts\n\n### `configure-named-credential.sh`\n\n**Purpose:** Sets API keys for Enhanced Named Credentials using ConnectApi\n\n**Usage:**\n```bash\n./scripts/configure-named-credential.sh \u003corg-alias>\n```\n\n**What it does:**\n1. Validates org connection via `sf org display`\n2. Checks External Credential exists via SOQL\n3. Prompts for API key securely (input hidden)\n4. Generates Apex using `ConnectApi.NamedCredentials.createCredential()`\n5. Handles create vs. patch automatically\n\n**Under the hood:**\n```apex\nConnectApi.CredentialInput creds = new ConnectApi.CredentialInput();\ncreds.externalCredential = 'YourExternalCredential';\ncreds.principalName = 'yourPrincipalName';\ncreds.authenticationProtocol = ConnectApi.CredentialAuthenticationProtocol.Custom;\n\nMap\u003cString, ConnectApi.CredentialValueInput> params = new Map\u003cString, ConnectApi.CredentialValueInput>();\nConnectApi.CredentialValueInput apiKey = new ConnectApi.CredentialValueInput();\napiKey.encrypted = true;\napiKey.value = 'YOUR_API_KEY';\nparams.put('apiKey', apiKey);\n\ncreds.credentials = params;\nConnectApi.NamedCredentials.createCredential(creds);\n```\n\n### `set-api-credential.sh`\n\n**Purpose:** Stores API keys in Custom Settings (legacy/dev approach)\n\n**Usage:**\n```bash\n# Secure input (recommended)\n./scripts/set-api-credential.sh \u003csetting-name> - \u003corg-alias>\n\n# Direct input\n./scripts/set-api-credential.sh \u003csetting-name> \u003capi-key> \u003corg-alias>\n```\n\n**When to use:**\n- Dev/test environments\n- CI/CD pipelines (no Apex execution)\n- Simple API key auth via query parameters\n\n**Not recommended for production** - use Enhanced Named Credentials instead.\n\n---\n\n## Complete Example: Weather API\n\n### Step 1: Create Metadata\n\n**External Credential:**\n```xml\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\u003cExternalCredential xmlns=\"http://soap.sforce.com/2006/04/metadata\">\n \u003cauthenticationProtocol>Custom\u003c/authenticationProtocol>\n \u003cexternalCredentialParameters>\n \u003cparameterGroup>weatherAPIKey\u003c/parameterGroup>\n \u003cparameterName>weatherAPIKey\u003c/parameterName>\n \u003cparameterType>NamedPrincipal\u003c/parameterType>\n \u003csequenceNumber>1\u003c/sequenceNumber>\n \u003c/externalCredentialParameters>\n \u003clabel>Weather API\u003c/label>\n\u003c/ExternalCredential>\n```\n\n**Named Credential:**\n```xml\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\u003cNamedCredential xmlns=\"http://soap.sforce.com/2006/04/metadata\">\n \u003callowMergeFieldsInBody>false\u003c/allowMergeFieldsInBody>\n \u003callowMergeFieldsInHeader>true\u003c/allowMergeFieldsInHeader>\n \u003ccalloutStatus>Enabled\u003c/calloutStatus>\n \u003clabel>Weather API\u003c/label>\n \u003cnamedCredentialParameters>\n \u003cparameterName>Url\u003c/parameterName>\n \u003cparameterType>Url\u003c/parameterType>\n \u003cparameterValue>https://api.weather.com\u003c/parameterValue>\n \u003c/namedCredentialParameters>\n \u003cnamedCredentialParameters>\n \u003cexternalCredential>WeatherAPI\u003c/externalCredential>\n \u003cparameterName>ExternalCredential\u003c/parameterName>\n \u003cparameterType>Authentication\u003c/parameterType>\n \u003c/namedCredentialParameters>\n \u003cnamedCredentialType>SecuredEndpoint\u003c/namedCredentialType>\n\u003c/NamedCredential>\n```\n\n### Step 2: Deploy and Configure\n\n```bash\n# Deploy all metadata\nsf project deploy start --metadata ExternalCredential:WeatherAPI \\\n --metadata NamedCredential:WeatherAPI \\\n --metadata CspTrustedSite:WeatherAPI \\\n --target-org MyOrg\n\n# Configure API key\n./scripts/configure-named-credential.sh MyOrg\n```\n\n### Step 3: Use in Apex\n\n```apex\nHttpRequest req = new HttpRequest();\nreq.setEndpoint('callout:WeatherAPI/forecast?city=London');\nreq.setMethod('GET');\n\nHttpResponse res = new Http().send(req);\nSystem.debug(res.getBody());\n```\n\nThe API key is automatically included - no manual credential handling!\n\n---\n\n## Troubleshooting\n\n| Error | Cause | Fix |\n|-------|-------|-----|\n| \"External Credential not found\" | Not deployed or wrong name | Deploy first, check spelling |\n| \"Named Credential not found\" | Not deployed | Deploy after External Credential |\n| \"No existing credentials to update\" | Using patch instead of create | Script handles automatically |\n| \"Unable to connect\" | Missing endpoint security | Deploy CSP Trusted Site |\n\n---\n\n## Related Documentation\n\n- [named-credentials-guide.md](./named-credentials-guide.md) - Template reference\n- [external-services-guide.md](./external-services-guide.md) - OpenAPI integration\n- [security-best-practices.md](./security-best-practices.md) - Security patterns\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":6263,"content_sha256":"233cf327cfefce2371c00a47d30f73cc8ce1039e4a501ecbb500d1496a9fb741"},{"filename":"references/named-credentials-guide.md","content":"\u003c!-- Parent: sf-integration/SKILL.md -->\n# Named Credentials Guide\n\n## Overview\n\nNamed Credentials provide secure storage of authentication credentials and endpoint URLs for external system integrations. They eliminate the need to hardcode credentials in Apex code.\n\n## Architecture Evolution\n\n### Legacy Named Credentials (Pre-API 61)\n\n- Single principal per credential\n- Authentication configured directly on Named Credential\n- Simpler setup but less flexible\n\n### External Credentials (API 61+)\n\n- Separate External Credential and Named Credential\n- Named Principal and Per-User Principal support\n- Permission Set-based access control\n- More secure and flexible\n\n```\n┌─────────────────────────────────────────────────────────────────┐\n│ CREDENTIAL ARCHITECTURE (API 61+) │\n├─────────────────────────────────────────────────────────────────┤\n│ │\n│ External Credential │\n│ ├── Authentication Protocol (OAuth, JWT, Custom) │\n│ ├── OAuth/JWT Parameters │\n│ └── Principals │\n│ ├── Named Principal (shared service account) │\n│ └── Per-User Principal (individual auth) │\n│ │\n│ Named Credential │\n│ ├── Endpoint URL │\n│ └── References External Credential │\n│ │\n│ Permission Set │\n│ └── External Credential Principal Access │\n│ │\n└─────────────────────────────────────────────────────────────────┘\n```\n\n## Authentication Types\n\n### 1. OAuth 2.0 Client Credentials\n\n**Use Case**: Server-to-server integration without user context\n\n```apex\n// Apex usage - Named Credential handles auth automatically\nHttpRequest req = new HttpRequest();\nreq.setEndpoint('callout:MyOAuthCredential/api/resource');\nreq.setMethod('GET');\n// Authorization header added automatically\n```\n\n**Setup**:\n1. Create Auth Provider (optional, for complex OAuth)\n2. Create Named Credential with OAuth protocol\n3. Enter Client ID and Client Secret via UI\n\n### 2. OAuth 2.0 JWT Bearer\n\n**Use Case**: Certificate-based server-to-server auth\n\n**Prerequisites**:\n- Certificate in Setup → Certificate and Key Management\n- Connected App configured for JWT Bearer\n- External system configured to trust certificate\n\n**Flow**:\n1. Salesforce creates JWT with claims (iss, sub, aud, exp)\n2. JWT signed with certificate private key\n3. JWT exchanged for access token\n4. Access token used for API calls\n\n### 3. Certificate-Based (Mutual TLS)\n\n**Use Case**: High-security integrations requiring client certificate\n\n**Setup**:\n1. Obtain client certificate from CA or external system\n2. Import to Setup → Certificate and Key Management\n3. Configure Named Credential with certificate\n4. External system must trust Salesforce's certificate\n\n### 4. Basic Auth / API Key\n\n**Use Case**: Simple APIs, internal systems\n\n**Pattern for API Key**:\n```apex\nHttpRequest req = new HttpRequest();\nreq.setEndpoint('callout:MyCredential/api/resource');\nreq.setHeader('X-API-Key', '{!$Credential.Password}');\n```\n\n## Best Practices\n\n### DO\n\n- **Use Named Credentials** for ALL external callouts\n- **Rotate credentials** regularly using Named Credential update\n- **Use External Credentials** (API 61+) for new development\n- **Limit OAuth scopes** to minimum required\n- **Use Per-User Principal** when user context matters\n- **Test credentials** before deployment\n\n### DON'T\n\n- **Never hardcode** credentials in Apex\n- **Never commit** credentials to source control\n- **Don't share** service account credentials across environments\n- **Don't use** overly broad OAuth scopes\n\n## Common Patterns\n\n### Pattern 1: Service Integration\n\n```apex\npublic class ExternalServiceCallout {\n public static HttpResponse callService(String endpoint, String body) {\n HttpRequest req = new HttpRequest();\n req.setEndpoint('callout:ServiceCredential' + endpoint);\n req.setMethod('POST');\n req.setHeader('Content-Type', 'application/json');\n req.setBody(body);\n return new Http().send(req);\n }\n}\n```\n\n### Pattern 2: Multiple Environments\n\n```\nNamed Credentials:\n├── MyAPI_Dev → https://api-dev.example.com\n├── MyAPI_UAT → https://api-uat.example.com\n└── MyAPI_Prod → https://api.example.com\n```\n\nUse Custom Metadata or Custom Settings to select credential by environment.\n\n### Pattern 3: Per-User OAuth (API 61+)\n\nFor APIs requiring user-specific authentication:\n\n1. Create External Credential with Per-User Principal\n2. Create Named Credential referencing External Credential\n3. Users authenticate individually via OAuth flow\n4. Each user's callouts use their own token\n\n## Troubleshooting\n\n| Error | Cause | Solution |\n|-------|-------|----------|\n| `Named credential not found` | Credential doesn't exist or wrong name | Verify credential name in Setup |\n| `Authentication failed` | Invalid credentials | Update credentials via Setup UI |\n| `Insufficient privileges` | User lacks permission | Assign Permission Set with External Credential Principal Access |\n| `Connection refused` | Network/firewall issue | Check Remote Site Settings, firewall rules |\n| `Certificate error` | SSL/TLS issue | Verify certificate chain, expiration |\n\n## Migration: Legacy to External Credentials\n\n1. **Create External Credential** with same auth parameters\n2. **Create new Named Credential** referencing External Credential\n3. **Create Permission Set** with External Credential Principal Access\n4. **Assign Permission Set** to integration users\n5. **Update Apex code** to use new Named Credential\n6. **Test thoroughly** before decommissioning legacy credential\n7. **Delete legacy** Named Credential after validation\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":6677,"content_sha256":"d621edf6533e89d617988a7630ac2b1c8cfb617d67266ec8b4ff6880eb4b5b8c"},{"filename":"references/platform-events-guide.md","content":"\u003c!-- Parent: sf-integration/SKILL.md -->\n# Platform Events Guide\n\n## Overview\n\nPlatform Events enable event-driven architecture in Salesforce. They provide a scalable, asynchronous messaging system for real-time integrations.\n\n## Event Types\n\n### Standard Volume\n\n- Up to ~2,000 events per hour\n- Included in all Salesforce editions\n- Standard delivery guarantees\n\n### High Volume\n\n- Millions of events per day\n- At-least-once delivery\n- 24-hour retention for replay\n- May require additional entitlement\n\n## When to Use Platform Events\n\n| Scenario | Platform Events | Other Options |\n|----------|-----------------|---------------|\n| Real-time notifications | ✅ Best choice | - |\n| Decoupled integrations | ✅ Best choice | - |\n| High-volume streaming | ✅ High Volume | Change Data Capture |\n| Simple record sync | Consider | Change Data Capture |\n| External system notifications | ✅ Best choice | - |\n| Internal process triggers | ✅ Good choice | Process Builder, Flow |\n\n## Creating Platform Events\n\n### Via Metadata (Recommended)\n\n```xml\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\u003cCustomObject xmlns=\"http://soap.sforce.com/2006/04/metadata\">\n \u003cdeploymentStatus>Deployed\u003c/deploymentStatus>\n \u003ceventType>HighVolume\u003c/eventType>\n \u003clabel>Order Update Event\u003c/label>\n \u003cpluralLabel>Order Update Events\u003c/pluralLabel>\n \u003cpublishBehavior>PublishAfterCommit\u003c/publishBehavior>\n \u003cfields>\n \u003cfullName>Order_Id__c\u003c/fullName>\n \u003clabel>Order ID\u003c/label>\n \u003ctype>Text\u003c/type>\n \u003clength>18\u003c/length>\n \u003c/fields>\n \u003cfields>\n \u003cfullName>Status__c\u003c/fullName>\n \u003clabel>Status\u003c/label>\n \u003ctype>Text\u003c/type>\n \u003clength>50\u003c/length>\n \u003c/fields>\n\u003c/CustomObject>\n```\n\n### File Location\n\n```\nforce-app/main/default/objects/Order_Update_Event__e/Order_Update_Event__e.object-meta.xml\n```\n\n## Publishing Events\n\n### From Apex\n\n```apex\n// Single event\nOrder_Update_Event__e event = new Order_Update_Event__e();\nevent.Order_Id__c = orderId;\nevent.Status__c = 'Shipped';\n\nDatabase.SaveResult result = EventBus.publish(event);\nif (result.isSuccess()) {\n System.debug('Event published: ' + result.getId());\n}\n\n// Multiple events\nList\u003cOrder_Update_Event__e> events = new List\u003cOrder_Update_Event__e>();\n// ... populate events\nList\u003cDatabase.SaveResult> results = EventBus.publish(events);\n```\n\n### From Flow\n\n1. Create Record element\n2. Select Platform Event object\n3. Map field values\n\n### From Process Builder\n\n1. Add Immediate Action\n2. Select \"Create a Record\"\n3. Choose Platform Event\n\n## Subscribing to Events\n\n### Apex Trigger\n\n```apex\ntrigger OrderUpdateSubscriber on Order_Update_Event__e (after insert) {\n for (Order_Update_Event__e event : Trigger.new) {\n System.debug('Order ' + event.Order_Id__c + ' is now ' + event.Status__c);\n // Process event\n }\n\n // Set checkpoint for durability\n EventBus.TriggerContext.currentContext().setResumeCheckpoint(\n Trigger.new[Trigger.new.size() - 1].ReplayId\n );\n}\n```\n\n### Flow (Record-Triggered)\n\n1. Create Platform Event-Triggered Flow\n2. Select Platform Event object\n3. Build logic with event data\n\n### External (CometD)\n\nExternal systems can subscribe using CometD streaming:\n\n```\n/event/Order_Update_Event__e\n```\n\n## Publish Behavior\n\n### PublishAfterCommit (Default)\n\n- Event published after transaction commits\n- If transaction rolls back, event NOT published\n- **Recommended for most cases**\n\n### PublishImmediately\n\n- Event published immediately\n- Event still published even if transaction rolls back\n- Use when external system must be notified regardless of outcome\n\n## Durability & Replay\n\n### Replay ID\n\nEach event has a unique `ReplayId` for tracking and replay:\n\n```apex\nString replayId = event.ReplayId;\n```\n\n### Resume Checkpoint\n\nSet checkpoint to ensure durability:\n\n```apex\n// In trigger\nEventBus.TriggerContext.currentContext().setResumeCheckpoint(lastReplayId);\n```\n\nIf trigger fails after checkpoint, processing resumes from that point.\n\n### Retention\n\n- High Volume events: 24 hours\n- Standard Volume: 24 hours\n\n## Best Practices\n\n### Publishing\n\n1. **Batch events** when publishing multiple\n2. **Check SaveResults** for publish failures\n3. **Use meaningful correlation IDs** for tracking\n4. **Include timestamp** for ordering\n5. **Keep payloads small** - use IDs, not full records\n\n### Subscribing\n\n1. **Always set resume checkpoint** in triggers\n2. **Don't throw exceptions** - catch and log errors\n3. **Process idempotently** - events may replay\n4. **Keep processing lightweight** - queue heavy work\n5. **Handle duplicates** using correlation ID\n\n### Design\n\n1. **Event granularity** - not too fine, not too coarse\n2. **Include enough context** but not entire records\n3. **Version your events** if schema evolves\n4. **Document event contracts** for consumers\n\n## Error Handling\n\n### Publish Errors\n\n```apex\nList\u003cDatabase.SaveResult> results = EventBus.publish(events);\nfor (Integer i = 0; i \u003c results.size(); i++) {\n if (!results[i].isSuccess()) {\n for (Database.Error err : results[i].getErrors()) {\n System.debug('Publish failed: ' + err.getMessage());\n }\n }\n}\n```\n\n### Subscriber Errors\n\n```apex\ntrigger MySubscriber on My_Event__e (after insert) {\n for (My_Event__e event : Trigger.new) {\n try {\n processEvent(event);\n } catch (Exception e) {\n // Log error, don't throw\n System.debug('Error processing ' + event.ReplayId + ': ' + e.getMessage());\n // Create error log record\n }\n }\n\n // Still set checkpoint even if some failed\n EventBus.TriggerContext.currentContext().setResumeCheckpoint(lastReplayId);\n}\n```\n\n## Monitoring\n\n### Setup → Platform Events\n\n- View event definitions\n- Check usage metrics\n- Monitor delivery status\n\n### Event Delivery Failures\n\nCheck for:\n- Unhandled exceptions in triggers\n- Apex CPU timeout\n- Governor limit errors\n\n### Event Publishing\n\nQuery `EventBusSubscriber` for subscription health:\n\n```apex\nSELECT Id, Position, ExternalId, Name, Status, Tip\nFROM EventBusSubscriber\nWHERE Topic = 'Order_Update_Event__e'\n```\n\n## Limits\n\n| Limit | Standard Volume | High Volume |\n|-------|-----------------|-------------|\n| Events per hour | ~2,000 | Millions |\n| Retention | 24 hours | 24 hours |\n| Max event size | 1 MB | 1 MB |\n| Fields per event | 100 | 100 |\n\n## External Integration\n\n### Subscribe from External System\n\nUse CometD client to connect to Streaming API:\n\n```\nEndpoint: /cometd/62.0\nChannel: /event/Order_Update_Event__e\n```\n\n### Publish from External System\n\nUse REST API:\n\n```http\nPOST /services/data/v66.0/sobjects/Order_Update_Event__e\nContent-Type: application/json\n\n{\n \"Order_Id__c\": \"001xx000003NGSFAA4\",\n \"Status__c\": \"Shipped\"\n}\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":6748,"content_sha256":"c0440a3aff5a177999792c71c61ac15d530f957a063866120bf98290d83aa497"},{"filename":"references/rest-callout-patterns.md","content":"\u003c!-- Parent: sf-integration/SKILL.md -->\n# REST Callout Patterns\n\n## Overview\n\nThis guide covers patterns for making HTTP callouts from Salesforce Apex to external REST APIs.\n\n## Synchronous vs Asynchronous\n\n### When to Use Synchronous\n\n- User needs immediate response\n- Called from Visualforce, LWC, or Aura\n- NOT triggered by DML operations\n- Response required before next action\n\n### When to Use Asynchronous\n\n- Called from triggers (REQUIRED)\n- Fire-and-forget operations\n- Background processing\n- Long-running operations\n\n```\n┌─────────────────────────────────────────────────────────────────┐\n│ CALLOUT CONTEXT DECISION │\n├─────────────────────────────────────────────────────────────────┤\n│ │\n│ Is this called from a trigger or after DML? │\n│ ├── YES → Use Queueable with Database.AllowsCallouts │\n│ └── NO → Synchronous OK │\n│ │\n│ Does user need immediate response? │\n│ ├── YES → Synchronous (if allowed) │\n│ └── NO → Consider async for better UX │\n│ │\n└─────────────────────────────────────────────────────────────────┘\n```\n\n## Basic Request Pattern\n\n```apex\npublic class RestCallout {\n\n public static HttpResponse makeRequest(String method, String endpoint, String body) {\n HttpRequest req = new HttpRequest();\n req.setEndpoint('callout:MyCredential' + endpoint);\n req.setMethod(method);\n req.setHeader('Content-Type', 'application/json');\n req.setHeader('Accept', 'application/json');\n req.setTimeout(120000); // 120 seconds max\n\n if (String.isNotBlank(body)) {\n req.setBody(body);\n }\n\n return new Http().send(req);\n }\n}\n```\n\n## HTTP Methods\n\n| Method | Use Case | Body |\n|--------|----------|------|\n| GET | Retrieve resource | No |\n| POST | Create resource | Yes |\n| PUT | Full update | Yes |\n| PATCH | Partial update | Yes |\n| DELETE | Remove resource | Usually No |\n\n## Response Handling\n\n### Status Code Categories\n\n```apex\nInteger statusCode = response.getStatusCode();\n\nif (statusCode >= 200 && statusCode \u003c 300) {\n // Success (2xx)\n} else if (statusCode >= 400 && statusCode \u003c 500) {\n // Client error (4xx) - don't retry\n} else if (statusCode >= 500) {\n // Server error (5xx) - may retry\n}\n```\n\n### Common Status Codes\n\n| Code | Meaning | Action |\n|------|---------|--------|\n| 200 | OK | Process response |\n| 201 | Created | Resource created |\n| 204 | No Content | Success, no body |\n| 400 | Bad Request | Fix request |\n| 401 | Unauthorized | Check credentials |\n| 403 | Forbidden | Check permissions |\n| 404 | Not Found | Resource doesn't exist |\n| 429 | Too Many Requests | Rate limited, retry later |\n| 500 | Server Error | Retry with backoff |\n| 503 | Service Unavailable | Retry later |\n\n## Error Handling Pattern\n\n```apex\npublic class ApiClient {\n\n public static Map\u003cString, Object> callApi(String endpoint) {\n try {\n HttpResponse res = makeRequest('GET', endpoint, null);\n\n if (res.getStatusCode() == 200) {\n return (Map\u003cString, Object>) JSON.deserializeUntyped(res.getBody());\n }\n\n // Handle specific errors\n if (res.getStatusCode() == 404) {\n throw new NotFoundException('Resource not found: ' + endpoint);\n }\n\n if (res.getStatusCode() == 429) {\n String retryAfter = res.getHeader('Retry-After');\n throw new RateLimitedException('Rate limited. Retry after: ' + retryAfter);\n }\n\n throw new ApiException('API Error: ' + res.getStatusCode() + ' - ' + res.getBody());\n\n } catch (CalloutException e) {\n // Network error, timeout, SSL error\n throw new ApiException('Connection failed: ' + e.getMessage(), e);\n }\n }\n\n public class ApiException extends Exception {}\n public class NotFoundException extends Exception {}\n public class RateLimitedException extends Exception {}\n}\n```\n\n## Retry Pattern\n\n```apex\npublic class RetryableCallout {\n\n private static final Integer MAX_RETRIES = 3;\n private static final Set\u003cInteger> RETRYABLE_CODES = new Set\u003cInteger>{\n 408, 429, 500, 502, 503, 504\n };\n\n public static HttpResponse callWithRetry(HttpRequest request) {\n Integer attempts = 0;\n\n while (attempts \u003c MAX_RETRIES) {\n HttpResponse res = new Http().send(request);\n\n if (!RETRYABLE_CODES.contains(res.getStatusCode())) {\n return res;\n }\n\n attempts++;\n System.debug('Retry ' + attempts + ' for ' + res.getStatusCode());\n }\n\n throw new CalloutException('Max retries exceeded');\n }\n}\n```\n\n## Queueable Pattern (Async)\n\n```apex\npublic class AsyncCallout implements Queueable, Database.AllowsCallouts {\n\n private Id recordId;\n\n public AsyncCallout(Id recordId) {\n this.recordId = recordId;\n }\n\n public void execute(QueueableContext context) {\n // Query record\n Account acc = [SELECT Id, Name FROM Account WHERE Id = :recordId];\n\n // Make callout\n HttpRequest req = new HttpRequest();\n req.setEndpoint('callout:MyAPI/accounts');\n req.setMethod('POST');\n req.setBody(JSON.serialize(new Map\u003cString, Object>{\n 'name' => acc.Name,\n 'sfId' => acc.Id\n }));\n\n HttpResponse res = new Http().send(req);\n\n // Update record with result\n if (res.getStatusCode() == 201) {\n acc.External_Id__c = extractId(res.getBody());\n update acc;\n }\n }\n\n private String extractId(String body) {\n Map\u003cString, Object> result = (Map\u003cString, Object>) JSON.deserializeUntyped(body);\n return (String) result.get('id');\n }\n}\n\n// Usage from trigger:\n// System.enqueueJob(new AsyncCallout(accountId));\n```\n\n## Pagination Pattern\n\n```apex\npublic class PaginatedApiClient {\n\n public static List\u003cMap\u003cString, Object>> getAllRecords(String endpoint) {\n List\u003cMap\u003cString, Object>> allRecords = new List\u003cMap\u003cString, Object>>();\n String nextPageUrl = endpoint;\n\n while (String.isNotBlank(nextPageUrl)) {\n HttpResponse res = makeRequest('GET', nextPageUrl, null);\n Map\u003cString, Object> response = (Map\u003cString, Object>) JSON.deserializeUntyped(res.getBody());\n\n // Add records from this page\n List\u003cObject> records = (List\u003cObject>) response.get('data');\n for (Object rec : records) {\n allRecords.add((Map\u003cString, Object>) rec);\n }\n\n // Get next page URL\n nextPageUrl = (String) response.get('nextPage');\n }\n\n return allRecords;\n }\n}\n```\n\n## Governor Limits\n\n| Limit | Value |\n|-------|-------|\n| Callouts per transaction | 100 |\n| Maximum timeout | 120,000 ms (120 seconds) |\n| Maximum request size | 6 MB |\n| Maximum response size | 6 MB |\n| Concurrent long-running requests | 10 |\n\n## Testing Callouts\n\n```apex\n@isTest\nprivate class ApiClientTest {\n\n @isTest\n static void testSuccessfulCallout() {\n // Set mock\n Test.setMock(HttpCalloutMock.class, new MockSuccess());\n\n Test.startTest();\n Map\u003cString, Object> result = ApiClient.callApi('/endpoint');\n Test.stopTest();\n\n System.assertEquals('value', result.get('key'));\n }\n\n private class MockSuccess implements HttpCalloutMock {\n public HttpResponse respond(HttpRequest req) {\n HttpResponse res = new HttpResponse();\n res.setStatusCode(200);\n res.setBody('{\"key\": \"value\"}');\n return res;\n }\n }\n}\n```\n\n## Best Practices\n\n1. **Always use Named Credentials** - Never hardcode endpoints or credentials\n2. **Set appropriate timeouts** - Default may be too short for slow APIs\n3. **Handle all error cases** - Don't assume success\n4. **Log requests and responses** - Essential for debugging\n5. **Use async for trigger contexts** - Queueable with AllowsCallouts\n6. **Implement retry logic** - For transient failures\n7. **Monitor governor limits** - Especially callout count\n8. **Parse errors gracefully** - APIs return errors in various formats\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":8968,"content_sha256":"0800e90e025fbab4371c79714e7e6d97aee839e0ef431a537c2130ec8b27e282"},{"filename":"references/scoring-rubric.md","content":"\u003c!-- Parent: sf-integration/SKILL.md -->\n\n# Scoring System (120 Points)\n\n## Category Breakdown\n\n| Category | Points | Evaluation Criteria |\n|----------|--------|---------------------|\n| **Security** | 30 | Named Credentials used (no hardcoded secrets), OAuth scopes minimized, certificate auth where applicable |\n| **Error Handling** | 25 | Retry logic present, timeout handling (120s max), specific exception types, logging implemented |\n| **Bulkification** | 20 | Batch callouts considered, CDC bulk handling, event batching for Platform Events |\n| **Architecture** | 20 | Async patterns for DML-triggered callouts, proper service layer separation, single responsibility |\n| **Best Practices** | 15 | Governor limit awareness, proper HTTP methods, idempotency for retries |\n| **Documentation** | 10 | Clear intent documented, endpoint versioning noted, API contract documented |\n\n## Scoring Thresholds\n\n| Rating | Score Range | Description |\n|--------|------------|-------------|\n| Excellent | 108-120 | Production-ready, follows all best practices |\n| Very Good | 90-107 | Minor improvements suggested |\n| Good | 72-89 | Acceptable with noted improvements |\n| Needs Work | 54-71 | Address issues before deployment |\n| Block | \u003c54 | CRITICAL issues, do not deploy |\n\n## Scoring Output Format\n\n```\n📊 INTEGRATION SCORE: XX/120 ⭐⭐⭐⭐ Rating\n════════════════════════════════════════════════════\n\n🔐 Security XX/30 ████████░░ XX%\n├─ Named Credentials used: ✅\n├─ No hardcoded secrets: ✅\n└─ OAuth scopes minimal: ✅\n\n⚠️ Error Handling XX/25 ████████░░ XX%\n├─ Retry logic: ✅\n├─ Timeout handling: ✅\n└─ Logging: ✅\n\n📦 Bulkification XX/20 ████████░░ XX%\n├─ Batch callouts: ✅\n└─ Event batching: ✅\n\n🏗️ Architecture XX/20 ████████░░ XX%\n├─ Async patterns: ✅\n└─ Service separation: ✅\n\n✅ Best Practices XX/15 ████████░░ XX%\n├─ Governor limits: ✅\n└─ Idempotency: ✅\n\n📝 Documentation XX/10 ████████░░ XX%\n├─ Clear intent: ✅\n└─ API versioning: ✅\n\n════════════════════════════════════════════════════\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2465,"content_sha256":"066feeb2eddee7779028303da4719bfa6d980c383d14ba72a55c746ebe3f43ed"},{"filename":"references/security-best-practices.md","content":"\u003c!-- Parent: sf-integration/SKILL.md -->\n# Integration Security Best Practices\n\n## Overview\n\nSecurity is critical for integrations. This guide covers best practices for securing Salesforce integrations with external systems.\n\n## Credential Management\n\n### DO: Use Named Credentials\n\n```apex\n// ✅ CORRECT - Named Credential handles auth\nHttpRequest req = new HttpRequest();\nreq.setEndpoint('callout:MySecureAPI/resource');\n```\n\n### DON'T: Hardcode Credentials\n\n```apex\n// ❌ WRONG - Never hardcode credentials\nreq.setHeader('Authorization', 'Bearer sk_live_abc123...');\nreq.setEndpoint('https://api.example.com');\n```\n\n### Credential Storage Rules\n\n| Item | Storage Location | Never Store In |\n|------|------------------|----------------|\n| API Keys | Named Credential | Apex code, Custom Settings |\n| Client Secrets | Named Credential / External Credential | Source control |\n| Certificates | Certificate & Key Management | Static Resources |\n| Passwords | Named Credential | Custom Metadata |\n\n## OAuth Best Practices\n\n### Scope Minimization\n\nRequest only necessary scopes:\n\n```\n✅ read:orders write:orders\n❌ admin:* read:* write:*\n```\n\n### Token Handling\n\n- Never log access tokens\n- Don't expose tokens in error messages\n- Use short-lived tokens when possible\n- Implement token refresh handling\n\n### PKCE for Public Clients\n\nFor mobile or SPA clients:\n\n```\nUse Authorization Code with PKCE, not Implicit flow\n```\n\n## Network Security\n\n### Remote Site Settings\n\n- Only allow necessary domains\n- Don't use wildcard domains\n- Review and audit regularly\n\n### Certificate Validation\n\n- Use trusted CA certificates\n- Don't disable SSL/TLS verification\n- Monitor certificate expiration\n\n### IP Restrictions\n\nFor Connected Apps:\n- Configure IP relaxation carefully\n- Use \"Enforce IP restrictions\" when possible\n\n## Input Validation\n\n### Validate External Data\n\n```apex\n// Validate before processing\npublic static void processExternalData(String externalId) {\n // Validate format\n if (!Pattern.matches('[A-Za-z0-9]{10,20}', externalId)) {\n throw new ValidationException('Invalid external ID format');\n }\n\n // Sanitize for SOQL\n String safeId = String.escapeSingleQuotes(externalId);\n}\n```\n\n### Output Encoding\n\n```apex\n// Encode data sent to external systems\nString encodedData = EncodingUtil.urlEncode(userData, 'UTF-8');\n```\n\n## Error Handling Security\n\n### Don't Expose Internal Details\n\n```apex\n// ❌ WRONG - Exposes internal structure\nthrow new CalloutException('Failed: ' + response.getBody());\n\n// ✅ CORRECT - User-friendly, log details separately\nSystem.debug(LoggingLevel.ERROR, 'API Error: ' + response.getBody());\nthrow new CalloutException('Unable to complete request. Contact support.');\n```\n\n### Log Securely\n\n```apex\n// ❌ WRONG - Logs sensitive data\nSystem.debug('Request: ' + JSON.serialize(request)); // May contain PII\n\n// ✅ CORRECT - Redact sensitive fields\nSystem.debug('Request to: ' + endpoint + ', Status: ' + statusCode);\n```\n\n## API Security Patterns\n\n### Rate Limiting Awareness\n\n```apex\nif (response.getStatusCode() == 429) {\n String retryAfter = response.getHeader('Retry-After');\n // Implement backoff, don't hammer the API\n}\n```\n\n### Idempotency Keys\n\nFor POST requests that shouldn't duplicate:\n\n```apex\nreq.setHeader('Idempotency-Key', generateUniqueKey());\n```\n\n### Request Signing\n\nFor APIs requiring signature:\n\n```apex\nString signature = generateHmacSignature(payload, secretKey);\nreq.setHeader('X-Signature', signature);\n```\n\n## User Context Security\n\n### Per-User vs Named Principal\n\n| Scenario | Use |\n|----------|-----|\n| User-specific data access | Per-User Principal |\n| Background/batch jobs | Named Principal |\n| Service integrations | Named Principal |\n| User-initiated with audit | Per-User Principal |\n\n### Audit Logging\n\n```apex\npublic static void logIntegrationActivity(String operation, Id userId, String externalSystem) {\n Integration_Log__c log = new Integration_Log__c(\n Operation__c = operation,\n User__c = userId,\n External_System__c = externalSystem,\n Timestamp__c = Datetime.now()\n );\n insert log;\n}\n```\n\n## Platform Event Security\n\n### Sensitive Data in Events\n\n- Don't include PII in event payloads when avoidable\n- Use record IDs and query for details\n- Consider encryption for sensitive fields\n\n### Event Consumer Validation\n\n```apex\ntrigger SecureEventHandler on My_Event__e (after insert) {\n for (My_Event__e event : Trigger.new) {\n // Validate event source/origin if possible\n if (!isValidEventSource(event)) {\n System.debug(LoggingLevel.WARN, 'Suspicious event: ' + event.ReplayId);\n continue;\n }\n processEvent(event);\n }\n}\n```\n\n## Security Checklist\n\n### Before Deployment\n\n- [ ] Named Credentials used for all external calls\n- [ ] No hardcoded credentials in code\n- [ ] OAuth scopes minimized\n- [ ] Remote Site Settings restricted\n- [ ] Error messages don't expose internals\n- [ ] Sensitive data not logged\n- [ ] Input validation implemented\n- [ ] Rate limiting handled\n- [ ] Certificate expiration monitored\n\n### Regular Review\n\n- [ ] Audit Named Credential usage\n- [ ] Review integration user permissions\n- [ ] Check for unused credentials\n- [ ] Monitor integration error logs\n- [ ] Validate certificate validity\n- [ ] Review OAuth app authorizations\n\n## Compliance Considerations\n\n### GDPR / Data Privacy\n\n- Minimize data transferred\n- Document data flows\n- Implement data deletion for integrated records\n- Encrypt PII in transit and at rest\n\n### SOC 2 / Security Audits\n\n- Maintain integration documentation\n- Log all external access\n- Implement change management\n- Regular security assessments\n\n### HIPAA (Healthcare)\n\n- Business Associate Agreements\n- Encryption requirements\n- Access logging\n- Minimum necessary standard\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5823,"content_sha256":"565462c2ef3f33a4527511e5883a5c21354bff03ba55e3b76dca88dcd9085ca7"},{"filename":"scripts/configure-named-credential.sh","content":"#!/bin/bash\n#\n# configure-named-credential.sh\n#\n# Modern script to configure Enhanced Named Credentials with External Credentials\n# Uses ConnectApi.NamedCredentials.createCredential() for secure credential storage\n#\n# PREREQUISITES:\n# 1. External Credential metadata deployed (.externalCredential-meta.xml)\n# 2. Named Credential metadata deployed (.namedCredential-meta.xml)\n# 3. CSP Trusted Site OR Remote Site Setting deployed\n#\n# Usage:\n# ./configure-named-credential.sh \u003cexternal-credential-name> \u003cprincipal-name> \u003corg-alias>\n#\n# Example:\n# ./configure-named-credential.sh VisualCrossingWeather weatherAPIKey AIZoom\n#\n# The script will:\n# 1. Prompt for API key securely (won't echo to terminal)\n# 2. Generate Apex code to configure the credential\n# 3. Execute the Apex code to store the credential securely\n#\n\nset -e # Exit on error\n\n# Colors for output\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nBLUE='\\033[0;34m'\nCYAN='\\033[0;36m'\nNC='\\033[0m' # No Color\n\n# Usage function\nusage() {\n echo -e \"${BLUE}Usage:${NC}\"\n echo \" $0 \u003cexternal-credential-name> \u003cprincipal-name> \u003corg-alias>\"\n echo \"\"\n echo -e \"${BLUE}Parameters:${NC}\"\n echo \" external-credential-name - Developer name of the External Credential\"\n echo \" principal-name - Principal name (from External Credential metadata)\"\n echo \" org-alias - Org alias for deployment\"\n echo \"\"\n echo -e \"${BLUE}Example:${NC}\"\n echo \" $0 VisualCrossingWeather weatherAPIKey AIZoom\"\n echo \"\"\n echo -e \"${BLUE}Available External Credentials in this project:${NC}\"\n find . -name \"*.externalCredential-meta.xml\" -type f 2>/dev/null | while read file; do\n basename \"$file\" .externalCredential-meta.xml | sed 's/^/ - /'\n done\n echo \"\"\n echo -e \"${BLUE}Available Orgs:${NC}\"\n sf org list --json 2>/dev/null | jq -r '.result.nonScratchOrgs[]? | \" - \\(.alias // .username) (\\(.username))\"' 2>/dev/null || echo \" Run 'sf org list' to see available orgs\"\n echo \"\"\n echo -e \"${CYAN}Order of Operations:${NC}\"\n echo \" 1. Deploy External Credential metadata\"\n echo \" 2. Deploy Named Credential metadata (references External Credential)\"\n echo \" 3. Deploy CSP Trusted Site OR Remote Site Setting\"\n echo \" 4. Run THIS script to set the API key\"\n exit 1\n}\n\n# Check arguments\nif [ $# -ne 3 ]; then\n echo -e \"${RED}Error: Wrong number of arguments${NC}\"\n echo \"\"\n usage\nfi\n\nEXTERNAL_CREDENTIAL_NAME=$1\nPRINCIPAL_NAME=$2\nORG_ALIAS=$3\n\n# Validate sf CLI is installed\nif ! command -v sf &> /dev/null; then\n echo -e \"${RED}Error: Salesforce CLI (sf) is not installed${NC}\"\n echo \"Install from: https://developer.salesforce.com/tools/salesforcecli\"\n exit 1\nfi\n\n# Banner\necho -e \"${CYAN}\"\ncat \u003c\u003c 'EOF'\n╔══════════════════════════════════════════════════════════════╗\n║ ║\n║ Enhanced Named Credential Configuration Tool ║\n║ ║\n╚══════════════════════════════════════════════════════════════╝\nEOF\necho -e \"${NC}\"\n\n# Validate org exists\necho -e \"${BLUE}► Validating org connection...${NC}\"\nif ! sf org display --target-org \"$ORG_ALIAS\" &> /dev/null; then\n echo -e \"${RED}✗ Cannot connect to org '$ORG_ALIAS'${NC}\"\n echo \"Run: sf org list\"\n exit 1\nfi\n\necho -e \"${GREEN}✓ Connected to org: $ORG_ALIAS${NC}\"\necho \"\"\n\n# Verify External Credential exists\necho -e \"${BLUE}► Checking External Credential...${NC}\"\nEXT_CRED_CHECK=$(sf data query \\\n --query \"SELECT Id, DeveloperName FROM ExternalCredential WHERE DeveloperName = '$EXTERNAL_CREDENTIAL_NAME' LIMIT 1\" \\\n --target-org \"$ORG_ALIAS\" \\\n --json 2>&1 || echo '{\"status\":1}')\n\nEXT_CRED_ID=$(echo \"$EXT_CRED_CHECK\" | jq -r '.result.records[0].Id // empty' 2>/dev/null)\n\nif [ -z \"$EXT_CRED_ID\" ]; then\n echo -e \"${RED}✗ External Credential '$EXTERNAL_CREDENTIAL_NAME' not found${NC}\"\n echo \"\"\n echo \"Deploy it first:\"\n echo \" sf project deploy start --source-dir force-app/main/default/externalCredentials/${EXTERNAL_CREDENTIAL_NAME}.externalCredential-meta.xml --target-org $ORG_ALIAS\"\n exit 1\nfi\n\necho -e \"${GREEN}✓ Found External Credential (ID: $EXT_CRED_ID)${NC}\"\necho \"\"\n\n# Prompt for API key (securely - won't echo to terminal)\necho -e \"${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\"\necho -e \"${YELLOW}Enter API Key for '$EXTERNAL_CREDENTIAL_NAME'${NC}\"\necho -e \"${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\"\necho \"\"\necho -e \"${BLUE}API Key (input hidden):${NC}\"\nread -s API_KEY\necho \"\"\n\nif [ -z \"$API_KEY\" ]; then\n echo -e \"${RED}Error: API key cannot be empty${NC}\"\n exit 1\nfi\n\necho -e \"${GREEN}✓ API key received${NC}\"\necho \"\"\n\n# Generate Apex code to configure credential\necho -e \"${BLUE}► Generating Apex code to configure credential...${NC}\"\n\nTEMP_APEX=$(mktemp /tmp/set-credential-XXXXXX.apex)\n\ncat > \"$TEMP_APEX\" \u003c\u003c EOF\n// Auto-generated by configure-named-credential.sh\n// Configures External Credential: $EXTERNAL_CREDENTIAL_NAME\n\n// Define the new credential input\nConnectApi.CredentialInput newCredentials = new ConnectApi.CredentialInput();\n\n// Specify the External Credential\nnewCredentials.externalCredential = '$EXTERNAL_CREDENTIAL_NAME';\n\n// Set authentication protocol\nnewCredentials.authenticationProtocol = ConnectApi.CredentialAuthenticationProtocol.Custom;\n\n// Define the principal\nnewCredentials.principalType = ConnectApi.CredentialPrincipalType.NamedPrincipal;\nnewCredentials.principalName = '$PRINCIPAL_NAME';\n\n// Create credentials map\nMap\u003cString, ConnectApi.CredentialValueInput> creds = new Map\u003cString, ConnectApi.CredentialValueInput>();\n\n// Create API key parameter\nConnectApi.CredentialValueInput apiKeyParam = new ConnectApi.CredentialValueInput();\napiKeyParam.encrypted = true; // Required for security\napiKeyParam.value = '$API_KEY';\n\n// Add to credentials map\ncreds.put('apiKey', apiKeyParam);\n\n// Assign to credential input\nnewCredentials.credentials = creds;\n\ntry {\n // Create the credential (first-time setup)\n ConnectApi.NamedCredentials.createCredential(newCredentials);\n System.debug('✓ External Credential configured successfully!');\n System.debug('Principal: $PRINCIPAL_NAME');\n} catch (Exception e) {\n // If already exists, try patching instead\n if (e.getMessage().contains('already exists')) {\n try {\n ConnectApi.NamedCredentials.patchCredential(newCredentials);\n System.debug('✓ External Credential updated successfully!');\n System.debug('Principal: $PRINCIPAL_NAME');\n } catch (Exception e2) {\n System.debug('✗ Error updating credential: ' + e2.getMessage());\n throw e2;\n }\n } else {\n System.debug('✗ Error creating credential: ' + e.getMessage());\n throw e;\n }\n}\nEOF\n\necho -e \"${GREEN}✓ Apex code generated${NC}\"\necho \"\"\n\n# Execute Apex code\necho -e \"${BLUE}► Executing Apex code to configure credential...${NC}\"\n\nAPEX_RESULT=$(sf apex run --file \"$TEMP_APEX\" --target-org \"$ORG_ALIAS\" 2>&1)\n\n# Check if successful\nif echo \"$APEX_RESULT\" | grep -q \"✓ External Credential\"; then\n echo -e \"${GREEN}✓ Credential configured successfully!${NC}\"\n\n # Cleanup temp file\n rm -f \"$TEMP_APEX\"\n\n echo \"\"\n echo -e \"${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\"\n echo -e \"${GREEN}✓ Configuration Complete!${NC}\"\n echo -e \"${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\"\n echo \"\"\n echo -e \"${BLUE}What was configured:${NC}\"\n echo \" • External Credential: $EXTERNAL_CREDENTIAL_NAME\"\n echo \" • Principal: $PRINCIPAL_NAME\"\n echo \" • Org: $ORG_ALIAS\"\n echo \"\"\n echo -e \"${BLUE}You can now use the Named Credential in your callouts!${NC}\"\n echo \"\"\nelse\n echo -e \"${RED}✗ Failed to configure credential${NC}\"\n echo \"\"\n echo -e \"${YELLOW}Apex execution output:${NC}\"\n echo \"$APEX_RESULT\"\n echo \"\"\n echo -e \"${YELLOW}Temp Apex file saved at: $TEMP_APEX${NC}\"\n echo \"Review the file and run manually if needed\"\n exit 1\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":8950,"content_sha256":"a1860a8fa23347a4c060af10ec0cf2a92b23c8a460fcf7d3dbd23b478d26db3e"},{"filename":"scripts/README.md","content":"# sf-integration Helper Scripts\n\nAutomation scripts for configuring Salesforce integrations without manual UI steps.\n\n> **Mirror note:** These files mirror the canonical generic helpers in the repository root `scripts/` directory. Keep both locations aligned until the packaging layout is consolidated.\n\n## Scripts\n\n| Script | Purpose | Auth Type |\n|--------|---------|-----------|\n| `configure-named-credential.sh` | Set API keys via ConnectApi | Enhanced Named Credentials |\n| `set-api-credential.sh` | Store keys in Custom Settings | Legacy (dev/test) |\n\n## Quick Start\n\n```bash\n# Enhanced Named Credentials (recommended for production)\n./configure-named-credential.sh \u003cexternal-credential-name> \u003cprincipal-name> \u003corg-alias>\n\n# Custom Settings (legacy, for dev/test)\n./set-api-credential.sh \u003csetting-name> - \u003corg-alias>\n```\n\n## Prerequisites\n\n- **Salesforce CLI v2+** (`sf` command)\n- **Authenticated org** (`sf org login web -a \u003calias>`)\n- **Deployed metadata** (External Credential, Named Credential, CSP)\n\n## Usage Examples\n\n### Configure Named Credential\n\n```bash\n# Interactive mode - prompts for API key securely\n./configure-named-credential.sh VisualCrossingWeather weatherAPIKey MyDevOrg\n\n# The script will:\n# 1. Validate org connection\n# 2. Check External Credential exists\n# 3. Prompt for API key (hidden input)\n# 4. Execute ConnectApi Apex to store encrypted\n```\n\n### Custom Settings (Legacy)\n\n```bash\n# Secure input (dash prompts for hidden input)\n./set-api-credential.sh WeatherAPI - MyDevOrg\n\n# Direct input (less secure, for CI/CD)\n./set-api-credential.sh WeatherAPI sk_live_abc123 MyDevOrg\n```\n\n## Templates\n\nThe `templates/` directory contains customizable scripts for new integrations:\n\n| Template | Purpose |\n|----------|---------|\n| `setup-credentials-with-csp.sh` | Full setup with CSP Trusted Sites |\n\n### Using Templates\n\n```bash\n# Copy template for your integration\ncp templates/setup-credentials-with-csp.sh my-integration-setup.sh\n\n# Edit configuration variables\n# - SKILL_NAME\n# - CSP_NAME\n# - API_KEY_URL\n```\n\n## Auto-Run Behavior\n\nWhen you create credential metadata files, Claude will automatically suggest running these scripts:\n\n| File Pattern | Suggested Script |\n|--------------|------------------|\n| `*.namedCredential-meta.xml` | `configure-named-credential.sh` |\n| `*.externalCredential-meta.xml` | `configure-named-credential.sh` |\n\n## Troubleshooting\n\n**\"sf: command not found\"**\n- Install Salesforce CLI: `npm install -g @salesforce/cli`\n\n**\"Not authenticated\"**\n- Run: `sf org login web -a \u003calias>`\n\n**\"External Credential not found\"**\n- Deploy External Credential first\n- Check developer name matches\n\n## Related Documentation\n\n- [Named Credentials Automation Guide](../references/named-credentials-automation.md)\n- [Named Credentials Template Reference](../references/named-credentials-guide.md)\n- [Security Best Practices](../references/security-best-practices.md)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2911,"content_sha256":"5bcd6461b7c779d3f3c66a8c84da6530a51034b99006b8cc27cdcdecbc5471fd"},{"filename":"scripts/set-api-credential.sh","content":"#!/bin/bash\n#\n# set-api-credential.sh\n#\n# Simple script to set API credentials programmatically using Custom Settings\n# Alternative to Named Credentials that allows full automation\n#\n# Usage:\n# ./set-api-credential.sh \u003csetting-name> \u003capi-key> \u003corg-alias>\n#\n# Example:\n# ./set-api-credential.sh BlandAI sk_live_abc123 AIZoom\n#\n# For Named Credentials, use this to store the API key in a Custom Setting,\n# then reference it in your Apex code\n#\n\nset -e\n\n# Colors\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nBLUE='\\033[0;34m'\nNC='\\033[0m'\n\nusage() {\n echo -e \"${BLUE}Usage:${NC}\"\n echo \" $0 \u003csetting-name> \u003capi-key> \u003corg-alias>\"\n echo \"\"\n echo -e \"${BLUE}Example:${NC}\"\n echo \" $0 BlandAI sk_live_abc123xyz AIZoom\"\n echo \"\"\n echo -e \"${BLUE}Or use secure input (recommended):${NC}\"\n echo \" $0 BlandAI - AIZoom\"\n echo \" (Script will prompt for API key securely)\"\n exit 1\n}\n\nif [ $# -ne 3 ]; then\n usage\nfi\n\nSETTING_NAME=$1\nAPI_KEY=$2\nORG_ALIAS=$3\n\n# If API key is \"-\", prompt securely\nif [ \"$API_KEY\" = \"-\" ]; then\n echo -e \"${YELLOW}Enter API key (input hidden):${NC}\"\n read -s API_KEY\n echo \"\"\nfi\n\n# Validate inputs\nif [ -z \"$API_KEY\" ]; then\n echo -e \"${RED}Error: API key cannot be empty${NC}\"\n exit 1\nfi\n\n# Validate org\necho -e \"${BLUE}Validating org connection...${NC}\"\nif ! sf org display --target-org \"$ORG_ALIAS\" &> /dev/null; then\n echo -e \"${RED}Error: Cannot connect to org '$ORG_ALIAS'${NC}\"\n exit 1\nfi\n\necho -e \"${GREEN}✓ Connected to org: $ORG_ALIAS${NC}\"\n\n# Check if Custom Setting exists\necho -e \"${BLUE}Checking for API_Credentials__c Custom Setting...${NC}\"\n\nSETTING_CHECK=$(sf data query \\\n --query \"SELECT Id FROM API_Credentials__c WHERE Name = '$SETTING_NAME' LIMIT 1\" \\\n --target-org \"$ORG_ALIAS\" \\\n --json 2>&1 || echo '{\"status\":1}')\n\nif echo \"$SETTING_CHECK\" | grep -q \"sObject type 'API_Credentials__c' is not supported\"; then\n echo -e \"${YELLOW}⚠️ API_Credentials__c Custom Setting not found${NC}\"\n echo \"\"\n echo -e \"${BLUE}Creating Custom Setting...${NC}\"\n\n # Create the Custom Setting metadata\n mkdir -p force-app/main/default/objects/API_Credentials__c\n\n cat > force-app/main/default/objects/API_Credentials__c/API_Credentials__c.object-meta.xml \u003c\u003c 'EOF'\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\u003cCustomObject xmlns=\"http://soap.sforce.com/2006/04/metadata\">\n \u003ccustomSettingsType>Hierarchy\u003c/customSettingsType>\n \u003cenableFeeds>false\u003c/enableFeeds>\n \u003clabel>API Credentials\u003c/label>\n \u003cvisibility>Protected\u003c/visibility>\n\u003c/CustomObject>\nEOF\n\n cat > force-app/main/default/objects/API_Credentials__c/fields/API_Key__c.field-meta.xml \u003c\u003c 'EOF'\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\u003cCustomField xmlns=\"http://soap.sforce.com/2006/04/metadata\">\n \u003cfullName>API_Key__c\u003c/fullName>\n \u003clabel>API Key\u003c/label>\n \u003ctype>Text\u003c/type>\n \u003clength>255\u003c/length>\n \u003crequired>false\u003c/required>\n\u003c/CustomField>\nEOF\n\n echo -e \"${BLUE}Deploying Custom Setting...${NC}\"\n sf project deploy start \\\n --source-dir force-app/main/default/objects/API_Credentials__c \\\n --target-org \"$ORG_ALIAS\" \\\n --wait 10\n\n echo -e \"${GREEN}✓ Custom Setting created${NC}\"\nfi\n\n# Insert or update the credential\nEXISTING_ID=$(echo \"$SETTING_CHECK\" | jq -r '.result.records[0].Id // empty' 2>/dev/null)\n\nif [ -z \"$EXISTING_ID\" ]; then\n echo -e \"${BLUE}Creating new credential record...${NC}\"\n sf data create record \\\n --sobject API_Credentials__c \\\n --values \"Name='$SETTING_NAME' API_Key__c='$API_KEY'\" \\\n --target-org \"$ORG_ALIAS\"\n echo -e \"${GREEN}✓ Credential created${NC}\"\nelse\n echo -e \"${BLUE}Updating existing credential...${NC}\"\n sf data update record \\\n --sobject API_Credentials__c \\\n --record-id \"$EXISTING_ID\" \\\n --values \"API_Key__c='$API_KEY'\" \\\n --target-org \"$ORG_ALIAS\"\n echo -e \"${GREEN}✓ Credential updated${NC}\"\nfi\n\necho \"\"\necho -e \"${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\"\necho -e \"${GREEN}✓ API Credential configured successfully!${NC}\"\necho -e \"${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\"\necho \"\"\necho -e \"${BLUE}How to use in Apex:${NC}\"\necho \"\"\necho -e \"${YELLOW}API_Credentials__c cred = API_Credentials__c.getInstance('$SETTING_NAME');${NC}\"\necho -e \"${YELLOW}String apiKey = cred.API_Key__c;${NC}\"\necho -e \"${YELLOW}req.setHeader('Authorization', apiKey);${NC}\"\necho \"\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":4711,"content_sha256":"7643a227aa65a4c377579229c6d3cbce6eb7eb35b6b5f2020360b61a8c056fac"},{"filename":"scripts/templates/setup-credentials-with-csp.sh","content":"#!/bin/bash\n#\n# Setup Credentials Template with CSP Trusted Sites / Remote Site Settings\n#\n# TEMPLATE FOR CREATING NEW INTEGRATION SKILLS\n#\n# This template includes automatic deployment of endpoint security\n# (CSP Trusted Sites or Remote Site Settings) along with credential configuration.\n#\n# How to use this template:\n# 1. Copy to your skill: cp scripts/templates/setup-credentials-with-csp.sh my-skill/scripts/setup-credentials.sh\n# 2. Replace all {{PLACEHOLDERS}} with your values\n# 3. Create corresponding .cspTrustedSite-meta.xml and .remoteSite-meta.xml in assets/\n# 4. Test with your API\n#\n\nset -e\n\n# Colors\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nBLUE='\\033[0;34m'\nCYAN='\\033[0;36m'\nNC='\\033[0m'\n\n# Configuration - REPLACE THESE\nSKILL_NAME=\"{{SkillName}}\" # e.g., \"Bland.ai\", \"Stripe\", \"Twilio\"\nCUSTOM_SETTING_NAME=\"{{SettingName}}\" # e.g., \"BlandAI\", \"StripeAPI\", \"TwilioAPI\"\nCSP_NAME=\"{{CSPName}}\" # e.g., \"BlandAPI\", \"StripeAPI\", \"TwilioAPI\"\nAPI_KEY_URL=\"{{APIKeyURL}}\" # e.g., \"https://app.bland.ai/settings/api\"\n\n# Banner\necho -e \"${CYAN}\"\ncat \u003c\u003c EOF\n╔══════════════════════════════════════════════════════════════╗\n║ ║\n║ ${SKILL_NAME} Integration - Credential Setup ║\n║ ║\n╚══════════════════════════════════════════════════════════════╝\nEOF\necho -e \"${NC}\"\n\n# Usage\nusage() {\n echo -e \"${BLUE}Usage:${NC}\"\n echo \" $0 \u003corg-alias>\"\n echo \"\"\n echo -e \"${BLUE}Example:${NC}\"\n echo \" $0 AIZoom\"\n echo \"\"\n echo -e \"${YELLOW}Get your ${SKILL_NAME} API key at:${NC}\"\n echo -e \" ${CYAN}${API_KEY_URL}${NC}\"\n exit 1\n}\n\nif [ $# -ne 1 ]; then\n echo -e \"${RED}Error: Missing org alias${NC}\"\n echo \"\"\n usage\nfi\n\nORG_ALIAS=$1\n\n# Validate sf CLI\nif ! command -v sf &> /dev/null; then\n echo -e \"${RED}Error: Salesforce CLI (sf) is not installed${NC}\"\n exit 1\nfi\n\n# Validate org\necho -e \"${BLUE}► Validating org connection...${NC}\"\nif ! sf org display --target-org \"$ORG_ALIAS\" &> /dev/null; then\n echo -e \"${RED}✗ Cannot connect to org '$ORG_ALIAS'${NC}\"\n exit 1\nfi\necho -e \"${GREEN}✓ Connected to org: $ORG_ALIAS${NC}\"\necho \"\"\n\n# Get API key\necho -e \"${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\"\necho -e \"${YELLOW}Enter your ${SKILL_NAME} API key${NC}\"\necho -e \"${CYAN}Get it from: ${API_KEY_URL}${NC}\"\necho -e \"${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\"\necho \"\"\necho -e \"${BLUE}API Key (input hidden):${NC}\"\nread -s API_KEY\necho \"\"\n\nif [ -z \"$API_KEY\" ]; then\n echo -e \"${RED}Error: API key cannot be empty${NC}\"\n exit 1\nfi\n\necho -e \"${GREEN}✓ API key received${NC}\"\necho \"\"\n\n# Configure Custom Setting (same logic as before)\necho -e \"${BLUE}► Checking for API_Credentials__c Custom Setting...${NC}\"\n# ... [rest of Custom Setting logic - same as bland-ai-calls] ...\n\n# Configure CSP Trusted Site / Remote Site Settings\necho -e \"${BLUE}► Configuring endpoint security (CSP Trusted Sites)...${NC}\"\n\nCSP_CHECK=$(sf data query \\\n --query \"SELECT Id FROM CspTrustedSite WHERE DeveloperName = '${CSP_NAME}' LIMIT 1\" \\\n --target-org \"$ORG_ALIAS\" \\\n --json 2>&1 || echo '{\"status\":1}')\n\nif echo \"$CSP_CHECK\" | grep -q \"sObject type 'CspTrustedSite' is not supported\"; then\n # Fallback to Remote Site Settings\n echo -e \"${YELLOW}⚠ CSP Trusted Sites not supported. Using Remote Site Settings...${NC}\"\n\n REMOTE_SITE_CHECK=$(sf data query \\\n --query \"SELECT Id FROM RemoteSiteSetting WHERE SiteName = '${CSP_NAME}' LIMIT 1\" \\\n --target-org \"$ORG_ALIAS\" \\\n --json 2>&1 || echo '{\"status\":1}')\n\n REMOTE_SITE_ID=$(echo \"$REMOTE_SITE_CHECK\" | jq -r '.result.records[0].Id // empty' 2>/dev/null)\n\n if [ -z \"$REMOTE_SITE_ID\" ]; then\n SCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n SKILL_DIR=\"$(dirname \"$SCRIPT_DIR\")\"\n\n sf project deploy start \\\n --source-dir \"$SKILL_DIR/assets/${CSP_NAME}.remoteSite-meta.xml\" \\\n --target-org \"$ORG_ALIAS\" \\\n --wait 5 > /dev/null 2>&1 || true\n\n echo -e \"${GREEN}✓ Remote Site Setting configured${NC}\"\n else\n echo -e \"${GREEN}✓ Remote Site Setting already exists${NC}\"\n fi\nelse\n CSP_ID=$(echo \"$CSP_CHECK\" | jq -r '.result.records[0].Id // empty' 2>/dev/null)\n\n if [ -z \"$CSP_ID\" ]; then\n SCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n SKILL_DIR=\"$(dirname \"$SCRIPT_DIR\")\"\n\n sf project deploy start \\\n --source-dir \"$SKILL_DIR/assets/${CSP_NAME}.cspTrustedSite-meta.xml\" \\\n --target-org \"$ORG_ALIAS\" \\\n --wait 5 > /dev/null 2>&1 || true\n\n echo -e \"${GREEN}✓ CSP Trusted Site configured${NC}\"\n else\n echo -e \"${GREEN}✓ CSP Trusted Site already exists${NC}\"\n fi\nfi\n\necho \"\"\necho -e \"${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\"\necho -e \"${GREEN}✓ ${SKILL_NAME} integration configured successfully!${NC}\"\necho -e \"${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\"\necho \"\"\n\necho -e \"${BLUE}🎉 Setup complete!${NC}\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":6003,"content_sha256":"dc7def30b1df8f296a41fe29132c65e6038a658f72b826e2a76bfac105f14f91"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"sf-integration: Salesforce Integration Patterns Expert","type":"text"}]},{"type":"paragraph","content":[{"text":"Use this skill when the user needs ","type":"text"},{"text":"integration architecture and runtime plumbing","type":"text","marks":[{"type":"strong"}]},{"text":": Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, CDC, and event-driven integration design.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"When This Skill Owns the Task","type":"text"}]},{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"sf-integration","type":"text","marks":[{"type":"code_inline"}]},{"text":" when the work involves:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":".namedCredential-meta.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":" or External Credential metadata","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"outbound REST/SOAP callouts","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"External Service registration from OpenAPI specs","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Platform Events, CDC, and event-driven architecture","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"choosing sync vs async integration patterns","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Delegate elsewhere when the user is:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"configuring the OAuth app itself → ","type":"text"},{"text":"sf-connected-apps","type":"text","marks":[{"type":"link","attrs":{"href":"../sf-connected-apps/SKILL.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"writing Apex-only business logic → ","type":"text"},{"text":"sf-apex","type":"text","marks":[{"type":"link","attrs":{"href":"../sf-apex/SKILL.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"deploying metadata → ","type":"text"},{"text":"sf-deploy","type":"text","marks":[{"type":"link","attrs":{"href":"../sf-deploy/SKILL.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"importing/exporting data → ","type":"text"},{"text":"sf-data","type":"text","marks":[{"type":"link","attrs":{"href":"../sf-data/SKILL.md","title":null}}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Required Context to Gather First","type":"text"}]},{"type":"paragraph","content":[{"text":"Ask for or infer:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"integration style: outbound callout, inbound event, External Service, CDC, platform event","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"auth method","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"sync vs async requirement","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"system endpoint / spec details","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rate limits, retry expectations, and failure tolerance","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"whether this is net-new design or repair of an existing integration","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Recommended Workflow","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"1. Choose the integration pattern","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Need","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Default pattern","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"authenticated outbound API call","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Named Credential / External Credential + Apex or Flow","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"spec-driven API client","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"External Service","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"trigger-originated callout","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"async callout pattern","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"decoupled event publishing","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Platform Events","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"change-stream consumption","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CDC","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"2. Choose the auth model","type":"text"}]},{"type":"paragraph","content":[{"text":"Prefer secure runtime-managed auth:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Named Credentials / External Credentials","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"OAuth or JWT via the right credential model","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"no hardcoded secrets in code","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"3. Generate from the right templates","type":"text"}]},{"type":"paragraph","content":[{"text":"Use the provided assets under:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/named-credentials/","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/external-credentials/","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/external-services/","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/callouts/","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/platform-events/","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/cdc/","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/soap/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"4. Validate operational safety","type":"text"}]},{"type":"paragraph","content":[{"text":"Check:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"timeout and retry handling","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"async strategy for trigger-originated work","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"logging / observability","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"event retention and subscriber implications","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"5. Hand off deployment or implementation details","type":"text"}]},{"type":"paragraph","content":[{"text":"Use:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"sf-deploy","type":"text","marks":[{"type":"link","attrs":{"href":"../sf-deploy/SKILL.md","title":null}}]},{"text":" for deployment","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"sf-apex","type":"text","marks":[{"type":"link","attrs":{"href":"../sf-apex/SKILL.md","title":null}}]},{"text":" for deeper service / retry code","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"sf-flow","type":"text","marks":[{"type":"link","attrs":{"href":"../sf-flow/SKILL.md","title":null}}]},{"text":" for declarative HTTP callout orchestration","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"High-Signal Rules","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"never hardcode credentials","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"do not do synchronous callouts from triggers","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"define timeout behavior explicitly","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"plan retries for transient failures","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"use middleware / event-driven patterns when outbound volume is high","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"prefer External Credentials architecture for new development when supported","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Common anti-patterns:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"sync trigger callouts","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"no retry or dead-letter strategy","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"no request/response logging","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"mixing auth setup responsibilities with runtime integration design","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Output Format","type":"text"}]},{"type":"paragraph","content":[{"text":"When finishing, report in this order:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Integration pattern chosen","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Auth model chosen","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Files created or updated","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Operational safeguards","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Deployment / testing next step","type":"text","marks":[{"type":"strong"}]}]}]}]},{"type":"paragraph","content":[{"text":"Suggested shape:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"text"},"content":[{"text":"Integration: \u003csummary>\nPattern: \u003cnamed credential / external service / event / cdc / callout>\nFiles: \u003cpaths>\nSafety: \u003ctimeouts, retries, async, logging>\nNext step: \u003cdeploy, register, test, or implement>","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Cross-Skill Integration","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Need","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Delegate to","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Reason","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OAuth app setup","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sf-connected-apps","type":"text","marks":[{"type":"link","attrs":{"href":"../sf-connected-apps/SKILL.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"consumer key / cert / app config","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"advanced callout service code","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sf-apex","type":"text","marks":[{"type":"link","attrs":{"href":"../sf-apex/SKILL.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Apex implementation","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"declarative HTTP callout / Flow wrapper","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sf-flow","type":"text","marks":[{"type":"link","attrs":{"href":"../sf-flow/SKILL.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Flow orchestration","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"deploy integration metadata","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sf-deploy","type":"text","marks":[{"type":"link","attrs":{"href":"../sf-deploy/SKILL.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"validation and rollout","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"use integration from Agentforce","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sf-ai-agentscript","type":"text","marks":[{"type":"link","attrs":{"href":"../sf-ai-agentscript/SKILL.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"agent action composition","type":"text"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Reference Map","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Start here","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/named-credentials-guide.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/named-credentials-guide.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/external-services-guide.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/external-services-guide.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/callout-patterns.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/callout-patterns.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/security-best-practices.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/security-best-practices.md","title":null}}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Event-driven / platform patterns","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/event-patterns.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/event-patterns.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/platform-events-guide.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/platform-events-guide.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/cdc-guide.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/cdc-guide.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/event-driven-architecture-guide.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/event-driven-architecture-guide.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/messaging-api-v2.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/messaging-api-v2.md","title":null}}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"CLI / automation / scoring","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/cli-reference.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/cli-reference.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/named-credentials-automation.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/named-credentials-automation.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/scoring-rubric.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/scoring-rubric.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/","type":"text","marks":[{"type":"link","attrs":{"href":"assets/","title":null}}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Score Guide","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Score","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Meaning","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"108+","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"strong production-ready integration design","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"90–107","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"good design with some hardening left","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"72–89","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"workable but needs architectural review","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\u003c 72","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"unsafe / incomplete for deployment","type":"text"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"sf-integration","author":"@skillopedia","source":{"stars":416,"repo_name":"sf-skills","origin_url":"https://github.com/jaganpro/sf-skills/blob/HEAD/skills/sf-integration/SKILL.md","repo_owner":"jaganpro","body_sha256":"4b95e76dea224ca05cb845d65c55af32e61fbd9c9aa8a45673579241b6aacc9a","cluster_key":"c06f0955de87df8158c678ce6d96743a1462efc2af4f0f035cc9126d0bb17b1d","clean_bundle":{"format":"clean-skill-bundle-v1","source":"jaganpro/sf-skills/skills/sf-integration/SKILL.md","attachments":[{"id":"35be3451-1a39-578a-abbf-ed79064be5ea","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/35be3451-1a39-578a-abbf-ed79064be5ea/attachment.md","path":"CREDITS.md","size":3954,"sha256":"b0c7586c68aec45583117b6b00f4378b8112d4279e693edac23b021d60a8a003","contentType":"text/markdown; charset=utf-8"},{"id":"201fae46-52b1-51c4-89e6-45dd1c658832","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/201fae46-52b1-51c4-89e6-45dd1c658832/attachment.md","path":"README.md","size":3691,"sha256":"02151d251879745e9ff98a01ae2a51b1f70de16d9f79ba094df655bf94768905","contentType":"text/markdown; charset=utf-8"},{"id":"4b9eeab8-b462-53f0-84cd-78636eacc289","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4b9eeab8-b462-53f0-84cd-78636eacc289/attachment.cls","path":"assets/callouts/callout-retry-handler.cls","size":6166,"sha256":"5881539b8a1996faa8e7a7b8bd3dc4fe54458a990b69e5a78e9a5d2bc71b0772","contentType":"text/x-tex"},{"id":"93bd850b-d19f-58e0-8732-dcb7e1e152ef","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/93bd850b-d19f-58e0-8732-dcb7e1e152ef/attachment.cls","path":"assets/callouts/http-response-handler.cls","size":8729,"sha256":"deafd133eaebeeb4fa60f30b1255084a99f4d88858ab273adcbbb5f0ac00ac75","contentType":"text/x-tex"},{"id":"87218f52-e3bf-513d-85a4-a2cbe5a106f2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/87218f52-e3bf-513d-85a4-a2cbe5a106f2/attachment.cls","path":"assets/callouts/rest-queueable-callout.cls","size":8974,"sha256":"5b273fbccf572204ee671610eb85fe6b61cd032d17850d7ee59d19cf63b86b6b","contentType":"text/x-tex"},{"id":"39a89478-5974-57f5-9be9-0eb0d234d6bf","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/39a89478-5974-57f5-9be9-0eb0d234d6bf/attachment.cls","path":"assets/callouts/rest-sync-callout.cls","size":7589,"sha256":"4faa1c89a4d4341f4f0865b36106f38ff2b2676f5accbceac458a573f327a644","contentType":"text/x-tex"},{"id":"19e82697-e729-5aa9-b51d-edda8781cfaf","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/19e82697-e729-5aa9-b51d-edda8781cfaf/attachment.cls","path":"assets/cdc/cdc-handler.cls","size":8571,"sha256":"584693b9884cf9d44137a57037623021e54777d1e2dfaba43049ef8d7f4b165d","contentType":"text/x-tex"},{"id":"374f17c3-217a-54f4-8c31-6774dff372bd","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/374f17c3-217a-54f4-8c31-6774dff372bd/attachment.trigger","path":"assets/cdc/cdc-subscriber-trigger.trigger","size":5444,"sha256":"0c9698e712bf7285ef4c663d763f633ecaefd236525965234d007a09ea69b1a5","contentType":"text/plain; charset=utf-8"},{"id":"aa545456-06ba-5e60-94b1-576fe6ba0215","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/aa545456-06ba-5e60-94b1-576fe6ba0215/attachment.xml","path":"assets/endpoint-security/example.cspTrustedSite-meta.xml","size":1878,"sha256":"9db74f0a52e25d1c216d058c940e77c188abd6fb543d781a8a511a9759b41e1c","contentType":"application/xml"},{"id":"6bb922e0-7474-51ce-994c-4788ce7a4a0e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6bb922e0-7474-51ce-994c-4788ce7a4a0e/attachment.xml","path":"assets/endpoint-security/example.remoteSite-meta.xml","size":1362,"sha256":"0cf603f49d7cdbeed97abc81466eb314ff4635a59590b7a4a40f8bb7c79cd0b0","contentType":"application/xml"},{"id":"c2d95ab5-0012-51d0-aff3-ab5ae7d16344","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c2d95ab5-0012-51d0-aff3-ab5ae7d16344/attachment.xml","path":"assets/external-credentials/jwt-external-credential.externalCredential-meta.xml","size":3237,"sha256":"062eec122091ce5455ddde85be0dba228790902775e6b886700ece1d294eacd0","contentType":"application/xml"},{"id":"0ec5e72a-06de-5e7d-97d5-abd7bce2d072","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0ec5e72a-06de-5e7d-97d5-abd7bce2d072/attachment.xml","path":"assets/external-credentials/oauth-external-credential.externalCredential-meta.xml","size":3036,"sha256":"837162a79d3dd588e2a93d98fbc162ce25f943f96a708e818f61ad98f77b8e16","contentType":"application/xml"},{"id":"797d7bc1-d8c9-5852-8a97-86c107ee1044","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/797d7bc1-d8c9-5852-8a97-86c107ee1044/attachment.md","path":"assets/external-services/external-service-operations.md","size":5586,"sha256":"3d99750cedcb23c30a3dae48320eca1a84115e5369415125a498778bb435a2b4","contentType":"text/markdown; charset=utf-8"},{"id":"b55795c2-cb6c-5d60-930b-3795e0f21d05","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b55795c2-cb6c-5d60-930b-3795e0f21d05/attachment.xml","path":"assets/external-services/openapi-registration.externalServiceRegistration-meta.xml","size":4947,"sha256":"5e73e384a2bdea8f9d88228a13fe8969757f027a612298690e3484da7f668c11","contentType":"application/xml"},{"id":"07d8108a-cb27-55ae-8207-5bbe4e899d79","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/07d8108a-cb27-55ae-8207-5bbe4e899d79/attachment.xml","path":"assets/named-credentials/certificate-auth.namedCredential-meta.xml","size":2205,"sha256":"bad234c82c0111ddd3e33c3f5911df9e724cc48cd6c798fc6682ece75ac1db0c","contentType":"application/xml"},{"id":"07b896c9-1242-52f7-8da4-5c7522b8129b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/07b896c9-1242-52f7-8da4-5c7522b8129b/attachment.xml","path":"assets/named-credentials/custom-auth.namedCredential-meta.xml","size":2267,"sha256":"799026a374ad192c9338d4581e646b360e3bda54b83355b5f60b6222c47336ab","contentType":"application/xml"},{"id":"7b1a46b1-2f35-5d8f-9d1b-c9507836733a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7b1a46b1-2f35-5d8f-9d1b-c9507836733a/attachment.xml","path":"assets/named-credentials/oauth-client-credentials.namedCredential-meta.xml","size":1857,"sha256":"7f27dbd8fa022ad66e90cbbc732ac76585906f57aeecc94350f7a6dd1230c9ee","contentType":"application/xml"},{"id":"6a3fb1d2-f972-5672-8428-7d92dc9e892b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6a3fb1d2-f972-5672-8428-7d92dc9e892b/attachment.xml","path":"assets/named-credentials/oauth-jwt-bearer.namedCredential-meta.xml","size":2242,"sha256":"6978b0992d7e56c7b249b0d360d14ad8e0a973e364f1985a4bee6e8fad5f0767","contentType":"application/xml"},{"id":"d4c44f08-4065-595e-9887-ab04e2330166","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d4c44f08-4065-595e-9887-ab04e2330166/attachment.cls","path":"assets/platform-events/event-publisher.cls","size":6580,"sha256":"b474341e295d31420540ca795e873b021e555dd52c6942188f6bfba7e97e72a5","contentType":"text/x-tex"},{"id":"85ea8bb5-4f6c-5958-9944-9c9053b590ff","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/85ea8bb5-4f6c-5958-9944-9c9053b590ff/attachment.cls","path":"assets/platform-events/event-subscriber-action.cls","size":9754,"sha256":"5953b26419b39b64954bec1c05c27bfe5cfcd8f69207f28748065335fe234ef1","contentType":"text/x-tex"},{"id":"6b7cb9d0-c285-5798-9837-f050708c8706","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6b7cb9d0-c285-5798-9837-f050708c8706/attachment.trigger","path":"assets/platform-events/event-subscriber-trigger.trigger","size":3958,"sha256":"f0c091c66d2f6c5372c5b04ca8c6c4d7b3da13e4be93bafec220e9893b8dbaef","contentType":"text/plain; charset=utf-8"},{"id":"52aa7007-a1e1-522f-bdf1-937f51c25ab1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/52aa7007-a1e1-522f-bdf1-937f51c25ab1/attachment.xml","path":"assets/platform-events/platform-event-definition.object-meta.xml","size":3800,"sha256":"5e61d31d7a816917436be5369161e6a14bdeb35ec39b5caad00a5d96ca5b19e9","contentType":"application/xml"},{"id":"72223510-6561-5d60-8ab4-a10e51d960d6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/72223510-6561-5d60-8ab4-a10e51d960d6/attachment.cls","path":"assets/soap/soap-callout-service.cls","size":6580,"sha256":"fa94e27e5a4edd1cf47391cb514da5547aaaddbcc77705e40e95b1c328b359dc","contentType":"text/x-tex"},{"id":"9ebadfd0-a2cb-5a2b-bd88-fef247634465","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9ebadfd0-a2cb-5a2b-bd88-fef247634465/attachment.md","path":"assets/soap/wsdl2apex-guide.md","size":6348,"sha256":"6195e9f690d783b2d047769298b1652327721dffea9648107094a629b0415336","contentType":"text/markdown; charset=utf-8"},{"id":"7ea57a0a-a208-5e5d-a041-a30040a2b401","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7ea57a0a-a208-5e5d-a041-a30040a2b401/attachment.py","path":"hooks/scripts/suggest_credential_setup.py","size":9466,"sha256":"fd3fd470d2d3e1b6ab683c16aa3ee7f9129b9ee58e1b458a1c7112ff46c787dc","contentType":"text/x-python; charset=utf-8"},{"id":"da1c0e7d-695f-5c9e-b570-637504e18429","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/da1c0e7d-695f-5c9e-b570-637504e18429/attachment.py","path":"hooks/scripts/validate_integration.py","size":13738,"sha256":"c4774545553a418f3df9c9a68586aad735a7a9e290f0b38a39a93560b858bb96","contentType":"text/x-python; charset=utf-8"},{"id":"883d5cb8-dc78-589d-9270-113946d1526d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/883d5cb8-dc78-589d-9270-113946d1526d/attachment.md","path":"references/callout-patterns.md","size":22053,"sha256":"0f90de0e09372b5d79eb0e8ff9c6aaec9b88916565969aa0a4f3b9c5c5f0857a","contentType":"text/markdown; charset=utf-8"},{"id":"2e1e72ba-2456-5354-92f3-2427d73c7593","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2e1e72ba-2456-5354-92f3-2427d73c7593/attachment.md","path":"references/cdc-guide.md","size":8387,"sha256":"d15d0f4a08f9b07d1c135cb362a95da7e1fccbd4907f4515d23e0b3212c3d14a","contentType":"text/markdown; charset=utf-8"},{"id":"913c1e69-0896-5d03-b56c-47af50a8dc86","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/913c1e69-0896-5d03-b56c-47af50a8dc86/attachment.md","path":"references/cli-reference.md","size":2936,"sha256":"c86e410c06c8d6766f8d59f4ee0617f01d8b577b23a24ecb4d59e707e6ae87fd","contentType":"text/markdown; charset=utf-8"},{"id":"025d93a3-b335-5f59-bccd-61dead47eb6d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/025d93a3-b335-5f59-bccd-61dead47eb6d/attachment.md","path":"references/event-driven-architecture-guide.md","size":10275,"sha256":"23039fc1fd8efaf29af87d1e581063af1d6f96261406f2bdee08e940d479fbfc","contentType":"text/markdown; charset=utf-8"},{"id":"138663ca-6cfb-5428-bc25-32a13ce0a00c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/138663ca-6cfb-5428-bc25-32a13ce0a00c/attachment.md","path":"references/event-patterns.md","size":27722,"sha256":"6aa1926e1fade2bc46f0a687e5f5d0a03101114d0fb81aba5431daa654d1af19","contentType":"text/markdown; charset=utf-8"},{"id":"190d67df-8945-5e41-a4c1-1b5833b42448","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/190d67df-8945-5e41-a4c1-1b5833b42448/attachment.md","path":"references/external-services-guide.md","size":8425,"sha256":"324c56e1e29d2e76daa4fc404ab9b3d7158fccca876a9d83544df55cabee0598","contentType":"text/markdown; charset=utf-8"},{"id":"742e72a0-0f63-5a2a-abe1-66d0c3bd7fea","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/742e72a0-0f63-5a2a-abe1-66d0c3bd7fea/attachment.md","path":"references/messaging-api-v2.md","size":18249,"sha256":"2671c42347ad8ea429a80fbac2f210ffcb798ee55eae5ef4adf99709ab6d1963","contentType":"text/markdown; charset=utf-8"},{"id":"9975903d-f459-5785-893e-e5b13a3c3f8c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9975903d-f459-5785-893e-e5b13a3c3f8c/attachment.md","path":"references/named-credentials-automation.md","size":6263,"sha256":"233cf327cfefce2371c00a47d30f73cc8ce1039e4a501ecbb500d1496a9fb741","contentType":"text/markdown; charset=utf-8"},{"id":"65b5e2e6-0f93-5350-b0f8-c729bcb2bb46","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/65b5e2e6-0f93-5350-b0f8-c729bcb2bb46/attachment.md","path":"references/named-credentials-guide.md","size":6677,"sha256":"d621edf6533e89d617988a7630ac2b1c8cfb617d67266ec8b4ff6880eb4b5b8c","contentType":"text/markdown; charset=utf-8"},{"id":"a44add57-c6c7-5162-baf5-d74d9e696fbd","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a44add57-c6c7-5162-baf5-d74d9e696fbd/attachment.md","path":"references/platform-events-guide.md","size":6748,"sha256":"c0440a3aff5a177999792c71c61ac15d530f957a063866120bf98290d83aa497","contentType":"text/markdown; charset=utf-8"},{"id":"bea9337a-0acb-518e-8346-968cf8c82197","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/bea9337a-0acb-518e-8346-968cf8c82197/attachment.md","path":"references/rest-callout-patterns.md","size":8968,"sha256":"0800e90e025fbab4371c79714e7e6d97aee839e0ef431a537c2130ec8b27e282","contentType":"text/markdown; charset=utf-8"},{"id":"f0d2c820-7713-5c8c-9067-d2fbb129edcf","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f0d2c820-7713-5c8c-9067-d2fbb129edcf/attachment.md","path":"references/scoring-rubric.md","size":2465,"sha256":"066feeb2eddee7779028303da4719bfa6d980c383d14ba72a55c746ebe3f43ed","contentType":"text/markdown; charset=utf-8"},{"id":"06cf5dba-ed7e-5b1d-9ab1-babcdd565ee8","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/06cf5dba-ed7e-5b1d-9ab1-babcdd565ee8/attachment.md","path":"references/security-best-practices.md","size":5823,"sha256":"565462c2ef3f33a4527511e5883a5c21354bff03ba55e3b76dca88dcd9085ca7","contentType":"text/markdown; charset=utf-8"},{"id":"0b7184a6-0851-5067-ba04-efc169da1aa1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0b7184a6-0851-5067-ba04-efc169da1aa1/attachment.md","path":"scripts/README.md","size":2911,"sha256":"5bcd6461b7c779d3f3c66a8c84da6530a51034b99006b8cc27cdcdecbc5471fd","contentType":"text/markdown; charset=utf-8"},{"id":"87a1a031-0f09-567e-b007-776186aba61d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/87a1a031-0f09-567e-b007-776186aba61d/attachment.sh","path":"scripts/configure-named-credential.sh","size":8950,"sha256":"a1860a8fa23347a4c060af10ec0cf2a92b23c8a460fcf7d3dbd23b478d26db3e","contentType":"application/x-sh; charset=utf-8"},{"id":"b7d47295-57c4-57fc-878f-189786db9abf","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b7d47295-57c4-57fc-878f-189786db9abf/attachment.sh","path":"scripts/set-api-credential.sh","size":4711,"sha256":"7643a227aa65a4c377579229c6d3cbce6eb7eb35b6b5f2020360b61a8c056fac","contentType":"application/x-sh; charset=utf-8"},{"id":"d6341a36-74d1-5eed-8dc2-6c2514ed32a9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d6341a36-74d1-5eed-8dc2-6c2514ed32a9/attachment.sh","path":"scripts/templates/setup-credentials-with-csp.sh","size":6003,"sha256":"dc7def30b1df8f296a41fe29132c65e6038a658f72b826e2a76bfac105f14f91","contentType":"application/x-sh; charset=utf-8"}],"bundle_sha256":"28ad1cbf526aa519682eba1f124602873d16635032344c2bd732869085df4fa1","attachment_count":43,"text_attachments":41,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":2,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"skills/sf-integration/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"data-analytics","category_label":"Data"},"exact_dupes_collapsed_into_this":0},"license":"MIT","version":"v1","category":"data-analytics","metadata":{"author":"Jag Valaiyapathy","scoring":"120 points across 6 categories","version":"1.2.0"},"import_tag":"clean-skills-v1","description":"Salesforce integration architecture with 120-point scoring. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use sf-connected-apps), Apex-only logic (use sf-apex), or data import/export (use sf-data).\n"}},"renderedAt":1782980263954}

sf-integration: Salesforce Integration Patterns Expert Use this skill when the user needs integration architecture and runtime plumbing : Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, CDC, and event-driven integration design. When This Skill Owns the Task Use when the work involves: - or External Credential metadata - outbound REST/SOAP callouts - External Service registration from OpenAPI specs - Platform Events, CDC, and event-driven architecture - choosing sync vs async integration patterns Delegate elsewhere when the user is: - co…