sf-forcekit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/License +21 -0
- package/README.md +106 -0
- package/bin/cli.js +85 -0
- package/package.json +44 -0
- package/templates/ai-dir/README.md +155 -0
- package/templates/ai-dir/agentforce.md +213 -0
- package/templates/ai-dir/architecture.md +123 -0
- package/templates/ai-dir/commands.md +276 -0
- package/templates/ai-dir/context-snapshots/TEMPLATE.md +64 -0
- package/templates/ai-dir/conventions.md +242 -0
- package/templates/ai-dir/current-state.md +113 -0
- package/templates/ai-dir/debugging-notes.md +165 -0
- package/templates/ai-dir/deployment.md +161 -0
- package/templates/ai-dir/integrations.md +199 -0
- package/templates/ai-dir/inventory.md +209 -0
- package/templates/ai-dir/known-issues.md +124 -0
- package/templates/ai-dir/org-context.md +110 -0
- package/templates/ai-dir/performance.md +312 -0
- package/templates/ai-dir/prompts/agentforce.md +163 -0
- package/templates/ai-dir/prompts/apex.md +165 -0
- package/templates/ai-dir/prompts/flows.md +125 -0
- package/templates/ai-dir/prompts/lwc.md +230 -0
- package/templates/ai-dir/prompts/security.md +181 -0
- package/templates/ai-dir/prompts/testing.md +269 -0
- package/templates/ai-dir/rules.md +238 -0
- package/templates/ai-dir/scripts/update_state.py +1406 -0
- package/templates/ai-dir/source-of-truth.md +180 -0
- package/templates/ai-dir/templates/Selector.cls +113 -0
- package/templates/ai-dir/templates/Service.cls +132 -0
- package/templates/ai-dir/templates/TestClass.cls +143 -0
- package/templates/ai-dir/templates/TriggerHandler.cls +67 -0
- package/templates/ai-dir/testing-strategy.md +342 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# Performance Guide
|
|
2
|
+
|
|
3
|
+
> Governor limits, SOQL optimization, heap management, and async patterns.
|
|
4
|
+
> Consult this before writing any code that touches large datasets or complex logic.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Governor Limits — Quick Reference
|
|
9
|
+
|
|
10
|
+
| Limit | Sync | Async | Monitor With |
|
|
11
|
+
|-------|------|-------|--------------|
|
|
12
|
+
| SOQL queries | 100 | 200 | `Limits.getQueries()` |
|
|
13
|
+
| SOQL rows returned | 50,000 | 50,000 | `Limits.getQueryRows()` |
|
|
14
|
+
| DML statements | 150 | 150 | `Limits.getDmlStatements()` |
|
|
15
|
+
| DML rows | 10,000 | 10,000 | `Limits.getDmlRows()` |
|
|
16
|
+
| CPU time | 10,000 ms | 60,000 ms | `Limits.getCpuTime()` |
|
|
17
|
+
| Heap size | 6 MB | 12 MB | `Limits.getHeapSize()` |
|
|
18
|
+
| Callouts | 100 | 100 | `Limits.getCallouts()` |
|
|
19
|
+
| Future calls | 50 | 0 | `Limits.getFutureCalls()` |
|
|
20
|
+
| Queueable jobs | 50 | 1 | `Limits.getQueueableJobs()` |
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## SOQL Optimization
|
|
25
|
+
|
|
26
|
+
### Indexing Rules
|
|
27
|
+
|
|
28
|
+
Salesforce auto-indexes:
|
|
29
|
+
- `Id`, `Name`, `OwnerId`, `CreatedDate`, `LastModifiedDate`, `SystemModstamp`
|
|
30
|
+
- All lookup/master-detail fields
|
|
31
|
+
- `RecordTypeId`
|
|
32
|
+
- External ID fields (`__c` with External ID checked)
|
|
33
|
+
|
|
34
|
+
**Custom indexes** — Request via Salesforce Support for high-volume objects.
|
|
35
|
+
|
|
36
|
+
### Query Performance Checklist
|
|
37
|
+
|
|
38
|
+
```apex
|
|
39
|
+
// ✅ GOOD — Selective, uses indexed field
|
|
40
|
+
[SELECT Id, Name FROM Account
|
|
41
|
+
WHERE Id IN :accountIds
|
|
42
|
+
WITH USER_MODE]
|
|
43
|
+
|
|
44
|
+
// ✅ GOOD — Narrow date range on indexed field
|
|
45
|
+
[SELECT Id FROM Case
|
|
46
|
+
WHERE CreatedDate = LAST_N_DAYS:30
|
|
47
|
+
WITH USER_MODE]
|
|
48
|
+
|
|
49
|
+
// ❌ BAD — Leading wildcard forces table scan
|
|
50
|
+
[SELECT Id FROM Account
|
|
51
|
+
WHERE Name LIKE '%test%'
|
|
52
|
+
WITH USER_MODE]
|
|
53
|
+
|
|
54
|
+
// ❌ BAD — Negative filter, not selective
|
|
55
|
+
[SELECT Id FROM Account
|
|
56
|
+
WHERE Industry != 'Technology'
|
|
57
|
+
WITH USER_MODE]
|
|
58
|
+
|
|
59
|
+
// ❌ BAD — OR across non-indexed fields
|
|
60
|
+
[SELECT Id FROM Account
|
|
61
|
+
WHERE Custom_Field_1__c = 'X' OR Custom_Field_2__c = 'Y'
|
|
62
|
+
WITH USER_MODE]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Query Plan Analysis
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
/services/data/v66.0/query?explain=SELECT+Id+FROM+Account+WHERE+Name='Test'
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
| Result | Meaning | Action |
|
|
72
|
+
|--------|---------|--------|
|
|
73
|
+
| `Index` | Uses index | ✅ Good |
|
|
74
|
+
| `TableScan` | Full table scan | ❌ Add index or restructure |
|
|
75
|
+
| `Selectivity: 0.xx` | % of table scanned | < 10% is good |
|
|
76
|
+
|
|
77
|
+
### Reducing SOQL Count
|
|
78
|
+
|
|
79
|
+
```apex
|
|
80
|
+
// ❌ BAD — Separate queries per object (wastes SOQL budget)
|
|
81
|
+
List<Account> accounts = [SELECT Id FROM Account WHERE Id IN :ids WITH USER_MODE];
|
|
82
|
+
List<Contact> contacts = [SELECT Id FROM Contact WHERE AccountId IN :ids WITH USER_MODE];
|
|
83
|
+
List<Case> cases = [SELECT Id FROM Case WHERE AccountId IN :ids WITH USER_MODE];
|
|
84
|
+
|
|
85
|
+
// ✅ GOOD — Single parent query with subqueries
|
|
86
|
+
List<Account> accounts = [
|
|
87
|
+
SELECT Id, Name,
|
|
88
|
+
(SELECT Id, Name FROM Contacts),
|
|
89
|
+
(SELECT Id, Subject FROM Cases)
|
|
90
|
+
FROM Account
|
|
91
|
+
WHERE Id IN :ids
|
|
92
|
+
WITH USER_MODE
|
|
93
|
+
];
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Heap Management
|
|
99
|
+
|
|
100
|
+
### When Heap Matters
|
|
101
|
+
|
|
102
|
+
| Scenario | Risk Level |
|
|
103
|
+
|----------|-----------|
|
|
104
|
+
| Processing 50K+ records | 🔴 High |
|
|
105
|
+
| JSON deserializing large payloads | 🔴 High |
|
|
106
|
+
| Building large Maps/Sets | 🟡 Medium |
|
|
107
|
+
| String concatenation in loops | 🟡 Medium |
|
|
108
|
+
| Normal CRUD operations | 🟢 Low |
|
|
109
|
+
|
|
110
|
+
### Heap Reduction Patterns
|
|
111
|
+
|
|
112
|
+
```apex
|
|
113
|
+
// ❌ BAD — Loads entire result set into memory
|
|
114
|
+
List<Account> allAccounts = [SELECT Id, Name, Description FROM Account WITH USER_MODE];
|
|
115
|
+
|
|
116
|
+
// ✅ GOOD — Use SOQL for filtering, only load what you need
|
|
117
|
+
List<Account> activeAccounts = [
|
|
118
|
+
SELECT Id, Name
|
|
119
|
+
FROM Account
|
|
120
|
+
WHERE IsActive__c = true
|
|
121
|
+
WITH USER_MODE
|
|
122
|
+
LIMIT 2000
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
// ✅ BETTER — For 50K+ records, use Batch Apex with QueryLocator
|
|
126
|
+
global class LargeDataBatch implements Database.Batchable<SObject> {
|
|
127
|
+
global Database.QueryLocator start(Database.BatchableContext bc) {
|
|
128
|
+
return Database.getQueryLocator([
|
|
129
|
+
SELECT Id, Name FROM Account WITH USER_MODE
|
|
130
|
+
]);
|
|
131
|
+
}
|
|
132
|
+
// QueryLocator bypasses 50K row limit — processes up to 50M rows
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Apex Cursors (v66.0+)
|
|
137
|
+
|
|
138
|
+
```apex
|
|
139
|
+
// ✅ NEW — Process large result sets in chunks without heap pressure
|
|
140
|
+
Database.Cursor cursor = Database.getCursor(
|
|
141
|
+
'SELECT Id, Name FROM Account WITH USER_MODE'
|
|
142
|
+
);
|
|
143
|
+
Integer total = cursor.getNumRecords();
|
|
144
|
+
|
|
145
|
+
// Fetch in chunks of 200
|
|
146
|
+
for (Integer offset = 0; offset < total; offset += 200) {
|
|
147
|
+
List<Account> chunk = (List<Account>) cursor.fetch(offset, 200);
|
|
148
|
+
processChunk(chunk);
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## CPU Time Optimization
|
|
155
|
+
|
|
156
|
+
### Common CPU Hogs
|
|
157
|
+
|
|
158
|
+
| Pattern | Fix |
|
|
159
|
+
|---------|-----|
|
|
160
|
+
| Nested loops (`O(n²)`) | Use Map-based lookups |
|
|
161
|
+
| String concatenation in loops | Use `List<String>.join()` |
|
|
162
|
+
| `Schema.getGlobalDescribe()` in loops | Cache outside loop |
|
|
163
|
+
| Complex SOQL field references in loops | Pre-build maps |
|
|
164
|
+
| Regular expressions | Pre-compile Pattern |
|
|
165
|
+
|
|
166
|
+
### Map-Based Lookup Pattern
|
|
167
|
+
|
|
168
|
+
```apex
|
|
169
|
+
// ❌ O(n²) — Nested loop
|
|
170
|
+
for (Contact c : contacts) {
|
|
171
|
+
for (Account a : accounts) {
|
|
172
|
+
if (c.AccountId == a.Id) {
|
|
173
|
+
// process
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ✅ O(n) — Map lookup
|
|
179
|
+
Map<Id, Account> accountMap = new Map<Id, Account>(accounts);
|
|
180
|
+
for (Contact c : contacts) {
|
|
181
|
+
Account a = accountMap.get(c.AccountId);
|
|
182
|
+
if (a != null) {
|
|
183
|
+
// process
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Async Pattern Selection
|
|
191
|
+
|
|
192
|
+
| Need | Pattern | Why |
|
|
193
|
+
|------|---------|-----|
|
|
194
|
+
| Simple fire-and-forget | `@future` | Lowest overhead |
|
|
195
|
+
| Complex types / chaining | `Queueable` | **Preferred default** |
|
|
196
|
+
| 50K+ records | `Batch Apex` | QueryLocator = 50M rows |
|
|
197
|
+
| Time-based processing | `Schedulable` → Batch | Cron trigger |
|
|
198
|
+
| Cross-system / decoupled | `Platform Events` | Event-driven |
|
|
199
|
+
| Mixed DML | `@future` or `Queueable` | Separate transactions |
|
|
200
|
+
|
|
201
|
+
### Queueable Pattern (Preferred)
|
|
202
|
+
|
|
203
|
+
```apex
|
|
204
|
+
public class ProcessAccountsQueueable implements Queueable {
|
|
205
|
+
private List<Id> accountIds;
|
|
206
|
+
private Integer retryCount;
|
|
207
|
+
|
|
208
|
+
public ProcessAccountsQueueable(List<Id> accountIds) {
|
|
209
|
+
this(accountIds, 0);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
public ProcessAccountsQueueable(List<Id> accountIds, Integer retryCount) {
|
|
213
|
+
this.accountIds = accountIds;
|
|
214
|
+
this.retryCount = retryCount;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
public void execute(QueueableContext context) {
|
|
218
|
+
try {
|
|
219
|
+
AccountService.heavyProcessing(accountIds);
|
|
220
|
+
} catch (Exception e) {
|
|
221
|
+
if (retryCount < 3) {
|
|
222
|
+
System.enqueueJob(new ProcessAccountsQueueable(accountIds, retryCount + 1));
|
|
223
|
+
} else {
|
|
224
|
+
Logger.error('Max retries exceeded', e);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## DML Optimization
|
|
234
|
+
|
|
235
|
+
### Partial DML for Fault Tolerance
|
|
236
|
+
|
|
237
|
+
```apex
|
|
238
|
+
// ✅ Use Database methods with allOrNone=false for bulk tolerance
|
|
239
|
+
Database.SaveResult[] results = Database.insert(records, false);
|
|
240
|
+
for (Database.SaveResult sr : results) {
|
|
241
|
+
if (!sr.isSuccess()) {
|
|
242
|
+
for (Database.Error err : sr.getErrors()) {
|
|
243
|
+
Logger.error('Insert failed for record: ' + err.getMessage());
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Batch DML at End of Transaction
|
|
250
|
+
|
|
251
|
+
```apex
|
|
252
|
+
// ❌ BAD — Scattered DML throughout method
|
|
253
|
+
public static void processOrder(Order__c order) {
|
|
254
|
+
update order; // DML 1
|
|
255
|
+
insert lineItems; // DML 2
|
|
256
|
+
update relatedAccount; // DML 3
|
|
257
|
+
EventBus.publish(statusEvent); // DML 4
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ✅ GOOD — Collect and batch
|
|
261
|
+
public static void processOrder(Order__c order) {
|
|
262
|
+
List<SObject> toUpdate = new List<SObject>();
|
|
263
|
+
List<SObject> toInsert = new List<SObject>();
|
|
264
|
+
|
|
265
|
+
toUpdate.add(order);
|
|
266
|
+
toInsert.addAll(lineItems);
|
|
267
|
+
toUpdate.add(relatedAccount);
|
|
268
|
+
|
|
269
|
+
insert toInsert;
|
|
270
|
+
update toUpdate;
|
|
271
|
+
EventBus.publish(statusEvent);
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## In-Code Monitoring
|
|
278
|
+
|
|
279
|
+
Add this to performance-critical methods:
|
|
280
|
+
|
|
281
|
+
```apex
|
|
282
|
+
public static void criticalMethod(List<Account> accounts) {
|
|
283
|
+
Long startCpu = Limits.getCpuTime();
|
|
284
|
+
Integer startQueries = Limits.getQueries();
|
|
285
|
+
Integer startHeap = Limits.getHeapSize();
|
|
286
|
+
|
|
287
|
+
// ... method logic ...
|
|
288
|
+
|
|
289
|
+
System.debug(LoggingLevel.INFO, String.format(
|
|
290
|
+
'>>> Performance: CPU={0}ms, SOQL={1}, Heap={2}KB',
|
|
291
|
+
new List<Object>{
|
|
292
|
+
Limits.getCpuTime() - startCpu,
|
|
293
|
+
Limits.getQueries() - startQueries,
|
|
294
|
+
(Limits.getHeapSize() - startHeap) / 1024
|
|
295
|
+
}
|
|
296
|
+
));
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## Red Flags (Performance Code Smells)
|
|
303
|
+
|
|
304
|
+
| Smell | Risk | Fix |
|
|
305
|
+
|-------|------|-----|
|
|
306
|
+
| SOQL inside `for` loop | 🔴 101 limit | Move query before loop, use Map |
|
|
307
|
+
| DML inside `for` loop | 🔴 151 limit | Collect, single DML after loop |
|
|
308
|
+
| `Schema.getGlobalDescribe()` per record | 🔴 CPU timeout | Cache once, reuse |
|
|
309
|
+
| `String += ` in loop with 1000+ iterations | 🟡 Heap + CPU | Use `List<String>` + `.join()` |
|
|
310
|
+
| Loading all fields (`SELECT *` equivalent) | 🟡 Heap | Select only needed fields |
|
|
311
|
+
| Recursive trigger without guard | 🔴 Stack overflow | Static `Set<Id>` guard |
|
|
312
|
+
| `for (SObject s : [SELECT ...])` inside another loop | 🔴 101 limit | Pre-query, use Map |
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Agentforce Development Rules
|
|
2
|
+
|
|
3
|
+
> Instructions for AI agents when building Agentforce Topics, Actions, and Prompt Templates.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Core Principle
|
|
8
|
+
|
|
9
|
+
**Declarative First, Code Second.**
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
Can a Standard Action do it? → Use Standard Action
|
|
13
|
+
Can a Flow do it? → Use Flow Action
|
|
14
|
+
Need complex logic? → Use @InvocableMethod Apex Action
|
|
15
|
+
Need AI-generated text? → Use Prompt Template Action
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Action Design Rules
|
|
21
|
+
|
|
22
|
+
### 1. Descriptions Are Everything
|
|
23
|
+
|
|
24
|
+
The Atlas Reasoning Engine chooses actions based on natural language descriptions. Write them like you're explaining to a smart colleague what this action does and WHEN to use it.
|
|
25
|
+
|
|
26
|
+
```apex
|
|
27
|
+
// ✅ GOOD
|
|
28
|
+
@InvocableMethod(
|
|
29
|
+
label='Get Account Health Score'
|
|
30
|
+
description='Calculates a health score (0-100) for an Account based on recent activity, '
|
|
31
|
+
+ 'open cases, and engagement metrics. Use when the user asks about account health, '
|
|
32
|
+
+ 'risk level, engagement, or churn probability.'
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
// ❌ BAD
|
|
36
|
+
@InvocableMethod(
|
|
37
|
+
label='Process Account'
|
|
38
|
+
description='Does account stuff'
|
|
39
|
+
)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2. Input/Output Descriptions
|
|
43
|
+
|
|
44
|
+
Every `@InvocableVariable` needs a clear description:
|
|
45
|
+
|
|
46
|
+
```apex
|
|
47
|
+
public class AccountHealthRequest {
|
|
48
|
+
@InvocableVariable(
|
|
49
|
+
label='Account ID'
|
|
50
|
+
description='The 18-character Salesforce ID of the Account to analyze'
|
|
51
|
+
required=true
|
|
52
|
+
)
|
|
53
|
+
public Id accountId;
|
|
54
|
+
|
|
55
|
+
@InvocableVariable(
|
|
56
|
+
label='Include Forecast'
|
|
57
|
+
description='Set to true to include a 90-day churn forecast in the response'
|
|
58
|
+
required=false
|
|
59
|
+
)
|
|
60
|
+
public Boolean includeForecast;
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 3. Return Meaningful Responses
|
|
65
|
+
|
|
66
|
+
Actions should return structured data that the agent can reason about:
|
|
67
|
+
|
|
68
|
+
```apex
|
|
69
|
+
public class AccountHealthResponse {
|
|
70
|
+
@InvocableVariable(label='Health Score' description='Score from 0-100, higher is healthier')
|
|
71
|
+
public Integer healthScore;
|
|
72
|
+
|
|
73
|
+
@InvocableVariable(label='Risk Level' description='LOW, MEDIUM, or HIGH')
|
|
74
|
+
public String riskLevel;
|
|
75
|
+
|
|
76
|
+
@InvocableVariable(label='Summary' description='Human-readable summary of account health')
|
|
77
|
+
public String summary;
|
|
78
|
+
|
|
79
|
+
@InvocableVariable(label='Recommendations' description='List of recommended actions')
|
|
80
|
+
public List<String> recommendations;
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Topic Design
|
|
87
|
+
|
|
88
|
+
### Naming
|
|
89
|
+
- Use clear, business-domain names: "Order Management", "Account Health", "Case Resolution"
|
|
90
|
+
- Avoid technical names: "API Handler", "Data Processor"
|
|
91
|
+
|
|
92
|
+
### Scope
|
|
93
|
+
- Each Topic should cover a coherent set of related tasks
|
|
94
|
+
- Don't make Topics too broad (entire CRM) or too narrow (single field update)
|
|
95
|
+
|
|
96
|
+
### Instructions
|
|
97
|
+
Write Topic instructions as if briefing a new team member:
|
|
98
|
+
```
|
|
99
|
+
You help customers with their orders. You can look up order status, modify shipping
|
|
100
|
+
addresses for orders that haven't shipped yet, and process return requests. Always
|
|
101
|
+
verify the customer's identity before making changes. If an order has already shipped,
|
|
102
|
+
direct the customer to the returns process instead.
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Prompt Template Rules
|
|
108
|
+
|
|
109
|
+
1. **Ground in data** — Use merge fields from Data Cloud or CRM objects
|
|
110
|
+
2. **Be specific** — Tell the LLM exactly what format you want
|
|
111
|
+
3. **Set guardrails** — Include what the agent should NOT do
|
|
112
|
+
4. **Test extensively** — Different inputs, edge cases, adversarial prompts
|
|
113
|
+
|
|
114
|
+
### Template Structure
|
|
115
|
+
```
|
|
116
|
+
You are a {role} helping with {domain}.
|
|
117
|
+
|
|
118
|
+
## Context
|
|
119
|
+
{Merge fields from Salesforce data}
|
|
120
|
+
|
|
121
|
+
## Task
|
|
122
|
+
{What to generate/analyze}
|
|
123
|
+
|
|
124
|
+
## Format
|
|
125
|
+
{Expected output format}
|
|
126
|
+
|
|
127
|
+
## Guardrails
|
|
128
|
+
- Do NOT {restrictions}
|
|
129
|
+
- Always {requirements}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Guardrails & Safety
|
|
135
|
+
|
|
136
|
+
| Mechanism | Purpose |
|
|
137
|
+
|-----------|---------|
|
|
138
|
+
| **Agent Script** | Enforce deterministic if/then workflows for critical tasks |
|
|
139
|
+
| **Content Safety Policies** | Filter inappropriate output |
|
|
140
|
+
| **Einstein Trust Layer** | PII masking before LLM, audit logging |
|
|
141
|
+
| **Topic Scoping** | Limit what each agent can access |
|
|
142
|
+
| **Command Center** | Monitor performance, audit trails, policy enforcement |
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Testing Agentforce Components
|
|
147
|
+
|
|
148
|
+
1. **Unit test** all `@InvocableMethod` classes (standard Apex testing)
|
|
149
|
+
2. **Preview** in Agent Builder with sample conversations
|
|
150
|
+
3. **Validate action selection** — Does Atlas pick the right action for the right query?
|
|
151
|
+
4. **Edge cases** — Empty results, invalid input, error responses
|
|
152
|
+
5. **Multi-turn** — Test conversation flows, not just single queries
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Checklist
|
|
157
|
+
|
|
158
|
+
- [ ] Action descriptions are clear, specific, and include "use when" guidance
|
|
159
|
+
- [ ] All input/output variables have labels and descriptions
|
|
160
|
+
- [ ] Apex actions follow all standard Apex conventions (bulkified, secure, tested)
|
|
161
|
+
- [ ] Prompt templates are grounded in live data (not hallucination-prone)
|
|
162
|
+
- [ ] Guardrails configured for sensitive operations
|
|
163
|
+
- [ ] Tested in Agent Builder preview before deployment
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Apex Generation Rules
|
|
2
|
+
|
|
3
|
+
> Instructions for AI agents when generating Apex code in this project.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Before Writing Any Apex
|
|
8
|
+
|
|
9
|
+
1. **Check `conventions.md`** for naming, architecture, and security rules.
|
|
10
|
+
2. **Check `architecture.md`** for existing classes and patterns in this project.
|
|
11
|
+
3. **Identify the correct layer** — is this a Trigger, Handler, Service, or Selector?
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Mandatory Rules
|
|
16
|
+
|
|
17
|
+
### Architecture
|
|
18
|
+
|
|
19
|
+
- **One trigger per object** → delegates to `{Object}TriggerHandler`
|
|
20
|
+
- **Handler** routes events → calls `{Object}Service` methods
|
|
21
|
+
- **Service** contains all business logic → calls `{Object}Selector` for data
|
|
22
|
+
- **Selector** contains all SOQL → returns typed collections
|
|
23
|
+
- **Zero business logic in triggers or handlers**
|
|
24
|
+
|
|
25
|
+
### Security
|
|
26
|
+
|
|
27
|
+
```apex
|
|
28
|
+
// EVERY class starts with:
|
|
29
|
+
public with sharing class ClassName {
|
|
30
|
+
|
|
31
|
+
// EVERY SOQL query uses:
|
|
32
|
+
[SELECT ... FROM ... WITH USER_MODE]
|
|
33
|
+
|
|
34
|
+
// Use stripInaccessible when handling external data:
|
|
35
|
+
SObjectAccessDecision decision = Security.stripInaccessible(AccessType.CREATABLE, records);
|
|
36
|
+
insert decision.getRecords();
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Bulkification
|
|
40
|
+
|
|
41
|
+
```apex
|
|
42
|
+
// ✅ ALWAYS — Collections and maps
|
|
43
|
+
public static void processContacts(List<Contact> contacts) {
|
|
44
|
+
Set<Id> accountIds = new Set<Id>();
|
|
45
|
+
for (Contact c : contacts) {
|
|
46
|
+
accountIds.add(c.AccountId);
|
|
47
|
+
}
|
|
48
|
+
Map<Id, Account> accountMap = new Map<Id, Account>(
|
|
49
|
+
AccountSelector.getByIds(accountIds)
|
|
50
|
+
);
|
|
51
|
+
// Process using map lookups, not queries
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ❌ NEVER
|
|
55
|
+
for (Contact c : contacts) {
|
|
56
|
+
Account a = [SELECT Id FROM Account WHERE Id = :c.AccountId]; // SOQL IN LOOP!
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Error Handling
|
|
61
|
+
|
|
62
|
+
```apex
|
|
63
|
+
// Custom exceptions for domain errors
|
|
64
|
+
public class OrderProcessingException extends Exception {}
|
|
65
|
+
|
|
66
|
+
// Structured try/catch with context
|
|
67
|
+
try {
|
|
68
|
+
// risky operation
|
|
69
|
+
} catch (SpecificException e) {
|
|
70
|
+
Logger.error('Context: what was happening', e);
|
|
71
|
+
throw new DomainException('User-friendly message', e);
|
|
72
|
+
}
|
|
73
|
+
// NEVER: catch (Exception e) { } — empty catch
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Templates
|
|
79
|
+
|
|
80
|
+
When generating a new class, use these skeletons:
|
|
81
|
+
|
|
82
|
+
### Trigger
|
|
83
|
+
```apex
|
|
84
|
+
trigger {Object}Trigger on {Object__c} (
|
|
85
|
+
before insert, before update, before delete,
|
|
86
|
+
after insert, after update, after delete, after undelete
|
|
87
|
+
) {
|
|
88
|
+
new {Object}TriggerHandler().run();
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Trigger Handler
|
|
93
|
+
```apex
|
|
94
|
+
public with sharing class {Object}TriggerHandler extends TriggerHandler {
|
|
95
|
+
public override void beforeInsert() {
|
|
96
|
+
{Object}Service.onBeforeInsert((List<{Object__c}>) Trigger.new);
|
|
97
|
+
}
|
|
98
|
+
public override void afterUpdate() {
|
|
99
|
+
{Object}Service.onAfterUpdate(
|
|
100
|
+
(List<{Object__c}>) Trigger.new,
|
|
101
|
+
(Map<Id, {Object__c}>) Trigger.oldMap
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
// Add only the events you need
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Service
|
|
109
|
+
```apex
|
|
110
|
+
public with sharing class {Object}Service {
|
|
111
|
+
public static void onBeforeInsert(List<{Object__c}> newRecords) {
|
|
112
|
+
// Business logic here
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public static void onAfterUpdate(List<{Object__c}> newRecords, Map<Id, {Object__c}> oldMap) {
|
|
116
|
+
// Change detection
|
|
117
|
+
List<{Object__c}> changed = new List<{Object__c}>();
|
|
118
|
+
for ({Object__c} record : newRecords) {
|
|
119
|
+
if (record.Status__c != oldMap.get(record.Id).Status__c) {
|
|
120
|
+
changed.add(record);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (!changed.isEmpty()) {
|
|
124
|
+
processStatusChange(changed);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Selector
|
|
131
|
+
```apex
|
|
132
|
+
public with sharing class {Object}Selector {
|
|
133
|
+
public static List<{Object__c}> getByIds(Set<Id> ids) {
|
|
134
|
+
return [
|
|
135
|
+
SELECT Id, Name, Status__c, Account__c
|
|
136
|
+
FROM {Object__c}
|
|
137
|
+
WHERE Id IN :ids
|
|
138
|
+
WITH USER_MODE
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public static List<{Object__c}> getByAccountIds(Set<Id> accountIds) {
|
|
143
|
+
return [
|
|
144
|
+
SELECT Id, Name, Status__c, Account__c
|
|
145
|
+
FROM {Object__c}
|
|
146
|
+
WHERE Account__c IN :accountIds
|
|
147
|
+
WITH USER_MODE
|
|
148
|
+
ORDER BY CreatedDate DESC
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Checklist Before Returning Apex Code
|
|
157
|
+
|
|
158
|
+
- [ ] Uses `with sharing`
|
|
159
|
+
- [ ] All SOQL has `WITH USER_MODE`
|
|
160
|
+
- [ ] No SOQL or DML in loops
|
|
161
|
+
- [ ] Handles 200+ records
|
|
162
|
+
- [ ] Custom exception for domain errors
|
|
163
|
+
- [ ] try/catch on callouts with logging
|
|
164
|
+
- [ ] Follows naming conventions from `conventions.md`
|
|
165
|
+
- [ ] Methods have Javadoc comments
|