sf-soql: Salesforce SOQL Query Expert Use this skill when the user needs SOQL/SOSL authoring or optimization : natural-language-to-query generation, relationship queries, aggregates, query-plan analysis, and performance/safety improvements for Salesforce queries. When This Skill Owns the Task Use when the work involves: - files - query generation from natural language - relationship queries and aggregate queries - query optimization and selectivity analysis - SOQL/SOSL syntax and governor-aware design Delegate elsewhere when the user is: - performing bulk data operations → sf-data - embedding…

, '', content, flags=re.MULTILINE)\n clean = re.sub(r'//.*

sf-soql: Salesforce SOQL Query Expert Use this skill when the user needs SOQL/SOSL authoring or optimization : natural-language-to-query generation, relationship queries, aggregates, query-plan analysis, and performance/safety improvements for Salesforce queries. When This Skill Owns the Task Use when the work involves: - files - query generation from natural language - relationship queries and aggregate queries - query optimization and selectivity analysis - SOQL/SOSL syntax and governor-aware design Delegate elsewhere when the user is: - performing bulk data operations → sf-data - embedding…

, '', clean, flags=re.MULTILINE)\n clean = re.sub(r'/\\*[\\s\\S]*?\\*/', '', clean)\n\n # Check for WHERE clause\n result['has_where_clause'] = bool(re.search(r'\\bWHERE\\b', clean, re.IGNORECASE))\n\n # Check for LIMIT\n result['has_limit'] = bool(re.search(r'\\bLIMIT\\s+\\d+', clean, re.IGNORECASE))\n\n # Check for ORDER BY\n result['has_order_by'] = bool(re.search(r'\\bORDER\\s+BY\\b', clean, re.IGNORECASE))\n\n # Check for hardcoded IDs (15 or 18 char alphanumeric in quotes)\n result['has_hardcoded_ids'] = bool(\n re.search(r\"'[a-zA-Z0-9]{15}'\", clean) or\n re.search(r\"'[a-zA-Z0-9]{18}'\", clean)\n )\n\n # Check for indexed fields in WHERE\n indexed_fields = ['Id', 'Name', 'OwnerId', 'CreatedDate', 'LastModifiedDate', 'RecordTypeId']\n where_match = re.search(r'\\bWHERE\\b(.*?)(?:\\bORDER\\b|\\bGROUP\\b|\\bLIMIT\\b|$)', clean, re.IGNORECASE | re.DOTALL)\n if where_match:\n where_clause = where_match.group(1)\n for field in indexed_fields:\n if re.search(rf'\\b{field}\\b', where_clause, re.IGNORECASE):\n result['uses_indexed_fields'] = True\n break\n\n # Syntax validation\n # Check for SELECT without FROM\n if re.search(r'\\bSELECT\\b', clean, re.IGNORECASE):\n if not re.search(r'\\bFROM\\b', clean, re.IGNORECASE):\n result['issues'].append({\n 'severity': 'HIGH',\n 'message': 'SELECT statement missing FROM clause'\n })\n result['is_valid'] = False\n\n # Check for SELECT *\n if re.search(r'\\bSELECT\\s+\\*', clean, re.IGNORECASE):\n result['issues'].append({\n 'severity': 'HIGH',\n 'message': 'SELECT * is not valid in SOQL - specify field names'\n })\n result['is_valid'] = False\n\n # Check for == instead of =\n if re.search(r'==', clean):\n result['issues'].append({\n 'severity': 'HIGH',\n 'message': 'Invalid operator \"==\" - use \"=\" in SOQL'\n })\n\n # Check for unbalanced parentheses\n if clean.count('(') != clean.count(')'):\n result['issues'].append({\n 'severity': 'HIGH',\n 'message': 'Unbalanced parentheses'\n })\n\n # Add recommendations\n if not result['has_where_clause']:\n result['recommendations'].append('Add WHERE clause for better query selectivity')\n\n if not result['has_limit']:\n result['recommendations'].append('Add LIMIT clause to prevent large result sets')\n\n if result['has_hardcoded_ids']:\n result['recommendations'].append('Avoid hardcoded IDs - use bind variables instead')\n\n if result['has_where_clause'] and not result['uses_indexed_fields']:\n result['recommendations'].append('Add an indexed field (Id, Name, CreatedDate) to WHERE for better performance')\n\n return result\n\n\ndef main():\n \"\"\"\n Main hook entry point.\n\n Reads hook input from stdin, validates SOQL files.\n \"\"\"\n try:\n # Read hook input from stdin\n hook_input = json.load(sys.stdin)\n\n # Extract file path from tool input\n tool_input = hook_input.get(\"tool_input\", {})\n file_path = tool_input.get(\"file_path\", \"\")\n\n # Check if operation was successful\n tool_response = hook_input.get(\"tool_response\", {})\n if not tool_response.get(\"success\", True):\n print(json.dumps({\"continue\": True}))\n return 0\n\n # Only validate .soql files\n result = {\"continue\": True}\n\n if file_path.lower().endswith(\".soql\"):\n result = validate_soql_file(file_path)\n\n # Output result\n print(json.dumps(result))\n return 0\n\n except json.JSONDecodeError:\n print(json.dumps({\"continue\": True}))\n return 0\n except Exception as e:\n print(json.dumps({\n \"continue\": True,\n \"output\": f\"⚠️ Hook error: {e}\"\n }))\n return 0\n\n\nif __name__ == \"__main__\":\n sys.exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":12149,"content_sha256":"bb0e77803ea62bfb39729a8331b7c1e230097cf3ba87ec754ee0d46b0a57ab56"},{"filename":"README.md","content":"# sf-soql\n\nSalesforce SOQL query generation, optimization, and analysis skill with 100-point scoring. Convert natural language into performant SOQL and validate queries for security, selectivity, and governor-limit awareness.\n\n## Features\n\n- **Natural Language to SOQL**: Convert requests into executable queries\n- **Query Optimization**: Improve selectivity, LIMIT usage, and field selection\n- **Relationship Queries**: Parent-child, child-parent, and polymorphic patterns\n- **Security Guidance**: `WITH USER_MODE`, `WITH SECURITY_ENFORCED`, and Apex-safe usage\n- **100-Point Scoring**: Performance, correctness, security, and readability checks\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-soql\n```\n\n## Quick Start\n\n### 1. Invoke the skill\n\n```\nSkill: sf-soql\nRequest: \"Find Accounts with open Opportunities created this quarter\"\n```\n\n### 2. Typical use cases\n\n- Generate SOQL from plain-English requirements\n- Optimize slow or non-selective queries\n- Build aggregates and relationship queries\n- Validate queries before using them in Apex or CLI workflows\n\n## Documentation\n\n- [SKILL.md](SKILL.md) - Core workflow and optimization guidance\n- [references/query-optimization.md](references/query-optimization.md) - Selectivity and performance tuning\n- [references/soql-syntax-reference.md](references/soql-syntax-reference.md) - Syntax, relationships, and aggregates\n- [references/selector-patterns.md](references/selector-patterns.md) - Reusable Apex selector patterns\n- [references/cli-commands.md](references/cli-commands.md) - sf CLI query execution examples\n\n## Related Skills\n\n- `sf-data` - For data creation/import/export workflows\n- `sf-apex` - For inline SOQL inside Apex code\n- `sf-testing` - For validating query behavior in tests\n\n## License\n\nMIT License. See the repository root [LICENSE](../../LICENSE).\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1939,"content_sha256":"65fa376c9e0ae3f14b443cf00cc1b7e02fe26500e632e3b7e8eea92502f055c6"},{"filename":"references/anti-patterns.md","content":"\u003c!-- Parent: sf-soql/SKILL.md -->\n# SOQL Anti-Patterns: What to Avoid\n\nA catalog of common SOQL mistakes and their solutions. Avoiding these patterns will help you stay within governor limits, improve query performance, and write more maintainable code.\n\n> **Sources**: [Apex Hours](https://www.apexhours.com/soql-best-practices/), [Beyond the Cloud](https://blog.beyondthecloud.dev/), [Medium - Bulkification Patterns](https://medium.com/@saurabh.samirs/salesforce-apex-triggers-5-bulkification-patterns-to-avoid-soql-dml-limits-f4e9c8bbfb3a)\n\n---\n\n## Anti-Pattern #1: SOQL Inside Loops\n\n**The Problem**: Executing queries inside a loop quickly exhausts the 100 SOQL query limit.\n\n```apex\n// ❌ ANTI-PATTERN: Query per record\nfor (Contact c : Trigger.new) {\n Account a = [SELECT Name FROM Account WHERE Id = :c.AccountId];\n c.Account_Name__c = a.Name;\n}\n// 200 contacts = 200 queries = LIMIT EXCEEDED\n```\n\n**The Solution**: Query once, use a Map for lookups.\n\n```apex\n// ✅ CORRECT: Single query with Map lookup\nSet\u003cId> accountIds = new Set\u003cId>();\nfor (Contact c : Trigger.new) {\n accountIds.add(c.AccountId);\n}\n\nMap\u003cId, Account> accountMap = new Map\u003cId, Account>(\n [SELECT Id, Name FROM Account WHERE Id IN :accountIds]\n);\n\nfor (Contact c : Trigger.new) {\n Account a = accountMap.get(c.AccountId);\n if (a != null) {\n c.Account_Name__c = a.Name;\n }\n}\n// 200 contacts = 1 query = SAFE\n```\n\n**Key Insight**: Collect IDs first, query once with `IN` clause, then use Map for O(1) lookups.\n\n---\n\n## Anti-Pattern #2: Non-Selective WHERE Clauses\n\n**The Problem**: Queries on non-indexed fields cause full table scans, which fail on large objects (100k+ records).\n\n```apex\n// ❌ ANTI-PATTERN: Non-selective filter\nSELECT Id FROM Lead WHERE Status = 'Open'\n// Status is not indexed - scans ALL Lead records\n```\n\n**The Solution**: Add an indexed field to make the query selective.\n\n```apex\n// ✅ CORRECT: Add indexed field filter\nSELECT Id FROM Lead\nWHERE Status = 'Open'\nAND CreatedDate = LAST_N_DAYS:30\n// CreatedDate is indexed - uses index\n\n// ✅ ALTERNATIVE: Use OwnerId (indexed)\nSELECT Id FROM Lead\nWHERE Status = 'Open'\nAND OwnerId = :UserInfo.getUserId()\n```\n\n**Indexed Fields** (Always use these in WHERE):\n- `Id`, `Name`, `OwnerId`, `CreatedDate`, `LastModifiedDate`\n- `RecordTypeId`, External ID fields, Master-Detail fields\n- Standard indexed fields: `Account.AccountNumber`, `Contact.Email`, `Case.CaseNumber`\n\n---\n\n## Anti-Pattern #3: Leading Wildcards\n\n**The Problem**: `LIKE '%value'` cannot use indexes and scans all records.\n\n```apex\n// ❌ ANTI-PATTERN: Leading wildcard\nSELECT Id FROM Account WHERE Name LIKE '%Corporation'\n// Cannot use index - full table scan\n```\n\n**The Solution**: Use trailing wildcards or exact matches.\n\n```apex\n// ✅ CORRECT: Trailing wildcard (uses index)\nSELECT Id FROM Account WHERE Name LIKE 'Acme%'\n\n// ✅ CORRECT: Exact match\nSELECT Id FROM Account WHERE Name = 'Acme Corporation'\n\n// ✅ CORRECT: Contains check (if absolutely necessary)\n// Do the filtering in Apex after a selective query\nList\u003cAccount> allAccounts = [\n SELECT Id, Name FROM Account\n WHERE CreatedDate = THIS_YEAR\n];\nList\u003cAccount> filtered = new List\u003cAccount>();\nfor (Account a : allAccounts) {\n if (a.Name.contains('Corporation')) {\n filtered.add(a);\n }\n}\n```\n\n---\n\n## Anti-Pattern #4: Negative Operators\n\n**The Problem**: `!=`, `NOT IN`, `NOT LIKE` often prevent index usage.\n\n```apex\n// ❌ ANTI-PATTERN: Negative operators\nSELECT Id FROM Opportunity WHERE StageName != 'Closed Lost'\nSELECT Id FROM Contact WHERE AccountId NOT IN :excludedIds\n```\n\n**The Solution**: Query for what you want, not what you don't want.\n\n```apex\n// ✅ CORRECT: Positive filter with specific values\nSELECT Id FROM Opportunity\nWHERE StageName IN ('Prospecting', 'Qualification', 'Proposal', 'Negotiation')\n\n// ✅ CORRECT: Use a formula field for complex exclusions\n// Create IsExcluded__c formula, then:\nSELECT Id FROM Contact WHERE IsExcluded__c = false\n```\n\n---\n\n## Anti-Pattern #5: Querying for NULL\n\n**The Problem**: `WHERE Field = null` is non-selective and scans all records.\n\n```apex\n// ❌ ANTI-PATTERN: Null check in WHERE\nSELECT Id FROM Contact WHERE Email = null\n// Non-selective - scans all contacts\n```\n\n**The Solution**: Combine with selective filters or redesign data model.\n\n```apex\n// ✅ CORRECT: Add selective filter\nSELECT Id FROM Contact\nWHERE Email = null\nAND CreatedDate = LAST_N_DAYS:30\n\n// ✅ BETTER: Use a checkbox field\n// Create HasEmail__c formula checkbox\nSELECT Id FROM Contact WHERE HasEmail__c = false\n```\n\n---\n\n## Anti-Pattern #6: SELECT * (All Fields)\n\n**The Problem**: Querying all fields wastes resources and can hit heap limits.\n\n```apex\n// ❌ ANTI-PATTERN: Selecting everything\nSELECT FIELDS(ALL) FROM Account LIMIT 200\n// Loads ALL fields into memory\n\n// ❌ ANTI-PATTERN: Listing every field manually\nSELECT Id, Name, Description, BillingStreet, BillingCity,\n BillingState, BillingPostalCode, BillingCountry, ...\nFROM Account\n```\n\n**The Solution**: Query only the fields you need.\n\n```apex\n// ✅ CORRECT: Minimal field selection\nSELECT Id, Name, Industry FROM Account\n\n// ✅ FOR DISPLAY: Just display fields\nSELECT Id, Name FROM Account\n\n// ✅ FOR PROCESSING: Just processing fields\nSELECT Id, Status__c, ProcessedDate__c FROM Account\n```\n\n---\n\n## Anti-Pattern #7: No LIMIT on Queries\n\n**The Problem**: Unbounded queries can return 50,000 records and consume heap memory.\n\n```apex\n// ❌ ANTI-PATTERN: No limit\nSELECT Id, Name FROM Account\n// Could return 50,000 records!\n\n// ❌ ANTI-PATTERN: Excessive limit\nSELECT Id, Name FROM Account LIMIT 50000\n```\n\n**The Solution**: Use appropriate limits for your use case.\n\n```apex\n// ✅ CORRECT: Reasonable limit for UI display\nSELECT Id, Name FROM Account LIMIT 200\n\n// ✅ CORRECT: Pagination\nSELECT Id, Name FROM Account\nORDER BY Name\nLIMIT 50 OFFSET 0\n\n// ✅ CORRECT: Single record lookup\nSELECT Id, Name FROM Account WHERE Name = 'Acme' LIMIT 1\n\n// ✅ CORRECT: Existence check\nSELECT Id FROM Account WHERE Name = 'Acme' LIMIT 1\n// In Apex: if (!results.isEmpty()) { /* exists */ }\n```\n\n---\n\n## Anti-Pattern #8: Deep Relationship Traversal\n\n**The Problem**: Deep nesting (>3 levels) hurts performance and readability.\n\n```apex\n// ❌ ANTI-PATTERN: Deep traversal\nSELECT Id,\n Account.Owner.Manager.Department.Name\nFROM Contact\n// 4 levels deep - hard to maintain, performance hit\n```\n\n**The Solution**: Flatten queries or use multiple queries.\n\n```apex\n// ✅ CORRECT: Flatten to 1-2 levels\nSELECT Id, Account.Name, Account.OwnerId FROM Contact\n\n// Then query Owner separately if needed\nMap\u003cId, User> owners = new Map\u003cId, User>(\n [SELECT Id, ManagerId FROM User WHERE Id IN :ownerIds]\n);\n```\n\n---\n\n## Anti-Pattern #9: Unfiltered Subqueries\n\n**The Problem**: Child subqueries without filters can return massive datasets.\n\n```apex\n// ❌ ANTI-PATTERN: Unfiltered subquery\nSELECT Id,\n (SELECT Id FROM Contacts),\n (SELECT Id FROM Opportunities)\nFROM Account\n// Could return thousands of child records per account\n```\n\n**The Solution**: Always filter and limit subqueries.\n\n```apex\n// ✅ CORRECT: Filtered and limited subqueries\nSELECT Id,\n (SELECT Id, Name FROM Contacts\n WHERE IsActive__c = true\n LIMIT 5),\n (SELECT Id, Name FROM Opportunities\n WHERE StageName != 'Closed Lost'\n LIMIT 5)\nFROM Account\nWHERE Industry = 'Technology'\n```\n\n---\n\n## Anti-Pattern #10: Formula Fields in WHERE\n\n**The Problem**: Formula fields are not indexed and require full table scans.\n\n```apex\n// ❌ ANTI-PATTERN: Filter on formula field\nSELECT Id FROM Opportunity\nWHERE Days_Since_Created__c > 30\n// Formula field - cannot use index\n```\n\n**The Solution**: Use the underlying indexed field.\n\n```apex\n// ✅ CORRECT: Use base field\nSELECT Id FROM Opportunity\nWHERE CreatedDate \u003c LAST_N_DAYS:30\n\n// ✅ ALTERNATIVE: Store computed value in regular field\n// Use workflow/flow to update a Number field\nSELECT Id FROM Opportunity\nWHERE Days_Open__c > 30\n```\n\n---\n\n## Quick Reference: Selectivity Rules\n\n```\nA filter is SELECTIVE when:\n├── Uses an indexed field, AND\n├── Returns \u003c 10% of first million records, OR\n├── Returns \u003c 5% of records beyond first million\n└── Absolute max: 333,333 records (1M / 3)\n```\n\n**Always Indexed Fields**:\n- `Id`, `Name`, `OwnerId`, `CreatedDate`, `LastModifiedDate`\n- `RecordTypeId`, External ID fields, Master-Detail relationship fields\n\n**Request Custom Index**: Contact Salesforce Support with:\n- Object name and field API name\n- Sample SOQL query\n- Cardinality (unique values count)\n- Business justification\n\n---\n\n## Testing Checklist\n\nBefore deploying SOQL to production:\n\n1. [ ] Run Query Plan tool (Developer Console or CLI)\n2. [ ] Verify `LeadingOperationType` is \"Index\" not \"TableScan\"\n3. [ ] Test with 200+ records in trigger context\n4. [ ] Verify query count stays under 100 per transaction\n5. [ ] Check heap usage for large result sets\n\n```bash\n# CLI Query Plan\nsf data query \\\n --query \"SELECT Id FROM Account WHERE Name = 'Test'\" \\\n --target-org my-org \\\n --use-tooling-api \\\n --plan\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":9146,"content_sha256":"3454394769bebd6b23e4df9639a6dae21ad6b754afbcd65d1e5723240eeb2588"},{"filename":"references/cli-commands.md","content":"\u003c!-- Parent: sf-soql/SKILL.md -->\n# Salesforce CLI SOQL Commands\n\n## Quick Reference\n\n| Task | Command |\n|------|---------|\n| Run query | `sf data query --query \"SELECT...\"` |\n| JSON output | `sf data query --query \"...\" --json` |\n| CSV output | `sf data query --query \"...\" --result-format csv` |\n| Bulk export | `sf data export bulk --query \"SELECT...\" --target-org alias` |\n| Query plan | `sf api request rest '/query/?explain=\u003cSOQL>' --target-org alias` |\n\n---\n\n## Basic Queries\n\n### Run a Query\n\n```bash\nsf data query \\\n --query \"SELECT Id, Name, Industry FROM Account LIMIT 10\" \\\n --target-org my-sandbox\n```\n\n### Query with Filters\n\n```bash\nsf data query \\\n --query \"SELECT Id, Name FROM Account WHERE Industry = 'Technology'\" \\\n --target-org my-sandbox\n```\n\n### Query Relationships\n\n```bash\n# Child-to-parent\nsf data query \\\n --query \"SELECT Id, Name, Account.Name FROM Contact LIMIT 10\" \\\n --target-org my-sandbox\n\n# Parent-to-child\nsf data query \\\n --query \"SELECT Id, Name, (SELECT Id, Name FROM Contacts) FROM Account LIMIT 5\" \\\n --target-org my-sandbox\n```\n\n---\n\n## Output Formats\n\n### Human-Readable (Default)\n\n```bash\nsf data query \\\n --query \"SELECT Id, Name FROM Account LIMIT 5\" \\\n --target-org my-sandbox\n```\n\n### JSON\n\n```bash\nsf data query \\\n --query \"SELECT Id, Name FROM Account LIMIT 5\" \\\n --target-org my-sandbox \\\n --json\n```\n\n### CSV\n\n```bash\nsf data query \\\n --query \"SELECT Id, Name, Industry FROM Account\" \\\n --target-org my-sandbox \\\n --result-format csv > accounts.csv\n```\n\n### Direct to File\n\n```bash\nsf data query \\\n --query \"SELECT Id, Name, Industry FROM Account\" \\\n --target-org my-sandbox \\\n --result-format csv \\\n --output-file accounts.csv\n```\n\n---\n\n## Bulk Data Export\n\nFor large result sets (> 2,000 records), use the dedicated bulk export command:\n\n```bash\n# Export to CSV (default)\nsf data export bulk \\\n --query \"SELECT Id, Name FROM Account\" \\\n --target-org my-sandbox \\\n --output-file accounts.csv\n\n# Export as JSON\nsf data export bulk \\\n --query \"SELECT Id, Name FROM Account\" \\\n --target-org my-sandbox \\\n --output-file accounts.json \\\n --result-format json\n```\n\n> **Note**: `--bulk` and `--wait` flags on `sf data query` were removed in v2.87.7. Use `sf data export bulk` instead.\n\n---\n\n## Query Plan Analysis\n\nAnalyze query performance before running:\n\n```bash\nsf data query \\\n --query \"SELECT Id FROM Account WHERE Name = 'Acme'\" \\\n --target-org my-sandbox \\\n --use-tooling-api \\\n --plan\n```\n\n### Understanding Query Plan Output\n\n```json\n{\n \"plans\": [{\n \"cardinality\": 50, // Estimated rows returned\n \"fields\": [\"Name\"], // Fields used for filtering\n \"leadingOperationType\": \"Index\", // Index = good, TableScan = bad\n \"relativeCost\": 0.1, // Lower is better\n \"sobjectCardinality\": 10000, // Total records in object\n \"sobjectType\": \"Account\"\n }]\n}\n```\n\n**Key Indicators:**\n- `leadingOperationType: \"Index\"` = Query uses index (good)\n- `leadingOperationType: \"TableScan\"` = Full table scan (bad for large tables)\n- `relativeCost \u003c 1` = Efficient query\n- `cardinality` = Expected number of results\n\n---\n\n## Tooling API Queries\n\nQuery metadata objects:\n\n```bash\n# Query ApexClass\nsf data query \\\n --query \"SELECT Id, Name, Body FROM ApexClass WHERE Name = 'MyController'\" \\\n --target-org my-sandbox \\\n --use-tooling-api\n\n# Query CustomField\nsf data query \\\n --query \"SELECT Id, DeveloperName, TableEnumOrId FROM CustomField WHERE TableEnumOrId = 'Account'\" \\\n --target-org my-sandbox \\\n --use-tooling-api\n```\n\n---\n\n## Query from File\n\nStore query in file and execute:\n\n```bash\n# Create query file\necho \"SELECT Id, Name FROM Account WHERE Industry = 'Technology'\" > query.soql\n\n# Execute from file\nsf data query \\\n --file query.soql \\\n --target-org my-sandbox\n```\n\n---\n\n## Useful Patterns\n\n### Get Record Count\n\n```bash\nsf data query \\\n --query \"SELECT COUNT() FROM Account\" \\\n --target-org my-sandbox\n```\n\n### Export to File\n\n```bash\n# CSV export\nsf data query \\\n --query \"SELECT Id, Name, Industry, Phone FROM Account\" \\\n --target-org my-sandbox \\\n --result-format csv > accounts.csv\n\n# JSON export\nsf data query \\\n --query \"SELECT Id, Name, Industry FROM Account\" \\\n --target-org my-sandbox \\\n --json > accounts.json\n```\n\n### Query with jq Processing\n\n```bash\n# Get just the names\nsf data query \\\n --query \"SELECT Name FROM Account LIMIT 10\" \\\n --target-org my-sandbox \\\n --json | jq -r '.result.records[].Name'\n\n# Count records\nsf data query \\\n --query \"SELECT Id FROM Account\" \\\n --target-org my-sandbox \\\n --json | jq '.result.totalSize'\n\n# Filter in shell\nsf data query \\\n --query \"SELECT Id, Name, Industry FROM Account\" \\\n --target-org my-sandbox \\\n --json | jq '.result.records[] | select(.Industry == \"Technology\")'\n```\n\n### Query with Dates\n\n```bash\n# Records created today\nsf data query \\\n --query \"SELECT Id, Name FROM Account WHERE CreatedDate = TODAY\" \\\n --target-org my-sandbox\n\n# Records from last 30 days\nsf data query \\\n --query \"SELECT Id, Name FROM Account WHERE CreatedDate = LAST_N_DAYS:30\" \\\n --target-org my-sandbox\n```\n\n### Aggregate Queries\n\n```bash\n# Count by industry\nsf data query \\\n --query \"SELECT Industry, COUNT(Id) FROM Account GROUP BY Industry\" \\\n --target-org my-sandbox\n\n# Sum of amounts\nsf data query \\\n --query \"SELECT SUM(Amount) FROM Opportunity WHERE StageName = 'Closed Won'\" \\\n --target-org my-sandbox\n```\n\n---\n\n## Troubleshooting\n\n### Query Timeout\n\nFor long-running queries, export via bulk API:\n\n```bash\nsf data export bulk \\\n --query \"SELECT Id, Name FROM Account\" \\\n --target-org my-sandbox \\\n --output-file results.csv\n```\n\n### Too Many Results\n\nAdd LIMIT or filter:\n\n```bash\n# With limit\nsf data query \\\n --query \"SELECT Id, Name FROM Account LIMIT 1000\" \\\n --target-org my-sandbox\n\n# With filter\nsf data query \\\n --query \"SELECT Id, Name FROM Account WHERE CreatedDate = THIS_YEAR\" \\\n --target-org my-sandbox\n```\n\n### Non-Selective Query Error\n\nAdd indexed field to WHERE:\n\n```bash\n# Add CreatedDate filter (indexed)\nsf data query \\\n --query \"SELECT Id FROM Lead WHERE Status = 'Open' AND CreatedDate = LAST_N_DAYS:90\" \\\n --target-org my-sandbox\n```\n\n### Permission Errors\n\nCheck field-level security:\n\n```bash\n# Query accessible fields only\nsf data query \\\n --query \"SELECT Id, Name FROM Account\" \\\n --target-org my-sandbox\n```\n\n---\n\n## Integration with Other Tools\n\n### Pipe to File\n\n```bash\nsf data query \\\n --query \"SELECT Id, Name FROM Account\" \\\n --target-org my-sandbox \\\n --result-format csv | tee accounts.csv\n```\n\n### Use in Scripts\n\n```bash\n#!/bin/bash\n\nORG=${1:-\"my-sandbox\"}\n\n# Get count\nCOUNT=$(sf data query \\\n --query \"SELECT COUNT() FROM Account\" \\\n --target-org $ORG \\\n --json | jq -r '.result.totalSize')\n\necho \"Total accounts: $COUNT\"\n\n# Get top accounts\nsf data query \\\n --query \"SELECT Name, AnnualRevenue FROM Account ORDER BY AnnualRevenue DESC LIMIT 10\" \\\n --target-org $ORG\n```\n\n### Compare Orgs\n\n```bash\n#!/bin/bash\n\nPROD_COUNT=$(sf data query --query \"SELECT COUNT() FROM Account\" --target-org prod --json | jq '.result.totalSize')\nSANDBOX_COUNT=$(sf data query --query \"SELECT COUNT() FROM Account\" --target-org sandbox --json | jq '.result.totalSize')\n\necho \"Production accounts: $PROD_COUNT\"\necho \"Sandbox accounts: $SANDBOX_COUNT\"\necho \"Difference: $((PROD_COUNT - SANDBOX_COUNT))\"\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":7351,"content_sha256":"6f2dbd6dd1dfcd62ee14e20a129399471525f2bb7e35713cc3ca524945ebfc40"},{"filename":"references/field-coverage-rules.md","content":"\u003c!-- Parent: sf-soql/SKILL.md -->\n# SOQL Field Coverage Rules\n\nThis guide documents field coverage validation rules for SOQL queries—ensuring that all fields accessed in Apex code are actually queried. This is a common source of runtime errors, especially in LLM-generated code.\n\n> **Source**: [LLM Mistakes in Apex & LWC - Salesforce Diaries](https://salesforcediaries.com/2026/01/16/llm-mistakes-in-apex-lwc-salesforce-code-generation-rules/)\n\n---\n\n## Table of Contents\n\n1. [The Field Coverage Problem](#the-field-coverage-problem)\n2. [Direct Field Access](#direct-field-access)\n3. [Relationship Field Access](#relationship-field-access)\n4. [Dynamic Field Access](#dynamic-field-access)\n5. [Aggregate Queries](#aggregate-queries)\n6. [Subquery Fields](#subquery-fields)\n7. [Validation Patterns](#validation-patterns)\n\n---\n\n## The Field Coverage Problem\n\nWhen you query an sObject, only the fields in the SELECT clause are populated. Accessing any other field results in a runtime error:\n\n```\nSystem.SObjectException: SObject row was retrieved via SOQL without querying the requested field: Account.Industry\n```\n\nThis error is particularly common in LLM-generated code because the LLM may:\n1. Query some fields but access others in subsequent code\n2. Forget to include relationship fields (e.g., `Account.Name` on Contact)\n3. Access fields in conditional logic that weren't anticipated in the query\n\n---\n\n## Direct Field Access\n\n### Rule: Every field accessed must be in the SELECT clause\n\n### ❌ BAD: Accessing Unqueried Fields\n\n```apex\n// Query only includes Id and Name\nList\u003cAccount> accounts = [SELECT Id, Name FROM Account];\n\nfor (Account acc : accounts) {\n // RUNTIME ERROR: Industry was not queried\n if (acc.Industry == 'Technology') {\n // RUNTIME ERROR: Description was not queried\n acc.Description = 'Tech company';\n }\n\n // RUNTIME ERROR: AnnualRevenue was not queried\n Decimal revenue = acc.AnnualRevenue;\n}\n```\n\n### ✅ GOOD: Query All Accessed Fields\n\n```apex\n// Query ALL fields that will be accessed\nList\u003cAccount> accounts = [\n SELECT Id, Name, Industry, Description, AnnualRevenue\n FROM Account\n];\n\nfor (Account acc : accounts) {\n if (acc.Industry == 'Technology') {\n acc.Description = 'Tech company'; // OK - queried\n }\n Decimal revenue = acc.AnnualRevenue; // OK - queried\n}\n```\n\n### Field Access Locations to Check\n\nFields can be accessed in many places—ensure coverage for all:\n\n| Access Location | Example | Must Query |\n|-----------------|---------|------------|\n| Conditional (`if`) | `if (acc.Industry == 'Tech')` | `Industry` |\n| Assignment | `acc.Description = 'Text'` | `Description` |\n| Variable assignment | `String name = acc.Name` | `Name` |\n| Method argument | `sendEmail(acc.Email__c)` | `Email__c` |\n| Collection key | `map.put(acc.Name, acc)` | `Name` |\n| String interpolation | `'Hello ' + acc.Name` | `Name` |\n| SOQL bind | `[SELECT Id FROM Contact WHERE AccountId = :acc.Id]` | `Id` (usually included) |\n\n---\n\n## Relationship Field Access\n\n### Rule: Parent relationship fields require dot notation in SELECT\n\n### ❌ BAD: Missing Relationship Fields\n\n```apex\n// Contact query without Account relationship fields\nList\u003cContact> contacts = [SELECT Id, Name, AccountId FROM Contact];\n\nfor (Contact c : contacts) {\n // RUNTIME ERROR: Account.Name was not queried\n String accountName = c.Account.Name;\n\n // RUNTIME ERROR: Account.Industry was not queried\n if (c.Account.Industry == 'Technology') {\n // ...\n }\n}\n```\n\n### ✅ GOOD: Include Relationship Fields\n\n```apex\n// Use dot notation to include parent fields\nList\u003cContact> contacts = [\n SELECT Id, Name, AccountId,\n Account.Name, // Parent field\n Account.Industry, // Parent field\n Account.Owner.Name // Grandparent field (up to 5 levels)\n FROM Contact\n];\n\nfor (Contact c : contacts) {\n String accountName = c.Account.Name; // OK - queried\n\n if (c.Account.Industry == 'Technology') { // OK - queried\n String ownerName = c.Account.Owner.Name; // OK - queried\n }\n}\n```\n\n### Relationship Traversal Limits\n\n| Direction | Limit | Example |\n|-----------|-------|---------|\n| Parent (lookup/master-detail) | 5 levels | `Contact.Account.Owner.Manager.Name` |\n| Child (subquery) | 1 level | `Account -> Contacts` (cannot nest subqueries) |\n\n### ❌ BAD: Assuming Relationship is Populated\n\n```apex\nList\u003cContact> contacts = [SELECT Id, AccountId FROM Contact];\n\nfor (Contact c : contacts) {\n // AccountId is queried, but Account object is NOT populated\n // This will throw: Account.Name not queried\n if (c.Account != null) {\n String name = c.Account.Name; // ERROR!\n }\n}\n```\n\n### ✅ GOOD: Query Relationship or Use Separate Query\n\n```apex\n// Option 1: Include relationship field\nList\u003cContact> contacts = [SELECT Id, AccountId, Account.Name FROM Contact];\n\nfor (Contact c : contacts) {\n if (c.Account != null) {\n String name = c.Account.Name; // OK\n }\n}\n\n// Option 2: Separate query using collected IDs\nList\u003cContact> contacts = [SELECT Id, AccountId FROM Contact];\nSet\u003cId> accountIds = new Set\u003cId>();\nfor (Contact c : contacts) {\n if (c.AccountId != null) {\n accountIds.add(c.AccountId);\n }\n}\n\nMap\u003cId, Account> accountMap = new Map\u003cId, Account>(\n [SELECT Id, Name FROM Account WHERE Id IN :accountIds]\n);\n\nfor (Contact c : contacts) {\n Account acc = accountMap.get(c.AccountId);\n if (acc != null) {\n String name = acc.Name; // OK\n }\n}\n```\n\n---\n\n## Dynamic Field Access\n\n### Rule: Dynamic field access (using `get()`) also requires queried fields\n\n### ❌ BAD: Dynamic Access to Unqueried Field\n\n```apex\nList\u003cAccount> accounts = [SELECT Id, Name FROM Account];\nString fieldName = 'Industry'; // Dynamic field name\n\nfor (Account acc : accounts) {\n // RUNTIME ERROR: Industry was not queried\n Object value = acc.get(fieldName);\n}\n```\n\n### ✅ GOOD: Query Fields Used Dynamically\n\n```apex\n// If you know which fields will be accessed dynamically, query them\nList\u003cAccount> accounts = [SELECT Id, Name, Industry FROM Account];\nString fieldName = 'Industry';\n\nfor (Account acc : accounts) {\n Object value = acc.get(fieldName); // OK - Industry is queried\n}\n```\n\n### ✅ GOOD: Build Dynamic Query\n\n```apex\n// For truly dynamic scenarios, build the query dynamically\nSet\u003cString> fieldsToQuery = new Set\u003cString>{'Id', 'Name'};\nfieldsToQuery.addAll(dynamicFieldList); // Add dynamic fields\n\nString query = 'SELECT ' + String.join(new List\u003cString>(fieldsToQuery), ', ') +\n ' FROM Account WHERE Id IN :accountIds';\n\nList\u003cAccount> accounts = Database.query(query);\n```\n\n---\n\n## Aggregate Queries\n\n### Rule: Aggregate queries return `AggregateResult`, not sObjects\n\n### ❌ BAD: Treating Aggregate as sObject\n\n```apex\n// This returns AggregateResult, not Account\nList\u003cAccount> accounts = [\n SELECT Industry, COUNT(Id) cnt\n FROM Account\n GROUP BY Industry\n]; // COMPILE ERROR - wrong type\n\n// Even with correct type, can't access normal fields\nAggregateResult[] results = [\n SELECT Industry, COUNT(Id) cnt\n FROM Account\n GROUP BY Industry\n];\n\nfor (AggregateResult ar : results) {\n // Cannot access like sObject fields\n String industry = ar.Industry; // COMPILE ERROR\n}\n```\n\n### ✅ GOOD: Use get() for Aggregate Results\n\n```apex\nAggregateResult[] results = [\n SELECT Industry, COUNT(Id) cnt, SUM(AnnualRevenue) totalRevenue\n FROM Account\n GROUP BY Industry\n];\n\nfor (AggregateResult ar : results) {\n // Use get() with field alias\n String industry = (String) ar.get('Industry');\n Integer count = (Integer) ar.get('cnt');\n Decimal totalRevenue = (Decimal) ar.get('totalRevenue');\n\n System.debug(industry + ': ' + count + ' accounts,

sf-soql: Salesforce SOQL Query Expert Use this skill when the user needs SOQL/SOSL authoring or optimization : natural-language-to-query generation, relationship queries, aggregates, query-plan analysis, and performance/safety improvements for Salesforce queries. When This Skill Owns the Task Use when the work involves: - files - query generation from natural language - relationship queries and aggregate queries - query optimization and selectivity analysis - SOQL/SOSL syntax and governor-aware design Delegate elsewhere when the user is: - performing bulk data operations → sf-data - embedding…

+ totalRevenue);\n}\n```\n\n### Aggregate Field Aliases\n\n| Function | Default Alias | Example |\n|----------|---------------|---------|\n| `COUNT(Field)` | `expr0`, `expr1`, etc. | Use explicit alias: `COUNT(Id) cnt` |\n| `SUM(Field)` | `expr0`, `expr1`, etc. | Use explicit alias: `SUM(Amount) total` |\n| `AVG(Field)` | `expr0`, `expr1`, etc. | Use explicit alias: `AVG(Age) avgAge` |\n| `MIN(Field)` | `expr0`, `expr1`, etc. | Use explicit alias: `MIN(CreatedDate) earliest` |\n| `MAX(Field)` | `expr0`, `expr1`, etc. | Use explicit alias: `MAX(Amount) largest` |\n| `GROUP BY Field` | Field API name | Access with field name: `ar.get('Industry')` |\n\n---\n\n## Subquery Fields\n\n### Rule: Child relationship subqueries create nested lists\n\n### ❌ BAD: Accessing Subquery Fields Incorrectly\n\n```apex\n// Query with contact subquery\nList\u003cAccount> accounts = [\n SELECT Id, Name,\n (SELECT Id, Name FROM Contacts)\n FROM Account\n];\n\nfor (Account acc : accounts) {\n // ERROR: Contacts is a List, not a single Contact\n String contactName = acc.Contacts.Name;\n\n // ERROR: Cannot access unqueried field from subquery\n for (Contact c : acc.Contacts) {\n String email = c.Email; // Email not in subquery SELECT!\n }\n}\n```\n\n### ✅ GOOD: Proper Subquery Field Access\n\n```apex\n// Query all needed fields in subquery\nList\u003cAccount> accounts = [\n SELECT Id, Name,\n (SELECT Id, Name, Email, Phone FROM Contacts)\n FROM Account\n];\n\nfor (Account acc : accounts) {\n // Contacts is a List\u003cContact>\n List\u003cContact> contacts = acc.Contacts;\n\n if (contacts != null && !contacts.isEmpty()) {\n for (Contact c : contacts) {\n String name = c.Name; // OK - in subquery SELECT\n String email = c.Email; // OK - in subquery SELECT\n String phone = c.Phone; // OK - in subquery SELECT\n }\n }\n}\n```\n\n### Subquery Null Safety\n\n```apex\nList\u003cAccount> accounts = [\n SELECT Id, (SELECT Id FROM Contacts)\n FROM Account\n];\n\nfor (Account acc : accounts) {\n // Subquery result can be null if no child records\n if (acc.Contacts != null) {\n for (Contact c : acc.Contacts) {\n // Process contact\n }\n }\n\n // Or use null-safe size check\n Integer contactCount = acc.Contacts?.size() ?? 0;\n}\n```\n\n---\n\n## Validation Patterns\n\n### Pattern 1: Field-to-Query Mapping\n\nCreate a systematic approach to track field usage:\n\n```apex\npublic class AccountProcessor {\n // Document required fields at the top\n private static final Set\u003cString> REQUIRED_FIELDS = new Set\u003cString>{\n 'Id', 'Name', 'Industry', 'Description', 'AnnualRevenue',\n 'OwnerId', 'Owner.Name', 'Owner.Email'\n };\n\n // Single method for consistent querying\n public static List\u003cAccount> queryAccounts(Set\u003cId> accountIds) {\n return [\n SELECT Id, Name, Industry, Description, AnnualRevenue,\n OwnerId, Owner.Name, Owner.Email\n FROM Account\n WHERE Id IN :accountIds\n ];\n }\n\n public static void processAccounts(List\u003cAccount> accounts) {\n for (Account acc : accounts) {\n // All fields in REQUIRED_FIELDS are safe to access\n if (acc.Industry == 'Technology') {\n acc.Description = 'Tech: ' + acc.Name;\n }\n }\n }\n}\n```\n\n### Pattern 2: Selector Layer\n\nUse a selector pattern to centralize query field management:\n\n```apex\npublic class AccountSelector {\n\n // Default fields for most operations\n private static final List\u003cString> DEFAULT_FIELDS = new List\u003cString>{\n 'Id', 'Name', 'Industry', 'Type', 'OwnerId'\n };\n\n // Extended fields for detailed views\n private static final List\u003cString> DETAIL_FIELDS = new List\u003cString>{\n 'Id', 'Name', 'Industry', 'Type', 'OwnerId',\n 'Description', 'AnnualRevenue', 'NumberOfEmployees',\n 'BillingCity', 'BillingState', 'BillingCountry',\n 'Owner.Name', 'Owner.Email'\n };\n\n public List\u003cAccount> selectById(Set\u003cId> ids) {\n return selectByIdWithFields(ids, DEFAULT_FIELDS);\n }\n\n public List\u003cAccount> selectByIdDetailed(Set\u003cId> ids) {\n return selectByIdWithFields(ids, DETAIL_FIELDS);\n }\n\n private List\u003cAccount> selectByIdWithFields(Set\u003cId> ids, List\u003cString> fields) {\n String query = 'SELECT ' + String.join(fields, ', ') +\n ' FROM Account WHERE Id IN :ids';\n return Database.query(query);\n }\n}\n```\n\n### Pattern 3: Field Validation Helper\n\n```apex\npublic class SObjectFieldValidator {\n\n /**\n * Check if a field was queried on an sObject\n * @param obj The sObject to check\n * @param fieldName The API name of the field\n * @return true if the field is populated (was queried)\n */\n public static Boolean isFieldPopulated(SObject obj, String fieldName) {\n try {\n obj.get(fieldName);\n return true;\n } catch (SObjectException e) {\n return false;\n }\n }\n\n /**\n * Get field value with default if not queried\n * @param obj The sObject\n * @param fieldName The field API name\n * @param defaultValue Value to return if field not queried\n * @return The field value or default\n */\n public static Object getFieldOrDefault(SObject obj, String fieldName, Object defaultValue) {\n try {\n Object value = obj.get(fieldName);\n return value != null ? value : defaultValue;\n } catch (SObjectException e) {\n return defaultValue;\n }\n }\n}\n```\n\n---\n\n## Quick Reference: Field Coverage Checklist\n\nBefore running code that processes SOQL results:\n\n### Direct Fields\n- [ ] All fields in `if` conditions are queried\n- [ ] All fields on left side of assignments are queried\n- [ ] All fields passed to methods are queried\n- [ ] All fields used in map keys/values are queried\n\n### Relationship Fields\n- [ ] Parent fields use dot notation (e.g., `Account.Name`)\n- [ ] Parent object null checks before field access\n- [ ] Relationship traversal doesn't exceed 5 levels\n\n### Subqueries\n- [ ] Child records accessed as List, not single record\n- [ ] Subquery SELECT includes all accessed child fields\n- [ ] Null check before iterating subquery results\n\n### Dynamic Access\n- [ ] Fields accessed via `get(fieldName)` are queried\n- [ ] Dynamic queries include all needed fields\n\n---\n\n## Common LLM Mistakes Summary\n\n| Mistake | Example | Fix |\n|---------|---------|-----|\n| Query subset, use superset | Query `Id, Name`, use `Industry` | Add `Industry` to SELECT |\n| Forget relationship field | Use `c.Account.Name` without querying | Add `Account.Name` to SELECT |\n| Assume AccountId = Account | Query `AccountId`, access `Account.Name` | Query `Account.Name` explicitly |\n| Wrong subquery access | `acc.Contacts.Email` | `for (Contact c : acc.Contacts) { c.Email }` |\n| Missing subquery field | Subquery `SELECT Id`, use `Email` | Add `Email` to subquery SELECT |\n\n---\n\n## Reference\n\n- **SOQL Anti-Patterns**: See `references/anti-patterns.md` for general SOQL mistakes\n- **Selector Patterns**: See `references/selector-patterns.md` for query organization\n- **Source**: [Salesforce Diaries - LLM Mistakes](https://salesforcediaries.com/2026/01/16/llm-mistakes-in-apex-lwc-salesforce-code-generation-rules/)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":15079,"content_sha256":"62b0df0c67b332f63ffdaa8fdcdbbd39a8d2e896c8ba2f8a13506807657d58ee"},{"filename":"references/query-optimization.md","content":"\u003c!-- Parent: sf-soql/SKILL.md -->\n\n# Query Optimization & Governor Limits\n\n## Indexing Strategy\n\n**Indexed Fields** (Always Selective):\n- Id, Name, OwnerId, CreatedDate, LastModifiedDate, RecordTypeId\n- External ID fields, Master-Detail relationship fields\n- Lookup fields (when unique)\n\n**Standard Indexed Fields by Object**:\n- Account: AccountNumber, Site\n- Contact: Email\n- Lead: Email\n- Case: CaseNumber\n\n## Selectivity Rules\n\n```\nA filter is selective when it returns:\n- \u003c 10% of total records for first 1 million\n- \u003c 5% of total records for additional records\n- OR uses an indexed field\n```\n\n## Optimization Patterns\n\n```sql\n-- ❌ NON-SELECTIVE (scans all records)\nSELECT Id FROM Lead WHERE Status = 'Open'\n\n-- ✅ SELECTIVE (uses index + selective filter)\nSELECT Id FROM Lead\nWHERE Status = 'Open'\nAND CreatedDate = LAST_N_DAYS:30\nLIMIT 10000\n\n-- ❌ LEADING WILDCARD (can't use index)\nSELECT Id FROM Account WHERE Name LIKE '%corp'\n\n-- ✅ TRAILING WILDCARD (uses index)\nSELECT Id FROM Account WHERE Name LIKE 'Acme%'\n```\n\n## Query Plan Analysis\n\n```bash\n# Get query plan\nsf data query \\\n --query \"SELECT Id FROM Account WHERE Name = 'Test'\" \\\n --target-org my-org \\\n --use-tooling-api \\\n --plan\n```\n\n**Plan Output Interpretation**:\n- `Cardinality`: Estimated rows returned\n- `Cost`: Relative query cost (lower is better)\n- `Fields`: Index fields used\n- `LeadingOperationType`: How the query starts (Index vs TableScan)\n\n---\n\n## Governor Limits\n\n| Limit | Synchronous | Asynchronous |\n|-------|-------------|--------------|\n| Total SOQL Queries | 100 | 200 |\n| Records Retrieved | 50,000 | 50,000 |\n| Query Rows (queryMore) | 2,000 | 2,000 |\n| Query Locator Rows | 10 million | 10 million |\n\n### Efficient Patterns\n\n```sql\n-- ❌ Query all, filter in Apex\nSELECT Id, Name FROM Account\n-- Then filter 50,000 records in Apex\n\n-- ✅ Filter in SOQL\nSELECT Id, Name FROM Account\nWHERE Industry = 'Technology' AND IsActive__c = true\nLIMIT 1000\n\n-- ❌ Multiple queries in loop\nfor (Contact c : contacts) {\n Account a = [SELECT Name FROM Account WHERE Id = :c.AccountId];\n}\n\n-- ✅ Single query with Map\nMap\u003cId, Account> accounts = new Map\u003cId, Account>(\n [SELECT Id, Name FROM Account WHERE Id IN :accountIds]\n);\n```\n\n## SOQL FOR Loops\n\n```apex\n// For large datasets - doesn't load all into heap\nfor (Account acc : [SELECT Id, Name FROM Account WHERE Industry = 'Technology']) {\n // Process one record at a time\n // Governor: Uses queryMore internally (200 at a time)\n}\n\n// With explicit batch size\nfor (List\u003cAccount> accs : [SELECT Id, Name FROM Account]) {\n // Process 200 records at a time\n}\n```\n\n## Security Patterns\n\n### WITH SECURITY_ENFORCED\n\n```sql\n-- Throws exception if user lacks FLS\nSELECT Id, Name, Phone\nFROM Account\nWITH SECURITY_ENFORCED\n```\n\n### WITH USER_MODE / SYSTEM_MODE\n\n```sql\n-- Respects sharing rules (default in Apex)\nSELECT Id, Name FROM Account WITH USER_MODE\n\n-- Bypasses sharing rules (use with caution)\nSELECT Id, Name FROM Account WITH SYSTEM_MODE\n```\n\n### In Apex: stripInaccessible\n\n```apex\n// Strip inaccessible fields instead of throwing\nSObjectAccessDecision decision = Security.stripInaccessible(\n AccessType.READABLE,\n [SELECT Id, Name, SecretField__c FROM Account]\n);\nList\u003cAccount> safeAccounts = decision.getRecords();\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3292,"content_sha256":"915c8b06eb70404cf3632e6f57d458be6797c1c237a2e2a50af63c8659fcff53"},{"filename":"references/selector-patterns.md","content":"\u003c!-- Parent: sf-soql/SKILL.md -->\n# Selector Patterns: Query Abstraction in Vanilla Apex\n\nThis guide teaches query abstraction patterns using pure Apex - no external libraries required. These patterns improve testability, maintainability, and security compliance.\n\n> **Sources**: [James Simone - Repository Pattern](https://www.jamessimone.net/blog/joys-of-apex/repository-pattern/), [Beyond the Cloud - Selector Layer](https://blog.beyondthecloud.dev/blog/why-do-you-need-selector-layer)\n\n---\n\n## Why Use a Selector Layer?\n\n### The Problem\n\nWithout abstraction, SOQL queries are scattered everywhere:\n\n```apex\n// In TriggerHandler.cls\nList\u003cAccount> accounts = [SELECT Id, Name FROM Account WHERE Id IN :accountIds];\n\n// In BatchJob.cls (duplicate!)\nList\u003cAccount> accounts = [SELECT Id, Name FROM Account WHERE Id IN :ids];\n\n// In ServiceClass.cls (slightly different fields!)\nList\u003cAccount> accounts = [SELECT Id, Name, Industry FROM Account WHERE Id IN :accountIds];\n```\n\n**Problems**:\n1. **Duplication**: Same query logic repeated\n2. **Inconsistency**: Different fields queried in different places\n3. **Fragility**: Field deletion breaks multiple classes\n4. **Testability**: Must create real records to test\n5. **Security**: FLS/sharing often forgotten\n\n### The Solution\n\nCentralize queries in Selector classes:\n\n```apex\n// Single source of truth\npublic class AccountSelector {\n public static List\u003cAccount> byIds(Set\u003cId> accountIds) {\n return [\n SELECT Id, Name, Industry\n FROM Account\n WHERE Id IN :accountIds\n WITH SECURITY_ENFORCED\n ];\n }\n}\n\n// Usage everywhere\nList\u003cAccount> accounts = AccountSelector.byIds(accountIds);\n```\n\n---\n\n## Pattern 1: Basic Selector Class\n\nThe simplest approach - a class with static query methods.\n\n```apex\n/**\n * AccountSelector - Centralized queries for Account object\n *\n * @see https://blog.beyondthecloud.dev/blog/why-do-you-need-selector-layer\n */\npublic inherited sharing class AccountSelector {\n\n // ═══════════════════════════════════════════════════════════════════\n // FIELD SETS (centralized field lists)\n // ═══════════════════════════════════════════════════════════════════\n\n private static final List\u003cSObjectField> STANDARD_FIELDS = new List\u003cSObjectField>{\n Account.Id,\n Account.Name,\n Account.Industry,\n Account.AnnualRevenue,\n Account.OwnerId\n };\n\n // ═══════════════════════════════════════════════════════════════════\n // QUERY METHODS\n // ═══════════════════════════════════════════════════════════════════\n\n /**\n * Query accounts by their IDs\n */\n public static List\u003cAccount> byIds(Set\u003cId> accountIds) {\n if (accountIds == null || accountIds.isEmpty()) {\n return new List\u003cAccount>();\n }\n return [\n SELECT Id, Name, Industry, AnnualRevenue, OwnerId\n FROM Account\n WHERE Id IN :accountIds\n WITH SECURITY_ENFORCED\n ];\n }\n\n /**\n * Query accounts by Owner\n */\n public static List\u003cAccount> byOwnerId(Id ownerId) {\n return [\n SELECT Id, Name, Industry, AnnualRevenue, OwnerId\n FROM Account\n WHERE OwnerId = :ownerId\n WITH SECURITY_ENFORCED\n LIMIT 1000\n ];\n }\n\n /**\n * Query accounts with their contacts\n */\n public static List\u003cAccount> withContactsByIds(Set\u003cId> accountIds) {\n return [\n SELECT Id, Name,\n (SELECT Id, FirstName, LastName, Email\n FROM Contacts\n WHERE IsActive__c = true\n LIMIT 50)\n FROM Account\n WHERE Id IN :accountIds\n WITH SECURITY_ENFORCED\n ];\n }\n}\n```\n\n**Usage**:\n```apex\n// Clean, readable, testable\nList\u003cAccount> accounts = AccountSelector.byIds(accountIdSet);\nList\u003cAccount> myAccounts = AccountSelector.byOwnerId(UserInfo.getUserId());\n```\n\n---\n\n## Pattern 2: Selector with Sharing Modes\n\nControl sharing rules at the selector level.\n\n```apex\n/**\n * ContactSelector with sharing mode control\n */\npublic class ContactSelector {\n\n // ═══════════════════════════════════════════════════════════════════\n // USER MODE (respects sharing rules - default)\n // ═══════════════════════════════════════════════════════════════════\n\n public inherited sharing class UserMode {\n public static List\u003cContact> byAccountIds(Set\u003cId> accountIds) {\n return [\n SELECT Id, FirstName, LastName, Email, AccountId\n FROM Contact\n WHERE AccountId IN :accountIds\n WITH USER_MODE\n ];\n }\n }\n\n // ═══════════════════════════════════════════════════════════════════\n // SYSTEM MODE (bypasses sharing - use carefully!)\n // ═══════════════════════════════════════════════════════════════════\n\n public without sharing class SystemMode {\n public static List\u003cContact> byAccountIds(Set\u003cId> accountIds) {\n return [\n SELECT Id, FirstName, LastName, Email, AccountId\n FROM Contact\n WHERE AccountId IN :accountIds\n WITH SYSTEM_MODE\n ];\n }\n }\n}\n\n// Usage\nList\u003cContact> visibleContacts = ContactSelector.UserMode.byAccountIds(ids);\nList\u003cContact> allContacts = ContactSelector.SystemMode.byAccountIds(ids);\n```\n\n---\n\n## Pattern 3: Mockable Selector (for Unit Tests)\n\nEnable query mocking without database calls.\n\n```apex\n/**\n * OpportunitySelector with mocking support\n *\n * @see https://www.jamessimone.net/blog/joys-of-apex/repository-pattern/\n */\npublic inherited sharing class OpportunitySelector {\n\n // Test-visible mock data\n @TestVisible\n private static List\u003cOpportunity> mockData;\n\n /**\n * Query opportunities by Account IDs\n * Returns mock data in tests if set\n */\n public static List\u003cOpportunity> byAccountIds(Set\u003cId> accountIds) {\n if (Test.isRunningTest() && mockData != null) {\n return mockData;\n }\n return [\n SELECT Id, Name, StageName, Amount, CloseDate, AccountId\n FROM Opportunity\n WHERE AccountId IN :accountIds\n WITH SECURITY_ENFORCED\n ];\n }\n\n /**\n * Set mock data for testing\n */\n @TestVisible\n private static void setMockData(List\u003cOpportunity> opportunities) {\n mockData = opportunities;\n }\n}\n```\n\n**Test Usage**:\n```apex\n@IsTest\nprivate class OpportunitySelectorTest {\n @IsTest\n static void testByAccountIds_returnsMockData() {\n // Arrange - no database records needed!\n List\u003cOpportunity> mockOpps = new List\u003cOpportunity>{\n new Opportunity(Name = 'Test Opp', StageName = 'Prospecting', Amount = 1000)\n };\n OpportunitySelector.setMockData(mockOpps);\n\n // Act\n List\u003cOpportunity> result = OpportunitySelector.byAccountIds(new Set\u003cId>());\n\n // Assert\n System.assertEquals(1, result.size());\n System.assertEquals('Test Opp', result[0].Name);\n }\n}\n```\n\n---\n\n## Pattern 4: Query Builder (Dynamic SOQL)\n\nFor complex, dynamic queries that vary at runtime.\n\n```apex\n/**\n * Dynamic query builder for flexible SOQL construction\n *\n * @see https://www.jamessimone.net/blog/joys-of-apex/you-need-a-strongly-typed-query-builder/\n */\npublic inherited sharing class QueryBuilder {\n\n private String objectName;\n private Set\u003cString> fields = new Set\u003cString>();\n private List\u003cString> conditions = new List\u003cString>();\n private Map\u003cString, Object> bindings = new Map\u003cString, Object>();\n private String orderByClause;\n private Integer limitCount;\n\n /**\n * Constructor\n */\n public QueryBuilder(String objectName) {\n this.objectName = objectName;\n }\n\n /**\n * Add fields to select\n */\n public QueryBuilder selectFields(List\u003cString> fieldList) {\n fields.addAll(fieldList);\n return this;\n }\n\n /**\n * Add fields using SObjectField tokens (type-safe!)\n */\n public QueryBuilder selectFields(List\u003cSObjectField> fieldTokens) {\n for (SObjectField token : fieldTokens) {\n fields.add(String.valueOf(token));\n }\n return this;\n }\n\n /**\n * Add WHERE condition with binding\n */\n public QueryBuilder whereEquals(String field, Object value) {\n String bindName = 'bind' + bindings.size();\n conditions.add(field + ' = :' + bindName);\n bindings.put(bindName, value);\n return this;\n }\n\n /**\n * Add WHERE IN condition\n */\n public QueryBuilder whereIn(String field, Set\u003cId> ids) {\n String bindName = 'bind' + bindings.size();\n conditions.add(field + ' IN :' + bindName);\n bindings.put(bindName, ids);\n return this;\n }\n\n /**\n * Add ORDER BY\n */\n public QueryBuilder orderBy(String field, Boolean ascending) {\n orderByClause = field + (ascending ? ' ASC' : ' DESC');\n return this;\n }\n\n /**\n * Add LIMIT\n */\n public QueryBuilder setLimit(Integer count) {\n limitCount = count;\n return this;\n }\n\n /**\n * Build and execute the query\n */\n public List\u003cSObject> execute() {\n String query = buildQuery();\n return Database.queryWithBinds(query, bindings, AccessLevel.USER_MODE);\n }\n\n /**\n * Build query string (for debugging)\n */\n public String buildQuery() {\n List\u003cString> parts = new List\u003cString>();\n\n // SELECT\n if (fields.isEmpty()) {\n fields.add('Id');\n }\n parts.add('SELECT ' + String.join(new List\u003cString>(fields), ', '));\n\n // FROM\n parts.add('FROM ' + objectName);\n\n // WHERE\n if (!conditions.isEmpty()) {\n parts.add('WHERE ' + String.join(conditions, ' AND '));\n }\n\n // ORDER BY\n if (orderByClause != null) {\n parts.add('ORDER BY ' + orderByClause);\n }\n\n // LIMIT\n if (limitCount != null) {\n parts.add('LIMIT ' + limitCount);\n }\n\n return String.join(parts, ' ');\n }\n}\n```\n\n**Usage**:\n```apex\n// Fluent API for dynamic queries\nList\u003cSObject> results = new QueryBuilder('Account')\n .selectFields(new List\u003cSObjectField>{Account.Id, Account.Name, Account.Industry})\n .whereEquals('Industry', 'Technology')\n .whereIn('Id', accountIds)\n .orderBy('Name', true)\n .setLimit(100)\n .execute();\n\n// Debug the generated query\nSystem.debug(new QueryBuilder('Account').selectFields(...).buildQuery());\n// \"SELECT Id, Name, Industry FROM Account WHERE Industry = :bind0 AND Id IN :bind1 ORDER BY Name ASC LIMIT 100\"\n```\n\n---\n\n## Pattern 5: Bulkified Query Pattern\n\nThe Map-based lookup pattern for bulk operations.\n\n```apex\n/**\n * BulkQueryHelper - Reusable bulk query patterns\n */\npublic inherited sharing class BulkQueryHelper {\n\n /**\n * Get Accounts by ID as a Map (O(1) lookup)\n */\n public static Map\u003cId, Account> getAccountMapByIds(Set\u003cId> accountIds) {\n return new Map\u003cId, Account>([\n SELECT Id, Name, Industry\n FROM Account\n WHERE Id IN :accountIds\n WITH SECURITY_ENFORCED\n ]);\n }\n\n /**\n * Get Contacts grouped by AccountId\n */\n public static Map\u003cId, List\u003cContact>> getContactsByAccountId(Set\u003cId> accountIds) {\n Map\u003cId, List\u003cContact>> contactsByAccount = new Map\u003cId, List\u003cContact>>();\n\n for (Contact c : [\n SELECT Id, FirstName, LastName, Email, AccountId\n FROM Contact\n WHERE AccountId IN :accountIds\n WITH SECURITY_ENFORCED\n ]) {\n if (!contactsByAccount.containsKey(c.AccountId)) {\n contactsByAccount.put(c.AccountId, new List\u003cContact>());\n }\n contactsByAccount.get(c.AccountId).add(c);\n }\n\n return contactsByAccount;\n }\n}\n```\n\n**Usage in Trigger**:\n```apex\n// ❌ WRONG: Query per record\nfor (Opportunity opp : Trigger.new) {\n Account a = [SELECT Name FROM Account WHERE Id = :opp.AccountId];\n}\n\n// ✅ CORRECT: Bulk query with Map lookup\nSet\u003cId> accountIds = new Set\u003cId>();\nfor (Opportunity opp : Trigger.new) {\n accountIds.add(opp.AccountId);\n}\n\nMap\u003cId, Account> accountMap = BulkQueryHelper.getAccountMapByIds(accountIds);\n\nfor (Opportunity opp : Trigger.new) {\n Account a = accountMap.get(opp.AccountId);\n if (a != null) {\n // Use account data\n }\n}\n```\n\n---\n\n## Best Practices Summary\n\n| Practice | Benefit |\n|----------|---------|\n| Centralize in Selector classes | One place to update field lists |\n| Use `WITH SECURITY_ENFORCED` | Automatic FLS enforcement |\n| Return empty List, not null | Prevents NullPointerException |\n| Use `inherited sharing` | Respects caller's sharing context |\n| Make fields list a constant | Easy to update across queries |\n| Add null/empty checks | Prevent unnecessary queries |\n| Support mocking in tests | Faster tests, no database dependencies |\n\n---\n\n## When to Use Each Pattern\n\n| Scenario | Pattern |\n|----------|---------|\n| Simple, static queries | Pattern 1: Basic Selector |\n| Need sharing mode control | Pattern 2: Sharing Modes |\n| Heavy unit testing | Pattern 3: Mockable Selector |\n| Dynamic filters at runtime | Pattern 4: Query Builder |\n| Trigger/batch bulk operations | Pattern 5: Bulkified Query |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":14465,"content_sha256":"9b6067943613fb8d36264f2fd46b71741cba5f4c5dbfa7107485064132b6a66f"},{"filename":"references/soql-reference.md","content":"\u003c!-- Parent: sf-soql/SKILL.md -->\n# SOQL Quick Reference\n\n## Query Structure\n\n```sql\nSELECT fields\nFROM object\n[WHERE conditions]\n[WITH filter]\n[GROUP BY fields]\n[HAVING conditions]\n[ORDER BY fields [ASC|DESC] [NULLS FIRST|LAST]]\n[LIMIT number]\n[OFFSET number]\n[FOR UPDATE]\n```\n\n---\n\n## Operators\n\n### Comparison Operators\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `=` | Equal | `Name = 'Acme'` |\n| `!=` | Not equal | `Status != 'Closed'` |\n| `\u003c` | Less than | `Amount \u003c 1000` |\n| `\u003c=` | Less than or equal | `Amount \u003c= 1000` |\n| `>` | Greater than | `Amount > 1000` |\n| `>=` | Greater than or equal | `Amount >= 1000` |\n| `LIKE` | Pattern match | `Name LIKE 'Acme%'` |\n| `IN` | In list | `Status IN ('New', 'Open')` |\n| `NOT IN` | Not in list | `Type NOT IN ('Other')` |\n| `INCLUDES` | Multi-select contains | `Skills__c INCLUDES ('Java')` |\n| `EXCLUDES` | Multi-select excludes | `Skills__c EXCLUDES ('Java')` |\n\n### Logical Operators\n\n| Operator | Description | Example |\n|----------|-------------|---------|\n| `AND` | Both conditions | `A = 1 AND B = 2` |\n| `OR` | Either condition | `A = 1 OR B = 2` |\n| `NOT` | Negate condition | `NOT (A = 1)` |\n\n### LIKE Patterns\n\n| Pattern | Matches |\n|---------|---------|\n| `'Acme%'` | Starts with \"Acme\" |\n| `'%Corp'` | Ends with \"Corp\" |\n| `'%test%'` | Contains \"test\" |\n| `'A_me'` | \"A\" + any char + \"me\" |\n\n---\n\n## Date Literals\n\n### Relative Dates\n\n| Literal | Description |\n|---------|-------------|\n| `TODAY` | Current day |\n| `YESTERDAY` | Previous day |\n| `TOMORROW` | Next day |\n| `THIS_WEEK` | Current week (Sun-Sat) |\n| `LAST_WEEK` | Previous week |\n| `NEXT_WEEK` | Next week |\n| `THIS_MONTH` | Current month |\n| `LAST_MONTH` | Previous month |\n| `NEXT_MONTH` | Next month |\n| `THIS_QUARTER` | Current quarter |\n| `LAST_QUARTER` | Previous quarter |\n| `NEXT_QUARTER` | Next quarter |\n| `THIS_YEAR` | Current year |\n| `LAST_YEAR` | Previous year |\n| `NEXT_YEAR` | Next year |\n| `THIS_FISCAL_QUARTER` | Current fiscal quarter |\n| `THIS_FISCAL_YEAR` | Current fiscal year |\n\n### N Days/Weeks/Months/Years\n\n| Literal | Description |\n|---------|-------------|\n| `LAST_N_DAYS:n` | Last n days |\n| `NEXT_N_DAYS:n` | Next n days |\n| `LAST_N_WEEKS:n` | Last n weeks |\n| `NEXT_N_WEEKS:n` | Next n weeks |\n| `LAST_N_MONTHS:n` | Last n months |\n| `NEXT_N_MONTHS:n` | Next n months |\n| `LAST_N_QUARTERS:n` | Last n quarters |\n| `NEXT_N_QUARTERS:n` | Next n quarters |\n| `LAST_N_YEARS:n` | Last n years |\n| `NEXT_N_YEARS:n` | Next n years |\n\n### Specific Dates\n\n```sql\n-- Date only\nWHERE CloseDate = 2024-12-31\n\n-- DateTime\nWHERE CreatedDate >= 2024-01-01T00:00:00Z\n```\n\n---\n\n## Aggregate Functions\n\n| Function | Description | Example |\n|----------|-------------|---------|\n| `COUNT()` | Count all rows | `SELECT COUNT() FROM Account` |\n| `COUNT(field)` | Count non-null values | `SELECT COUNT(Email) FROM Contact` |\n| `COUNT_DISTINCT(field)` | Count unique values | `SELECT COUNT_DISTINCT(Industry) FROM Account` |\n| `SUM(field)` | Sum of values | `SELECT SUM(Amount) FROM Opportunity` |\n| `AVG(field)` | Average of values | `SELECT AVG(Amount) FROM Opportunity` |\n| `MIN(field)` | Minimum value | `SELECT MIN(Amount) FROM Opportunity` |\n| `MAX(field)` | Maximum value | `SELECT MAX(Amount) FROM Opportunity` |\n\n---\n\n## Date Functions\n\n| Function | Returns | Example |\n|----------|---------|---------|\n| `CALENDAR_YEAR(date)` | Year (e.g., 2024) | `SELECT CALENDAR_YEAR(CloseDate) FROM Opportunity` |\n| `CALENDAR_QUARTER(date)` | Quarter (1-4) | `SELECT CALENDAR_QUARTER(CloseDate) FROM Opportunity` |\n| `CALENDAR_MONTH(date)` | Month (1-12) | `SELECT CALENDAR_MONTH(CloseDate) FROM Opportunity` |\n| `DAY_IN_MONTH(date)` | Day (1-31) | `SELECT DAY_IN_MONTH(CreatedDate) FROM Account` |\n| `DAY_IN_WEEK(date)` | Day (1=Sun, 7=Sat) | `SELECT DAY_IN_WEEK(CreatedDate) FROM Account` |\n| `DAY_IN_YEAR(date)` | Day (1-366) | `SELECT DAY_IN_YEAR(CreatedDate) FROM Account` |\n| `WEEK_IN_MONTH(date)` | Week (1-5) | `SELECT WEEK_IN_MONTH(CreatedDate) FROM Account` |\n| `WEEK_IN_YEAR(date)` | Week (1-53) | `SELECT WEEK_IN_YEAR(CreatedDate) FROM Account` |\n| `HOUR_IN_DAY(date)` | Hour (0-23) | `SELECT HOUR_IN_DAY(CreatedDate) FROM Account` |\n| `FISCAL_YEAR(date)` | Fiscal year | `SELECT FISCAL_YEAR(CloseDate) FROM Opportunity` |\n| `FISCAL_QUARTER(date)` | Fiscal quarter | `SELECT FISCAL_QUARTER(CloseDate) FROM Opportunity` |\n| `FISCAL_MONTH(date)` | Fiscal month | `SELECT FISCAL_MONTH(CloseDate) FROM Opportunity` |\n\n---\n\n## Relationship Queries\n\n### Child-to-Parent (Dot Notation)\n\n```sql\n-- Standard objects\nSELECT Contact.Name, Contact.Account.Name FROM Contact\n\n-- Custom objects (use __r)\nSELECT Child__c.Name, Child__c.Parent__r.Name FROM Child__c\n```\n\n### Parent-to-Child (Subquery)\n\n```sql\n-- Standard objects\nSELECT Id, (SELECT Id FROM Contacts) FROM Account\n\n-- Custom objects (use __r)\nSELECT Id, (SELECT Id FROM Children__r) FROM Parent__c\n```\n\n---\n\n## WITH Clauses\n\n| Clause | Description |\n|--------|-------------|\n| `WITH SECURITY_ENFORCED` | Enforce FLS (throws exception if no access) |\n| `WITH USER_MODE` | Respect sharing and FLS |\n| `WITH SYSTEM_MODE` | Bypass sharing rules |\n\n---\n\n## Governor Limits\n\n| Limit | Synchronous | Asynchronous |\n|-------|-------------|--------------|\n| Total SOQL Queries | 100 | 200 |\n| Records Retrieved | 50,000 | 50,000 |\n| QueryLocator Rows | 10,000,000 | 10,000,000 |\n| OFFSET Maximum | 2,000 | 2,000 |\n| Subqueries | 20 | 20 |\n| Relationship Depth | 5 levels | 5 levels |\n\n---\n\n## Index Usage\n\n### Always Indexed\n\n- `Id`\n- `Name`\n- `OwnerId`\n- `CreatedDate`\n- `LastModifiedDate`\n- `RecordTypeId`\n- External ID fields\n- Master-Detail fields\n\n### Selective Query Rules\n\n- Query is selective if WHERE returns \u003c 10% of first 1M records\n- Or uses an indexed field with \u003c 1M matching records\n- Non-selective queries on large tables fail\n\n---\n\n## CLI Commands\n\n```bash\n# Basic query\nsf data query --query \"SELECT Id, Name FROM Account LIMIT 10\" --target-org my-org\n\n# JSON output\nsf data query --query \"SELECT Id, Name FROM Account\" --target-org my-org --json\n\n# CSV output\nsf data query --query \"SELECT Id, Name FROM Account\" --result-format csv --target-org my-org\n\n# Bulk export (for large results, > 2,000 records)\nsf data export bulk --query \"SELECT Id, Name FROM Account\" --target-org my-org --output-file accounts.csv\n\n# Query plan (uses REST API explain endpoint)\nsf api request rest \"/query/?explain=SELECT+Id+FROM+Account+WHERE+Name='Test'\" --target-org my-org --json\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":6506,"content_sha256":"62fff5bca0bc81746adae25d154487f20316bc85177c5a7ac7a82a1dc758f45b"},{"filename":"references/soql-syntax-reference.md","content":"\u003c!-- Parent: sf-soql/SKILL.md -->\n\n# SOQL Syntax Reference\n\n## Basic Query Structure\n\n```sql\nSELECT field1, field2, ...\nFROM ObjectName\nWHERE condition1 AND condition2\nORDER BY field1 ASC/DESC\nLIMIT number\nOFFSET number\n```\n\n### Field Selection\n\n```sql\n-- Specific fields (recommended)\nSELECT Id, Name, Industry FROM Account\n\n-- All fields (avoid in Apex - use only in Developer Console)\nSELECT FIELDS(ALL) FROM Account LIMIT 200\n\n-- Standard fields only\nSELECT FIELDS(STANDARD) FROM Account\n```\n\n### WHERE Clause Operators\n\n| Operator | Example | Notes |\n|----------|---------|-------|\n| `=` | `Name = 'Acme'` | Exact match |\n| `!=` | `Status != 'Closed'` | Not equal |\n| `\u003c`, `>`, `\u003c=`, `>=` | `Amount > 1000` | Comparison |\n| `LIKE` | `Name LIKE 'Acme%'` | Wildcard match |\n| `IN` | `Status IN ('New', 'Open')` | Multiple values |\n| `NOT IN` | `Type NOT IN ('Other')` | Exclude values |\n| `INCLUDES` | `Interests__c INCLUDES ('Golf')` | Multi-select picklist |\n| `EXCLUDES` | `Interests__c EXCLUDES ('Golf')` | Multi-select exclude |\n\n### Date Literals\n\n| Literal | Meaning |\n|---------|---------|\n| `TODAY` | Current day |\n| `YESTERDAY` | Previous day |\n| `THIS_WEEK` | Current week (Sun-Sat) |\n| `LAST_WEEK` | Previous week |\n| `THIS_MONTH` | Current month |\n| `LAST_MONTH` | Previous month |\n| `THIS_QUARTER` | Current quarter |\n| `THIS_YEAR` | Current year |\n| `LAST_N_DAYS:n` | Last n days |\n| `NEXT_N_DAYS:n` | Next n days |\n\n```sql\n-- Created in last 30 days\nSELECT Id FROM Account WHERE CreatedDate = LAST_N_DAYS:30\n\n-- Modified this month\nSELECT Id FROM Contact WHERE LastModifiedDate = THIS_MONTH\n```\n\n---\n\n## Relationship Queries\n\n### Child-to-Parent (Dot Notation)\n\n```sql\n-- Access parent fields\nSELECT Id, Name, Account.Name, Account.Industry\nFROM Contact\nWHERE Account.AnnualRevenue > 1000000\n\n-- Up to 5 levels\nSELECT Id, Contact.Account.Owner.Manager.Name\nFROM Case\n```\n\n### Parent-to-Child (Subquery)\n\n```sql\n-- Get parent with related children\nSELECT Id, Name,\n (SELECT Id, FirstName, LastName FROM Contacts),\n (SELECT Id, Name, Amount FROM Opportunities WHERE StageName = 'Closed Won')\nFROM Account\nWHERE Industry = 'Technology'\n```\n\n### Standard Relationship Names\n\n| Object | Relationship Name | Example |\n|--------|-------------------|---------|\n| Account → Contacts | `Contacts` | `(SELECT Id FROM Contacts)` |\n| Account → Opportunities | `Opportunities` | `(SELECT Id FROM Opportunities)` |\n| Account → Cases | `Cases` | `(SELECT Id FROM Cases)` |\n| Contact → Cases | `Cases` | `(SELECT Id FROM Cases)` |\n| Opportunity → OpportunityLineItems | `OpportunityLineItems` | `(SELECT Id FROM OpportunityLineItems)` |\n\n### Custom Object Relationships\n\n```sql\n-- Custom relationship: add __r suffix\nSELECT Id, Name, Custom_Object__r.Name\nFROM Another_Object__c\n\n-- Child relationship: add __r suffix\nSELECT Id, (SELECT Id FROM Custom_Children__r)\nFROM Parent_Object__c\n```\n\n---\n\n## Aggregate Queries\n\n### Basic Aggregates\n\n```sql\n-- Count all records\nSELECT COUNT() FROM Account\n\n-- Count with alias\nSELECT COUNT(Id) cnt FROM Account\n\n-- Sum, Average, Min, Max\nSELECT SUM(Amount), AVG(Amount), MIN(Amount), MAX(Amount)\nFROM Opportunity\nWHERE StageName = 'Closed Won'\n```\n\n### GROUP BY\n\n```sql\n-- Count by field\nSELECT Industry, COUNT(Id)\nFROM Account\nGROUP BY Industry\n\n-- Multiple groupings\nSELECT StageName, CALENDAR_YEAR(CloseDate), COUNT(Id)\nFROM Opportunity\nGROUP BY StageName, CALENDAR_YEAR(CloseDate)\n```\n\n### HAVING Clause\n\n```sql\n-- Filter aggregated results\nSELECT Industry, COUNT(Id) cnt\nFROM Account\nGROUP BY Industry\nHAVING COUNT(Id) > 10\n```\n\n### GROUP BY ROLLUP\n\n```sql\n-- Subtotals\nSELECT LeadSource, Rating, COUNT(Id)\nFROM Lead\nGROUP BY ROLLUP(LeadSource, Rating)\n```\n\n---\n\n## Advanced Features\n\n### Polymorphic Relationships (What)\n\n```sql\n-- Query polymorphic fields\nSELECT Id, What.Name, What.Type\nFROM Task\nWHERE What.Type IN ('Account', 'Opportunity')\n\n-- TYPEOF for conditional fields\nSELECT\n TYPEOF What\n WHEN Account THEN Name, Phone\n WHEN Opportunity THEN Name, Amount\n END\nFROM Task\n```\n\n### Semi-Joins and Anti-Joins\n\n```sql\n-- Semi-join: Records that HAVE related records\nSELECT Id, Name FROM Account\nWHERE Id IN (SELECT AccountId FROM Contact)\n\n-- Anti-join: Records that DON'T HAVE related records\nSELECT Id, Name FROM Account\nWHERE Id NOT IN (SELECT AccountId FROM Opportunity)\n```\n\n### Format and Currency\n\n```sql\n-- Format currency/date in results\nSELECT FORMAT(Amount), FORMAT(CloseDate) FROM Opportunity\n\n-- Convert to user's currency\nSELECT Id, convertCurrency(Amount) FROM Opportunity\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":4589,"content_sha256":"5e6580512b16dbb487def2f63cb50447dcb82de64b7ecb4a50f5c9e169165317"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"sf-soql: Salesforce SOQL Query Expert","type":"text"}]},{"type":"paragraph","content":[{"text":"Use this skill when the user needs ","type":"text"},{"text":"SOQL/SOSL authoring or optimization","type":"text","marks":[{"type":"strong"}]},{"text":": natural-language-to-query generation, relationship queries, aggregates, query-plan analysis, and performance/safety improvements for Salesforce queries.","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-soql","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":".soql","type":"text","marks":[{"type":"code_inline"}]},{"text":" files","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"query generation from natural language","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"relationship queries and aggregate queries","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"query optimization and selectivity analysis","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"SOQL/SOSL syntax and governor-aware design","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":"performing bulk data operations → ","type":"text"},{"text":"sf-data","type":"text","marks":[{"type":"link","attrs":{"href":"../sf-data/SKILL.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"embedding query logic inside broader Apex implementation → ","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":"debugging via logs rather than query shape → ","type":"text"},{"text":"sf-debug","type":"text","marks":[{"type":"link","attrs":{"href":"../sf-debug/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":"target object(s)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"fields needed","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"filter criteria","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"sort / limit requirements","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"whether the query is for display, automation, reporting-like analysis, or Apex usage","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"whether performance / selectivity is already a concern","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. Generate the simplest correct query","type":"text"}]},{"type":"paragraph","content":[{"text":"Prefer:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"only needed fields","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"clear WHERE criteria","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"reasonable LIMIT when appropriate","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"relationship depth only as deep as necessary","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"2. Choose the right query shape","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":"parent data from child","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"child-to-parent traversal","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"child rows from parent","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"subquery","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"counts / rollups","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"aggregate query","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"records with / without related rows","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"semi-join / anti-join","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"text search across objects","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SOSL","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"3. Optimize for selectivity and safety","type":"text"}]},{"type":"paragraph","content":[{"text":"Check:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"indexed / selective filters","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"no unnecessary fields","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"no avoidable wildcard or scan-heavy patterns","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"security enforcement expectations","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"4. Validate execution path if needed","type":"text"}]},{"type":"paragraph","content":[{"text":"If the user wants runtime verification, hand off execution to:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"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":"High-Signal Rules","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"never use ","type":"text"},{"text":"SELECT *","type":"text","marks":[{"type":"code_inline"}]},{"text":" style thinking; query only required fields","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"do not query inside loops in Apex contexts","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"prefer filtering in SOQL rather than post-filtering in Apex","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"use aggregates for counts and grouped summaries instead of loading unnecessary records","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"evaluate wildcard usage carefully; leading wildcards often defeat indexes","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"account for security mode / field access requirements when queries move into Apex","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":"Query purpose","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Final SOQL/SOSL","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Why this shape was chosen","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Optimization or security notes","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Execution suggestion if needed","type":"text","marks":[{"type":"strong"}]}]}]}]},{"type":"paragraph","content":[{"text":"Suggested shape:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"text"},"content":[{"text":"Query goal: \u003csummary>\nQuery: \u003csoql or sosl>\nDesign: \u003crelationship / aggregate / filter choices>\nNotes: \u003cselectivity, limits, security, governor awareness>\nNext step: \u003crun in sf-data or embed in Apex>","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":"run the query against an org","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sf-data","type":"text","marks":[{"type":"link","attrs":{"href":"../sf-data/SKILL.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"execution and export","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"embed the query in services/selectors","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":"implementation context","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"analyze slow-query symptoms from logs","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sf-debug","type":"text","marks":[{"type":"link","attrs":{"href":"../sf-debug/SKILL.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"runtime evidence","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"wire query-backed UI","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sf-lwc","type":"text","marks":[{"type":"link","attrs":{"href":"../sf-lwc/SKILL.md","title":null}}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"frontend integration","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/soql-syntax-reference.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/soql-syntax-reference.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/query-optimization.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/query-optimization.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/cli-commands.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/cli-commands.md","title":null}}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Specialized guidance","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/soql-reference.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/soql-reference.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/anti-patterns.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/anti-patterns.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/selector-patterns.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/selector-patterns.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/field-coverage-rules.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/field-coverage-rules.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":"90+","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"production-optimized query","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"80–89","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"good query with minor improvements possible","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"70–79","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"functional but performance concerns remain","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\u003c 70","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"needs revision before production use","type":"text"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"sf-soql","author":"@skillopedia","source":{"stars":416,"repo_name":"sf-skills","origin_url":"https://github.com/jaganpro/sf-skills/blob/HEAD/skills/sf-soql/SKILL.md","repo_owner":"jaganpro","body_sha256":"ae4ebc1b613e02d085f373a6dac49fde3de7935418ee839cf9766b959f06c0f9","cluster_key":"3863b5b53c2fc8a49cca1b5c22532d70600d01b4814d23243700887eb5a90653","clean_bundle":{"format":"clean-skill-bundle-v1","source":"jaganpro/sf-skills/skills/sf-soql/SKILL.md","attachments":[{"id":"97d4e4b1-cb1d-5b33-b161-3b1f1f1faf9a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/97d4e4b1-cb1d-5b33-b161-3b1f1f1faf9a/attachment.md","path":"CREDITS.md","size":3593,"sha256":"ff67976deef4a4ff84354c3e0bc357c21dfd7249418e8d8bf9707da9587bf1d2","contentType":"text/markdown; charset=utf-8"},{"id":"ca7208da-d066-5bcd-b105-47fab8e6ed62","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ca7208da-d066-5bcd-b105-47fab8e6ed62/attachment.md","path":"README.md","size":1939,"sha256":"65fa376c9e0ae3f14b443cf00cc1b7e02fe26500e632e3b7e8eea92502f055c6","contentType":"text/markdown; charset=utf-8"},{"id":"c659007c-47bf-5e72-8181-92db84953f05","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c659007c-47bf-5e72-8181-92db84953f05/attachment.soql","path":"assets/aggregate-queries.soql","size":9578,"sha256":"e0c6356187b242f9511eb5237a4c499f03461f986043c4e566fc9aa42a5694d0","contentType":"text/plain; charset=utf-8"},{"id":"725ead68-046c-5458-9f86-ff4bb32781c9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/725ead68-046c-5458-9f86-ff4bb32781c9/attachment.soql","path":"assets/basic-queries.soql","size":7628,"sha256":"aa1ca9353c5644e8d06a726e56143e14ef7ec38debc4194c4699f0097579991f","contentType":"text/plain; charset=utf-8"},{"id":"9cda77e0-6bfa-5b6c-8a97-b1ae2ffa3d85","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9cda77e0-6bfa-5b6c-8a97-b1ae2ffa3d85/attachment.cls","path":"assets/bulkified-query-pattern.cls","size":10928,"sha256":"27badff6152eb6aed544a9481a194d81d0e85fc2d8df67c818868b4a6c2ac1ad","contentType":"text/x-tex"},{"id":"4c879929-bfda-5362-9149-3d0fd81d18a9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4c879929-bfda-5362-9149-3d0fd81d18a9/attachment.soql","path":"assets/optimization-patterns.soql","size":11158,"sha256":"9ed63426f012a76569446324f43d8c2a877bd1e35f9bde95aeca00f6d20a96d4","contentType":"text/plain; charset=utf-8"},{"id":"07a0979b-0946-56bd-8698-c8cdd71c457c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/07a0979b-0946-56bd-8698-c8cdd71c457c/attachment.soql","path":"assets/relationship-queries.soql","size":7725,"sha256":"37aded190eb371e3d9b2d5a937a1ee7ccb335cf727efcb772841556daa868bb6","contentType":"text/plain; charset=utf-8"},{"id":"e8b54588-1f90-5319-b4c4-e3a82c7ce89c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e8b54588-1f90-5319-b4c4-e3a82c7ce89c/attachment.cls","path":"assets/selector-class.cls","size":8598,"sha256":"b53ccbd77050a9f9b7616d8bcfc059698d3e4f91b8bb185bb56a0c8c5fd9df1f","contentType":"text/x-tex"},{"id":"67c48007-cc32-5b00-a7df-b1957911e246","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/67c48007-cc32-5b00-a7df-b1957911e246/attachment.py","path":"hooks/scripts/post-tool-validate.py","size":12149,"sha256":"bb0e77803ea62bfb39729a8331b7c1e230097cf3ba87ec754ee0d46b0a57ab56","contentType":"text/x-python; charset=utf-8"},{"id":"6298b118-d487-5469-85dc-1623fb828b0f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6298b118-d487-5469-85dc-1623fb828b0f/attachment.md","path":"references/anti-patterns.md","size":9146,"sha256":"3454394769bebd6b23e4df9639a6dae21ad6b754afbcd65d1e5723240eeb2588","contentType":"text/markdown; charset=utf-8"},{"id":"0cbfe2ee-9846-5d2c-886b-b37047f43a5b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0cbfe2ee-9846-5d2c-886b-b37047f43a5b/attachment.md","path":"references/cli-commands.md","size":7351,"sha256":"6f2dbd6dd1dfcd62ee14e20a129399471525f2bb7e35713cc3ca524945ebfc40","contentType":"text/markdown; charset=utf-8"},{"id":"977bc652-54a0-526b-abd5-daf3703546ed","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/977bc652-54a0-526b-abd5-daf3703546ed/attachment.md","path":"references/field-coverage-rules.md","size":15079,"sha256":"62b0df0c67b332f63ffdaa8fdcdbbd39a8d2e896c8ba2f8a13506807657d58ee","contentType":"text/markdown; charset=utf-8"},{"id":"90a97db5-346e-5357-8f1f-4665b45e8c3c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/90a97db5-346e-5357-8f1f-4665b45e8c3c/attachment.md","path":"references/query-optimization.md","size":3292,"sha256":"915c8b06eb70404cf3632e6f57d458be6797c1c237a2e2a50af63c8659fcff53","contentType":"text/markdown; charset=utf-8"},{"id":"4a180822-7596-555c-ba59-0d2769ba26f4","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4a180822-7596-555c-ba59-0d2769ba26f4/attachment.md","path":"references/selector-patterns.md","size":14465,"sha256":"9b6067943613fb8d36264f2fd46b71741cba5f4c5dbfa7107485064132b6a66f","contentType":"text/markdown; charset=utf-8"},{"id":"d5cd0687-d2e9-5c98-80f5-18840ce86577","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d5cd0687-d2e9-5c98-80f5-18840ce86577/attachment.md","path":"references/soql-reference.md","size":6506,"sha256":"62fff5bca0bc81746adae25d154487f20316bc85177c5a7ac7a82a1dc758f45b","contentType":"text/markdown; charset=utf-8"},{"id":"9538f64f-cf3f-5163-b22a-44d063dd3ee7","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9538f64f-cf3f-5163-b22a-44d063dd3ee7/attachment.md","path":"references/soql-syntax-reference.md","size":4589,"sha256":"5e6580512b16dbb487def2f63cb50447dcb82de64b7ecb4a50f5c9e169165317","contentType":"text/markdown; charset=utf-8"}],"bundle_sha256":"036f737482cafb0bc30fea72f953aa73cea08bed61868014f5314165da57a3d2","attachment_count":16,"text_attachments":12,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":4,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"skills/sf-soql/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":"100 points across 5 categories","version":"1.1.0"},"import_tag":"clean-skills-v1","description":"SOQL query generation, optimization, and analysis with 100-point scoring. TRIGGER when: user writes, optimizes, or debugs SOQL/SOSL queries, touches .soql files, or asks about relationship queries, aggregates, or query performance. DO NOT TRIGGER when: bulk data operations (use sf-data), Apex DML logic (use sf-apex), or report/dashboard queries.\n"}},"renderedAt":1782987571953}

sf-soql: Salesforce SOQL Query Expert Use this skill when the user needs SOQL/SOSL authoring or optimization : natural-language-to-query generation, relationship queries, aggregates, query-plan analysis, and performance/safety improvements for Salesforce queries. When This Skill Owns the Task Use when the work involves: - files - query generation from natural language - relationship queries and aggregate queries - query optimization and selectivity analysis - SOQL/SOSL syntax and governor-aware design Delegate elsewhere when the user is: - performing bulk data operations → sf-data - embedding…