Imported from PranavNagrecha/Salesforce-Intelligence (
.claude/skills/architect-async-and-events/SKILL.md). Install upstream withnpx skills add PranavNagrecha/Salesforce-Intelligence --skill architect-async-and-events. Copyright stays with the author.
Architect async and events
Overview
This skill is the v2.8 EXTENSION to the architect persona's
integration-topology skill (architect-integration-topology). Where
v1.5 ships the declared integration tier (integration_map,
event_subscribers, the async / API-surface property booleans), v2.8
adds the async-deep tier covering four categories v1.5 EXCLUDED:
- Change Data Capture (CDC) subscription detection. v1.5's
event_subscribersis scoped to Platform Events (__esuffix on the ApiName); CDC events use a different name pattern ({ObjectName}ChangeEventfor standard objects,{Object}__ChangeEventfor custom objects). v2.8'ssfi.cdc_subscribersrecognizes both shapes via name pattern. - Async chain depth analysis. v1.5 emits one-hop
dispatchesAsyncedges (Queueable / Schedulable / Batchable / @future caller → job). v2.8'ssfi.async_chain_depthwalks the transitive chain — a job that queues another job that queues another job, surfacing the depth, every branch point, and any cycles. - Scheduled job catalog. v1.5 detects
System.schedule(name, cron, instance)invocations but does not promote the schedule itself to a first-class catalog surface. v2.8'ssfi.scheduled_job_catalogwalks every ApexClass withisSchedulable: trueAND everydispatchesAsyncedge withdispatchMechanism: 'schedule', producing the cron-driven topology in one shot. - OutboundMessage promotion. v1.3 left
WorkflowRule
<outboundMessages>as dangling-by-design references. v2.8 promotes each entry to a realOutboundMessagenode (the new v2.8 ComponentType), so the admin can catalog every SOAP outbound destination separately from the WorkflowRule that fires it.
Plus a fifth, unified surface:
- Endpoint catalog.
sfi.endpoint_catalogreturns every URL the org touches in one structured response — inbound (RESTexposessynthetic ids) + outbound (OutboundMessage endpointUrl + ExternalDataSource endpoint + NamedCredential url). This is the URL-axis sibling ofsfi.integration_map(which surfaces NODES + their wiring) andsfi.outbound_message_catalog(which surfaces ONE category in depth).
The boundary that matters for architects: v2.8 does NOT probe, validate, or confirm any endpoint URL. The cataloged URLs are verbatim from metadata XML or Apex source — runtime registration (a NamedCredential resolved via custom metadata at runtime) may carry a stored URL that differs from the actual production destination. The architect verifies separately.
The CDC boundary, stated accurately (two older claims here were overtaken by shipped work — do not repeat them):
- CDC SUBSCRIBER detection is name-pattern only. The tool
recognizes
{Object}ChangeEvent/*__ChangeEventshapes on triggers, classes and Flows. That half of the disclosure still holds. - Per-channel filter expressions ARE extracted. A dedicated
platformEventChannelMemberextractor reads*.platformEventChannelMember-meta.xmland emits the member node plus areferencesedge carrying the declaredfilterExpressionverbatim.cdc_subscriberssurfaces them aschannelMembers[](memberId,channelId,channelType,selectedEntity,filterExpression). The real boundary is narrower: thefilterExpressionis the DECLARED XML text, NOT runtime filter EVALUATION — which records actually flow through the channel needs record-level data the vault does not hold. Confidence isdeclared. - Apex
EventBus.subscribe(...)remains invisible FOR CDC. The Apex scanner does now resolve staticEventBus.subscribechannel args into alistensToedge — but it is gated on the__esuffix, so Platform Events are covered and CDC*ChangeEventchannels are deliberately SKIPPED. For CDC the "programmatic registration is invisible" disclosure still holds verbatim; for Platform Events (sfi.event_subscribers) it no longer does.
There is no isCdcEnabled property. Nothing in packages/*/src
writes it — no extractor, no enricher, no graph-build step. The
producer-side question ("is this object enabled for CDC?") is answered
by sfi.event_topology: cdcEntities[] lists the entities a
PlatformEventChannelMember SELECTS (that selection IS the enablement
declaration), each carrying the channel it is bound onto and the
declared filterExpression. An empty cdcEntities quotes the manifest
coverage row, so it reads as a CHECKED zero or as NOT CHECKED — never
as a bare "no CDC". Do not call
sfi.get_component on a CustomObject and read properties.isCdcEnabled
— it will be undefined for every object in every vault, and an
undefined read as "not CDC-enabled" is a fabricated negative.
The v2.8 chainAsync non-persistence: the transitive chain edge
is composed AT QUERY TIME by sfi.async_chain_depth. The
architect cannot query sfi.get_edges for chainAsync — only
this tool surfaces the transitive shape. State this when
explaining why a chain-walk question can't be answered via
get_edges directly.
The cron expression is NOT available on the vault plane at all.
There is no parsedCron, no rawCronExpression, and no cron parser
(cron-parser is not a dependency of this repo). scheduled_job_catalog
declares two cron-shaped fields — cronExpressions[] on an entry and
cronExpression on each scheduledByCalls[] row — and BOTH are read
defensively against a producer that does not exist: the Apex scanner's
System.schedule(name, cron, new X()) regex captures only the CLASS
NAME and discards the cron argument, and the dispatchesAsync edge it
emits carries { dispatchMechanism, offset, length } and nothing else.
So cronExpressions is [] and cronExpression is null on every
entry in every vault today. Read an empty cron as "cron UNAVAILABLE on
this plane", never as "this job has no schedule".
When to fire
Fire this skill on async / event / scheduled-job / outbound-message / endpoint-catalog phrasing. Concrete triggers:
CDC subscription shape
- "Who subscribes to CDC events on
Account?" / "What's the CDC topology forAccount?" — Usesfi.event_topologywith{ "objectApiName": "Account" }. - "What change-data-capture subscribers do we have?" /
"Audit our CDC subscribers." — Use
sfi.event_topologywith{ "filter": "cdc" }. - "Who listens to
AccountChangeEvent?" — Same; the event-name pattern is recognized.
sfi.cdc_subscribers is a retired back-compat alias. It is not
advertised under the default core profile and is not directly
invokable there — reach it, if you must, through
sfi.run_analysis { name: 'sfi.cdc_subscribers' }. Prefer
sfi.event_topology.
Async chain depth shape
- "What's the async chain depth from
ApexClass:AccountIndexer?" / "Walk the async chain rooted at this Queueable." — Usesfi.async_chain_depthwithrootApexClassId: 'ApexClass:AccountIndexer'. - "How deep does this Queueable chain go?" —
Same; default
maxDepth: 10. - "Are there cycles in our async dispatch graph?" /
"Show me where Queueables enqueue themselves." — Use
sfi.async_chain_depthon each suspected root; surface thecyclesDetected[]array.
Scheduled job shape
- "Show me every scheduled job in this org." /
"Audit our cron-driven Apex." /
"What runs on a schedule?" — Use
sfi.scheduled_job_catalog. - "What's the cron for
NightlyAccountsRefresh?" / "Decode this scheduled job." — This is an HONEST GAP on the vault plane: the cron string is not extracted (see above).sfi.scheduled_job_catalogcan tell you the class is Schedulable and which classes callSystem.scheduleon it; it cannot tell you the expression. Say so, and offersfi.live_scheduled_jobs(opt-in live plane) — it reads the actualCronTrigger.CronExpression,State, andNextFireTimefrom the org.
Outbound message shape
- "What outbound messages does this org send?" /
"Catalog our SOAP outbound messages." — Use
sfi.outbound_message_catalog. - "What's the outbound destination from
Accountworkflow rules?" — Usesfi.outbound_message_catalogwithobjectFilter: 'Account'. - "Which WorkflowRule fires
Notify_Marketing_Automation?" — Same; the catalog entry surfacesinvokedByWorkflowRules.
Unified endpoint catalog shape
- "Show me every URL this org touches." /
"Give me the endpoint inventory." /
"Audit our endpoint footprint." — Use
sfi.endpoint_catalog.
When NOT to fire
Defer to another skill when:
- The user asks "what subscribes to
Account_Change__e?" (a Platform Event,__esuffix). That's v1.5'ssfi.event_subscribersshape; defer toarchitect-integration-topology. CDC and Platform Events are separate axes. - The user asks for the broad integration map ("draw me our
integration map", "what auth providers / named credentials /
external data sources do we have"). That's v1.5's
sfi.integration_map; defer toarchitect-integration-topology. - The user asks for one-hop async dispatch info ("what calls
enqueueJob(new AccountIndexer())?", "find every@futuremethod"). v1.5 covers one-hop; defer toarchitect-integration-topology. v2.8'ssfi.async_chain_depthis for the TRANSITIVE walk specifically. - The user asks "what breaks if I disable this trigger?" That's
v2.3 what-if; defer to
developer-impact-and-reachability→sfi.what_if_disable_trigger. - The user asks for code-level reachability ("where is
AccountIndexerreachable from?"). That's v2.7sfi.call_graphovercallsApexedges; defer todeveloper-impact-and-reachability.sfi.async_chain_depthwalksdispatchesAsynconly. - The user wants live data ("how many times did
NightlyAccountsRefreshrun last week?"). v2.8 is offline; the Tooling API gap means the actual scheduled job registration is invisible. Tell the user to query Setup → Scheduled Jobs. - The user names an Experience Cloud / Sites surface. v2.8 doesn't cover that; v1.5's boundary applies.
The cascade
Five tools, five distinct surfaces. Pick the right entry point.
1. sfi.event_topology — the event plane (Platform Events + CDC)
Start here for anything event-shaped. One call returns the org's
Platform Events with their declared eventType / publishBehavior
and their publishers, subscribers and channel bindings; the entities
whose Change Data Capture stream a PlatformEventChannelMember
SELECTS (that selection IS the enablement declaration); the channels
carrying both; the events the org NAMES but the vault never retrieved;
and a coverage block naming the counts all of the above were computed
under.
{} // the whole event plane
{ "filter": "cdc" } // only the CDC half
{ "objectApiName": "Contact" } // is CDC enabled for Contact?
Two boundaries to surface verbatim, because they are the ones a reader
gets wrong: an empty cdcEntities list quotes the manifest coverage
row, so it reads as a CHECKED zero or as NOT CHECKED — never as a bare
"no CDC"; and a permission grant naming a *ChangeEvent entity is NOT
CDC enablement (those entities exist on every org), so it appears only
under referencedNotRetrieved.
sfi.cdc_subscribers below is a RETIRED back-compat alias: still
dispatchable by name, no longer advertised. Prefer event_topology.
1b. sfi.cdc_subscribers (retired alias) — CDC subscribers only
Walks listensTo edges WHOSE TARGET MATCHES the CDC name
pattern: {ObjectName}ChangeEvent for standard objects,
*__ChangeEvent for custom objects.
{ "sObjectFilter": "Account" } // narrow to one object
{} // every CDC-subscribed object
When sObjectFilter is supplied, resolves the synthetic
ChangeEvent id from the filter (e.g., Account →
AccountChangeEvent; Order__c → Order__ChangeEvent) and
scans incoming listensTo edges for that single event. When
omitted, walks every CustomObject whose apiName matches the CDC
name pattern and aggregates their incoming listensTo edges.
Subscribers are restricted to ApexTrigger, ApexClass, Flow — the
three v1.5 R3 listensTo producers.
Do not fire this by choice — sfi.event_topology returns the same
CDC facts alongside the platform-event half. It survives only for
callers that already name it. The disclosure it carries applies to
both: CDC subscription detection recognizes by NAME PATTERN only, and
programmatic EventBus.subscribe(...) registration is INVISIBLE.
There is no properties.isCdcEnabled flag on CustomObject — the
signal has zero producers in the codebase, so reading it returns
undefined for every object. The producer-side question ("is this
object bound onto a change-event channel?") belongs to
sfi.event_topology's cdcEntities[]. This alias returns the same
binding as channelMembers[], where a member whose selectedEntity is
the object IS the declared CDC binding, with its channelId /
channelType / filterExpression. An empty channelMembers[] means no
*.platformEventChannelMember-meta.xml bound the object in the
retrieved metadata — not proof CDC is off.
2. sfi.async_chain_depth — transitive dispatchesAsync walk
BFS from rootApexClassId over outgoing dispatchesAsync
edges. Each edge is recorded as a (fromId, toId, depth) tuple
so the renderer can draw the chain.
{ "rootApexClassId": "ApexClass:AccountIndexer", "maxDepth": 10 }
The depth cap is 10. Chains deeper than 10 hops are TRUNCATED
(the truncated flag flips true). This is the v2.8 honesty axis
— runtime async chains can be arbitrarily long, but the v2.8
contract caps the static walk to keep the answer bounded.
Cycle detection: a frontier node whose id was already visited
flips cyclesDetected: true. The walk does NOT abort on cycles
— it continues to surface every reachable node — but the flag is
the honest signal that the chain has a loop. The most common
cycle is the self-enqueueing Queueable
(AccountIndexer.execute() ending with
System.enqueueJob(new AccountIndexer()) for chunking).
Branch points: a class with more than one outgoing
dispatchesAsync edge to DIFFERENT targets is a branch. The
response surfaces every class with branchCount >= 2 in the
branchPoints array.
Note the chainAsync non-persistence: the transitive chain edge
is composed AT QUERY TIME; it's NOT in the graph. State this when
the user asks why the chain doesn't show up in sfi.get_edges.
3. sfi.scheduled_job_catalog — cron-driven Apex topology
Walks the graph for two distinct signals:
- ApexClass nodes with
properties.isSchedulable === true. These are classes implementing theSchedulableinterface; they are schedule-CAPABLE but not necessarily currently scheduled (the actual schedule lives inCronTrigger/AsyncApexJob, which v2.8 does NOT query in the offline path). dispatchesAsyncedges withproperties.dispatchMechanism === 'schedule'. The v1.5 R3 producer'sSystem.schedule(...)call sites the Apex scanner detected; each edge names the target class plus the caller.
{}
Catalog is per-class — one entry per Schedulable class. Classes
that are Schedulable but lack any System.schedule(...) call
site surface with empty scheduledByCalls[] arrays (they exist
on disk as Schedulable but no Apex source actually schedules
them — usually scheduled via the UI or external tooling).
Surface the verbatim honesty axis: scanning for
System.schedule(...) invocations is heuristic; the actual
runtime schedule lives in the CronTrigger / AsyncApexJob
Tooling API surface and is invisible to the offline DX-source
scanner. A class flagged "schedulable" may NOT be currently
scheduled — the schedule is a runtime registration.
An entry's cronExpressions[] is empty and every
scheduledByCalls[].cronExpression is null — always, in every vault.
The scanner never captures the cron argument. Surface that as an
availability gap, not a finding:
This catalog names the Schedulable classes and the
System.schedulecall sites that reference them. The cron EXPRESSION itself is not extracted — the scanner captures the scheduled class, not the schedule string. For the actual expression, next fire time, and whether the job is currently registered at all, usesfi.live_scheduled_jobs(opt-in live plane, readsCronTrigger).
Also surface likelyUnscheduled when it is true, with its own meaning:
a System.schedule() call site that lives ONLY inside an @isTest
class does not schedule anything at runtime.
4. sfi.outbound_message_catalog — SOAP outbound destination catalog
Walks the OutboundMessage node family v2.8 promotes from
WorkflowRule dangling references into real nodes.
{} // every OutboundMessage
{ "objectFilter": "Account" } // narrow to one parent object
Each catalog entry carries:
- Identity (
id,apiName,name). - Four endpoint properties (
endpointUrl,includeSessionId,useDeadLetterQueue,integrationUser). fields[](the SOAP body's payload shape — array of field API names in source order).- Parent CustomObject id.
invokedByWorkflowRules[]— every WorkflowRule with an<actions>reference to this outbound message (the v1.3 reference shape preserved by the v2.8 promotion).
Surface the URL-not-probed disclosure: the endpoint URL is captured verbatim — v2.8 does NOT probe the URL, does NOT validate the destination exists, and does NOT confirm the message is actually invoked at runtime. The architect verifies destination reachability separately.
5. sfi.endpoint_catalog — unified URL inventory
The URL-axis composite. Composes four categories into one response:
| Category | Source | Direction |
|---|---|---|
inboundApis |
v1.5 exposes edges → ExternalApi:{kind}/{path} synthetic targets (REST / Aura / Invocable) |
inbound |
outboundMessages |
OutboundMessage endpointUrl properties |
outbound |
externalDataSources |
ExternalDataSource endpoint properties (v1.5 R2) |
outbound |
namedCredentials |
NamedCredential url properties (v0.2) |
outbound |
{}
Takes no arguments. Each entry carries endpointKind
discriminator, direction, sourceComponentId, and url.
Use this when the architect asks the URL-centric question ("show
me every URL"); use sfi.integration_map (v1.5) when they ask
the topology question ("how is this org wired"); use
sfi.outbound_message_catalog when they want the per-message
invokers.
Surface the verbatim disclosure: URLs are captured verbatim — v2.8 does NOT probe, does NOT validate, and does NOT confirm any destination exists or is reachable. Runtime registrations (e.g., a NamedCredential resolved via custom metadata at runtime) may carry a stored URL that differs from the actual production destination.
Honesty axes
v2.8 universal — URL not validated (verbatim)
v2.8 captures endpoint URLs verbatim from metadata XML and Apex source. v2.8 does NOT probe any URL, does NOT validate the destination exists, and does NOT confirm the integration is actually invoked at runtime. Runtime registrations (e.g., a NamedCredential resolved via custom metadata at runtime) may carry a stored URL that differs from the actual production destination. The architect verifies destination reachability separately.
sfi.cdc_subscribers — name-pattern detection (verbatim)
CDC subscription detection here recognizes by NAME PATTERN only. The recognized shapes are
{ObjectName}ChangeEvent(for standard objects) and*__ChangeEvent(for custom objects with the__csuffix replaced). The programmaticEventBus.subscribe(...)registration path is invisible for CDC — the scanner's subscribe heuristic is gated on the__ePlatform Event suffix and skips*ChangeEventchannels. Per-member filter expressions in*.platformEventChannelMember-meta.xmlARE extracted and surface inchannelMembers[].filterExpression— but as DECLARED XML text, never as runtime evaluation of which records actually flow.
channelMembersis also the CDC ENABLEMENT signal: a member that selects a Change Event means CDC is on for that object even when no code subscribes. An emptysubscribers[]with a non-emptychannelMembers[]is "enabled, no modeled subscriber", NOT "CDC unused."
sfi.async_chain_depth — depth cap + heuristic edges (verbatim)
The walk is capped at depth 10. Chains deeper than 10 hops are truncated; the
truncatedflag flips true and any frontier-at-depth-10 nodes are NOT walked further. The underlyingdispatchesAsyncedges are produced by the v0.3 Apex scanner; reflective async dispatch (Type.forName(...).newInstance()) and helper-wrapper dispatch are invisible to the scanner, so the walked chain may UNDERSTATE the real runtime chain depth.The
chainAsynctransitive edge is NOT persisted to the graph — v2.8 composes it at query time. The architect cannot querysfi.get_edgesforchainAsync; this tool is the only surface.
sfi.scheduled_job_catalog — Tooling API gap (verbatim)
Scanning for
System.schedule(...)invocations is heuristic; the actual runtime schedule lives in theCronTrigger/AsyncApexJobTooling API surface and is invisible to the offline DX-source scanner. A class flaggedSchedulablemay NOT be currently scheduled — the schedule is a runtime registration. Conversely, a class with noSystem.schedule(...)call site may still be scheduled via the Salesforce UI or external tooling; surface as "schedulable but no detected scheduling call" in the response.
sfi.scheduled_job_catalog — cron is UNAVAILABLE on this plane
There is no cron-parse-failure disclosure, because there is no cron
parser and no cron producer. cronExpressions[] is [] and each
scheduledByCalls[] row's cronExpression is null on every entry
in every vault — the Apex scanner's System.schedule(name, cron, new X()) regex captures the CLASS NAME and discards the cron argument.
Say "cron UNAVAILABLE on this plane"; never "this job has no
schedule", and never quote a cron string this tool did not return.
The live CronTrigger registration is a Tooling-API fact, outside the
offline vault.
sfi.outbound_message_catalog — URL-not-probed (verbatim)
The endpoint URL is captured verbatim — v2.8 does NOT probe the URL, does NOT validate the destination exists, and does NOT confirm the message is actually invoked at runtime. The architect verifies destination reachability separately.
sfi.endpoint_catalog — universal URL disclaimer (verbatim)
URLs are captured verbatim across four categories (
inboundApis,outboundMessages,externalDataSources,namedCredentials). v2.8 does NOT probe, does NOT validate, and does NOT confirm any destination exists or is reachable.
Worked example
User: "Walk the async chain from ApexClass:AccountIndexer.
How deep does it go?"
Claude's flow:
- Classify → async chain depth shape.
- Fire
sfi.run_analysiswith{ "name": "sfi.async_chain_depth", "args": { … } }with{ "rootApexClassId": "ApexClass:AccountIndexer" }(the input key isrootApexClassId;componentId/rootIdare accepted aliases). Note the INPUT key and the OUTPUT key differ — the response echoesrootClassId/rootFlowId. - Receive (illustrative):
{
"data": {
"rootClassId": "ApexClass:AccountIndexer",
"rootFlowId": null,
"maxDepth": 4,
"truncated": false,
"cyclesDetected": true,
"branchPoints": [
{ "classId": "ApexClass:AccountIndexer", "branchCount": 2 }
],
"chains": [
{ "fromId": "ApexClass:AccountIndexer", "toId": "ApexClass:AccountIndexer", "depth": 1, "dispatchMechanism": "queueable", "confidence": "declared", "note": "self-enqueue (chunking pattern)" },
{ "fromId": "ApexClass:AccountIndexer", "toId": "ApexClass:AccountIndexNotifier", "depth": 1, "dispatchMechanism": "queueable", "confidence": "declared" },
{ "fromId": "ApexClass:AccountIndexNotifier", "toId": "ApexClass:EmailSender", "depth": 2, "dispatchMechanism": "future", "confidence": "heuristic" },
{ "fromId": "ApexClass:EmailSender", "toId": "ApexClass:EmailLogger", "depth": 3, "dispatchMechanism": "queueable", "confidence": "declared" },
{ "fromId": "ApexClass:EmailLogger", "toId": "ApexClass:RetryHandler", "depth": 4, "dispatchMechanism": "queueable", "confidence": "declared" }
],
"disclosure": "The walk is capped at depth 10... [URL-not-validated + chain-depth verbatim disclosure]"
}
}
- Respond:
Async chain from
ApexClass:AccountIndexer:
- Max depth: 4 (
maxDepth; truncation: false).- Branch points: 1 (the root,
ApexClass:AccountIndexer, has 2 outgoingdispatchesAsyncedges).- Cycles:
cyclesDetected: true— a self-enqueue at the root (chunking pattern). Note the field is a BOOLEAN, not a list: the walker reports THAT a cycle exists, not which nodes formed it. Find the cycle yourself by looking for achains[]edge whosefromId === toId, or a repeated pair.Chain edges (5):
Depth From To Mechanism Confidence 1 ApexClass:AccountIndexerApexClass:AccountIndexerqueueable (self) declared1 ApexClass:AccountIndexerApexClass:AccountIndexNotifierqueueable declared2 ApexClass:AccountIndexNotifierApexClass:EmailSenderfuture heuristic3 ApexClass:EmailSenderApexClass:EmailLoggerqueueable declared4 ApexClass:EmailLoggerApexClass:RetryHandlerqueueable declaredThe chunking self-enqueue at the root is the textbook Queueable pattern (processes one batch, then enqueues itself for the next batch) — NOT a bug. The walker reports it as a cycle of length 1 for completeness.
The depth-2 edge from
AccountIndexNotifiertoEmailSenderisconfidence: heuristic(the@futuredispatch mechanism). The v0.3 Apex scanner heuristically tags@futurecallers; verify the actual call site before treating as authoritative.Important v2.8 boundaries (verbatim disclosure):
- The walk is capped at
maxDepth: 10; chains deeper than 10 would surfacetruncated: truebut yours fits within 4.- The
dispatchesAsyncedges are produced by the v0.3 Apex scanner. Reflective async dispatch (Type.forName(...).newInstance()) and helper-wrapper dispatch are INVISIBLE. If your chain has a hop via a customAsyncDispatcher.enqueue(...)helper, the chain depth here may UNDERSTATE reality.- The
chainAsynctransitive edge is NOT persisted to the graph — v2.8 composes it at query time. You can't querysfi.get_edgesforchainAsync; this tool is the only surface.Follow-up suggestions:
- To see the one-hop dispatch sources from
ApexClass:AccountIndexer(who enqueues IT?), usesfi.get_edgeswithedgeType: 'dispatchesAsync',direction: 'in'.- To audit all scheduled jobs in this org (a sibling concern), run
sfi.run_analysiswith{ "name": "sfi.scheduled_job_catalog", "args": { … } }.- To check if
EmailLoggerorRetryHandlerend up calling out to an external system, follow up withsfi.endpoint_catalogand cross-reference theoutboundMessages/namedCredentialsarrays.
The response leads with the depth + branch + cycle summary, surfaces
the chain edges as a table with per-edge dispatchMechanism +
confidence, calls out the self-enqueue chunking pattern explicitly
(distinguishing it from a "real" cycle), and appends the verbatim
v2.8 disclosure with the depth-cap + reflective-dispatch + chainAsync
non-persistence notes.
Anti-patterns
| Mistake | Why it's wrong |
|---|---|
Treating an sfi.async_chain_depth self-enqueue cycle as "the architecture is broken". |
Self-enqueueing Queueables are the textbook chunking pattern: process one batch, enqueue self for next batch. Surface the cycle's length explicitly (length 1) and explain its purpose; don't editorialize. |
Calling sfi.cdc_subscribers against a non-CDC event id. |
The tool validates the name pattern ({Object}ChangeEvent / *__ChangeEvent); a Platform Event (*__e) would belong to v1.5's sfi.event_subscribers. Re-route to the right tool, don't retry. |
Treating an empty cdc_subscribers response as "no CDC topology in this org". |
The v2.8 EventBus.subscribe(...) registration path is invisible. An org with programmatic CDC subscribers will return empty here. Surface the disclosure and suggest the architect check Setup → Change Data Capture. |
Treating an sfi.scheduled_job_catalog Schedulable class with no scheduledByCalls[] as "not scheduled". |
The Tooling API gap means the actual CronTrigger / AsyncApexJob registration is invisible. A class with no detected call site may still be scheduled via the UI. Surface the verbatim Tooling API gap disclosure. |
Reporting an empty cronExpressions[] as "this job has no schedule". |
The cron string is never extracted — the scanner captures the scheduled CLASS and discards the cron argument. Every entry in every vault has cronExpressions: [] and cronExpression: null. That is an availability gap, not a finding. Route the actual expression to sfi.live_scheduled_jobs. |
Reading properties.isCdcEnabled, parsedCron, rawCronExpression, or maxDepthObserved. |
None of these exist — zero producers in packages/*/src. A read returns undefined, and undefined narrated as "not enabled" / "no schedule" / "depth 0" is a fabricated negative finding. The real keys are channelMembers[].selectedEntity (CDC binding), cronExpressions[] / cronExpression (both always empty), and maxDepth. |
Calling sfi.endpoint_catalog when the user asked "draw me the integration topology". |
The endpoint catalog is the URL-axis composite; the integration map is the topology composite. Defer to architect-integration-topology for the topology question. |
Surfacing an outbound_message_catalog endpoint URL as "this URL is reachable". |
v2.8 does NOT probe. The URL is captured verbatim; verify reachability separately. State the v2.8 disclaimer. |
| Confusing CDC events with Platform Events. | They're separate axes — __e suffix versus the {Object}ChangeEvent / *__ChangeEvent pattern — and sfi.event_topology is the front door for both, reporting each half separately. sfi.event_subscribers is the single-event detail view for a Platform Event; it covers neither CDC nor referenced-but-not-retrieved events. |
Treating the chainAsync non-persistence as a bug. |
It's a deliberate scope decision. The transitive edge would explode the graph size for queries that don't need it; sfi.async_chain_depth composes it at query time. State this when explaining why the chain doesn't appear in sfi.get_edges results. |
Treating confidence: heuristic on an async edge (typically @future dispatch) as authoritative. |
The v0.3 Apex scanner heuristically tags @future callers because the dispatch mechanism is a method-level annotation rather than a System.X(...) call. Cite confidence and recommend verification. |
| Skipping the URL-not-validated boundary on an endpoint-catalog response. | The disclosure is the architect's protection against treating a catalog URL as a confirmed integration. ALWAYS surface, even when the catalog is short. |
See also
architect-integration-topology— for v1.5's declared integration tier. Use that skill forintegration_map,event_subscribers(Platform Events,__esuffix), and one-hop async dispatch via the v1.5 property booleans. v2.8 EXTENDS but does not replace; the two skills are complementary.architect-impact-analysis— for "what breaks if I delete this ExternalDataSource / NamedCredential / outbound message". v0.2'ssfi.get_impactwalks every incoming edge; v2.8 surfaces the cataloged URL but doesn't predict the impact.developer-impact-and-reachability— forsfi.call_graph(transitivecallsApexwalk) andsfi.what_if_disable_trigger(what-if disabling a trigger that subscribes to a Platform Event or CDC channel).sfi.async_chain_depthwalks thedispatchesAsyncaxis specifically;sfi.call_graphwalks thecallsApexaxis.admin-documentation-generators— for the v2.5 architecture- overview generator that COMPOSESsfi.integration_map+ the v2.8 endpoint catalog into one document. The doc surfaces both axes; this skill is the deep drill-in.
Verification
Before sending a response, confirm:
- I classified the question into one of the five shapes (CDC subscribers / async chain depth / scheduled jobs / outbound messages / unified endpoint catalog) and fired the right tool.
- For CDC questions, I verified the user's event reference is
a CDC name pattern (
{Object}ChangeEvent/*__ChangeEvent), NOT a Platform Event (*__e). I re-routed toarchitect-integration-topologyif it's a Platform Event. - I surfaced the verbatim per-tool disclosure (URL-not- validated, name-pattern detection for CDC, depth-cap of 10, Tooling API gap for cron registration).
- I did NOT read
parsedCron,rawCronExpression,isCdcEnabled, ormaxDepthObserved— none of those exist, and an absent signal read as a negative finding is the exact failure this skill is supposed to prevent. - For
async_chain_depthresults, I surfacedmaxDepth,truncated,cyclesDetected(a BOOLEAN), andbranchPoints[], and walkedchains[]with per-edgeedgeType/async/depth. I did not report amaxDepthObservedor acyclesDetected[]list — neither exists. - For self-enqueue cycles in
async_chain_depth, I called out the chunking pattern explicitly rather than treating it as a defect. - For
scheduled_job_catalogentries I stated that the cron EXPRESSION is unavailable on the vault plane (it is never extracted), rather than reporting an emptycronExpressions[]as "this job has no schedule" — and I pointed atsfi.live_scheduled_jobsfor the real registration. - For
outbound_message_catalogentries, I cited eachinvokedByWorkflowRules[]reference with its canonical id. - For
endpoint_catalogresults, I split the response bydirection(inbound vs outbound) andendpointKind. - I cited every canonical id in backticks
(
ChangeEvent:AccountChangeEvent,ScheduledJob:NightlyAccountsRefresh,OutboundMessage:Lead.Notify_Marketing,ApexClass:,Flow:, etc.). - I did NOT confuse Platform Events with CDC events, did NOT
treat empty results as "no topology" without surfacing the
invisibility disclosure, and did NOT treat the
chainAsyncnon-persistence as a bug. - When the question was about v1.5's broader integration
surface (auth providers, named credentials, OData sources
generally), I deferred to
architect-integration-topologycleanly.
Grounding & routing (shared contract). For a vague or broad ask, call sfi.route_question first — in the default hybrid mode it returns a meaning-ranked toolCandidates shortlist (which YOU pick from) plus a suggested plane and a route hint (and whether to sfi.resolve a name first). Default tool profile is core: only the core spine (including sfi.live_consent) is directly invokable. For every other sfi.* analysis, call sfi.run_analysis with { "name": "sfi.<tool>", "args": { … } } (or follow route_question.invoke, which already wraps non-core steps). Optional: sfi.describe_analysis first when args are unclear. Every org fact must come from an sfi.* tool call, cited by its canonical id — never from memory. Build the answer only from what the tools returned, then pass it through sfi.synthesize_answer, which flags any hallucinatedIds (canonical ids no tool produced). Full cascade: using-sf-intelligence.