AI Code Review for Apex: My Workflow
I shipped a Contact trigger to production last year with a SOQL query sitting inside a for loop. It passed every unit test, because the tests ran against five records. It passed two rounds of human review, because both reviewers were reading for logic, not for shape. Then a data load touched around 900 Contacts in one batch and the whole thing fell over on query limits at 3pm on a Wednesday.
Nobody was careless. The bug was invisible the way these bugs always are — obvious in isolation, easy to miss in a 400-line diff when you're the third file deep and it's late in the afternoon. That gap is exactly where AI code review earns its place, and over the past year I've wired it into the way my teams ship Apex. This is the workflow that survived contact with a real release cadence, not the demo version.
The Bug Class Humans Skim Past
AI is good at a specific category of defect: mechanical, pattern-based, and repetitive. The kind you can describe as a rule but nobody enforces consistently across fifteen files at 6pm.
The list I actually get value from is short and boring. SOQL or DML inside a loop. Trigger logic that assumes a single record and quietly breaks at bulk. Missing CRUD and FLS checks before a DML on a user-triggered path. Hardcoded record IDs, org IDs, and usernames. Test methods that hit the coverage number but assert nothing meaningful. Null dereferences waiting for the one record that has an empty related field.
None of that requires taste. It requires attention that doesn't degrade, and human attention degrades. A reviewer who catches a SOQL-in-loop on file two will skim right past the same pattern on file twelve, because they're tired and the diff is long. An AI reviewer reads file twelve with exactly the same focus it read file two. That consistency is the whole value — it's not smarter than a good engineer, it just never gets bored.
The SOQL patterns it flags are the ones I've written about at length before, and the security gaps map almost one to one onto the CRUD and FLS enforcement checklist I run manually. What AI does is apply those checklists to every diff without me having to remember to.
Here's the opinion I'll defend: AI review is not there to replace a reviewer. It's there to do the tedious pass so the human review can be about the things only a human can weigh — is this the right design, does this belong in a trigger at all, is the person who wrote this about to paint us into a corner. If your AI review is making those calls, you've pointed it at the wrong problem.
Wiring It Into the Pipeline
The mechanics are unglamorous, which is how I like infrastructure. It runs as a GitHub Actions job on every pull request that touches Apex, alongside the existing test and static analysis steps.
name: apex-ai-review
on:
pull_request:
paths:
- 'force-app/**/*.cls'
- 'force-app/**/*.trigger'
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Collect changed Apex
run: |
git diff --name-only origin/${{ github.base_ref }}...HEAD \
-- 'force-app/**/*.cls' 'force-app/**/*.trigger' > changed.txt
- name: Run AI review
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: ./scripts/ai-review.sh changed.txt > review.md
- name: Post one summary comment
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh pr comment ${{ github.event.pull_request.number }} --body-file review.md
Two choices in there are deliberate. The paths filter means the job never runs on a Flow-only or LWC-only change — no point spending tokens and wall-clock time reviewing files this prompt knows nothing useful about. And the review lands as a single summary comment, not a swarm of inline annotations.
That second choice matters more than it looks. Inline bot comments feel sophisticated for about a week, then they become wallpaper. One comment the reviewer can read top to bottom in thirty seconds gets read. Twenty inline threads on a PR get collapsed and ignored. I optimised for the thing people would actually look at, not the thing that demos well. This slots into the same pipeline discipline everything else on the platform lives under, and the AI step is genuinely just another job — it doesn't get special treatment.
The Prompt Does the Heavy Lifting
The script is thin. The prompt is the product. An unconstrained AI reviewer will comment on everything — naming, formatting, a missing comment it thinks should exist — and it will train your team to stop reading inside a fortnight. The most important part of the whole setup is the list of things it must not say.
The rubric I hand it looks roughly like this:
You are reviewing an Apex pull request for a Salesforce org.
Flag only these, and only when you are confident:
- SOQL or DML inside a for or while loop
- Trigger or handler logic that assumes a single record
- DML on a user-triggered path with no CRUD/FLS check
- Hardcoded record IDs, org IDs, or usernames
- Test methods with no assertions, or assertions weaker than
the behaviour the method name claims to test
Do NOT comment on:
- Naming, formatting, or code style
- Missing comments or documentation
- Anything you are less than confident about
For each finding return: file, approximate line, one sentence
on the problem, one sentence on the fix. If you find nothing,
say so in one line. Do not pad.
The "do not pad" and "only when confident" lines carry real weight. Without them the model finds five issues in clean code because it assumes it was invited to find issues. The negative list is what keeps the signal high enough that a senior developer still bothers to read the output on the fortieth PR.
In the first six weeks I ran this across 40-odd pull requests on a financial services build. It flagged 71 comments in total. Twenty-two of those were real — the kind that would have cost someone an afternoon of debugging or, in one case, a bulk failure in UAT that would have looked exactly like my Wednesday. The rest was noise, and about a third of that noise was the same three false positives repeating, which told me exactly what to tune. We never got the false-positive rate to zero. We got it to "people still read the comment," added a couple of examples to the prompt, and left it there. It's still on the list of things I'd like to be better.
The Trade-off I Made Explicit
Every choice in that rubric buys one thing at the cost of another, and it is worth naming the trade out loud rather than pretending the tool is free. A narrow prompt that flags a short list of bug classes and stays silent on everything else will miss things. Real defects — a subtle sharing violation, an integration that assumes a field is always populated — fall outside the list and sail through untouched. I know that. I chose it anyway.
The alternative is a broad prompt that comments on everything it can find. That version catches more on paper and nothing in practice, because the comments stop being read. I would rather the tool find most of the mechanical bugs and have every one of them actually looked at than find all of them and have the team trained to scroll past the lot. Precision I can trust beats coverage I cannot act on.
So the deal is explicit: I trade recall for trust. The AI misses some real bugs so that the ones it does surface land on a reviewer who still believes it. That only holds because the human pass is still there to catch what the narrow prompt lets through — the trade assumes a person is reading, not that the bot is the last line of defence.
The Noise Problem
False positives are not a cosmetic issue. They're the thing that quietly kills the whole practice.
There was a developer on that build — Ravi, six months in, sharp — who stopped reading the bot comments entirely around week three. I only noticed because he merged a PR with a genuine CRUD gap the AI had flagged clearly. He'd seen so many comments about "potential" issues that weren't real that he'd filed the whole channel under noise. That's the failure mode. It isn't the bot being wrong occasionally; it's the bot being wrong often enough that people stop distinguishing wrong from right.
The fix was two-part. Tighten the prompt so it flagged less and meant it more. And, just as importantly, keep the AI review advisory. It posts a comment. It is never a required status check that blocks the merge.
I'm firm on that last point. The moment a probabilistic reviewer can block a merge, you've handed release velocity to a system that is confidently wrong a predictable fraction of the time. A human decides what to do with the comment. The bot advises, the person merges. Anything else and you'll spend your Friday overriding a status check to ship a fix, which is precisely the excitement this whole setup exists to remove.
When AI Review Isn't the Answer
The honest limit is that AI reviews the diff, not the decision behind it. It will tell you the trigger is implemented sloppily. It will not tell you the trigger shouldn't exist, that this logic belongs in a record-triggered Flow, or that the whole feature is solving a problem the business stopped having two years ago. Those are architecture calls, and they need someone who understands the org's history and the team's maintenance posture.
It's also weak exactly where the stakes are highest — the judgement call on whether something passes a real security review, not just whether it's technically CRUD-safe. That's the same boundary I hit on the generation side of this workflow: AI is a strong accelerator for the mechanical work and a poor substitute for the parts that require actually knowing things. Review and generation fail in the same place, for the same reason.
What I'd do differently, if I were starting again, is introduce it more quietly. I turned it on across a whole team in one go and spent the first fortnight managing the noise reaction. If I did it now I'd run it silently against my own PRs for a month, tune the prompt against real diffs, and only then let the rest of the team see its output. First impressions with a tool like this are sticky, and a noisy debut costs you trust you then have to earn back.
Most of what I do with an unfamiliar codebase now starts with exactly this kind of pass before anyone points AI at writing new code — if you want a second pair of eyes wired into your pipeline, that's the kind of review I do.
A deploy that catches its own boring bugs is quiet, and quiet is the whole goal. The alternative is a Wednesday afternoon, a data load of around 900 records, and a query limit you could have seen coming.
Liked this?
New essays in your inbox when I publish. No cadence, no spam.
You might also like
How I Use Claude Code to Write Apex 5x Faster
The actual workflow I run for AI-assisted Apex development — what AI handles well, what it gets wrong, and the prompts that survive code review.
Read →AI-Assisted Salesforce Data Migration: A Real Walkthrough
How I actually use AI across a Salesforce data migration — profiling, field mapping, and the transform Apex — plus the near-miss that set my one hard rule.
Read →AI-Generated SOQL: Surprisingly Good, Occasionally Dangerous
AI writes cleaner SOQL than I do at 5pm — right relationships, right field names. But it can't see your data volumes. Here's where it helps and hurts.
Read →