@salesforce/afv-skills 1.17.0 → 1.18.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.
Files changed (30) hide show
  1. package/package.json +1 -1
  2. package/skills/building-sf-integrations/SKILL.md +1 -1
  3. package/skills/configuring-code-analyzer/SKILL.md +482 -0
  4. package/skills/configuring-code-analyzer/examples/apex-project-config.yml +41 -0
  5. package/skills/configuring-code-analyzer/examples/ci-github-actions.yml +96 -0
  6. package/skills/configuring-code-analyzer/examples/fullstack-project-config.yml +46 -0
  7. package/skills/configuring-code-analyzer/examples/lwc-project-config.yml +26 -0
  8. package/skills/configuring-code-analyzer/references/ci-cd-templates.md +648 -0
  9. package/skills/configuring-code-analyzer/references/config-schema.md +257 -0
  10. package/skills/configuring-code-analyzer/references/diagnostic-flow.md +70 -0
  11. package/skills/configuring-code-analyzer/references/engine-prerequisites.md +276 -0
  12. package/skills/configuring-code-analyzer/references/rule-name-resolution.md +67 -0
  13. package/skills/configuring-code-analyzer/references/troubleshooting.md +298 -0
  14. package/skills/configuring-code-analyzer/scripts/check-prerequisites.sh +189 -0
  15. package/skills/configuring-code-analyzer/scripts/generate-config.sh +143 -0
  16. package/skills/configuring-code-analyzer/scripts/validate-config.sh +153 -0
  17. package/skills/managing-cdc-enablement/SKILL.md +164 -0
  18. package/skills/managing-cdc-enablement/assets/PlatformEventChannel-template.xml +5 -0
  19. package/skills/managing-cdc-enablement/assets/PlatformEventChannelMember-template.xml +11 -0
  20. package/skills/managing-cdc-enablement/references/deploy-troubleshooting.md +73 -0
  21. package/skills/managing-cdc-enablement/references/filter-expressions.md +93 -0
  22. package/skills/running-code-analyzer/SKILL.md +264 -267
  23. package/skills/running-code-analyzer/references/post-scan-workflows.md +286 -0
  24. package/skills/running-code-analyzer/scripts/describe-rule.js +382 -0
  25. package/skills/running-code-analyzer/scripts/list-rules.js +260 -0
  26. package/skills/running-code-analyzer/scripts/query-results.js +230 -0
  27. package/skills/using-salesforce-archive/SKILL.md +121 -0
  28. package/skills/using-salesforce-archive/examples/monitor-failed-jobs.md +47 -0
  29. package/skills/using-salesforce-archive/references/archive-activity-entity.md +59 -0
  30. package/skills/using-salesforce-archive/references/connect-api-operations.md +157 -0
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env node
2
+ // Version: v1.0 | SHA256: placeholder
3
+ // List Code Analyzer rules matching a selector and return structured JSON
4
+ // Usage: node list-rules.js <selector> [options]
5
+ //
6
+ // This script runs `sf code-analyzer rules --rule-selector <selector>` and
7
+ // parses the table output into structured JSON for presentation.
8
+ //
9
+ // The CLI table format is:
10
+ // # Name Engine Severity Tags
11
+ // 1 @lwc/lwc/no-inner-html eslint 2 (High) Recommended, LWC, Security
12
+ //
13
+ // Output: JSON with {status, totalRules, rules: [{name, engine, severity, severityNum, tags}], engines, summary}
14
+
15
+ const { execSync } = require("child_process");
16
+
17
+ function printUsage() {
18
+ console.error(`Usage: node list-rules.js <selector> [options]
19
+
20
+ Arguments:
21
+ <selector> Rule selector (same syntax as --rule-selector)
22
+ Examples: "Security", "pmd", "eslint:Recommended",
23
+ "(pmd,eslint):Security:(1,2)", "Apex", "JavaScript"
24
+
25
+ Options:
26
+ --engine <name> Filter results to a specific engine after listing
27
+ --severity <n> Filter results to specific severity (1-5, comma-separated)
28
+ --top <n> Return at most N rules (default: 100)
29
+ --count-only Return only counts by engine/severity/category (no rule list)
30
+
31
+ Valid selector tokens:
32
+ Engines: eslint, regex, retire-js, flow, pmd, cpd, sfge
33
+ Severities: Critical/1, High/2, Moderate/3, Low/4, Info/5
34
+ Categories: Security, Performance, BestPractices, CodeStyle, Design, ErrorProne, Documentation
35
+ Languages: Apex, JavaScript, TypeScript, HTML, CSS, Visualforce, XML
36
+ Tags: Recommended, Custom, All, DevPreview, LWC, Fixable
37
+
38
+ Examples:
39
+ node list-rules.js "Security"
40
+ node list-rules.js "pmd:Security"
41
+ node list-rules.js "eslint:Recommended"
42
+ node list-rules.js "(pmd,eslint):Security:(1,2)"
43
+ node list-rules.js "Apex"
44
+ node list-rules.js "JavaScript:BestPractices"
45
+ node list-rules.js "Recommended" --count-only
46
+ node list-rules.js "all" --engine pmd --severity 1,2 --top 10`);
47
+ process.exit(1);
48
+ }
49
+
50
+ // Parse CLI arguments
51
+ const args = process.argv.slice(2);
52
+ if (args.length < 1 || args[0] === "--help" || args[0] === "-h") {
53
+ printUsage();
54
+ }
55
+
56
+ const selector = args[0];
57
+ const options = {
58
+ engine: null,
59
+ severity: null,
60
+ top: 100,
61
+ countOnly: false,
62
+ };
63
+
64
+ for (let i = 1; i < args.length; i++) {
65
+ switch (args[i]) {
66
+ case "--engine":
67
+ options.engine = (args[++i] || "").toLowerCase();
68
+ break;
69
+ case "--severity":
70
+ options.severity = (args[++i] || "")
71
+ .split(",")
72
+ .map((s) => parseInt(s.trim(), 10))
73
+ .filter((n) => n >= 1 && n <= 5);
74
+ break;
75
+ case "--top":
76
+ options.top = parseInt(args[++i] || "25", 10);
77
+ break;
78
+ case "--count-only":
79
+ options.countOnly = true;
80
+ break;
81
+ default:
82
+ console.error(`Unknown option: ${args[i]}`);
83
+ printUsage();
84
+ }
85
+ }
86
+
87
+ // Validate selector tokens before running CLI
88
+ const validationError = validateSelector(selector);
89
+ if (validationError) {
90
+ console.log(JSON.stringify({
91
+ status: "invalid_selector",
92
+ message: validationError,
93
+ hint: "Valid tokens: engines (pmd, eslint, cpd, retire-js, regex, flow, sfge), severities (1-5 or Critical/High/Moderate/Low/Info), categories (Security, Performance, BestPractices, CodeStyle, Design, ErrorProne, Documentation), languages (Apex, JavaScript, TypeScript, HTML, CSS, Visualforce, XML), tags (Recommended, Custom, All, DevPreview, LWC, Fixable)",
94
+ }));
95
+ process.exit(0);
96
+ }
97
+
98
+ // Run `sf code-analyzer rules`
99
+ let rawOutput;
100
+ try {
101
+ const cmd = `sf code-analyzer rules --rule-selector "${selector}" 2>&1`;
102
+ rawOutput = execSync(cmd, {
103
+ encoding: "utf8",
104
+ timeout: 60000,
105
+ maxBuffer: 2 * 1024 * 1024,
106
+ });
107
+ } catch (err) {
108
+ rawOutput = err.stdout || err.stderr || (err.output && err.output.join("")) || "";
109
+ if (!rawOutput) {
110
+ console.log(JSON.stringify({
111
+ status: "error",
112
+ message: `Failed to run sf code-analyzer rules: ${err.message}`,
113
+ }));
114
+ process.exit(0);
115
+ }
116
+ }
117
+
118
+ // Parse the table output
119
+ const rules = parseTableOutput(rawOutput);
120
+
121
+ if (rules.length === 0) {
122
+ console.log(JSON.stringify({
123
+ status: "no_rules_found",
124
+ message: `No rules matched selector "${selector}". Check the selector syntax or try a broader query.`,
125
+ hint: "Use tokens like: Security, pmd, eslint:Recommended, (1,2), Apex",
126
+ }));
127
+ process.exit(0);
128
+ }
129
+
130
+ // Apply post-filters
131
+ let filtered = rules;
132
+ if (options.engine) {
133
+ filtered = filtered.filter((r) => r.engine.toLowerCase() === options.engine);
134
+ }
135
+ if (options.severity) {
136
+ filtered = filtered.filter((r) => options.severity.includes(r.severityNum));
137
+ }
138
+
139
+ // Compute summary stats
140
+ const engineCounts = {};
141
+ const severityCounts = {};
142
+ const categoryCounts = {};
143
+ filtered.forEach((r) => {
144
+ engineCounts[r.engine] = (engineCounts[r.engine] || 0) + 1;
145
+ const sevKey = `${r.severityNum} (${severityName(r.severityNum)})`;
146
+ severityCounts[sevKey] = (severityCounts[sevKey] || 0) + 1;
147
+ (r.tags || []).forEach((tag) => {
148
+ const t = tag.trim();
149
+ if (["Security", "Performance", "BestPractices", "CodeStyle", "Design", "ErrorProne", "Documentation"].includes(t)) {
150
+ categoryCounts[t] = (categoryCounts[t] || 0) + 1;
151
+ }
152
+ });
153
+ });
154
+
155
+ // Build result
156
+ const result = {
157
+ status: "success",
158
+ selector,
159
+ totalRules: filtered.length,
160
+ summary: {
161
+ byEngine: engineCounts,
162
+ bySeverity: severityCounts,
163
+ byCategory: categoryCounts,
164
+ },
165
+ };
166
+
167
+ if (!options.countOnly) {
168
+ result.rules = filtered.slice(0, options.top).map((r) => ({
169
+ name: r.name,
170
+ engine: r.engine,
171
+ severity: r.severity,
172
+ severityNum: r.severityNum,
173
+ tags: r.tags,
174
+ }));
175
+ if (filtered.length > options.top) {
176
+ result.truncated = true;
177
+ result.showing = options.top;
178
+ }
179
+ }
180
+
181
+ console.log(JSON.stringify(result));
182
+
183
+ // --- Helper Functions ---
184
+
185
+ function parseTableOutput(output) {
186
+ const rules = [];
187
+ const lines = output.split("\n");
188
+
189
+ for (const line of lines) {
190
+ // Match table rows: starts with spaces + number
191
+ // Format: " 1 ruleName engine severity tags"
192
+ const match = line.match(/^\s+(\d+)\s{2,}(\S+)\s{2,}(\S+)\s{2,}(\d+\s*\([^)]+\))\s{2,}(.+)$/);
193
+ if (match) {
194
+ const severityStr = match[4].trim();
195
+ const sevNumMatch = severityStr.match(/^(\d)/);
196
+ rules.push({
197
+ name: match[2].trim(),
198
+ engine: match[3].trim(),
199
+ severity: severityStr,
200
+ severityNum: sevNumMatch ? parseInt(sevNumMatch[1], 10) : 0,
201
+ tags: match[5].trim().split(",").map((t) => t.trim()).filter(Boolean),
202
+ });
203
+ }
204
+ }
205
+
206
+ return rules;
207
+ }
208
+
209
+ function validateSelector(selector) {
210
+ if (!selector || !selector.trim()) {
211
+ return "Selector cannot be empty.";
212
+ }
213
+
214
+ const VALID_TOKENS = new Set([
215
+ // Engines
216
+ "eslint", "regex", "retire-js", "flow", "pmd", "cpd", "sfge",
217
+ // Severity names
218
+ "critical", "high", "moderate", "low", "info",
219
+ // Severity numbers
220
+ "1", "2", "3", "4", "5",
221
+ // General tags
222
+ "recommended", "custom", "all",
223
+ // Categories
224
+ "bestpractices", "codestyle", "design", "documentation", "errorprone", "security", "performance",
225
+ // Languages
226
+ "apex", "css", "html", "javascript", "typescript", "visualforce", "xml",
227
+ // Engine-specific
228
+ "devpreview", "lwc", "fixable",
229
+ ]);
230
+
231
+ // Split by : (AND), then handle () groups (OR)
232
+ const groups = selector.split(":").map((s) => s.trim()).filter(Boolean);
233
+ const invalid = [];
234
+
235
+ for (const group of groups) {
236
+ let tokens;
237
+ if (group.startsWith("(") && group.endsWith(")")) {
238
+ // OR group: (token1,token2)
239
+ tokens = group.slice(1, -1).split(",").map((t) => t.trim()).filter(Boolean);
240
+ } else {
241
+ tokens = [group];
242
+ }
243
+
244
+ for (const token of tokens) {
245
+ if (!VALID_TOKENS.has(token.toLowerCase())) {
246
+ invalid.push(token);
247
+ }
248
+ }
249
+ }
250
+
251
+ if (invalid.length > 0) {
252
+ return `Invalid selector token(s): ${invalid.join(", ")}. Did you misspell a token?`;
253
+ }
254
+ return null;
255
+ }
256
+
257
+ function severityName(num) {
258
+ const names = { 1: "Critical", 2: "High", 3: "Moderate", 4: "Low", 5: "Info" };
259
+ return names[num] || "Unknown";
260
+ }
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env node
2
+ // Version: v1.0 | SHA256: placeholder
3
+ // Query and filter Code Analyzer results JSON with rich filtering capabilities
4
+ // Usage: node query-results.js <results-file.json> [options]
5
+ //
6
+ // Options:
7
+ // --engine <name> Filter by engine (pmd, eslint, cpd, retire-js, etc.)
8
+ // --severity <n> Filter by severity (1-5, comma-separated for multiple)
9
+ // --category <tag> Filter by category/tag (Security, Performance, etc.)
10
+ // --rule <name> Filter by exact rule name (case-insensitive)
11
+ // --file <substring> Filter by file path substring
12
+ // --top <n> Return top N results (default: 10)
13
+ // --sort <field> Sort by: severity, rule, engine, file (default: severity)
14
+ // --sort-dir <dir> Sort direction: asc, desc (default: asc)
15
+ // --summary Show only summary counts (no individual violations)
16
+
17
+ const fs = require("fs");
18
+ const path = require("path");
19
+
20
+ function printUsage() {
21
+ console.error(`Usage: node query-results.js <results-file.json> [options]
22
+
23
+ Options:
24
+ --engine <name> Filter by engine (pmd, eslint, cpd, retire-js, etc.)
25
+ --severity <n> Filter by severity (1-5, comma-separated for multiple: 1,2)
26
+ --category <tag> Filter by category/tag (Security, Performance, BestPractices, etc.)
27
+ --rule <name> Filter by exact rule name (case-insensitive)
28
+ --file <substring> Filter by file path substring (case-insensitive)
29
+ --top <n> Return top N results (default: 10)
30
+ --sort <field> Sort by: severity, rule, engine, file (default: severity)
31
+ --sort-dir <dir> Sort direction: asc, desc (default: asc)
32
+ --summary Show only summary counts (no individual violations)
33
+
34
+ Examples:
35
+ node query-results.js results.json --engine pmd --severity 1,2
36
+ node query-results.js results.json --category Security --top 20
37
+ node query-results.js results.json --file AccountService.cls
38
+ node query-results.js results.json --rule ApexCRUDViolation
39
+ node query-results.js results.json --summary`);
40
+ process.exit(1);
41
+ }
42
+
43
+ // Parse CLI arguments
44
+ const args = process.argv.slice(2);
45
+ if (args.length < 1 || args[0] === "--help" || args[0] === "-h") {
46
+ printUsage();
47
+ }
48
+
49
+ const filePath = args[0];
50
+ const options = {
51
+ engine: null,
52
+ severity: null,
53
+ category: null,
54
+ rule: null,
55
+ file: null,
56
+ top: 10,
57
+ sort: "severity",
58
+ sortDir: "asc",
59
+ summary: false,
60
+ };
61
+
62
+ // Parse named options
63
+ for (let i = 1; i < args.length; i++) {
64
+ const arg = args[i];
65
+ switch (arg) {
66
+ case "--engine":
67
+ options.engine = (args[++i] || "").toLowerCase();
68
+ break;
69
+ case "--severity":
70
+ options.severity = (args[++i] || "")
71
+ .split(",")
72
+ .map((s) => parseInt(s.trim(), 10))
73
+ .filter((n) => n >= 1 && n <= 5);
74
+ break;
75
+ case "--category":
76
+ options.category = (args[++i] || "").toLowerCase();
77
+ break;
78
+ case "--rule":
79
+ options.rule = (args[++i] || "").toLowerCase();
80
+ break;
81
+ case "--file":
82
+ options.file = (args[++i] || "").toLowerCase();
83
+ break;
84
+ case "--top":
85
+ options.top = parseInt(args[++i] || "10", 10);
86
+ break;
87
+ case "--sort":
88
+ options.sort = args[++i] || "severity";
89
+ break;
90
+ case "--sort-dir":
91
+ options.sortDir = args[++i] || "asc";
92
+ break;
93
+ case "--summary":
94
+ options.summary = true;
95
+ break;
96
+ default:
97
+ console.error(`Unknown option: ${arg}`);
98
+ printUsage();
99
+ }
100
+ }
101
+
102
+ // Read and parse results file
103
+ let data;
104
+ try {
105
+ data = JSON.parse(fs.readFileSync(filePath, "utf8"));
106
+ } catch (err) {
107
+ console.error(`Error reading results file: ${err.message}`);
108
+ process.exit(1);
109
+ }
110
+
111
+ const runDir = data.runDir || "";
112
+ const violations = data.violations || [];
113
+
114
+ // Apply filters
115
+ let filtered = violations.filter((v) => {
116
+ if (options.engine && v.engine.toLowerCase() !== options.engine) return false;
117
+ if (options.severity && !options.severity.includes(v.severity)) return false;
118
+ if (options.category) {
119
+ const tags = (v.tags || []).map((t) => t.toLowerCase());
120
+ if (!tags.includes(options.category)) return false;
121
+ }
122
+ if (options.rule && v.rule.toLowerCase() !== options.rule) return false;
123
+ if (options.file) {
124
+ const loc = v.locations && v.locations[v.primaryLocationIndex || 0];
125
+ const fileLower = ((loc && loc.file) || "").toLowerCase();
126
+ if (!fileLower.includes(options.file)) return false;
127
+ }
128
+ return true;
129
+ });
130
+
131
+ // Sort
132
+ const sortMul = options.sortDir === "desc" ? -1 : 1;
133
+ filtered.sort((a, b) => {
134
+ let cmp = 0;
135
+ switch (options.sort) {
136
+ case "severity":
137
+ cmp = a.severity - b.severity;
138
+ break;
139
+ case "rule":
140
+ cmp = a.rule.localeCompare(b.rule);
141
+ break;
142
+ case "engine":
143
+ cmp = a.engine.localeCompare(b.engine);
144
+ break;
145
+ case "file": {
146
+ const aLoc = a.locations && a.locations[a.primaryLocationIndex || 0];
147
+ const bLoc = b.locations && b.locations[b.primaryLocationIndex || 0];
148
+ const aFile = (aLoc && aLoc.file) || "";
149
+ const bFile = (bLoc && bLoc.file) || "";
150
+ cmp = aFile.localeCompare(bFile);
151
+ break;
152
+ }
153
+ }
154
+ if (cmp !== 0) return cmp * sortMul;
155
+ // Secondary sort: severity ascending
156
+ return (a.severity - b.severity) * sortMul;
157
+ });
158
+
159
+ // Build output
160
+ const totalMatches = filtered.length;
161
+ const limited = filtered.slice(0, options.top);
162
+
163
+ // Compute severity breakdown of matches
164
+ const sevCounts = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
165
+ filtered.forEach((v) => {
166
+ if (sevCounts[v.severity] !== undefined) sevCounts[v.severity]++;
167
+ });
168
+
169
+ // Compute rule frequency for matches
170
+ const ruleCounts = {};
171
+ const ruleEngines = {};
172
+ filtered.forEach((v) => {
173
+ ruleCounts[v.rule] = (ruleCounts[v.rule] || 0) + 1;
174
+ if (!ruleEngines[v.rule]) ruleEngines[v.rule] = v.engine;
175
+ });
176
+ const topRules = Object.entries(ruleCounts)
177
+ .sort((a, b) => b[1] - a[1])
178
+ .slice(0, options.top)
179
+ .map(([rule, count]) => ({ rule, engine: ruleEngines[rule], count }));
180
+
181
+ // Compute file frequency for matches
182
+ const fileCounts = {};
183
+ filtered.forEach((v) => {
184
+ const loc = v.locations && v.locations[v.primaryLocationIndex || 0];
185
+ let file = (loc && loc.file) || "unknown";
186
+ if (runDir && file.startsWith(runDir)) file = file.substring(runDir.length + 1);
187
+ fileCounts[file] = (fileCounts[file] || 0) + 1;
188
+ });
189
+ const topFiles = Object.entries(fileCounts)
190
+ .sort((a, b) => b[1] - a[1])
191
+ .slice(0, options.top)
192
+ .map(([file, count]) => ({ file, count }));
193
+
194
+ // Build result object
195
+ const result = {
196
+ query: {
197
+ engine: options.engine,
198
+ severity: options.severity,
199
+ category: options.category,
200
+ rule: options.rule,
201
+ file: options.file,
202
+ top: options.top,
203
+ sort: options.sort,
204
+ sortDir: options.sortDir,
205
+ },
206
+ totalViolations: violations.length,
207
+ totalMatches,
208
+ severityCounts: sevCounts,
209
+ topRules,
210
+ topFiles,
211
+ };
212
+
213
+ if (!options.summary) {
214
+ result.violations = limited.map((v) => {
215
+ const loc = v.locations && v.locations[v.primaryLocationIndex || 0];
216
+ let file = (loc && loc.file) || "unknown";
217
+ if (runDir && file.startsWith(runDir)) file = file.substring(runDir.length + 1);
218
+ return {
219
+ rule: v.rule,
220
+ engine: v.engine,
221
+ severity: v.severity,
222
+ message: v.message,
223
+ file: file,
224
+ startLine: (loc && loc.startLine) || 0,
225
+ tags: v.tags || [],
226
+ };
227
+ });
228
+ }
229
+
230
+ console.log(JSON.stringify(result));
@@ -0,0 +1,121 @@
1
+ ---
2
+ name: using-salesforce-archive
3
+ description: "ALWAYS USE THIS SKILL for anything involving Salesforce Archive (also called Trusted Services Archive) — search, view, unarchive, analyze, mask, and erase (RTBF) archived records via the Archive Connect API, and reading archive job status from the ArchiveActivity object. TRIGGER when: user mentions Salesforce Archive, Trusted Services Archive, archive/unarchive records, ArchiveActivity, archive jobs, archive policy, archive analyzer, archived record search, archive storage, archive failure logs, right to be forgotten / RTBF on archived data, or masking archived PII — including phrasings like 'find records that were archived', 'restore archived data', 'why did the archive job fail', 'download the archive failure log', or 'monitor my archive jobs', AND even when they ask you to explain, give guidance, or write a runbook/doc about these topics rather than run code. SKIP when: the user wants generic data-export/backup unrelated to the Archive add-on, or wants to build the archive policy UI metadata."
4
+ metadata:
5
+ version: "1.0"
6
+ ---
7
+
8
+ # Salesforce Archive
9
+
10
+ Operate Salesforce Archive (also called Trusted Services Archive) through its Connect API and the `ArchiveActivity` job-metadata object. This skill covers how to search and restore archived records, run the analyzer, handle RTBF erasure and PII masking, check storage, and — the part most often missed — how to read archive job status from `ArchiveActivity` and use a job's Id + Type to download its logs.
11
+
12
+ ## Scope
13
+
14
+ - **In scope**: Calling the Archive Connect API operations under `/platform/data-resilience/archive/`; querying the `ArchiveActivity` object via SOQL/Connect; correlating a job's `ArchiveActivity` record with its log-download endpoints; the verify-after-write pattern for each async operation.
15
+ - **Out of scope**: Defining archive policies / `ArchivePolicyDefinition` metadata; building UI; generating Flows over archive data (`ArchiveActivity` is **not** Flow-queryable — see Gotchas); generic backup/export tooling unrelated to the add-on.
16
+
17
+ ---
18
+
19
+ ## Required Inputs
20
+
21
+ Gather or infer before acting:
22
+
23
+ - **Operation intent**: search, view, unarchive, analyze, mask, RTBF, storage check, or job-status/log lookup.
24
+ - **Target sObject** (`sobjectName`): required for search and unarchive.
25
+ - **Filters**: search and unarchive require `sobjectName` + at least one filter.
26
+ - **For log downloads**: the `requestId` (an `ArchiveActivity` Id, `8qv…` prefix) of a completed, log-producing job, and `reportType` = that activity's `Type`.
27
+
28
+ Preconditions (confirm or surface to the user if a call returns a not-permitted error):
29
+ - The **org** must have Salesforce Archive enabled (the `TrustedServicesArchive` / `TrustedServicesArchiveBt` org permission). Every operation is gated on this first.
30
+ - Each operation requires a **specific user permission** on top of the org gate — see the Permissions table below. There is no single "archive admin" role; access is per-capability.
31
+
32
+ ---
33
+
34
+ ## Permissions
35
+
36
+ Every operation first requires the org to have Salesforce Archive enabled (`hasTrustedServicesArchive` = `OrgPermissions.TrustedServicesArchive || TrustedServicesArchiveBt`). On top of that org gate, each capability is gated by a distinct **user permission**. A call the user isn't permitted for fails with a "not permitted" error — match the error to the missing permission below.
37
+
38
+ | Operation | User permission required |
39
+ |-----------|--------------------------|
40
+ | `search-archived-records`, `get-search-archived-records-next-page` | `ViewSearchPage` (Archive Search) — the Connect search endpoint runs the global-search path, gated by Archive Search, **not** `ViewArchivedRecords` |
41
+ | `search-archived-records-with-sharing-rules` (Agentforce) | `ViewArchivedRecords` |
42
+ | `unarchive-records` | `UnarchiveSdk` |
43
+ | `forget-archived-records` (RTBF) + `get-rtbf-status` | `Rtbf` |
44
+ | `mask-archived-records` + `get-masking-status` | `Rtbf` (masking shares the same `Rtbf` permission — **not** a separate entitlement) |
45
+ | `run-analyzer`, `get-analyzer-report`, `get-archive-storage-used` | `ArchiveAnalyzer` |
46
+ | `get-execution-details-stream-url`, `get-failed-records-stream-url` | `ViewActivitiesPage` (Archive Activities) |
47
+
48
+ > Source of truth: the Connect API resource classes in `trusted-services-archive-connect-impl` → `TrustedServicesArchiveSdkImpl` guards → `TrustedServicesArchive.accessChecks.xml`. Note `search-archived-records` routes through `performArchiverGlobalSearch` (gated by `canRunArchiveSearch` = `ViewSearchPage`); the separate Apex-SDK `searchArchivedRecords()` method is gated by `ViewArchivedRecords`, but the **Connect API** search endpoint does not use it.
49
+
50
+ ---
51
+
52
+ ## Workflow
53
+
54
+ All steps are sequential within a task. Read the referenced file the first time you touch that area.
55
+
56
+ 1. **Identify the operation and read the contract** — do not rely on general knowledge of the Archive API, which has non-obvious contracts. Load `references/connect-api-operations.md` for the exact request/response shape, required inputs, and per-operation gotchas of every Archive Connect API operation. Do this before constructing any call (e.g. `dateRanges` plural vs singular, `isSuccess` flag vs HTTP status, `url: null` meaning no log).
57
+
58
+ 2. **For job status / monitoring, read the data model** — when the task involves archive jobs, failures, progress, counts, or logs, load `references/archive-activity-entity.md` for the `ArchiveActivity` field reference and how it links to the Connect API. Query `ArchiveActivity` via SOQL or Connect — **not** Flow. For a worked end-to-end example (find failed/in-progress jobs, then pull their execution-detail and failed-records logs), load `examples/monitor-failed-jobs.md`.
59
+
60
+ 3. **Construct and send the call** — follow the contract exactly. For searches, supply `sobjectName` + ≥1 filter; for date filtering use the plural `dateRanges` array of `{field, from, to}` with full ISO-8601 datetimes.
61
+
62
+ 4. **Branch on the right signal** — some operations return HTTP 201 with a body-level success flag (`body.statusCode`, `body.isSuccess`). Read `references/connect-api-operations.md` for which signal to trust per operation; never assume the HTTP status alone means success.
63
+
64
+ 5. **Verify after every write** — re-read state to confirm the effect (see the Verify-After-Write table below). Async operations (analyzer, RTBF, masking) return a request id you must poll.
65
+
66
+ ---
67
+
68
+ ## Verify-After-Write
69
+
70
+ | After this write | Confirm by |
71
+ |------------------|-----------|
72
+ | `run-analyzer` | Poll `get-analyzer-report` until the report is populated |
73
+ | `unarchive-records` | Re-run `search-archived-records` — confirm records left the archive |
74
+ | `forget-archived-records` (RTBF) | Poll `get-rtbf-status` with the returned `request_id` |
75
+ | `mask-archived-records` | Poll `get-masking-status` with the returned `request_id` |
76
+
77
+ ---
78
+
79
+ ## Rules / Constraints
80
+
81
+ | Constraint | Rationale |
82
+ |-----------|-----------|
83
+ | Search & unarchive require `sobjectName` + at least one filter | The controller rejects an unfiltered request with "Search must be based on at least 1 field" — a full-object operation is never allowed. |
84
+ | Date filters must be full ISO-8601 datetimes (`2020-01-01T00:00:00Z`) | A date-only value (`2020-01-01`) returns `400 JSON_PARSER_ERROR` because the field is typed `xsd:dateTime`. |
85
+ | Search uses `dateRanges` (plural array); unarchive uses `dateRange` (singular) | They are genuinely different fields on the two endpoints; using the wrong shape silently drops the filter or 400s. |
86
+ | Stop pagination when `scroll_id == "-1"` | Calling `get-search-archived-records-next-page` with `"-1"` returns 500. |
87
+ | Log downloads need a real `ArchiveActivity` Id as `requestId` + that activity's `Type` as `reportType` | The backend resolves the log by the activity record; a mismatched `reportType` returns no log. |
88
+ | Never use the deprecated lookups | `global-search-by-id`, `get-global-search-results`, and `view-archived-records` are deprecated with no successor and currently 500. Use `search-archived-records` (+ next-page) instead. |
89
+ | Excluded objects are not retrievable | `Feed`, `History`, `Relation`, `Share` are not searchable; Files/Attachments are not retrievable via this API — do not promise them. |
90
+ | Query `ArchiveActivity` via SOQL/Connect, never Flow | `ArchiveActivity` has `isProcessEnabled=false`, so a Flow "Get Records" element on it fails with "You can't get ArchiveActivity records in a flow." |
91
+
92
+ ---
93
+
94
+ ## Gotchas
95
+
96
+ | Issue | Resolution |
97
+ |-------|------------|
98
+ | Treating HTTP 201 as success | Several operations return 201 with a body-level outcome. Branch on `body.statusCode` (search) or `body.isSuccess` (`with-sharing-rules`), not the HTTP code. |
99
+ | `run-analyzer.isRunning` used as a signal | It is **always** `null`; the endpoint only populates `message`. Poll `get-analyzer-report` to confirm completion instead. |
100
+ | `search-archived-records-with-sharing-rules` (Agentforce) filters as an array | `filtersJson` must be a JSON-encoded **object map** `{"Field":"Value"}`, not an array of `{field,value}`; the array form returns `isSuccess:false "No valid filters provided"`. |
101
+ | Log `url` treated as present because status is 201 | `get-*-stream-url` returns `{url}`; `url: null` means no log was resolved. Always check `url != null`. |
102
+ | Misreading `get-archive-storage-used` | `usedStorage[]`/`availableStorage[]` are parallel positional arrays: index 0=org DATA, 1=org FILE, 2=archive RECORDS, 3=archive FILE. `availableStorage[2]`/`[3]` are **always 0** (archive tier is unmetered) — that means "not tracked", not "full". |
103
+ | Expecting `ArchiveActivity` in a Flow | It is not Flow-enabled (`isProcessEnabled=false`). Use SOQL/Connect/Reports. |
104
+ | Hitting unarchive caps | Unarchive processes ≤1000 matched records per request and ≤50 requests/hour/org, and restores the whole archived hierarchy of each match. |
105
+ | RTBF/masking caps | `criteria` ≤10 entries (one per object); ≤10,000 root records/day (shared between RTBF and masking); masking is irreversible. Both RTBF and masking are gated by the same `Rtbf` user permission. |
106
+
107
+ ---
108
+
109
+ ## Output Expectations
110
+
111
+ This is a knowledge/API skill — it produces API calls and their interpreted results, plus SOQL against `ArchiveActivity`. It does not generate deployable metadata. Deliverables per task: the correct operation invocation(s), the right success-signal branching, and a verify-after-write confirmation.
112
+
113
+ ---
114
+
115
+ ## Reference File Index
116
+
117
+ | File | When to read |
118
+ |------|-------------|
119
+ | `references/connect-api-operations.md` | Before constructing any Archive Connect API call — full per-operation contracts, success signals, and limits |
120
+ | `references/archive-activity-entity.md` | For any job-status / failure / progress / log task — `ArchiveActivity` field reference and its link to the log-download endpoints |
121
+ | `examples/monitor-failed-jobs.md` | To follow an end-to-end monitoring flow: find failed/in-progress jobs, then download their logs |
@@ -0,0 +1,47 @@
1
+ # Example — Monitor failed archive jobs and download their logs
2
+
3
+ End-to-end flow for the most common archive-admin task: find the jobs that failed or stalled, read why, and pull the logs that show which records didn't make it.
4
+
5
+ ## 1. Find the jobs (query ArchiveActivity)
6
+
7
+ `ArchiveActivity` is not Flow-queryable, so use SOQL/Connect:
8
+
9
+ ```sql
10
+ SELECT Id, Name, Status, Type, StartTime, ProgressPercentage,
11
+ TotalRecordCount, SucceededCount, FailedCount, SkippedRootRecordsCount,
12
+ RootEntityName, FailureReason
13
+ FROM ArchiveActivity
14
+ WHERE (Status = 'Failed' OR Status = 'In Progress' OR Status = 'Ended With Errors')
15
+ AND StartTime >= LAST_N_DAYS:7
16
+ ORDER BY StartTime DESC
17
+ ```
18
+
19
+ Read each row's `ProgressPercentage`, `SucceededCount`/`FailedCount`, `RootEntityName`, and `FailureReason` to summarize health. In-progress jobs have a blank `EndTime`.
20
+
21
+ ## 2. For each failed job, get the logs
22
+
23
+ Take the job's `Id` (`8qv…`) as `requestId` and its `Type` as `reportType`. `{activity.Type}` below is a placeholder for that job's own `Type` field value (e.g. `Archive`, `Purge`, `Unarchive`, `Analyzer`) — substitute the actual value per job; a hardcoded/mismatched `reportType` returns no log.
24
+
25
+ **Execution-detail log:**
26
+ ```
27
+ GET /platform/data-resilience/archive/log/execution-details-stream-url
28
+ ?requestId={activity.Id}&reportType={activity.Type}
29
+ ```
30
+
31
+ **Failed-records log:**
32
+ ```
33
+ GET /platform/data-resilience/archive/log/failed-records-stream-url
34
+ ?requestId={activity.Id}&reportType={activity.Type}
35
+ ```
36
+
37
+ Each returns `{ url }`. **Check `url != null`** before using it — a `null` url means no log was resolved for that job/type (e.g. the job produced no failed-records file), not an error to retry blindly.
38
+
39
+ ## 3. Summarize
40
+
41
+ Per failed/in-progress job, report: Name, Status, ProgressPercentage, SucceededCount, FailedCount, RootEntityName (target object), FailureReason, and the two log URLs (or "no log available" when `url` is null).
42
+
43
+ ## Pitfalls this flow avoids
44
+
45
+ - **Not** attempting a Flow over `ArchiveActivity` (`isProcessEnabled=false` → "You can't get ArchiveActivity records in a flow").
46
+ - Passing the activity's real `Type` as `reportType` (a mismatch returns no log).
47
+ - Treating a 201 with `url: null` as a usable log.