Well-Architected anti-patterns
vulkro-sf ships two distinct commands:
vulkro-sf scan: security findings. The default Vulkro surface: CRUD/FLS, injection, XSS, secrets, sharing, posture, identity, third-party, AI / Agentforce. The output gates the AppExchange readiness report.vulkro-sf antipatterns: Salesforce Well-Architected anti-pattern detection. The output is a separate report.
The two commands are intentionally split. This page explains why, documents the AP-001 through AP-014 series, and shows the invocation.
What this command is not
vulkro-sf antipatterns is not security analysis. The findings
it emits do not map to the AppExchange Security Review checklist
sections, do not carry OWASP categories, and do not raise CVSS-style
severity. They are architecture-quality findings about
governor-limit risk, code maintainability, test-class coverage, and
the operational patterns Salesforce architects publish as the
Well-Architected Framework (see
Methodology section 1).
A SOQL query inside a for loop is a Well-Architected anti-pattern
(AP-001): the org will hit the SOQL query governor limit at scale
and the operation will fail at runtime. That is a reliability and
maintainability problem, not a security problem. Mixing it into the
security finding stream would confuse the signal both for engineers
triaging security and for reviewers reading the AppExchange
readiness report.
Why it is split from scan
Three reasons:
- Different signal class. Security findings answer "could this be exploited?". Anti-pattern findings answer "will this break at scale?" or "is this idiomatic Salesforce?". Mixing the two surfaces in one stream makes triage harder.
- Different severity model. Security severity tracks exploitability and blast radius. Anti-pattern severity tracks how reliably the pattern will fail at production volume and how expensive the fix is later. The two are not directly comparable and should not share a Critical / High / Medium / Low scale.
- Different consumer. AppExchange Security Reviewers read the security output; engineering leads and Salesforce architects read the anti-pattern output. The audiences want different reports; they get different commands.
The AP-001 through AP-014 series
Anti-pattern IDs are stable. Each ID is permanent once shipped; a finding's ID does not move between releases. The full series is in the table below; each row links to its detection deep-dive in the Methodology coverage matrix.
| ID | Pillar / Dimension | Pattern |
|---|---|---|
| AP-001 | Trusted / Reliable | SOQL query inside a for / while / do loop. Hits the per-transaction query governor limit (100 SOQL queries in synchronous Apex). Fix by bulkifying: collect IDs first, query once outside the loop. |
| AP-002 | Trusted / Reliable | DML statement inside a loop. Hits the per-transaction DML statement limit (150). Same bulkification fix as AP-001. |
| AP-003 | Adaptable / Future-Proof | Hardcoded Salesforce record ID. A 15- or 18-character ID baked into source. Breaks on org migration; couples the package to a single org. Use Custom Metadata, Custom Settings, or Named Credentials instead. |
| AP-004 | Trusted / Reliable | Apex class with no apparent test class. Cross-file: looks for a sibling-named *Test.cls or a substring reference in any @isTest class. AppExchange requires 75% code coverage. |
| AP-005 | Adaptable / Composable | Multiple triggers on the same SObject. Two or more trigger files target the same on SObject. Order of execution becomes implementation-defined. Use a single trigger plus a handler pattern. |
| AP-006 | Trusted / Reliable | Recursive trigger without a static guard. A trigger that performs DML on Trigger.new or Trigger.old without a static-boolean recursion guard. Re-enters on the same transaction. |
| AP-007 | Trusted / Reliable | Mixed-DML transaction (setup + non-setup sObjects). A single transaction touching a setup object (User, Group, UserRole, Profile, QueueSObject, etc.) and a non-setup object (Account, custom object, etc.) without separating them with System.runAs or async. Raises MIXED_DML_OPERATION at runtime. Excludes @isTest and explicit System.runAs blocks. |
| AP-008 | Easy / Automated | Schema.describe* call inside a loop. Schema.SObjectType.X.fields.getMap() and similar describes are expensive; calling them per iteration in a loop wastes CPU. Cache the describe result outside the loop. |
| AP-009 | Adaptable / Composable | @future method abuse: callout-violation patterns. @future(callout=true) invoked from a context that already issued a callout, or @future chained from another @future. Use Queueable for chained async or Continuation for parallel callouts. |
| AP-010 | Trusted / Reliable | @isTest(SeeAllData=true). Production data visibility in tests. Brittle (depends on org data state), forbidden by AppExchange Security Review, and prevents parallel test execution. Use a TestDataFactory pattern with @testSetup. |
| AP-011 | Trusted / Reliable | Mixed-DML transactional rules. Stricter check than AP-007: flags constructor patterns that build setup-object instances and non-setup-object instances within the same file scope, even when no DML has fired yet, because the pattern is a near-certain mixed-DML at the next persistence step. |
| AP-012 | Adaptable / Composable | Hardcoded URL in callout (use Named Credential). HttpRequest.setEndpoint('http://...') or setEndpoint('https://...') where the argument is a literal URL string rather than a callout:NamedCredentialName/path reference. Named Credentials let the org admin rotate the credential and the endpoint without a deployment. |
| AP-013 | Trusted / Reliable | Non-restrictive SOQL. SELECT ... FROM ... with no WHERE and no LIMIT. Full-object scan; hits the row limit on a real org. |
| AP-014 | Trusted / Reliable | Test class methods without assertions. Test methods that call into production code but do not call any Assert.* or System.assert*. The test runs and contributes to coverage but does not actually verify behavior. AppExchange Security Review reads this as low test quality. |
Resource Optimiser: the governor-limit group
A subset of the Trusted (Reliable) rules share one theme: they each burn a slice of a per-transaction governor limit. Vulkro for Salesforce groups them under a Resource Optimiser label so a reviewer can read all the "manage your governor limits" findings as one set. The group covers AP-001, AP-002, AP-008, AP-013, AP-040, AP-042 and the five callout / async-design rules below.
| ID | Pillar / Dimension | Pattern |
|---|---|---|
| AP-059 | Trusted / Reliable | HTTP callout inside a loop. An Http.send, new HttpRequest, .send(req), or Database.getRecords / getQueryLocator callout issued per record. Each iteration consumes one of the 100 callouts allowed per transaction. Hoist the callout out of the loop or batch the work into one request. |
| AP-065 | Trusted / Reliable | @future method invoked per-record. A same-file @future static method called from inside a loop. Apex caps @future at 50 invocations per transaction. Redesign the method to accept a collection and call it once. |
| AP-071 | Trusted / Reliable | System.enqueueJob inside a loop. A Queueable enqueued per record. A transaction may add at most 50 jobs to the async queue (one from inside a running Queueable chain). Enqueue a single job over the whole collection. |
| AP-072 | Trusted / Reliable | Batchable start() query not bounded. A Database.Batchable class whose start() SOQL returns a QueryLocator with no LIMIT. The locator can stream the entire object. Advisory: bound the scope with a WHERE clause and an explicit executeBatch scope size. |
| AP-073 | Trusted / Reliable | Method consumes a large share of the transaction budget. A single method body that statically issues more than 15 SOQL queries or more than 15 DML statements (even outside a loop). Intra-method advisory; distinct from the in-loop rules AP-001 / AP-002. |
In --format table output, every finding in this group is tagged
(Resource Optimiser) next to its [AP-xxx] line. This is a display-only
label: the serialized pillar field in --format json, sarif, and junit
output is always the underlying Trusted (Reliable) value, so machine
consumers are unaffected.
Invocation
vulkro-sf antipatterns ./force-app
Same input shape as vulkro-sf scan: an SFDX project root, a
retrieved metadata folder, or any directory containing Apex source.
The output is grouped by anti-pattern ID, lists every triggering
file and line, and follows the standard exit-code contract:
0: no anti-patterns triggered.1: at least one anti-pattern fired.2: error.
Output formats
--format accepts the same shapes as vulkro-sf scan:
vulkro-sf antipatterns ./force-app --format text # default
vulkro-sf antipatterns ./force-app --format sarif -o ap.sarif
vulkro-sf antipatterns ./force-app --format json -o ap.json
vulkro-sf antipatterns ./force-app --format html -o ap.html
The SARIF output uses a separate Vulkro tool name
(vulkro-sf-antipatterns) so downstream tools that consume both
streams can split the views cleanly.
CI gating
The recommended pattern is to gate vulkro-sf scan (security) on
PR, and to gate vulkro-sf antipatterns on the nightly run or
on main-branch pushes. Anti-patterns rarely need to block a single
PR; they need to block a release. See
CI/CD integration for the wiring.
Where to go next
- CLI reference:
vulkro-sf antipatterns: the full flag matrix and return-shape contract. - Methodology section 9: the per-anti-pattern detection deep-dive, with pillar mappings and the "what we cover, what is queued" rows.
- Salesforce Well-Architected Framework: the authoritative source for the three pillars (Trusted, Easy, Adaptable) the anti-pattern IDs map to.