Last week we walked through the bottlenecks we mapped — the four wins worth chasing and the three traps to refuse. This week the series gets concrete: the first tool in the stack.
We started with GitHub Copilot for one reason. Every developer on the team already had a GitHub account, the company already paid for GitHub Enterprise, and the marginal cost of turning it on was a single license upgrade. No new vendor review, no new SSO integration, no new CISO ticket. For a five-person squad trying to test a thesis, that lowest-friction starting point matters more than which tool is theoretically best.
What follows is what we found after a full release cycle of using Copilot on a real Salesforce codebase. It is not a review. It is the rules we ended up writing for ourselves.
Setup
Two paths got Copilot in front of our editors, and we use both.
The first is local VS Code. Install the GitHub Copilot extension alongside the Salesforce Extension Pack. The Salesforce extensions handle Apex language server, LWC tooling, and sf CLI integration; Copilot layers in completions and chat. The two coexist cleanly — they do not fight over autocomplete because the Apex language server still owns symbol resolution and Copilot only fills in the inline suggestion lane.
The second is the cloud IDE — the browser-based Salesforce VS Code environment that Salesforce ships for org-tied development. Copilot is available there too if your GitHub account has a license attached, since it is still VS Code under the hood.
On pricing, GitHub publishes five tiers: Free, Pro, Pro+, Business, Enterprise. Free has chat and completion limits low enough that it functions as a trial, not a daily-driver. Pro is the standard floor for an individual developer doing real work. We are on Business through the org account, which gives us policy controls — model selection, content exclusions, audit logs — that matter once more than two people use it on a shared codebase.
Where Copilot lands well
These are the cases where Copilot earns its tab key. The pattern across all of them: the work is structurally repetitive, the language is one Copilot trained heavily on, and the cost of a bad suggestion is bounded by your next compile.
LWC component scaffolds
Type a comment at the top of an empty .js file and Copilot generates a serviceable starting shape:
// LWC component for displaying a list of accounts with a search filter
import { LightningElement, wire, track } from 'lwc';
import getAccounts from '@salesforce/apex/AccountController.getAccounts';
export default class AccountList extends LightningElement {
@track searchKey = '';
@track accounts = [];
@track error;
@wire(getAccounts, { searchKey: '$searchKey' })
wiredAccounts({ error, data }) {
if (data) { this.accounts = data; this.error = undefined; }
else if (error) { this.error = error; this.accounts = []; }
}
handleSearch(event) {
this.searchKey = event.target.value;
}
}This is not finished code. The Apex method does not exist yet, the template is missing, and the error rendering is naive. But it is a 60-second head start on a file that would have taken five minutes to set up by hand — imports, decorators, the wire-service shape, the standard error handling. We delete what does not fit and keep typing.
Plain JS utilities
Anything that is JavaScript and not Salesforce-aware works as well in this codebase as it does in any TypeScript repo Copilot has seen. Date formatting helpers, debounce, deep-equal, an array chunker. Copilot has seen these patterns ten thousand times and produces clean output on the first try.
Jest tests
Hand Copilot a function and it scaffolds the describe/it/expect shell, including the obvious edge cases — empty input, null, the boundary value. We still write the assertions for the cases that actually matter to the business logic, but the file structure and the trivial cases come for free.
Simple Apex CRUD
For a single-record insert or update with a try/catch around it, Copilot is fine:
public with sharing class CaseService {
public static Case createCase(String subject, Id accountId) {
try {
Case c = new Case(Subject = subject, AccountId = accountId, Status = 'New');
insert c;
return c;
} catch (DmlException e) {
System.debug(LoggingLevel.ERROR, 'Failed to create case: ' + e.getMessage());
throw new AuraHandledException('Could not create case');
}
}
}The shape is right, the exception type is right, the with sharing keyword is there. We accept this kind of suggestion and move on. Note the failure mode hiding in plain sight, though — this method is single-record by design. The minute someone wraps it in a loop, we have a problem. Which leads to the next section.
Where Copilot misreads Salesforce
The single most useful thing in GitHub’s own documentation is the language support page. The first-class language list is C, C++, C#, Go, Java, JavaScript, Kotlin, PHP, Python, Ruby, Rust, Scala, Swift, and TypeScript. Apex is not on it. LWC, while ostensibly JavaScript, is not the JavaScript Copilot was tuned on.
Copilot still produces Apex and LWC suggestions because there is public Apex code on GitHub for it to have learned from. But the model has never been told that a SOQL query inside a for-loop is a bug, that a 200-record DML batch is a hard ceiling, or that WHERE Name LIKE '%foo%' against a 5M-row object will time out. The suggestions look fluent. They are not platform-aware.
Bulkification
This is the most common failure mode and the most expensive. Copilot generates code like this:
for (Account acc : accountsToProcess) {
List<Contact> contacts = [
SELECT Id, Email FROM Contact WHERE AccountId = :acc.Id
];
for (Contact c : contacts) {
c.Email_Verified__c = true;
update c;
}
}A SOQL query and a DML statement inside a loop over Accounts. With 200 Accounts you hit the SOQL governor limit on the 101st iteration. A Salesforce dev would query all child Contacts in a single SOQL call (WHERE AccountId IN :accountIds), build a map keyed by AccountId, and issue one bulk update outside the loop.
Copilot reaches for this pattern because it is what it has seen in Java and C#, where issuing a query per row is normal and the database tier handles the batching. On the Salesforce platform, the query limit is 100, the DML limit is 150, and there is no rescue tier underneath you.
Governor-limit-aware loops
Copilot has no awareness of the Limits class or the per-transaction ceilings. It will happily generate a method that processes a List<sObject> of arbitrary size with no chunking, no Database.Stateful consideration if this needs to be batchable, and no early exit. For anything that runs in a trigger context or against a large data volume, that is not just slow — it is a CPU timeout waiting for production traffic.
SOQL selectivity
Ask Copilot for a search query and it produces:
List<Account> matches = [
SELECT Id, Name FROM Account WHERE Name LIKE '%' + searchTerm + '%'
];The leading wildcard makes the query non-selective. Against a small org this is fine. Against a five-million-row Account table this is a query timeout. A Salesforce dev would either constrain the query with an indexed field, use SOSL for cross-object text search, or push the search to an external full-text engine. Copilot does not know the difference.
Namespace and metadata awareness
Copilot does not know what custom fields actually exist in your org. It will reference Account.Account_Tier__c, Contact.Lifecycle_Stage__c, plausible-sounding fields that may or may not be on your schema. Sometimes they exist. Sometimes the field is named AccountTier__c without the underscore, or Customer_Tier__c, or it is on Opportunity instead of Account. Every Copilot Apex suggestion that references a custom field has to be checked against the actual metadata before it deploys. This is the rule we hit most often in code review.
The adoption rules we wrote
After about six weeks of false starts and one near-miss in UAT, we wrote three rules. These are taped to the architect’s monitor in literal Sharpie.
1. One-line completions: tab through. A single line of Copilot completion is small enough to scan in a glance. If the line looks right, accept it and keep going. If it looks wrong, type your own version. This is the ninety-percent case and it is where Copilot pays for itself.
2. Multi-line completions: read every line before accepting. A multi-line block is where Copilot can sneak in a SOQL-in-loop, a missing with sharing, a non-existent custom field, a recursion footgun. We do not tab through these. We read them. If we are not going to read them, we delete the suggestion and write the block ourselves — the cost of catching it in PR review is higher than the cost of writing it from scratch.
3. Never accept a Copilot suggestion without compiling. Either run sf project deploy validate against a scratch org, or at minimum let the local Apex language server finish its analysis pass before committing. Copilot suggestions look syntactically correct and reference fields that may not exist. Compile is the first cheap signal.
These three rules cost us nothing to enforce and caught most of what we needed to catch. They are not policy documents. They are a shared agreement that “Copilot wrote it” does not mean “I read it.”
Closing
Copilot is a real productivity gain for the parts of our codebase that look like the rest of GitHub — JavaScript utilities, LWC scaffolds, Jest tests, simple Apex CRUD. It is unreliable on the parts that look like Salesforce — bulkification, governor limits, SOQL selectivity, custom metadata. The honest take is that one general-purpose assistant cannot do both jobs well, because the platform-specific job needs platform-specific training data and platform-specific context.
Next week: why Copilot alone wasn’t enough — and the second assistant we layered on top, one that actually knows what an sObject is. We cover Agentforce Vibes on top of Copilot, how the two coexist without stepping on each other, and the per-task decision rule we use to pick between them — including a side-by-side prompt comparison. If you want the platform-side context first, the Agentforce platform primer is the right starting point.
How did this article make you feel?
Comments
Salesforce Tip