
Investigating Agentforce D360
- 81 installs
- 787 repo stars
- Updated August 5, 2026
- forcedotcom/afv-library
Reconstructs a 360-degree view of a single Agentforce session from Data Cloud STDM and GenAI DMOs, or discovers sessions by time, agent, channel, or outcome.
About
Traces and renders a hierarchical reconstruction of one Agentforce session from Data Cloud runtime audit rows, and supports discovering sessions when no id is known. A developer uses it to inspect, summarize, or find a specific Agentforce session.
- Reconstructs a session from Data Cloud STDM and GenAI DMOs
- Supports session discovery by time, agent, channel, outcome, or text
Investigating Agentforce D360 by the numbers
- 81 all-time installs (skills.sh)
- Ranked #5,216 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/forcedotcom/afv-library --skill investigating-agentforce-d360Add your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 81 |
|---|---|
| repo stars | ★ 787 |
| Last updated | August 5, 2026 |
| Repository | forcedotcom/afv-library ↗ |
What it does
Reconstructs a 360-degree view of a single Agentforce session from Data Cloud STDM and GenAI DMOs, or discovers sessions by time, agent, channel, or outcome.
Files
investigating-agentforce-d360 — Data Cloud 360° session view
Hierarchical session reconstruction from Data Cloud STDM + GenAI DMOs for one Agentforce session. Three stages — fetch → assemble → render. Typical wall-clock: ~10–30s for a ~15-turn session.
The pipeline is DC-only: it reads runtime audit rows that Data Cloud has materialized. It is not a runtime-availability tool — see "DC-only blind spot" below for what this skill cannot answer.
If the user hasn't given enough to proceed
When invoked with no session id AND no discovery criteria, print this block verbatim — do not paraphrase, do not pre-run any script. Trigger condition: the input is empty OR contains no session-id shape (neither a UUID nor a 0Mw… messaging id) AND no discovery expression (no time phrase / --agent / --channel / --outcome / --grep / verbs like "find" / "list").
Which session should I pull from Data Cloud, and in which org?
>
I need:
- Session id — either an Agent Session UUID (019db7f6-…) or a MessagingSession id (0Mw…, 15/18 chars).
- No session id? — Tell me what you remember and I'll find it: how recent (e.g. "last 2 hours", "today", a date), which agent, which channel (Messaging / Builder / Voice), how it ended (escalated, user ended, transferred, timed out), or a phrase from the conversation. I'll show matching sessions as a numbered list — you pick one, I pull it.
- Org alias — forsfCLI auth (the alias you configured withsf org login).
>
Artifacts land in~/.vibe/data/investigating-agentforce-d360/<org_id15>/<agent>__<ver>/<session_id>/(override per-script with--data-dir <path>).
Session id forms — UUID or MessagingSession id
Both forms are accepted on --session:
| Form | Example | Resolution |
|---|---|---|
| Agent Session UUID | 019dface-0000-7000-8000-000000000002 | Pass-through |
MessagingSession id (0Mw prefix) | 0MwTESTMSG12345AAA | Resolved via resolve_session.py — live DC lookup on first fetch, disk-first thereafter |
Multi-match is real. One MessagingSession id can map to multiple Agent Session UUIDs. On multi-match the resolver prints every candidate and exits non-zero; the user re-invokes with a specific UUID.
Artifacts always land under ~/.vibe/data/investigating-agentforce-d360/<org_id15>/<agent>__<ver>/<session_id>/ (default; overridable per-script with --data-dir <path>) — the messaging id is a lookup key only, never a directory name. The dominant agent (first in sorted(agents_observed)) names the <agent>__<ver>/ segment.
Resolving the script prefix
The default install puts the skill under the runtime's plugin root. If the skill was cloned somewhere else (e.g. directly from the forcedotcom/sf-skills repo into a custom path), set PLUGIN_ROOT to point at the runtime's skills directory.
prefix="${SKILL_ROOT:-${PLUGIN_ROOT:-$HOME/.vibe/skills}/investigating-agentforce-d360}/scripts"Every subsequent invocation in this doc uses "$prefix/...".
Session discovery (no id yet)
When the user doesn't have a session id, run discover_sessions.py against the STDM session DMO. Prints a numbered picker; user picks one; proceed with the chosen UUID.
python3 "$prefix/discover_sessions.py" --org <alias> [filters...]Filters (all optional except --org): --since <expr> (default last 24h; accepts "last 2 hours", "today", ISO dates), --agent <api-name>, --channel <Messaging|Builder|Voice>, --outcome <USER_ENDED|ESCALATED|TRANSFERRED|TIMEOUT|NOT_SET>, --grep <substring> (conversation text), --tz <IANA>, --limit <N> (default 20).
Output: markdown table with #, UUID, Start (UTC), Agent, Channel, Duration, Outcome. User replies with a number; proceed with that UUID.
Pipeline — three stages
fetch_dc.py → 24 dc.<name>.json + dc._session_manifest.json (DC Query REST waterfall, 5 waves)
assemble_dc.py → dc._session_tree.json (pure in-memory hierarchical join)
render_dc.py → dc._session_summary.md (human summary, multi-section)Each stage is independently runnable. fetch_dc.py --session <sid> --org <alias> chains all three by default.
Invocation
python3 "$prefix/fetch_dc.py" --session <session-id-or-messaging-id> --org <alias>Flags: --verbose for per-DMO row counts; --no-assemble / --no-render to stop early. All entry scripts (fetch_dc.py, assemble_dc.py, render_dc.py, resolve_session.py, discover_sessions.py) accept --data-dir <path> and --cache-dir <path> to override the default ~/.vibe/{data,cache}/investigating-agentforce-d360/ roots — pass these when the host runtime needs artifacts under a different distribution layout.
Output artifacts
Everything lands under ~/.vibe/data/investigating-agentforce-d360/<org_id15>/<agent>__<ver>/<session_id>/ (default; override with --data-dir <path>):
dc.sessions.json dc.steps.json dc.gateway_requests.json
dc.interactions.json dc.messages.json dc.gateway_responses.json
dc.participants.json dc.generations.json dc.gateway_request_llm.json
dc.content_quality.json dc.content_category.json dc.gateway_request_metadata.json
dc.tags.json dc.tag_definitions.json dc.gateway_request_tags.json
dc.tag_associations.json dc.tag_definition_associations.json
dc.feedback.json dc.feedback_details.json dc.gateway_records.json
dc.moments.json dc.moment_interactions.json
dc.telemetry_spans.json dc.app_generation.json
dc._session_manifest.json (per-DMO row counts + empties)
dc._session_tree.json (hierarchical join — session → interactions → steps → messages → generations → gateway)
dc._session_summary.md (rendered human summary)Zero-row queries are recorded in the manifest with status: empty; no file is written. assemble_dc tolerates missing files. See references/artifacts.md for the full read order.
The DC-only blind spot — read before committing to a root cause
DC alone answers what happened — steps that ran, generations that fired, gateway requests that were logged. It does NOT answer what could have happened but didn't:
- Which topics were eligible for the classifier on a given turn (this lives in runtime planner telemetry, not DC).
- Which actions were declared on a topic vs. which survived rule expressions and were actually offered to the LLM.
- Why the LLM picked one topic/action over another (the full prompt + response text only lives in the planner runtime telemetry).
If the user's question is about why a particular topic or action was or wasn't used, DC-only is almost never sufficient. Tell the user: "Availability questions need the runtime planner trace for that turn — which is outside this skill's Data Cloud surface. Check the platform telemetry that mirrors the planner's logged decisions." Don't fabricate a root cause from runtime-only evidence.
What DC IS good at
- What ran — every step, every LLM call, every gateway request + response, in order, with timestamps and durations. Good for "walk me through the session".
- What the user saw — full message transcript (user + agent), ordered.
- What the LLM produced — generations, token counts, trust scores (toxicity, instruction adherence, content-category breakdown from
content_quality+content_category). - Tool invocations — action calls, inputs, outputs, errors (from
gateway_request_metadata+gateway_records). - Feedback + flags — user feedback, escalation markers, session-end type.
- Audit integrity — the 1:1 invariant between GatewayRequest and GatewayResponse is checked; drift is flagged in
counts.audit_chain_1to1_ok.
Prerequisites
| Tool | Required |
|---|---|
sf CLI (authenticated against the target org) | yes — sf org login web --alias <alias> |
| Data Cloud enabled on the target org | yes — the STDM + GenAI DMOs must have materialized for the session |
| Python 3.10+ | yes — pipeline scripts |
Typical prompts — what they map to
| User says | Skill does |
|---|---|
| "Trace session `<uuid>` in my-org" | fetch_dc.py --session <uuid> --org my-org → assemble → render |
| "Summarize what happened in `0Mw…`" | Resolve 0Mw… → UUID, then full DC pipeline |
| "Find escalated sessions today in my-org on Messaging" | Run discover_sessions.py --since today --outcome ESCALATED --channel Messaging, print picker, user picks, then DC pipeline |
| "Walk me through this session" | Same as trace — read the rendered summary top to bottom |
What comes back to the user
After the pipeline completes, the rendered dc._session_summary.md carries these top-level sections:
1. Session identity — UUID, start/end, duration, agent, channel, end type, participant counts 2. Session bootstrap — channel mode + bootstrap variables (identity.mode, identity.bootstrap_variables) 3. ID reference — full UUIDs for everything truncated in the hierarchical trace 4. Transcript — USER ↔ AGENT narrative per TURN interaction 5. Complete hierarchical trace — Interaction → Step → Generation → GatewayRequest, with +start + duration = +end math 6. Per-turn summary — one row per interaction 7. Planner LLM calls (full prompts + responses) — opt-in via --show-prompts; suppressed by default 8. Visual analysis — gantt + LLM-call overlay 9. Session counts — engineer-facing table of manifest counts 10. Empties diagnostics — one row per DMO with rows == 0 and a populated _unavailable_reason 11. Catalog (session-filtered) — TagDefinitions / TagDefinitionAssociations / Tags filtered to agents observed in the session
For deep-dive, open dc._session_tree.json — the single source of truth the summary was rendered from. See references/dc_pipeline_contract.md for the full pipeline contract and references/dc_dmo_fields.md for per-DMO field reference.
Caveats
- `gateway_requests_dropped_by_stdm` — when DC reports zero
gateway_requestsrows but runtime telemetry would show LLM calls did fire, this skill cannot definitively distinguish "STDM exporter dropped writes" from "logging genuinely disabled at the source". The session is reported asplanner_ran_no_gateway_logs; the operator can check platform telemetry to disambiguate. Seereferences/dc_pipeline_contract.md§2.8. - Latency — Generation and GatewayRequest carry single-write timestamps, not start/end pairs. The renderer does not compute "latencies" between them — that delta reflects DC's serialization order, not how long the LLM call took.
- Data Cloud materialization lag — fresh sessions may show
interactions_not_materialized_yetif STDM hasn't caught up. Re-run after a minute or two.
-- App-layer generation records — reusable for any WHERE filter.
-- DMO: GenAIAppGeneration__dlm
--
-- Placeholders (substituted by scripts/dc.py.load_sql):
-- WHERE_CLAUSE — the filter expression, no "WHERE" keyword
-- ORDER_BY — full "ORDER BY <col>" or empty string
--
-- Sibling of GenAIGeneration__dlm: where GenAIGeneration is the raw
-- gateway response, GenAIAppGeneration appears to be the app/feature-
-- layer view of the same generation (separate `id__c` from `generationId__c`).
--
-- No `ssot__` prefix — fields end in `__c` directly.
--
-- Join to a session: the same Step → Generation bridge used for
-- GenAIGeneration also works here. Pull session steps, collect
-- non-empty `ssot__GenerationId__c` values, then query here with
-- `generationId__c IN (...)`. The App-Generation row points at the
-- same underlying gateway generation.
--
-- The DMO is provisioned by default on orgs with generative AI audit
-- enabled, but row population depends on whether the org uses app-layer
-- regeneration / update flows. Verify presence with `sf ssot/metadata`
-- before relying on it.
SELECT
id__c,
generationId__c,
generationUpdate__c,
generationUpdateId__c,
feature__c,
timestamp__c,
orgId__c,
cloud__c
FROM GenAIAppGeneration__dlm
WHERE {{WHERE_CLAUSE}}
{{ORDER_BY}};
-- ============================================================================
-- EXAMPLE WHERE clauses (pass via where_clause=)
-- ============================================================================
-- App-generations for a set of step generation ids
-- WHERE → generationId__c IN ('<gen_id1>','<gen_id2>',...)
-- ORDER BY → ORDER BY timestamp__c
-- App-generations in a time window for one org
-- WHERE → orgId__c = '<org_id_18>'
-- AND timestamp__c >= '<iso_window_start>'
-- AND timestamp__c < '<iso_window_end>'
-- ORDER BY → ORDER BY timestamp__c
-- GenAI content category (per-detector rows) — reusable WHERE.
-- DMO: GenAIContentCategory__dlm
--
-- Placeholders (substituted by scripts/dc.py.load_sql):
-- WHERE_CLAUSE — the filter expression, no "WHERE" keyword
-- ORDER_BY — full "ORDER BY <col>" or empty string
--
-- Two shapes of parent FK:
-- - Direct on a generation: non-TOXICITY detectors (InstructionAdherence,
-- TaskResolution, PII, PROMPT_DEFENSE) — parent__c = generationId__c
-- - Via a quality row: TOXICITY sub-categories —
-- parent__c = GenAIContentQuality.id__c
--
-- NOTE: No `ssot__` prefix — fields end in `__c` directly.
SELECT
id__c,
parent__c,
detectorType__c,
category__c,
value__c,
timestamp__c,
orgId__c,
cloud__c
FROM GenAIContentCategory__dlm
WHERE {{WHERE_CLAUSE}}
{{ORDER_BY}};
-- ============================================================================
-- EXAMPLE WHERE clauses (pass via where_clause=)
-- ============================================================================
-- Non-TOXICITY detectors for a set of generations (direct join)
-- WHERE → parent__c IN ('<gen_id1>','<gen_id2>')
-- AND detectorType__c != 'TOXICITY'
-- TOXICITY sub-categories for a set of quality rows
-- WHERE → parent__c IN ('<quality_id1>','<quality_id2>')
-- AND detectorType__c = 'TOXICITY'
-- InstructionAdherence scores only
-- WHERE → parent__c IN ('<gen_id1>','<gen_id2>')
-- AND detectorType__c = 'InstructionAdherence'
-- GenAI content quality (per-generation quality rows) — reusable WHERE.
-- DMO: GenAIContentQuality__dlm
--
-- Placeholders (substituted by scripts/dc.py.load_sql):
-- WHERE_CLAUSE — the filter expression, no "WHERE" keyword
-- ORDER_BY — full "ORDER BY <col>" or empty string
--
-- Joined to a generation via `parent__c = generationId__c`. One row per
-- INPUT/OUTPUT side. `isToxicityDetected__c` is populated only on OUTPUT rows.
--
-- NOTE: No `ssot__` prefix — fields end in `__c` directly.
SELECT
id__c,
parent__c,
isToxicityDetected__c,
contentType__c,
feature__c,
timestamp__c,
orgId__c,
cloud__c
FROM GenAIContentQuality__dlm
WHERE {{WHERE_CLAUSE}}
{{ORDER_BY}};
-- ============================================================================
-- EXAMPLE WHERE clauses (pass via where_clause=)
-- ============================================================================
-- Quality rows for a set of generations
-- WHERE → parent__c IN ('<gen_id1>','<gen_id2>',...)
-- ORDER BY → ORDER BY timestamp__c
-- Only rows where toxicity was detected (OUTPUT rows)
-- WHERE → parent__c IN ('<gen_id1>','<gen_id2>')
-- AND isToxicityDetected__c = 'true'
-- Only OUTPUT-side rows
-- WHERE → parent__c IN ('<gen_id1>','<gen_id2>')
-- AND contentType__c = 'OUTPUT'
-- Session discovery — find candidate sessions by time/agent/channel/outcome/grep.
-- Produces a short row-per-session shape for the picker rendered by
-- scripts/discover_sessions.py. NOT used by the trace pipeline — once the user
-- picks a UUID, the full pipeline runs fetch_dc.py against the 24-DMO waterfall.
--
-- Placeholders (substituted by scripts/dc.py.load_sql):
-- SELECT_LIST — either `s.ssot__Id__c, s.ssot__StartTimestamp__c, s.ssot__EndTimestamp__c,
-- s.ssot__AiAgentChannelType__c, s.ssot__AiAgentSessionEndType__c`
-- OR `DISTINCT <same columns>` when JOINs are present (DC SQL requires
-- ORDER BY columns to appear in a DISTINCT projection).
-- JOINS — zero or more JOIN clauses, newline-separated, or empty string:
-- * `JOIN ssot__AiAgentSessionParticipant__dlm p ON s.ssot__Id__c = p.ssot__AiAgentSessionId__c`
-- (required when filtering by --agent)
-- * `JOIN ssot__AiAgentInteraction__dlm i ON s.ssot__Id__c = i.ssot__AiAgentSessionId__c
-- JOIN ssot__AiAgentInteractionMessage__dlm m ON i.ssot__Id__c = m.ssot__AiAgentInteractionId__c`
-- (required when filtering by --grep)
-- WHERE_CLAUSE — composed by the caller. No "WHERE" keyword. Always non-empty
-- (at minimum the time-range predicate). All user-supplied string
-- literals are single-quote-escaped by doubling quotes (O'Brien → O''Brien).
-- LIMIT — integer, 1..N. Default in caller is 20.
--
-- Field reference:
-- time range → s.ssot__StartTimestamp__c >= '<startISO>' AND s.ssot__StartTimestamp__c < '<endISO>'
-- outcome → s.ssot__AiAgentSessionEndType__c = '<USER_ENDED|ESCALATED|TRANSFERRED|TIMEOUT|NOT_SET>'
-- channel → s.ssot__AiAgentChannelType__c = '<Builder|SCRT2 - EmbeddedMessaging|Voice|...>'
-- agent → p.ssot__AiAgentApiName__c = '<AgentApiName>' (requires participant JOIN)
-- grep → m.ssot__ContentText__c LIKE '%<escaped-pattern>%' (requires interaction+message JOIN)
--
-- All STDM timestamps are UTC.
SELECT {{SELECT_LIST}}
FROM ssot__AIAgentSession__dlm s
{{JOINS}}
WHERE {{WHERE_CLAUSE}}
ORDER BY s.ssot__StartTimestamp__c DESC
LIMIT {{LIMIT}};
-- Free-text / structured detail attached to a feedback event.
-- DMO: GenAIFeedbackDetail__dlm
--
-- Placeholders (substituted by scripts/dc.py.load_sql):
-- WHERE_CLAUSE — the filter expression, no "WHERE" keyword
-- ORDER_BY — full "ORDER BY <col>" or empty string
--
-- Zero or more rows per feedback. `feedbackText__c` is what the user typed;
-- `appFeedback__c` is an app-layer tag (e.g. the reason bucket).
-- Joined via `parent__c = GenAIFeedback.feedbackId__c`.
--
-- NOTE: No `ssot__` prefix — fields end in `__c` directly.
SELECT
feedbackDetailId__c,
parent__c,
feedbackText__c,
appFeedback__c,
feature__c,
timestamp__c,
orgId__c,
cloud__c
FROM GenAIFeedbackDetail__dlm
WHERE {{WHERE_CLAUSE}}
{{ORDER_BY}};
-- ============================================================================
-- EXAMPLE WHERE clauses (pass via where_clause=)
-- ============================================================================
-- Details for a set of feedback rows
-- WHERE → parent__c IN ('<feedback_id1>','<feedback_id2>',...)
-- ORDER BY → ORDER BY timestamp__c
-- Free-text feedback only (DC Query returns "" for unset text, not NULL)
-- WHERE → parent__c IN ('<feedback_id1>','<feedback_id2>')
-- AND feedbackText__c IS NOT NULL AND feedbackText__c != ''
-- User feedback on generations — one row per feedback event.
-- DMO: GenAIFeedback__dlm
--
-- Placeholders (substituted by scripts/dc.py.load_sql):
-- WHERE_CLAUSE — the filter expression, no "WHERE" keyword
-- ORDER_BY — full "ORDER BY <col>" or empty string
--
-- Captures thumbs-up/thumbs-down (and richer `action__c`) that a user gave
-- a specific generation. Joined via `generationId__c = Generation.generationId__c`.
-- `feedbackId__c` is the PK that GenAIFeedbackDetail rows point at.
--
-- NOTE: No `ssot__` prefix — fields end in `__c` directly.
SELECT
feedbackId__c,
generationId__c,
generationUpdateId__c,
generationGroupId__c,
userId__c,
feedback__c,
action__c,
source__c,
feature__c,
appType__c,
timestamp__c,
orgId__c,
cloud__c
FROM GenAIFeedback__dlm
WHERE {{WHERE_CLAUSE}}
{{ORDER_BY}};
-- ============================================================================
-- EXAMPLE WHERE clauses (pass via where_clause=)
-- ============================================================================
-- Feedback for a set of generations (typical session trace path)
-- WHERE → generationId__c IN ('<gen_id1>','<gen_id2>',...)
-- ORDER BY → ORDER BY timestamp__c
-- Feedback by a specific user in a time window
-- WHERE → userId__c = '<user_id>'
-- AND timestamp__c >= '<iso_cutoff>'
-- Only thumbs-down
-- WHERE → generationId__c IN ('<gen_id1>','<gen_id2>')
-- AND feedback__c = 'DOWN'
-- Gateway object records — structured attachments on requests/feedback.
-- DMO: GenAIGtwyObjRecord__dlm
--
-- Placeholders (substituted by scripts/dc.py.load_sql):
-- WHERE_CLAUSE — the filter expression, no "WHERE" keyword
-- ORDER_BY — full "ORDER BY <col>" or empty string
--
-- Polymorphic attachment table. `parent__c` points at different parents
-- depending on `type__c`:
-- - `parent__c = GenAIGatewayRequest.gatewayRequestId__c` (grounded-record
-- attachments) — the forward-only path used by this skill's waterfall.
-- - `parent__c = GenAIFeedback.feedbackId__c` (feedback attachments) —
-- present only when the session has feedback rows.
-- The waterfall queries the gateway-request case (wave 3); feedback attachments
-- appear through the session only when feedback is present.
--
-- NOTE: No `ssot__` prefix — fields end in `__c` directly.
SELECT
id__c,
parent__c,
recordId__c,
type__c,
name__c,
value__c,
metadata__c,
feature__c,
timestamp__c,
orgId__c,
cloud__c
FROM GenAIGtwyObjRecord__dlm
WHERE {{WHERE_CLAUSE}}
{{ORDER_BY}};
-- ============================================================================
-- EXAMPLE WHERE clauses (pass via where_clause=)
-- ============================================================================
-- Records attached to a set of feedback rows
-- WHERE → parent__c IN ('<feedback_id1>','<feedback_id2>',...)
-- ORDER BY → ORDER BY timestamp__c
-- Records attached to a set of gateway request ids
-- WHERE → parent__c IN ('<req_id1>','<req_id2>')
-- Per-request LLM call diagnostics — reusable for any WHERE filter.
-- DMO: GenAIGtwyRequestLLM__dlm
--
-- Placeholders (substituted by scripts/dc.py.load_sql):
-- WHERE_CLAUSE — the filter expression, no "WHERE" keyword
-- ORDER_BY — full "ORDER BY <col>" or empty string
--
-- Child of GenAIGatewayRequest__dlm. Captures LLM invocation
-- diagnostics (latency, status, endpoint, region, model). Populated
-- only when Trust Layer gateway LLM telemetry is on.
--
-- The DMO is provisioned by default but rows only appear when the
-- gateway LLM telemetry is emitted. Join column pattern mirrors
-- GenAIGtwyRequestMetadata__dlm (same parent__c shape).
--
-- No `ssot__` prefix — fields end in `__c` directly. Note that org id
-- is `salesforceOrgId__c` on this DMO, not the usual `orgId__c`.
--
-- Joined via `parent__c = GatewayRequest.gatewayRequestId__c`.
SELECT
id__c,
parent__c,
endpoint__c,
region__c,
genAILLM__c,
llmCallStatus__c,
llmCallLatency__c,
llmErrorTrace__c,
metadata__c,
feature__c,
salesforceOrgId__c,
timestamp__c,
cloud__c
FROM GenAIGtwyRequestLLM__dlm
WHERE {{WHERE_CLAUSE}}
{{ORDER_BY}};
-- ============================================================================
-- EXAMPLE WHERE clauses (pass via where_clause=)
-- ============================================================================
-- All LLM diagnostic rows for a set of gateway request ids
-- WHERE → parent__c IN ('<req_id1>','<req_id2>',...)
-- ORDER BY → ORDER BY timestamp__c
-- Only failed LLM calls
-- WHERE → parent__c IN ('<req_id1>','<req_id2>')
-- AND llmCallStatus__c != 'success'
-- Additional per-request metadata — reusable for any WHERE filter.
-- DMO: GenAIGtwyRequestMetadata__dlm
--
-- Placeholders (substituted by scripts/dc.py.load_sql):
-- WHERE_CLAUSE — the filter expression, no "WHERE" keyword
-- ORDER_BY — full "ORDER BY <col>" or empty string
--
-- Child of GenAIGatewayRequest__dlm. Holds typed metadata rows for a
-- request — observed values include `metadataType__c = 'ToolCall'` and
-- `feature__c = 'plannerservice'`, so this is where planner/tool-call
-- metadata on a gateway request lives.
--
-- No `ssot__` prefix — fields end in `__c` directly.
--
-- Joined via `parent__c = GatewayRequest.gatewayRequestId__c`.
-- Join direction verified live: sampled row's parent__c matched exactly
-- one row in GenAIGatewayRequest__dlm. The table is usually heavily
-- populated on orgs with Trust Layer gateway logging enabled.
SELECT
id__c,
parent__c,
metadataType__c,
metadata__c,
feature__c,
timestamp__c,
orgId__c,
cloud__c
FROM GenAIGtwyRequestMetadata__dlm
WHERE {{WHERE_CLAUSE}}
{{ORDER_BY}};
-- ============================================================================
-- EXAMPLE WHERE clauses (pass via where_clause=)
-- ============================================================================
-- All metadata rows for a set of gateway request ids
-- WHERE → parent__c IN ('<req_id1>','<req_id2>',...)
-- ORDER BY → ORDER BY timestamp__c
-- Only ToolCall-type metadata
-- WHERE → parent__c IN ('<req_id1>','<req_id2>')
-- AND metadataType__c = 'ToolCall'
-- Gateway request tags — k/v metadata attached to a gateway request.
-- DMO: GenAIGatewayRequestTag__dlm
--
-- Placeholders (substituted by scripts/dc.py.load_sql):
-- WHERE_CLAUSE — the filter expression, no "WHERE" keyword
-- ORDER_BY — full "ORDER BY <col>" or empty string
--
-- Many rows per request. Common tags include:
-- - `prompt_template_dev_name` → which prompt template was used
-- - `user_utterance` → raw user input that triggered this request
--
-- Joined via `parent__c = GatewayRequest.gatewayRequestId__c`.
-- No `ssot__` prefix — fields end in `__c` directly.
SELECT
id__c,
parent__c,
tag__c,
tagValue__c,
timestamp__c,
orgId__c,
cloud__c
FROM GenAIGatewayRequestTag__dlm
WHERE {{WHERE_CLAUSE}}
{{ORDER_BY}};
-- ============================================================================
-- EXAMPLE WHERE clauses (pass via where_clause=)
-- ============================================================================
-- All tags for a set of request ids
-- WHERE → parent__c IN ('<req_id1>','<req_id2>',...)
-- ORDER BY → ORDER BY timestamp__c
-- Only `prompt_template_dev_name` tags
-- WHERE → parent__c IN ('<req_id1>','<req_id2>')
-- AND tag__c = 'prompt_template_dev_name'
-- Find requests where the user utterance matched a pattern
-- WHERE → tag__c = 'user_utterance'
-- AND tagValue__c LIKE '%refund%'
-- Gateway requests — one row per LLM request at the GenAI Gateway.
-- DMO: GenAIGatewayRequest__dlm
--
-- Placeholders (substituted by scripts/dc.py.load_sql):
-- WHERE_CLAUSE — the filter expression, no "WHERE" keyword
-- ORDER_BY — full "ORDER BY <col>" or empty string
--
-- Richer than GenAIGeneration — carries the actual prompt text, token counts,
-- model params (temperature/penalties), session/user IDs, bot version, and
-- the masked-prompt variant.
--
-- NOTE: No `ssot__` prefix — fields end in `__c` directly.
--
-- sessionId__c storage format (verified live): the value is stored as a
-- literal 40-char string INCLUDING surrounding double-quotes, e.g.
-- sessionId__c = "<session_uuid>"
-- Non-session features (prompt-builder previews, eval harnesses, etc.) store
-- the literal sentinel "no_session". Exact-match queries MUST include the
-- double-quotes:
-- WHERE sessionId__c = '"<session_uuid>"'
-- Or use LIKE with wildcards (robust against format variants):
-- WHERE sessionId__c LIKE '%<session_uuid>%'
-- Raw-UUID exact match returns 0 rows — the quotes are part of the stored value.
--
-- Forward join path from a session:
-- Session.ssot__Id__c → GatewayRequest.sessionId__c (LIKE or quoted match)
-- This is the authoritative and only supported entry point. GatewayRequest is
-- then the parent for all downstream audit-chain children — Response (via
-- generationRequestId__c), Tag/ObjRecord/Metadata/LLM (via parent__c).
-- See `scripts/fetch_dc.py` wave 3 and `references/dc_dmo_fields.md` "Cross-DMO
-- join map" for the full forward tree.
SELECT
gatewayRequestId__c,
generationGroupId__c,
sessionId__c,
userId__c,
botVersionId__c,
plannerId__c,
feature__c,
appType__c,
model__c,
provider__c,
promptTemplateDevName__c,
promptTemplateVersionNo__c,
prompt__c,
maskedPrompt__c,
parameters__c,
temperature__c,
frequencyPenalty__c,
presencePenalty__c,
stopSequences__c,
numGenerations__c,
promptTokens__c,
completionTokens__c,
totalTokens__c,
enableInputSafetyScoring__c,
enableOutputSafetyScoring__c,
enablePiiMasking__c,
timestamp__c,
orgId__c,
cloud__c
FROM GenAIGatewayRequest__dlm
WHERE {{WHERE_CLAUSE}}
{{ORDER_BY}};
-- ============================================================================
-- EXAMPLE WHERE clauses (pass via where_clause=)
-- ============================================================================
-- Requests for one session (direct FK; note the mandatory double-quoted form).
-- Two equivalent WHERE forms — both verified live, both return the same rows:
-- WHERE → sessionId__c = '"<session_id>"' (exact match on quoted string)
-- WHERE → sessionId__c LIKE '%<session_id>%' (format-tolerant)
-- ORDER BY → ORDER BY timestamp__c
-- Requests for a specific set of gatewayRequestIds (e.g. narrowing after a
-- session fetch, or lookup by ids harvested from another query)
-- WHERE → gatewayRequestId__c IN ('<req_id1>','<req_id2>',...)
-- All requests for a bot version in a time window
-- WHERE → botVersionId__c = '<version_id>'
-- AND timestamp__c >= '<iso_cutoff>'
-- ORDER BY → ORDER BY timestamp__c DESC
-- Requests using a specific prompt template
-- WHERE → promptTemplateDevName__c = '<template_dev_name>'
-- AND timestamp__c >= '<iso_cutoff>'
-- Gateway responses — one row per LLM call response at the GenAI Gateway.
-- DMO: GenAIGatewayResponse__dlm
--
-- Placeholders (substituted by scripts/dc.py.load_sql):
-- WHERE_CLAUSE — the filter expression, no "WHERE" keyword
-- ORDER_BY — full "ORDER BY <col>" or empty string
--
-- FK shape (small table; documented for reference only):
-- generationRequestId__c = GatewayRequest.gatewayRequestId__c
-- generationResponseId__c = Step.ssot__GenAiGatewayResponseId__c
-- = Generation.generationResponseId__c
--
-- Forward join path from a session:
-- Session → GatewayRequest (sessionId__c LIKE)
-- → GatewayResponse (generationRequestId__c IN {gw_req_ids})
-- This is the canonical and only supported direction. 1:1 invariant holds
-- in live data — every GatewayRequest for a session produces one Response
-- row (modulo in-flight calls at fetch time).
--
-- NOTE: No `ssot__` prefix — fields end in `__c` directly.
SELECT
generationResponseId__c,
generationRequestId__c,
parameters__c,
timestamp__c,
orgId__c,
cloud__c
FROM GenAIGatewayResponse__dlm
WHERE {{WHERE_CLAUSE}}
{{ORDER_BY}};
-- ============================================================================
-- EXAMPLE WHERE clauses (pass via where_clause=)
-- ============================================================================
-- Forward: Responses for the session's gateway requests (primary use case)
-- WHERE → generationRequestId__c IN ('<req_id1>','<req_id2>',...)
-- ORDER BY → ORDER BY timestamp__c
-- Ad-hoc lookup by specific response ids (not used by the waterfall)
-- WHERE → generationResponseId__c IN ('<resp_id1>','<resp_id2>',...)
-- GenAI gateway generations — reusable for any WHERE filter.
-- DMO: GenAIGeneration__dlm
--
-- Placeholders (substituted by scripts/dc.py.load_sql):
-- WHERE_CLAUSE — the filter expression, no "WHERE" keyword
-- ORDER_BY — full "ORDER BY <col>" or empty string
--
-- One row per LLM call at the gateway (Trust Layer).
--
-- NOTE: No `ssot__` prefix on this DMO — fields end in `__c` directly.
--
-- Forward join to a session: this DMO has NO session/trace/turn column.
-- The only supported path is Step.ssot__GenerationId__c → generationId__c,
-- driven forward from the session:
-- Session → Interaction (ssot__AiAgentSessionId__c)
-- → Step (ssot__AiAgentInteractionId__c)
-- → Generation (step.ssot__GenerationId__c IN {generationId__c})
-- Pull step rows for the session's interactions first, collect non-empty
-- `ssot__GenerationId__c` values (LLM_STEP rows populate it; others are
-- NOT_SET), then query here with `generationId__c IN (...)`.
SELECT
generationId__c,
generationResponseId__c,
responseText__c,
maskedResponseText__c,
responseParameters__c,
feature__c,
timestamp__c,
orgId__c,
cloud__c
FROM GenAIGeneration__dlm
WHERE {{WHERE_CLAUSE}}
{{ORDER_BY}};
-- ============================================================================
-- EXAMPLE WHERE clauses (pass via where_clause=)
-- ============================================================================
-- Generations for a set of step generation ids
-- WHERE → generationId__c IN ('<gen_id1>','<gen_id2>',...)
-- ORDER BY → ORDER BY timestamp__c
-- Generations in a time window for one org
-- WHERE → orgId__c = '<org_id_18>'
-- AND timestamp__c >= '<iso_window_start>'
-- AND timestamp__c < '<iso_window_end>'
-- ORDER BY → ORDER BY timestamp__c
-- Filter by feature (e.g. Copilot vs guardrails)
-- WHERE → feature__c = 'CopilotForDigitalChannels'
-- Session interactions (turns + session-end event) — reusable for any WHERE.
-- DMO: ssot__AIAgentInteraction__dlm
--
-- Placeholders (substituted by scripts/dc.py.load_sql):
-- WHERE_CLAUSE — the filter expression, no "WHERE" keyword
-- ORDER_BY — full "ORDER BY <col>" or empty string
--
-- One row per turn plus one SESSION_END row per session.
-- Type enum: TURN | SESSION_END.
--
-- Casing gotcha: DMO name uses `AIAgent` (uppercase AI). Field names use
-- `AiAgent` (lowercase i). See references/dc_dmo_fields.md.
--
-- trace_id gotcha: `ssot__TelemetryTraceId__c` is often empty on real orgs
-- (verified live). The runtime trace_id lives inside
-- `ssot__AttributeText__c` as HTML-escaped JSON, key `internalTraceId`.
-- Consumers must `html.unescape()` + regex-extract. Used to join with
-- GenAIGeneration (generationId via Step) and TelemetryTraceSpan.
SELECT
ssot__Id__c,
ssot__AiAgentSessionId__c,
ssot__AiAgentInteractionType__c,
ssot__TopicApiName__c,
ssot__StartTimestamp__c,
ssot__EndTimestamp__c,
ssot__PrevInteractionId__c,
ssot__SessionOwnerId__c,
ssot__IndividualId__c,
ssot__InternalOrganizationId__c,
ssot__TelemetryTraceId__c,
ssot__TelemetryTraceSpanId__c,
ssot__AttributeText__c
FROM ssot__AIAgentInteraction__dlm
WHERE {{WHERE_CLAUSE}}
{{ORDER_BY}};
-- ============================================================================
-- EXAMPLE WHERE clauses (pass via where_clause=)
-- ============================================================================
-- All interactions for one session
-- WHERE → ssot__AiAgentSessionId__c = '<session_id>'
-- ORDER BY → ORDER BY ssot__StartTimestamp__c
-- TURN rows only (exclude SESSION_END)
-- WHERE → ssot__AiAgentSessionId__c = '<session_id>'
-- AND ssot__AiAgentInteractionType__c = 'TURN'
-- Interactions handled by a specific topic across sessions
-- WHERE → ssot__TopicApiName__c = 'Order_Management'
-- AND ssot__StartTimestamp__c >= '2026-01-01T00:00:00.000Z'
-- User/agent messages — reusable for any WHERE filter.
-- DMO: ssot__AiAgentInteractionMessage__dlm
--
-- Placeholders (substituted by scripts/dc.py.load_sql):
-- WHERE_CLAUSE — the filter expression, no "WHERE" keyword
-- ORDER_BY — full "ORDER BY <col>" or empty string
--
-- One row per user/agent message. Type enum: Input | Output.
--
-- This DMO has a direct session FK (`ssot__AiAgentSessionId__c`) — verified
-- live against Data Cloud v66.0. Scope by session directly; no need to join through
-- interactions. It also has a participant FK and a parent-message FK for
-- threading, plus `MessageStartTimestamp__c` / `MessageEndTimestamp__c` for
-- voice-modality durations (richer than the single `MessageSentTimestamp__c`).
SELECT
ssot__Id__c,
ssot__AiAgentSessionId__c,
ssot__AiAgentInteractionId__c,
ssot__AiAgentSessionParticipantId__c,
ssot__ParentMessageId__c,
ssot__ContentText__c,
ssot__AiAgentInteractionMessageType__c,
ssot__AiAgentInteractionMsgContentType__c,
Modality__c,
ssot__MessageSentTimestamp__c,
MessageStartTimestamp__c,
MessageEndTimestamp__c,
ssot__InternalOrganizationId__c
FROM ssot__AiAgentInteractionMessage__dlm
WHERE {{WHERE_CLAUSE}}
{{ORDER_BY}};
-- ============================================================================
-- EXAMPLE WHERE clauses (pass via where_clause=)
-- ============================================================================
-- Messages for one session (direct FK — preferred)
-- WHERE → ssot__AiAgentSessionId__c = '<session_id>'
-- ORDER BY → ORDER BY ssot__MessageSentTimestamp__c
-- Messages for a specific interaction
-- WHERE → ssot__AiAgentInteractionId__c = '<interaction_id>'
-- ORDER BY → ORDER BY ssot__MessageSentTimestamp__c
-- Only user inputs for a session
-- WHERE → ssot__AiAgentSessionId__c = '<session_id>'
-- AND ssot__AiAgentInteractionMessageType__c = 'Input'
-- Voice-modality messages (use start/end timestamps for duration)
-- WHERE → ssot__AiAgentSessionId__c = '<session_id>'
-- AND Modality__c = 'Voice'
-- MessagingSession id → AI-agent session id lookup.
-- DMO: ssot__AIAgentSession__dlm
--
-- Given a Salesforce MessagingSession id (0Mw... prefix, 15 or 18 chars),
-- find every ssot__AIAgentSession__dlm row with matching
-- RelatedMessagingSessionId. Used by scripts/resolve_session.py to map a
-- messaging id to the canonical AI-agent session UUID that every other
-- script keys on.
--
-- Placeholders (substituted by scripts/dc.py.load_sql):
-- MSG_ID — the MessagingSession id (pre-validated by the caller:
-- is_messaging_id() enforces the `0Mw` key prefix plus an
-- exact 15 or 18 char length before this template is loaded.
-- A raw-UUID or free-text id can never reach this template.)
--
-- Returned rows:
-- * zero rows → caller raises SystemExit ("no messaging session found")
-- * one row → caller returns ssot__Id__c as the UUID
-- * many rows → caller prints every candidate with timestamps + end_type
-- + channel and exits non-zero so the user can pick one
-- and re-invoke with the specific UUID.
--
-- The `RelatedMessagingSessionId__c != 'NOT_SET'` clause is defensive —
-- a real msg_id cannot equal the literal 'NOT_SET', but the guard lets
-- the template be copy-pasted for other filters that might otherwise
-- accidentally match the sentinel.
SELECT
ssot__Id__c,
ssot__StartTimestamp__c,
ssot__EndTimestamp__c,
ssot__AiAgentSessionEndType__c,
ssot__AiAgentChannelType__c
FROM ssot__AIAgentSession__dlm
WHERE ssot__RelatedMessagingSessionId__c = '{{MSG_ID}}'
AND ssot__RelatedMessagingSessionId__c != 'NOT_SET'
ORDER BY ssot__StartTimestamp__c DESC;
-- Moment ↔ Interaction junction — which turns belong to a moment.
-- DMO: ssot__AiAgentMomentInteraction__dlm
--
-- Placeholders (substituted by scripts/dc.py.load_sql):
-- WHERE_CLAUSE — the filter expression, no "WHERE" keyword
-- ORDER_BY — full "ORDER BY <col>" or empty string
--
-- Agent Optimization add-on DMO. Provisioned only when Agent Optimization
-- is enabled. Junction between AiAgentMoment and AIAgentInteraction.
-- Observed live: one Moment per Interaction (N:1 direction). The junction
-- schema supports true many-to-many; the assembler emits Moment.interaction_ids[]
-- back-refs to preserve the schema-correct shape even when live data is 1:N.
SELECT
ssot__Id__c,
ssot__AiAgentMomentId__c,
ssot__AiAgentInteractionId__c,
ssot__StartTimestamp__c,
ssot__InternalOrganizationId__c
FROM ssot__AiAgentMomentInteraction__dlm
WHERE {{WHERE_CLAUSE}}
{{ORDER_BY}};
-- ============================================================================
-- EXAMPLE WHERE clauses (pass via where_clause=)
-- ============================================================================
-- Junction rows for a set of moments
-- WHERE → ssot__AiAgentMomentId__c IN ('<mom_id1>','<mom_id2>',...)
-- ORDER BY → ORDER BY ssot__StartTimestamp__c
-- Junction rows for a set of interactions (reverse lookup)
-- WHERE → ssot__AiAgentInteractionId__c IN ('<int_id1>','<int_id2>')
-- Session-level agent moment rollup — reusable for any WHERE filter.
-- DMO: ssot__AiAgentMoment__dlm
--
-- Placeholders (substituted by scripts/dc.py.load_sql):
-- WHERE_CLAUSE — the filter expression, no "WHERE" keyword
-- ORDER_BY — full "ORDER BY <col>" or empty string
--
-- When present, carries agent identity + a request/response summary for
-- the session. This is the only STDM DMO outside Participant that exposes
-- AiAgentApiName__c. Moments are absent on orgs without Agent Optimization
-- enabled; the assembler falls back to Participant (AGENT role) for agent
-- identity in that case.
SELECT
ssot__Id__c,
ssot__AiAgentSessionId__c,
ssot__AiAgentApiName__c,
ssot__AiAgentVersionApiName__c,
ssot__RequestSummaryText__c,
ssot__ResponseSummaryText__c,
ssot__StartTimestamp__c,
ssot__EndTimestamp__c,
ssot__InternalOrganizationId__c
FROM ssot__AiAgentMoment__dlm
WHERE {{WHERE_CLAUSE}}
{{ORDER_BY}};
-- ============================================================================
-- EXAMPLE WHERE clauses (pass via where_clause=)
-- ============================================================================
-- Moment(s) for one session — usually 0 or 1 row
-- WHERE → ssot__AiAgentSessionId__c = '<session_id>'
-- All sessions handled by a specific agent API name in a date range
-- WHERE → ssot__AiAgentApiName__c = 'MyAgent'
-- AND ssot__StartTimestamp__c >= '2026-01-01T00:00:00.000Z'
-- ORDER BY → ORDER BY ssot__StartTimestamp__c
-- Session participants from Data Cloud — reusable for any WHERE filter.
-- DMO: ssot__AiAgentSessionParticipant__dlm
--
-- Placeholders (substituted by scripts/dc.py.load_sql):
-- WHERE_CLAUSE — the filter expression, no "WHERE" keyword
-- ORDER_BY — full "ORDER BY <col>" or empty string
--
-- One row per participant per session. Roles: USER, AGENT.
-- AiAgentApiName__c is populated on AGENT rows only.
--
-- Casing gotcha: DMO name uses `AiAgent` (lowercase i), unlike the Session
-- DMO which uses `AIAgent` (uppercase AI). See references/dc_dmo_fields.md.
SELECT
ssot__Id__c,
ssot__AiAgentSessionId__c,
ssot__ParticipantId__c,
ssot__AiAgentApiName__c,
ssot__AiAgentType__c,
ssot__AiAgentTemplateApiName__c,
ssot__AiAgentVersionApiName__c,
ssot__AiAgentSessionParticipantRole__c,
ssot__ParticipantObject__c,
ssot__StartTimestamp__c,
ssot__EndTimestamp__c,
ssot__IndividualId__c,
ssot__InternalOrganizationId__c,
ssot__ParticipantAttributeText__c
FROM ssot__AiAgentSessionParticipant__dlm
WHERE {{WHERE_CLAUSE}}
{{ORDER_BY}};
-- ============================================================================
-- EXAMPLE WHERE clauses (pass via where_clause=)
-- ============================================================================
-- Participants for one session
-- WHERE → ssot__AiAgentSessionId__c = '<session_id>'
-- ORDER BY → ORDER BY ssot__StartTimestamp__c
-- Only AGENT rows (carry agent identity)
-- WHERE → ssot__AiAgentSessionId__c = '<session_id>'
-- AND ssot__AiAgentSessionParticipantRole__c = 'AGENT'
-- All sessions handled by a specific agent
-- WHERE → ssot__AiAgentApiName__c = 'MyAgent'
-- AND ssot__StartTimestamp__c >= '2026-01-01T00:00:00.000Z'
-- Sessions from Data Cloud — reusable for any WHERE filter.
-- DMO: ssot__AIAgentSession__dlm
--
-- Placeholders (substituted by scripts/dc.py._load):
-- WHERE_CLAUSE — the filter expression, no "WHERE" keyword
-- ORDER_BY — full "ORDER BY <col>" or empty string
--
-- See EXAMPLE QUERIES below for common WHERE patterns.
--
-- This query extracts session-level data including:
-- - Session ID and timestamps
-- - Channel type (how user connected)
-- - How the session ended (Completed, Abandoned, Escalated, etc.)
-- - Related messaging session (if applicable)
--
-- NOTE: Agent name is NOT on Session table. Join with Moment to get agent info.
SELECT
ssot__Id__c,
ssot__AiAgentChannelType__c,
ssot__StartTimestamp__c,
ssot__EndTimestamp__c,
ssot__AiAgentSessionEndType__c,
ssot__RelatedMessagingSessionId__c,
ssot__RelatedVoiceCallId__c,
ssot__InternalOrganizationId__c,
ssot__SessionOwnerId__c,
ssot__SessionOwnerObject__c,
ssot__IndividualId__c,
ssot__PreviousSessionId__c,
ssot__VariableText__c
FROM ssot__AIAgentSession__dlm
WHERE {{WHERE_CLAUSE}}
{{ORDER_BY}};
-- ============================================================================
-- EXAMPLE QUERIES (pass to sessions_sql via where_clause= / order_by=)
-- ============================================================================
-- One session by id (this skill's primary use case)
-- WHERE → ssot__Id__c = '<session_uuid>'
-- ORDER BY → ORDER BY ssot__StartTimestamp__c
-- Last 7 days of sessions
-- WHERE → ssot__StartTimestamp__c >= '<iso_cutoff_7d_ago>'
-- ORDER BY → ORDER BY ssot__StartTimestamp__c
-- Date range
-- WHERE → ssot__StartTimestamp__c >= '2026-01-01T00:00:00.000Z'
-- AND ssot__StartTimestamp__c < '2026-02-01T00:00:00.000Z'
-- ORDER BY → ORDER BY ssot__StartTimestamp__c
-- Failed / escalated sessions only
-- WHERE → ssot__AiAgentSessionEndType__c IN ('Escalated', 'Abandoned', 'Failed')
-- AND ssot__StartTimestamp__c >= '2026-01-01T00:00:00.000Z'
-- ORDER BY → ORDER BY ssot__StartTimestamp__c
-- Sessions by channel (e.g. embedded messaging only)
-- WHERE → ssot__AiAgentChannelType__c = 'SCRT2 - EmbeddedMessaging'
-- AND ssot__StartTimestamp__c >= '2026-01-01T00:00:00.000Z'
-- ORDER BY → ORDER BY ssot__StartTimestamp__c
-- Session count by end type (aggregate — SELECT list changes too; separate template)
-- SELECT
-- ssot__AiAgentSessionEndType__c,
-- COUNT(*) as session_count
-- FROM ssot__AIAgentSession__dlm
-- WHERE ssot__StartTimestamp__c >= '2026-01-01T00:00:00.000Z'
-- GROUP BY ssot__AiAgentSessionEndType__c;
-- Sessions by agent (requires Moment join — separate query shape, not this template)
-- SELECT DISTINCT s.*
-- FROM ssot__AIAgentSession__dlm s
-- JOIN ssot__AiAgentMoment__dlm m
-- ON m.ssot__AiAgentSessionId__c = s.ssot__Id__c
-- WHERE m.ssot__AiAgentApiName__c = 'MyAgent'
-- AND s.ssot__StartTimestamp__c >= '2026-01-01T00:00:00.000Z';
-- Planner steps — reusable for any WHERE filter.
-- DMO: ssot__AIAgentInteractionStep__dlm
--
-- Placeholders (substituted by scripts/dc.py.load_sql):
-- WHERE_CLAUSE — the filter expression, no "WHERE" keyword
-- ORDER_BY — full "ORDER BY <col>" or empty string
--
-- One row per planner step within an interaction. Type enum (verified live):
-- LLM_STEP | ACTION_STEP | TOPIC_STEP | TRUST_GUARDRAILS_STEP | SESSION_END.
--
-- NOTE: InputValueText__c / OutputValueText__c are HTML-escaped JSON.
-- Callers should `html.unescape()` then `json.loads()` after fetch.
-- No direct session FK — scope forward via ssot__AiAgentInteractionId__c
-- (harvested from the session's Interaction rows).
--
-- ssot__GenAiGatewayRequestId__c / ssot__GenAiGatewayResponseId__c are
-- included in the SELECT for completeness but are NOT used as join keys
-- anywhere in the waterfall. Gateway audit rows are fetched forward from
-- Session → GatewayRequest (via sessionId__c) — see gateway_requests.sql.
-- ssot__ErrorMessageText__c uses the sentinel 'NOT_SET' (never NULL) to
-- mean "no error"; filter with `!= 'NOT_SET'`, not `IS NOT NULL`.
SELECT
ssot__Id__c,
ssot__AiAgentInteractionId__c,
ssot__AiAgentInteractionStepType__c,
ssot__Name__c,
ssot__InputValueText__c,
ssot__OutputValueText__c,
ssot__PreStepVariableText__c,
ssot__PostStepVariableText__c,
ssot__GenerationId__c,
ssot__ErrorMessageText__c,
ssot__StartTimestamp__c,
ssot__EndTimestamp__c,
ssot__PrevStepId__c,
ssot__InternalOrganizationId__c,
ssot__TelemetryTraceSpanId__c,
ssot__AttributeText__c,
ssot__GenAiGatewayRequestId__c,
ssot__GenAiGatewayResponseId__c
FROM ssot__AIAgentInteractionStep__dlm
WHERE {{WHERE_CLAUSE}}
{{ORDER_BY}};
-- ============================================================================
-- EXAMPLE WHERE clauses (pass via where_clause=)
-- ============================================================================
-- Steps for a specific interaction
-- WHERE → ssot__AiAgentInteractionId__c = '<interaction_id>'
-- ORDER BY → ORDER BY ssot__StartTimestamp__c
-- Steps for a session (via interaction id IN list)
-- WHERE → ssot__AiAgentInteractionId__c IN ('<id1>','<id2>',...)
-- Only action steps (exclude LLM + topic planning)
-- WHERE → ssot__AiAgentInteractionId__c IN ('<id1>','<id2>')
-- AND ssot__AiAgentInteractionStepType__c = 'ACTION_STEP'
-- Steps with errors (sentinel value; NOT null)
-- WHERE → ssot__AiAgentInteractionId__c IN ('<id1>','<id2>')
-- AND ssot__ErrorMessageText__c != 'NOT_SET'
-- Tag ↔ target association — which tag was applied to a moment/session/interaction.
-- DMO: ssot__AiAgentTagAssociation__dlm
--
-- Placeholders (substituted by scripts/dc.py.load_sql):
-- WHERE_CLAUSE — the filter expression, no "WHERE" keyword
-- ORDER_BY — full "ORDER BY <col>" or empty string
--
-- Agent Optimization add-on DMO. THE applied-tag row — one per annotation.
-- Points at a tag value (`ssot__AiAgentTagId__c`) + a target
-- (moment | session | interaction) + the tag-definition-association that
-- authorized it. `ssot__AssociationReasonText__c` carries the LLM's
-- rationale for why it assigned this tag.
SELECT
ssot__Id__c,
ssot__AiAgentTagId__c,
ssot__AiAgentTagDefinitionAssociationId__c,
ssot__AiAgentMomentId__c,
ssot__AiAgentSessionId__c,
ssot__AiAgentInteractionId__c,
ssot__IsPassed__c,
ssot__AssociationReasonText__c,
ssot__CreatedDate__c,
ssot__InternalOrganizationId__c
FROM ssot__AiAgentTagAssociation__dlm
WHERE {{WHERE_CLAUSE}}
{{ORDER_BY}};
-- ============================================================================
-- EXAMPLE WHERE clauses (pass via where_clause=)
-- ============================================================================
-- All tag associations for one session
-- WHERE → ssot__AiAgentSessionId__c = '<session_id>'
-- ORDER BY → ORDER BY ssot__CreatedDate__c
-- Tags applied to a set of moments
-- WHERE → ssot__AiAgentMomentId__c IN ('<mom_id1>','<mom_id2>')
-- Only passed evaluations (quality pass/fail)
-- WHERE → ssot__AiAgentSessionId__c = '<session_id>'
-- AND ssot__IsPassed__c = true
-- Associations by tag id (reverse lookup)
-- WHERE → ssot__AiAgentTagId__c IN ('<tag_id1>','<tag_id2>')
-- Tag definition ↔ agent association — which tags are available for an agent.
-- DMO: ssot__AiAgentTagDefinitionAssociation__dlm
--
-- Placeholders (substituted by scripts/dc.py.load_sql):
-- WHERE_CLAUSE — the filter expression, no "WHERE" keyword
-- ORDER_BY — full "ORDER BY <col>" or empty string
--
-- Agent Optimization add-on DMO. Binds a TagDefinition to a specific agent
-- (by API name) and prompt template. This is what `AiAgentTagAssociation`
-- actually points at via `ssot__AiAgentTagDefinitionAssociationId__c`.
SELECT
ssot__Id__c,
ssot__AiAgentTagDefinitionId__c,
ssot__AiAgentApiName__c,
ssot__AiPromptTemplateId__c,
ssot__IsActive__c,
ssot__CreatedDate__c,
ssot__InternalOrganizationId__c
FROM ssot__AiAgentTagDefinitionAssociation__dlm
WHERE {{WHERE_CLAUSE}}
{{ORDER_BY}};
-- ============================================================================
-- EXAMPLE WHERE clauses (pass via where_clause=)
-- ============================================================================
-- Active associations for a specific agent
-- WHERE → ssot__AiAgentApiName__c = '<agent_api_name>'
-- AND ssot__IsActive__c = true
-- Associations pointing at a set of tag definitions
-- WHERE → ssot__AiAgentTagDefinitionId__c IN ('<def_id1>','<def_id2>',...)
-- Resolve by association id (from AiAgentTagAssociation)
-- WHERE → ssot__Id__c IN ('<assoc_id1>','<assoc_id2>',...)
-- Tag definitions — the schema/vocabulary for tags applied to moments or sessions.
-- DMO: ssot__AiAgentTagDefinition__dlm
--
-- Placeholders (substituted by scripts/dc.py.load_sql):
-- WHERE_CLAUSE — the filter expression, no "WHERE" keyword
-- ORDER_BY — full "ORDER BY <col>" or empty string
--
-- Agent Optimization add-on DMO. Defines a tag type (name, data type,
-- input scope, engine type, source) that can then be applied via
-- AiAgentTag + AiAgentTagAssociation.
SELECT
ssot__Id__c,
ssot__DeveloperName__c,
ssot__Name__c,
ssot__Description__c,
ssot__TagIdentifier__c,
ssot__DataType__c,
ssot__Status__c,
ssot__VersionNumber__c,
ssot__EngineType__c,
ssot__SourceType__c,
ssot__SourceTagReferenceName__c,
ssot__InputScope__c,
ssot__CreatedDate__c,
ssot__InternalOrganizationId__c
FROM ssot__AiAgentTagDefinition__dlm
WHERE {{WHERE_CLAUSE}}
{{ORDER_BY}};
-- ============================================================================
-- EXAMPLE WHERE clauses (pass via where_clause=)
-- ============================================================================
-- All active tag definitions
-- Live orgs use 'Available' (not 'Active') — verified via live
-- describe. Group by ssot__Status__c to confirm per-org.
-- If 'Available' returns 0 rows, the waterfall retries unfiltered
-- (`ssot__Id__c IS NOT NULL`). Callers replicating this template
-- outside fetch_dc.py should apply the same fallback or expect
-- empty catalogs on orgs where Status uses a different enum.
-- WHERE → ssot__Status__c = 'Available'
-- ORDER BY → ORDER BY ssot__Name__c
-- By developer name
-- WHERE → ssot__DeveloperName__c = 'IntentCategory'
-- Definitions for a set of ids (resolves foreign keys from AiAgentTag)
-- WHERE → ssot__Id__c IN ('<def_id1>','<def_id2>',...)
-- Tag instances — a specific tag value under a tag definition.
-- DMO: ssot__AiAgentTag__dlm
--
-- Placeholders (substituted by scripts/dc.py.load_sql):
-- WHERE_CLAUSE — the filter expression, no "WHERE" keyword
-- ORDER_BY — full "ORDER BY <col>" or empty string
--
-- Agent Optimization add-on DMO. A `AiAgentTag` belongs to a definition
-- (e.g. definition "IntentCategory" with tag value "Refund Request").
-- The tag is applied to a moment/session/interaction via AiAgentTagAssociation.
SELECT
ssot__Id__c,
ssot__AiAgentTagDefinitionId__c,
ssot__Value__c,
ssot__Description__c,
ssot__OrderNumber__c,
ssot__IsActive__c,
ssot__IsFallback__c,
ssot__CreatedDate__c,
ssot__InternalOrganizationId__c
FROM ssot__AiAgentTag__dlm
WHERE {{WHERE_CLAUSE}}
{{ORDER_BY}};
-- ============================================================================
-- EXAMPLE WHERE clauses (pass via where_clause=)
-- ============================================================================
-- Tags for a specific definition (all allowed values)
-- WHERE → ssot__AiAgentTagDefinitionId__c = '<def_id>'
-- AND ssot__IsActive__c = true
-- ORDER BY → ORDER BY ssot__OrderNumber__c
-- Resolve tags by id (from AiAgentTagAssociation.ssot__AiAgentTagId__c)
-- WHERE → ssot__Id__c IN ('<tag_id1>','<tag_id2>',...)
-- Agent Platform Tracing spans — OpenTelemetry span tree per trace.
-- DMO: ssot__TelemetryTraceSpan__dlm
--
-- Placeholders (substituted by scripts/dc.py.load_sql):
-- WHERE_CLAUSE — the filter expression, no "WHERE" keyword
-- ORDER_BY — full "ORDER BY <col>" or empty string
--
-- Provisioned only when **Agent Platform Tracing** is enabled (Setup →
-- Einstein Audit, Analytics, and Monitoring Setup). Captures spans from
-- Apex, Flows, Prompt Builder, Invocable Actions, Planner, AI Gateway,
-- LLM Gateway, DC Query Federator.
--
-- Join to STDM: `ssot__TelemetryTrace__c = Interaction.ssot__TelemetryTraceId__c`.
-- Parent/child within a trace: `ssot__TelemetryParentSpanId__c = <another span>.ssot__Id__c`.
SELECT
ssot__Id__c,
ssot__TelemetryTrace__c,
ssot__TelemetryParentSpanId__c,
ssot__OperationName__c,
ssot__ServiceName__c,
ssot__SpanKind__c,
ssot__StatusCode__c,
ssot__StartDateTime__c,
ssot__EndDateTime__c,
ssot__DurationNumber__c,
ssot__TelemetrySpanAttributeText__c,
ssot__InternalOrganizationId__c
FROM ssot__TelemetryTraceSpan__dlm
WHERE {{WHERE_CLAUSE}}
{{ORDER_BY}};
-- ============================================================================
-- EXAMPLE WHERE clauses (pass via where_clause=)
-- ============================================================================
-- All spans for one trace (joins to Interaction.ssot__TelemetryTraceId__c)
-- WHERE → ssot__TelemetryTrace__c = '<trace_id>'
-- ORDER BY → ORDER BY ssot__StartDateTime__c
-- All spans for a set of traces (whole session)
-- WHERE → ssot__TelemetryTrace__c IN ('<trace_id1>','<trace_id2>',...)
-- Only root spans (no parent)
-- WHERE → ssot__TelemetryTrace__c = '<trace_id>'
-- AND ssot__TelemetryParentSpanId__c = null
-- Only spans from a specific service
-- WHERE → ssot__TelemetryTrace__c = '<trace_id>'
-- AND ssot__ServiceName__c = 'Atlas Reasoning Engine'
-- Slow spans — duration > 1s (nanoseconds in DurationNumber)
-- WHERE → ssot__TelemetryTrace__c = '<trace_id>'
-- AND ssot__DurationNumber__c > 1000000000
investigating-agentforce-d360
Data Cloud 360° view of a single Agentforce session. Pulls 24 STDM + GenAI DMOs from Salesforce Data Cloud, assembles a hierarchical session tree (Interaction → Step → Generation → GatewayRequest), and renders a human-readable markdown summary.
This skill is DC-only — it reads runtime audit data that Salesforce Data Cloud has materialized for a session. It does not call into runtime telemetry, performance services, or any Splunk / observability surface.
Input: an Agent Session UUID (019d…) or a MessagingSession id (0Mw…, 15/18 chars), and an sf CLI org alias.
Output: per-DMO JSON artifacts plus three derived files under ~/.vibe/data/investigating-agentforce-d360/<org_id15>/<agent>__<version>/<session_id>/ (default; override per-script with --data-dir <path>):
dc.<name>.json— 24 raw DMO results (one per query in the waterfall)dc._session_manifest.json— per-DMO row counts, classifiedsession_shape, and empty-by-design reasonsdc._session_tree.json— hierarchical join (the primary artifact; the summary is rendered from this)dc._session_summary.md— human-readable summary, up to 11 sections
---
Runtime budget
~10–30s typical on a 15-turn session. The 5-wave fetch waterfall fans out 24 queries; later waves depend on ids harvested from earlier waves, so wave-to-wave is sequential, but each wave's queries run concurrently within the wave.
---
Prerequisites
| Tool | Why |
|---|---|
sf CLI (authenticated against the target org) | Shells sf org display --target-org <alias> --json for the Data Cloud Query REST API access token |
| Data Cloud enabled on the target org | Required — the STDM + GenAI DMOs must have materialized for the session |
| Python 3.10+ | pathlib, dataclasses, `\ |
---
Usage
Invoked conversationally through whatever skill-aware runtime hosts it. Example prompts:
| User says | Skill does |
|---|---|
trace session 019dface-... in my-org | Run the 3-stage pipeline: fetch → assemble → render |
summarize what happened in 0MwTESTMSG12345AAA | Resolve the messaging id → UUID, then run the pipeline |
find escalated sessions today on Messaging in my-org | Run discover_sessions.py, print a numbered picker, user picks one, then run the pipeline |
walk me through this session | Same as trace — the rendered summary reads top-to-bottom |
See SKILL.md for the full TRIGGER conditions, flag table, and the "DC-only blind spot" guidance.
---
Pipeline
Three stages, each independently runnable:
fetch_dc.py → 24 dc.<name>.json + dc._session_manifest.json (DC Query REST waterfall)
assemble_dc.py → dc._session_tree.json (in-memory hierarchical join)
render_dc.py → dc._session_summary.md (markdown rendering)fetch_dc.py --session <sid> --org <alias> chains all three by default. Pass --no-assemble / --no-render to stop early.
---
Artifacts read order
1. `dc._session_summary.md` — human-readable, top-to-bottom answers "what happened in this session?" 2. `dc._session_tree.json` — single source of truth, the hierarchical join the summary was rendered from 3. `dc._session_manifest.json` — open this when something looks missing in the tree (per-DMO row counts, empty-by-design reasons) 4. `dc.<name>.json` — raw per-DMO rows, only when the manifest reports an unexpected count
See references/artifacts.md for the full inventory.
---
What this skill does NOT answer
DC alone tells you what happened — every step, every LLM call, every gateway request, in order, with timestamps. It does not tell you what could have happened but didn't:
- Which topics were eligible for the classifier on a given turn
- Which actions survived rule expressions and were actually offered to the LLM
- Why the LLM picked one topic/action over another
If the user's question is about why a particular topic or action was or wasn't used, DC-only is almost never sufficient. See "DC-only blind spot" in SKILL.md.
For design-time architecture questions (topic/action tree, flow inventory, Apex classes, prompt templates), use the sibling skill investigating-agentforce-architecture instead.
---
Layout
investigating-agentforce-d360/
├── SKILL.md ← runtime-parsed entry point (TRIGGER / DO NOT TRIGGER, flags, prompts)
├── README.md ← this file
├── scripts/
│ ├── fetch_dc.py ← 5-wave DC fetch + chained pipeline driver
│ ├── assemble_dc.py ← in-memory hierarchical join → dc._session_tree.json
│ ├── render_dc.py ← markdown rendering → dc._session_summary.md
│ ├── discover_sessions.py ← session picker by time / agent / channel / outcome / grep
│ ├── resolve_session.py ← `0Mw…` MessagingSession id → Agent Session UUID
│ ├── dc.py ← DC Query REST API client (load_sql, post)
│ ├── storage.py ← per-session JSON writer (path-validated)
│ ├── config.py ← shared constants + DATA_ROOT re-export
│ ├── _shared/ ← path / SQL helpers (paths, fs_guard, sql)
│ └── tests/ ← pytest suite (372 tests + 18 subtests)
├── references/
│ ├── artifacts.md ← the full per-session artifact inventory
│ ├── dc_dmo_fields.md ← per-DMO field reference + cross-DMO join map
│ └── dc_pipeline_contract.md ← pipeline contract: tree shape + render-stage section list
└── assets/
└── dc/ ← 26 .sql templates loaded by dc.load_sql---
Authored by
Raghul Jayagopal (RJ), Salesforce ANZ FDE.
---
License
Apache-2.0. See repository root LICENSE.
Artifacts reference
Every successful trace lands artifacts under ~/.vibe/data/investigating-agentforce-d360/<org_id15>/<agent>__<ver>/<sid>/. Re-running the same (org, sid) overwrites in place. Listed below in the order they're produced.
~/.vibe/data/investigating-agentforce-d360/<org_id15>/<agent>__<ver>/<sid>/
├── dc._session_manifest.json ← per-query counts + session_shape + empties
├── dc._session_tree.json ← hierarchical join (primary artifact)
├── dc._session_summary.md ← human-readable summary, up to 11 sections (varies with session_shape + identity bootstrap + --show-prompts opt-in)
├── dc.sessions.json ← 1 row (STDM session)
├── dc.interactions.json ← N rows (TURN + SESSION_END)
├── dc.messages.json ← USER + AGENT messages
├── dc.steps.json ← LLM_STEP / ACTION_STEP / TOPIC_STEP / TRUST_GUARDRAILS_STEP / SESSION_END
├── dc.participants.json ← USER + AGENT participants
├── dc.generations.json ← LLM generations
├── dc.gateway_requests.json ← gateway-logged LLM calls
├── dc.gateway_responses.json ← 1:1 with gateway_requests
├── dc.gateway_request_tags.json ← tag rows (bot_id, agent_version_api_name, etc.)
├── dc.gateway_request_metadata.json ← per-call metadata
├── dc.content_quality.json ← Trust Layer quality scores
├── dc.content_category.json ← toxicity + other category rows
├── dc.feedback.json ← user thumbs-up/down (often empty)
├── dc.feedback_details.json ← feedback text (often empty)
├── dc.moments.json ← optional Agent Optimization rollup (often empty)
├── dc.moment_interactions.json ← moment→interaction junction (often empty)
├── dc.tag_*.json ← org-wide agent tag catalog (often empty)
├── dc.telemetry_spans.json ← usually empty (Agent Platform Tracing off)
├── dc.app_generation.json ← reserved, always empty today
├── dc.gateway_records.json ← grounded attachments (rare)
├── dc.gateway_request_llm.json ← writer inactive on observed orgs
└── dc.tag_associations.json ← agent↔tag links (often empty)Read order
1. `dc._session_summary.md` — human-readable, top-to-bottom answers "what happened in this session?" 2. `dc._session_tree.json` — single source of truth, the hierarchical join the summary was rendered from. Open this when the summary is missing a detail you need. 3. `dc._session_manifest.json` — per-DMO row counts, classified session_shape, and empty-by-design reasons for any DMO that returned zero rows. Open this when something looks missing in the tree. 4. `dc.<name>.json` — raw per-DMO rows. Only needed when the manifest reports an unexpected count or the assembler logs a parse warning.
STDM + GenAI DMO field reference
Convention. Executable SQL literals live under assets/dc/*.sqland are loaded by scripts via dc.load_sql. Reference docs (includingthis one) describe query shape, column meaning, and join topology in
prose — they do not contain the literal the engine would execute.
Rationale: loaders parse, comment-strip, and placeholder-substitute
the asset files; .md code fences are inert. A query literal in.md is orphan code with no linter coverage and no guarantee itmatches the live schema.
Field reference for the DMOs this skill touches. 24 queried by the scripts/fetch_dc.py waterfall (one .sql template each under assets/dc/). All field lists are copy-pasteable into assets/dc/*.sql SELECT lists. Schemas verified against live Data Cloud v66.0 via sf ssot/metadata describes; join paths verified by running the waterfall end-to-end on live sessions.
Source of truth: the live org (sf ssot/metadata?entityName=<dmo>). Official Salesforce Help pages list logical API names and aspirational enum values that frequently diverge from the physical names and runtime values the Data Cloud API actually exposes. When a Help page disagrees with a live describe, trust the live describe — the names and enums in this reference are what you query.
Official doc pointers (for context, not authority — Session Tracing doc uses logical API names and aspirational enum values that diverge from what the live sf ssot/metadata describe returns; trust the live describe):
- Session Tracing DMOs: https://help.salesforce.com/s/articleView?id=ai.generative_ai_session_trace_data_model.htm&type=5
- Generative AI Audit and Feedback DMOs: https://help.salesforce.com/s/articleView?id=ai.generative_ai_feedback_data_model.htm&type=5
- Agent Optimization DMOs: https://help.salesforce.com/s/articleView?id=ai.generative_ai_optimize_data_model.htm&type=5
- Agent Platform Tracing (
ssot__TelemetryTraceSpan__dlm): https://help.salesforce.com/s/articleView?id=ai.generative_ai_platform_trace.htm&type=5
Casing gotchas (important):
- DMO table casing is mixed — don't assume a single rule. Three STDM DMOs use uppercase
AIAgent(ssot__AIAgentSession__dlm,ssot__AIAgentInteraction__dlm,ssot__AIAgentInteractionStep__dlm); all otherssot__*AiAgent*__dlmtables use lowercaseAiAgent. Trust the exact name in each section header or the livedescribe. - Field names consistently use `AiAgent` (lowercase
i), e.g.ssot__AiAgentSessionId__c, across every DMO. - Generative AI Audit and Feedback DMOs (
GenAIGeneration,GenAIContentQuality,GenAIContentCategory, and the 9 other DMOs in that family — see below) do not use thessot__prefix. Their fields end in__c, notssot__*__c.
---
Cross-DMO join map
Every edge is strictly forward from Session. scripts/fetch_dc.py runs the tree as a 5-wave waterfall; each child query keys off ids harvested from parents in earlier waves. No backward lookups, no cross-validation paths — if a DMO can't be reached by a forward FK from Session, it's not fetched.
The audit/cost chain (GatewayRequest and its children) enters the tree forward through GenAIGatewayRequest.sessionId__c. Storage gotcha: the value is stored as a literal 40-char string INCLUDING surrounding double-quotes, e.g. "<session_uuid>". Non-session features store the sentinel "no_session". A raw-UUID exact match returns 0 rows; use sessionId__c LIKE '%<sid>%' or sessionId__c = '"<sid>"'.
Per-run row counts, empty reasons, and join paths are recorded in dc._session_manifest.json.
Legend: ★ = session-FK direct (one hop) ▸ = polymorphic parent__c ⚠ = requires feature provisioning
Session (ssot__AIAgentSession__dlm, PK ssot__Id__c)
│
├── ★ Participant (ssot__AiAgentSessionId__c)
│
├── ★ Message (ssot__AiAgentSessionId__c)
│ └── Message.participant (ssot__AiAgentSessionParticipantId__c → Participant.ssot__Id__c)
│
├── ★ Moment (ssot__AiAgentSessionId__c)
│ └── MomentInteraction (ssot__AiAgentMomentId__c, ssot__AiAgentInteractionId__c)
│
├── ★ TagAssociation ▸ (ssot__AiAgentSessionId__c when target is session;
│ ssot__AiAgentInteractionId__c when target is turn;
│ ssot__AiAgentMomentId__c when target is moment —
│ exactly one of the three is populated per row)
│ ├── → AiAgentTag (ssot__AiAgentTagId__c = Tag.ssot__Id__c)
│ │ └── TagDefinition (Tag.ssot__AiAgentTagDefinitionId__c =
│ │ TagDefinition.ssot__Id__c)
│ │ — catalog, not session-keyed; query with
│ │ ssot__Status__c = 'Available' (verified live;
│ │ Help docs' 'Active' value does not match live data)
│ └── → TagDefinitionAssociation
│ (ssot__AiAgentTagDefinitionAssociationId__c = TagDefAssoc.ssot__Id__c)
│ — catalog, not session-keyed; query by ssot__AiAgentApiName__c IN
│ {agent_api_names from Participant(role='AGENT') ∪ Moment}
│ (Participant is the primary source — Moment rows may be absent on
│ orgs without Agent Optimization enabled)
│
├── ★ Interaction (ssot__AiAgentSessionId__c)
│ ├── Step (ssot__AiAgentInteractionId__c)
│ │ └── GenAIGeneration (step.ssot__GenerationId__c IN {generationId__c})
│ │ │ (assembly bridge → references/dc_pipeline_contract.md:
│ │ │ Generation.generationResponseId__c =
│ │ │ GatewayResponse.generationResponseId__c)
│ │ ├── GenAIContentQuality (parent__c = generationId__c)
│ │ │ └── GenAIContentCategory ▸ (parent__c = Quality.id__c;
│ │ │ toxicity sub-category rows)
│ │ ├── GenAIContentCategory ▸ (parent__c = generationId__c;
│ │ │ non-toxicity detector rows)
│ │ ├── GenAIAppGeneration (generationId__c = Gen.generationId__c;
│ │ │ sibling record, often empty on observed orgs)
│ │ └── GenAIFeedback (generationId__c)
│ │ ├── GenAIFeedbackDetail (parent__c = feedback.feedbackId__c)
│ │ └── GenAIGtwyObjRecord ▸ (parent__c = feedback.feedbackId__c
│ │ when type__c is a feedback attachment)
│ └── ⚠ TelemetryTraceSpan (ssot__TelemetryTrace__c IN {trace_ids};
│ trace_ids extracted from Interaction.ssot__AttributeText__c via
│ html.unescape() + regex "internalTraceId":"([a-f0-9]+)" —
│ Interaction.ssot__TelemetryTraceId__c column is usually empty.
│ Requires Agent Platform Tracing enabled on the org.)
│
└── ★ GenAIGatewayRequest (sessionId__c LIKE '%<sid>%')
│ — one row per LLM call owned by the session,
│ across all features (plannerservice,
│ PromptTemplateGenerationsInvocable, etc.)
│
├── GenAIGatewayResponse (generationRequestId__c = gatewayRequestId__c)
├── GenAIGatewayRequestTag (parent__c = gatewayRequestId__c)
├── GenAIGtwyObjRecord ▸ (parent__c = gatewayRequestId__c
│ when type__c = grounded record attachment;
│ populated only for grounding features —
│ planner-only sessions produce 0 rows)
├── GenAIGtwyRequestMetadata (parent__c = gatewayRequestId__c;
│ typed metadata rows, e.g. ToolCall payloads)
└── ⚠ GenAIGtwyRequestLLM (parent__c = gatewayRequestId__c;
per-call LLM diagnostics; schema provisioned
but writer inactive on every sandbox observed
— treat as aspirational)How to "join all 24" from one session id
All 24 entries below are in the scripts/fetch_dc.py waterfall — the join expressions are exactly what the script runs, and each one has a .sql template under assets/dc/.
1. sessions WHERE ssot__Id__c = {sid}
2. interactions WHERE ssot__AiAgentSessionId__c = {sid}
3. messages WHERE ssot__AiAgentSessionId__c = {sid}
4. moments WHERE ssot__AiAgentSessionId__c = {sid}
5. participants WHERE ssot__AiAgentSessionId__c = {sid}
6. tag_associations WHERE ssot__AiAgentSessionId__c = {sid}
7. gateway_requests WHERE sessionId__c LIKE '%{sid}%'
— sessionId__c is stored quoted (e.g. '"<sid>"');
raw-UUID exact match returns 0. Equivalent
exact form: sessionId__c = '"{sid}"'
8. steps WHERE ssot__AiAgentInteractionId__c IN {interaction_ids}
9. moment_interactions WHERE ssot__AiAgentInteractionId__c IN {interaction_ids}
10. telemetry_spans WHERE ssot__TelemetryTrace__c IN {trace_ids_from_AttributeText}
11. generations WHERE generationId__c IN {step.ssot__GenerationId__c values}
12. gateway_request_tags WHERE parent__c IN {gateway_request_ids}
13. gateway_responses WHERE generationRequestId__c IN {gateway_request_ids}
14. gateway_records (→ GenAIGtwyObjRecord__dlm) WHERE parent__c IN {gateway_request_ids}
15. feedback WHERE generationId__c IN {generation_ids}
16. content_quality WHERE parent__c IN {generation_ids}
17. content_category WHERE parent__c IN ({generation_ids} ∪ {content_quality.id__c})
18. feedback_details WHERE parent__c IN {feedback.feedbackId__c}
19. tag_definitions WHERE ssot__Status__c = 'Available'
— not keyed by session (tag vocabulary)
20. tag_definition_associations WHERE ssot__AiAgentApiName__c IN
{agent_api_names from Participant(role='AGENT') ∪ Moment}
— keyed by agent, not session. Participant is primary
(Moment rows may be absent without Agent Optimization)
21. tags WHERE ssot__AiAgentTagDefinitionId__c IN
{tag_definition.ssot__Id__c}
— tag VALUES; session reaches them via TagAssociation
22. app_generation WHERE generationId__c IN {step.ssot__GenerationId__c values}
— app-layer twin of GenAIGeneration
23. gateway_request_metadata WHERE parent__c IN {gateway_request_ids}
— verified live: parent__c → GatewayRequest.gatewayRequestId__c
24. gateway_request_llm WHERE parent__c IN {gateway_request_ids}
— same parent pattern as Metadata; writer inactive on
observed sandboxes (0 rows expected until enabled)---
DMO materialization timing
Data Cloud DMOs don't all appear at the same time. Gateway DMOs materialize within minutes; STDM Interaction/Step/Message DMOs can take hours to days.
| DMO | Table | Materializes | Notes |
|---|---|---|---|
| Session | ssot__AIAgentSession__dlm | Minutes | Always query first |
| Participant | ssot__AiAgentSessionParticipant__dlm | Minutes | Fast |
| GatewayRequest | GenAIGatewayRequest__dlm | Minutes | Direct FK via sessionId__c LIKE |
| GatewayResponse | GenAIGatewayResponse__dlm | Minutes | Joins via generationRequestId__c |
| GatewayRequestTag | GenAIGatewayRequestTag__dlm | Minutes | parent__c = gatewayRequestId__c |
| GtwyRequestMetadata | GenAIGtwyRequestMetadata__dlm | Minutes | parent__c = gatewayRequestId__c |
| Moment | ssot__AiAgentMoment__dlm | Hours–days | Often empty on same-day |
| Interaction | ssot__AIAgentInteraction__dlm | Hours–days | Do NOT use as query anchor for fresh sessions — fetch_dc.py classifies this as session_shape=interactions_not_materialized_yet and renders the gateway-direct view |
| Message | ssot__AiAgentSessionMessage__dlm | Hours–days | Downstream of Interaction |
| Step | ssot__AIAgentInteractionStep__dlm | Hours–days | Downstream of Interaction |
| Generation | GenAIGeneration__dlm | Hours–days | Downstream of Step |
| GtwyRequestLLM | GenAIGtwyRequestLLM__dlm | N/A | Writer inactive on sandboxes |
---
Session Tracing DMOs (5)
ssot__AIAgentSession__dlm — one row per session
| Field | Type | Notes |
|---|---|---|
ssot__Id__c | string (PK) | Session UUID (what users pass in) |
ssot__StartTimestamp__c | string (ISO UTC) | |
ssot__EndTimestamp__c | string (ISO UTC) | null while active |
ssot__AiAgentSessionEndType__c | string | Completed / Abandoned / Escalated / etc. |
ssot__AiAgentChannelType__c | string | e.g. "SCRT2 - EmbeddedMessaging", "Voice" |
ssot__RelatedMessagingSessionId__c | string | |
ssot__RelatedVoiceCallId__c | string | |
ssot__InternalOrganizationId__c | string | 18-char org id |
ssot__SessionOwnerId__c | string | |
ssot__SessionOwnerObject__c | string | Owner type, e.g. "User" |
ssot__IndividualId__c | string | Data 360 individual id |
ssot__PreviousSessionId__c | string | Conversation-chain link to prior session |
ssot__VariableText__c | string | Session-level variables, JSON. Channel-specific bootstrap dict (e.g. __resolved_locale__, __supports_result_display__, __user_dst_offset_ms__). Parsed at assemble time into session.identity.bootstrap_variables. The presence of Builder-Previewer-only keys (__supports_result_display__ etc.) is one input to the derived session.identity.mode field — see dc_pipeline_contract.md §2.9a. |
Note: agent API name is NOT on Session. Join with Moment to get agent info.
ssot__AiAgentSessionParticipant__dlm — one row per participant per session
Roles: USER, AGENT.
| Field | Type | Notes |
|---|---|---|
ssot__Id__c | string (PK) | |
ssot__AiAgentSessionId__c | string (FK) | → Session.Id |
ssot__ParticipantId__c | string | MessagingEndUser id (USER) or GenAiPlannerDefinition id (AGENT) |
ssot__AiAgentApiName__c | string | Bot identity — omit on USER rows |
ssot__AiAgentType__c | string | e.g. "DemoAgentType" |
ssot__AiAgentTemplateApiName__c | string | |
ssot__AiAgentVersionApiName__c | string | e.g. "v5" |
ssot__AiAgentSessionParticipantRole__c | string | USER \ |
ssot__ParticipantObject__c | string | e.g. "MessagingEndUser", "GenAiPlannerDefinition" |
ssot__StartTimestamp__c | string | |
ssot__EndTimestamp__c | string | |
ssot__IndividualId__c | string | Data 360 individual id |
ssot__InternalOrganizationId__c | string | 18-char org id |
ssot__ParticipantAttributeText__c | string | JSON — per-participant metadata |
ssot__AIAgentInteraction__dlm — one row per turn (and session-end event)
Types: TURN, SESSION_END.
| Field | Type | Notes |
|---|---|---|
ssot__Id__c | string (PK) | Turn/interaction UUID |
ssot__AiAgentSessionId__c | string (FK) | → Session.Id |
ssot__AiAgentInteractionType__c | string | TURN \ |
ssot__TopicApiName__c | string | Which topic handled this turn |
ssot__StartTimestamp__c | string | |
ssot__EndTimestamp__c | string | |
ssot__PrevInteractionId__c | string | |
ssot__SessionOwnerId__c | string | |
ssot__IndividualId__c | string | |
ssot__InternalOrganizationId__c | string | |
ssot__TelemetryTraceId__c | string | Often empty on real orgs (verified live). See note below. |
ssot__TelemetryTraceSpanId__c | string | Often empty — same caveat as above. |
ssot__AttributeText__c | string | HTML-escaped JSON. Holds internalTraceId + internalSpanId — the real runtime trace_id when TelemetryTraceId__c is empty. Consumers: html.unescape() + regex "internalTraceId":"([a-f0-9]+)". |
ssot__AiAgentInteractionMessage__dlm — one row per user/agent message
Types: Input, Output.
| Field | Type | Notes |
|---|---|---|
ssot__Id__c | string (PK) | |
ssot__AiAgentSessionId__c | string (FK) | → Session.Id — direct session FK (verified v66.0) |
ssot__AiAgentInteractionId__c | string (FK) | → Interaction.Id |
ssot__AiAgentSessionParticipantId__c | string (FK) | → Participant.Id — who sent/received the message |
ssot__ParentMessageId__c | string | Threading — prior message in a nested conversation |
ssot__ContentText__c | string | The actual message text |
ssot__AiAgentInteractionMessageType__c | string | Input \ |
ssot__AiAgentInteractionMsgContentType__c | string | Content MIME/type (text/audio/etc.) |
Modality__c | string | e.g. Text, Voice — no ssot__ prefix |
ssot__MessageSentTimestamp__c | string | Single-point timestamp (text channels) |
MessageStartTimestamp__c | datetime | Start of message (voice/streaming) — no ssot__ prefix |
MessageEndTimestamp__c | datetime | End of message (voice/streaming) — no ssot__ prefix |
ssot__InternalOrganizationId__c | string | 18-char org id |
Messages have a direct session FK (ssot__AiAgentSessionId__c) — scope by session directly; earlier docs claiming otherwise were wrong. The interaction FK is still useful for per-turn joins.
ssot__AIAgentInteractionStep__dlm — one row per planner step
Types observed in live data: LLM_STEP, ACTION_STEP, TOPIC_STEP, TRUST_GUARDRAILS_STEP, SESSION_END.
| Field | Type | Notes |
|---|---|---|
ssot__Id__c | string (PK) | |
ssot__AiAgentInteractionId__c | string (FK) | → Interaction.Id |
ssot__AiAgentInteractionStepType__c | string | LLM_STEP \ |
ssot__Name__c | string | step/action name (e.g. "AiCopilot__ReactTopicPrompt") |
ssot__InputValueText__c | string | HTML-escaped JSON; parse after html.unescape |
ssot__OutputValueText__c | string | HTML-escaped JSON; parse after html.unescape |
ssot__PreStepVariableText__c | string | |
ssot__PostStepVariableText__c | string | |
ssot__GenerationId__c | string | → GenAIGeneration.generationId__c. Populated only on LLM_STEP rows (in live samples, most LLM_STEP rows populate it); NOT_SET on every other step type. This is the only Step→Generation join key used by the waterfall. |
ssot__ErrorMessageText__c | string | Sentinel NOT_SET when no error (never NULL). Filter errors with != 'NOT_SET', not IS NOT NULL. |
ssot__StartTimestamp__c | string | |
ssot__EndTimestamp__c | string | |
ssot__PrevStepId__c | string | |
ssot__InternalOrganizationId__c | string | |
ssot__TelemetryTraceSpanId__c | string | |
ssot__AttributeText__c | string | |
ssot__GenAiGatewayRequestId__c | string | Schema-only FK. Live data shows this is frequently NOT_SET even on LLM_STEP rows. Not used by the waterfall — GatewayRequest is fetched forward from Session via sessionId__c, which is the authoritative set and covers requests this FK doesn't reach. |
ssot__GenAiGatewayResponseId__c | string | Schema-only FK. Same as above — documented for schema completeness, not used as a join key. Following it back to GatewayResponse is a backward-reasoning pattern and rejected by the skill design. |
Join: steps lack a direct session FK — go through Interaction.
Step Gateway FKs are not join keys. The three Gateway-related FK columns (ssot__GenAiGatewayRequestId__c, ssot__GenAiGatewayResponseId__c) are included in the SELECT for schema completeness but are not used to reach any downstream DMO. GatewayRequest is entered forward from Session via GatewayRequest.sessionId__c (see gateway_requests section); its children flow forward from there. Only ssot__GenerationId__c is used as a forward Step → Generation key.
---
Generative AI Audit and Feedback DMOs (12 of 13 documented)
Canonical umbrella per the Salesforce Help article Data Model for Generative AI Audit and Feedback. Spans the full Einstein generative AI audit chain: LLM requests and responses at the gateway, trust-layer safety scoring, and user-side feedback on generations.
No `ssot__` prefix. Fields end in __c directly.
Data 360 Data Lake Objects (DLOs) that contain generative AI audit and feedback data map to custom DMOs (legacy) and standard DMOs. The 13 DMOs in this family per the Help article:
| DLO | Custom DMO (legacy) | Standard DMO | Queried by fetch_dc.py? |
|---|---|---|---|
| GenAIAppGeneration | GenAIAppGeneration__dlm | Ai Response App Generation | ✓ |
| GenAIContentCategory | GenAIContentCategory__dlm | Ai Content Quality Category | ✓ |
| GenAIContentQuality | GenAIContentQuality__dlm | Ai Content Quality | ✓ |
| GenAIFeedback | GenAIFeedback__dlm | Ai Feedback | ✓ |
| GenAIFeedbackDetail | GenAIFeedbackDetail__dlm | Ai Feedback Additional Info | ✓ |
| GenAIGatewayRequest | GenAIGatewayRequest__dlm | Ai Gateway Request | ✓ |
| GenAIGatewayRequestTag | GenAIGatewayRequestTag__dlm | Ai Gateway Request Tag | ✓ |
| GenAIGatewayResponse | GenAIGatewayResponse__dlm | Ai Gateway Response | ✓ |
| GenAIGeneration | GenAIGeneration__dlm | Ai Response Generation | ✓ |
| GenAIGtwyRequestMetadata | GenAIGtwyRequestMetadata__dlm | Ai Gateway Req Additional Info | ✓ |
| GenAIGtwyRequestLLM | GenAIGtwyRequestLLM__dlm | Ai Gateway Request Model Diagnostic | ✓ |
| GenAIGtwyObjRecord | GenAIGtwyObjRecord__dlm | Ai Gateway Request Object Record | ✓ |
The Help article lists one additional DMO in this family — GenAIGtwyObjRecCitation__dlm (standard DMO: Ai Gateway Req Object Record Citation). It is not documented here: live describe returns "DMO with developerName 'GenAIGtwyObjRecCitation' not found" on the tested org, so we have no verified schema or join column. Bring it in once it's provisioned on a test org.
All 12 DMOs marked ✓ above are queried by the 24-query fetch_dc.py waterfall. Their .sql templates are under assets/dc/ and their full schemas are documented in the per-DMO sections below.
GenAIGeneration__dlm — one row per LLM call at the gateway
Joining to a session: this DMO has NO sessionId__c, traceId__c, or turnId__c column (verified via live describe; 11 fields total, none reference session/trace/turn). The canonical join path is:
Session → Interaction → Step (.ssot__GenerationId__c) → Generation (.generationId__c)Pull session steps, collect non-empty ssot__GenerationId__c values (many steps have NOT_SET), then filter by generationId__c IN (...).
There is no backward chain to this DMO from GatewayRequest/Response. The waterfall only reaches GenAIGeneration__dlm via the forward Step→Generation path above; Generation rows for LLM calls that aren't owned by an LLM_STEP are not fetched by this skill.
| Field | Type | Notes |
|---|---|---|
generationId__c | string (PK) | Forward join key from Step.ssot__GenerationId__c. |
generationResponseId__c | string | Provider-issued response id (OpenAI-style chatcmpl-*, Gemini/Anthropic use their own prefixes). Not used as a join key. |
responseText__c | string | HTML-escaped JSON for tool-calling outputs; html.unescape() before parsing. |
maskedResponseText__c | string | PII-masked version |
responseParameters__c | string | JSON |
feature__c | string | e.g. "plannerservice", "Guardrails and Citations" |
timestamp__c | string | |
orgId__c | string | |
cloud__c | string | e.g. "Platform" |
GenAIContentQuality__dlm — per-generation quality row
Joined to a generation via parent__c = generationId__c.
| Field | Type | Notes |
|---|---|---|
id__c | string (PK) | |
parent__c | string (FK) | → GenAIGeneration.generationId__c |
isToxicityDetected__c | string | "true" / "false" — populated only on OUTPUT rows |
contentType__c | string | INPUT \ |
feature__c | string | |
timestamp__c | string | |
orgId__c | string | |
cloud__c | string |
GenAIContentCategory__dlm — per-category detector row
Joined either to a generation (direct, non-TOXICITY detectors like InstructionAdherence) or to a quality row (TOXICITY sub-categories). The parent__c FK points to whichever parent emitted it.
| Field | Type | Notes |
|---|---|---|
id__c | string (PK) | |
parent__c | string (FK) | → GenAIGeneration.generationId__c OR GenAIContentQuality.id__c |
detectorType__c | string | TOXICITY \ |
category__c | string | e.g. "violence", "safety_score", "Low" / "Medium" / "High" |
value__c | string | "0.0" – "1.0" as string; convert to float in analysis |
timestamp__c | string | |
orgId__c | string | |
cloud__c | string |
GenAIGatewayRequest__dlm — one row per LLM request at the gateway
Richer than GenAIGeneration — carries prompt text, tokens, model, and session/user/bot identifiers. Forward entry point for the entire audit chain, reached from Session via sessionId__c:
Session.ssot__Id__c → GatewayRequest.sessionId__c LIKE '%<sid>%'sessionId__c is stored as a literal quoted string (e.g. "<uuid>"), so a raw-UUID exact match returns 0 rows; use LIKE or sessionId__c = '"<sid>"'. See assets/dc/gateway_requests.sql for details.
GatewayRequest is the parent for all downstream audit DMOs — Response, RequestTag, GtwyObjRecord, RequestMetadata, RequestLLM — each keyed by generationRequestId__c or parent__c = gatewayRequestId__c.
| Field | Type | Notes |
|---|---|---|
gatewayRequestId__c | string (PK) | |
generationGroupId__c | string | Groups multiple generations for one user-visible turn |
sessionId__c | string | Session FK — the forward entry point. Stored as a quoted string (e.g. "<uuid>") or sentinel "no_session". Query with sessionId__c LIKE '%<sid>%' or exact sessionId__c = '"<sid>"'; raw-UUID match returns 0 rows. |
userId__c | string | End-user id |
botVersionId__c | string | Agent version id |
plannerId__c | string | Planner id (ReAct etc.) |
feature__c | string | e.g. CopilotForDigitalChannels |
appType__c | string | |
model__c | string | e.g. gpt-4o-2024-11-20 |
provider__c | string | e.g. openai, azureOpenAI, salesforce |
promptTemplateDevName__c | string | e.g. AiCopilot__ReactTopicPrompt, Atlas__AgentGraphReasoningPrompt. Surfaced through the hierarchical view as gateway_request.prompt_template_dev_name. |
promptTemplateVersionNo__c | string | |
prompt__c | string | Full input prompt sent to the model — role: system / role: user / role: assistant segments, tool definitions, conversation history. Up to 30 KB+ on observed sessions. Carried through the hierarchical view as gateway_request.prompt_text; surfaced verbatim by render_dc.py --show-prompts (off by default; per-prompt display capped at 64 KB). HTML-escaped on the wire (" etc.) — renderer unescapes before display. |
maskedPrompt__c | string | PII-masked variant of prompt__c. Empty when enablePiiMasking__c = "false"; populated when masking is enabled. |
parameters__c | string | JSON — extra invocation params |
temperature__c | number | |
frequencyPenalty__c | number | |
presencePenalty__c | number | |
stopSequences__c | string | |
numGenerations__c | number | |
promptTokens__c | number | |
completionTokens__c | number | |
totalTokens__c | number | |
enableInputSafetyScoring__c | string | "true" / "false" |
enableOutputSafetyScoring__c | string | "true" / "false" |
enablePiiMasking__c | string | "true" / "false" |
timestamp__c | datetime | |
orgId__c | string | |
cloud__c | string |
GenAIGatewayResponse__dlm — one row per LLM call response
Forward join: from GatewayRequest via generationRequestId__c IN {gw_req_ids}. Every GatewayRequest has one Response (modulo in-flight calls at fetch time) — 1:1 invariant verified live.
| Field | Type | Notes |
|---|---|---|
generationResponseId__c | string (PK) | Provider-issued response id (e.g. OpenAI chatcmpl-*). Same value also appears on Step.ssot__GenAiGatewayResponseId__c and Generation.generationResponseId__c, but those are not used as join keys. |
generationRequestId__c | string (FK) | → GatewayRequest.gatewayRequestId__c. The forward join key. |
parameters__c | string | JSON |
timestamp__c | datetime | |
orgId__c | string | |
cloud__c | string |
GenAIGatewayRequestTag__dlm — k/v tags per request
Joined via parent__c = GatewayRequest.gatewayRequestId__c. Multiple rows per request.
| Field | Type | Notes |
|---|---|---|
id__c | string (PK) | |
parent__c | string (FK) | → GatewayRequest.gatewayRequestId__c |
tag__c | string | e.g. prompt_template_dev_name, user_utterance |
tagValue__c | string | |
timestamp__c | datetime | |
orgId__c | string | |
cloud__c | string |
GenAIFeedback__dlm — user thumbs up/down on a generation
Joined via generationId__c = Generation.generationId__c. PK feedbackId__c is the parent for GenAIFeedbackDetail rows.
| Field | Type | Notes |
|---|---|---|
feedbackId__c | string (PK) | |
generationId__c | string (FK) | → Generation.generationId__c |
generationUpdateId__c | string | |
generationGroupId__c | string | |
userId__c | string | |
feedback__c | string | e.g. UP, DOWN |
action__c | string | Richer action, e.g. REGENERATE, COPY |
source__c | string | Channel/app that captured the feedback |
feature__c | string | |
appType__c | string | |
timestamp__c | datetime | |
orgId__c | string | |
cloud__c | string |
GenAIFeedbackDetail__dlm — free-text / structured detail per feedback
Joined via parent__c = Feedback.feedbackId__c. Zero or more rows per feedback.
| Field | Type | Notes |
|---|---|---|
feedbackDetailId__c | string (PK) | |
parent__c | string (FK) | → Feedback.feedbackId__c |
feedbackText__c | string | Free-text user input |
appFeedback__c | string | App-layer reason bucket |
feature__c | string | |
timestamp__c | datetime | |
orgId__c | string | |
cloud__c | string |
GenAIGtwyObjRecord__dlm — grounded record attachments on gateway requests
Polymorphic. parent__c is the id of whatever owns this record (GatewayRequest for grounded-content attachments, GenAIFeedback for feedback attachments, etc.) and type__c names the DMO the attached record lives in (e.g. ssot__KnowledgeArticleVersion__dlm).
Populated only on planners that perform grounded retrieval. Join path is clean and forward (Session → GatewayRequest → GtwyObjRecord), but the child table is planner-dependent — a planner that doesn't attach grounded records produces zero rows even when the session is fully traced. Verified live:
- Tool-calling / planner-style Agentforce sessions (request `feature__c =
'plannerservice'` only): 0 rows across all observed sessions. The planner doesn't emit grounded-record attachments.
- Sessions whose requests include
PromptTemplateGenerationsInvocableor
PromptBuilderPreview features do produce rows — e.g. one live turn produced a type__c = ssot__KnowledgeArticleVersion__dlm attachment via the forward chain.
If fetch returns 0 for an Agentforce agent session, first check the GatewayRequest rows' plannerId__c and feature__c — a planner that only shows plannerservice feature will not produce child records. This DMO is also org-gated: across a set of observed sandboxes with ~400K total GatewayRequest rows, only a minority had any GtwyObjRecord rows at all.
| Field | Type | Notes |
|---|---|---|
id__c | string (PK) | |
parent__c | string (FK) | → GatewayRequest.gatewayRequestId__c (forward from session) or GenAIFeedback.feedbackId__c (feedback attachment) |
recordId__c | string | Attached record's id in its home DMO |
type__c | string | DMO the record lives in, e.g. ssot__KnowledgeArticleVersion__dlm |
name__c | string | |
value__c | string | Often a deep-link URL to the record in the org |
metadata__c | string | JSON |
feature__c | string | |
timestamp__c | datetime | |
orgId__c | string | |
cloud__c | string |
The 3 DMOs below are wired into the fetch_dc.py waterfall as entries 22–24 with matching .sql templates under assets/dc/ (app_generation, gateway_request_metadata, gateway_request_llm). Field schemas below are from live describe. Population varies by DMO:
GenAIAppGeneration__dlm— provisioned but not populated on observed orgs.GenAIGtwyRequestMetadata__dlm— populated whenever the session's
GatewayRequest rows are populated; one row per request on Agentforce sessions (e.g. ToolCall payloads for plannerservice).
GenAIGtwyRequestLLM__dlm— 0 rows on every sandbox observed
(multiple orgs checked; total GatewayRequest rows ~400K, total GtwyRequestLLM rows 0). Schema exists, writer appears inactive.
GenAIAppGeneration__dlm — app-layer generation record
Standard DMO: Ai Response App Generation. Sibling of GenAIGeneration__dlm: where GenAIGeneration is the raw gateway response, GenAIAppGeneration appears to be the app/feature-layer view of the same generation (separate id__c from generationId__c).
Executable query: assets/dc/app_generation.sql.
Join path: generationId__c → GenAIGeneration.generationId__c → AiAgentInteractionStep.ssot__GenerationId__c → Interaction → Session. Same Step → Generation bridge used for GenAIGeneration__dlm.
| Field | Type | Notes |
|---|---|---|
id__c | string (PK) | App-generation row id — distinct from generationId__c |
generationId__c | string (FK) | → GenAIGeneration.generationId__c (session join path) |
generationUpdate__c | string | Likely a later revision of the generation |
generationUpdateId__c | string | id of the update row |
feature__c | string | |
timestamp__c | datetime | |
orgId__c | string | |
cloud__c | string |
GenAIGtwyRequestMetadata__dlm — additional per-request metadata
Standard DMO: Ai Gateway Req Additional Info. Child of GenAIGatewayRequest__dlm: holds typed metadata rows for a request. On live data a sampled row showed metadataType__c = "ToolCall" and feature__c = "plannerservice", so it's where tool-call/planner-side per-request metadata lives.
Executable query: assets/dc/gateway_request_metadata.sql.
Join verified live: parent__c → GatewayRequest.gatewayRequestId__c. A sampled parent__c value matched exactly one row in GenAIGatewayRequest__dlm WHERE gatewayRequestId__c = ….
| Field | Type | Notes |
|---|---|---|
id__c | string (PK) | |
parent__c | string (FK) | → GatewayRequest.gatewayRequestId__c (verified live) |
metadataType__c | string | Observed: ToolCall; other values likely |
metadata__c | string | JSON — the actual metadata payload |
feature__c | string | Observed: plannerservice |
timestamp__c | datetime | |
orgId__c | string | |
cloud__c | string |
GenAIGtwyRequestLLM__dlm — per-request LLM call diagnostics
Standard DMO: Ai Gateway Request Model Diagnostic. Child of GenAIGatewayRequest__dlm: captures LLM invocation diagnostics (latency, status, endpoint, region).
Cross-org reality check: 0 rows on every sandbox observed. Checked multiple sandboxes with a combined ~400K GatewayRequest rows — every one returned 0 GtwyRequestLLM rows. The schema is provisioned everywhere, but the writer appears inactive. Treat as aspirational until the feature is enabled and populated somewhere we can verify. The join path is declared (by analogy with GenAIGtwyRequestMetadata) but not live-verified.
Executable query: assets/dc/gateway_request_llm.sql.
Join: parent__c → GatewayRequest.gatewayRequestId__c (inferred from schema symmetry with GenAIGtwyRequestMetadata; not verifiable until an org populates the table).
| Field | Type | Notes |
|---|---|---|
id__c | string (PK) | |
parent__c | string (FK) | → GatewayRequest.gatewayRequestId__c (by analogy) |
endpoint__c | string | LLM endpoint URL / name |
region__c | string | Cloud region the call ran in |
genAILLM__c | string | Model/LLM identifier |
llmCallStatus__c | string | e.g. success / error |
llmCallLatency__c | number | Latency of the LLM call |
llmErrorTrace__c | string | Error trace when the call failed |
metadata__c | string | JSON — extra diagnostic info |
feature__c | string | |
salesforceOrgId__c | string | Note: salesforceOrgId__c, not orgId__c |
timestamp__c | datetime | |
cloud__c | string |
---
Agent Optimization DMOs (6)
Provisioned only when Agent Optimization is enabled (Enterprise/Performance/ Unlimited + an Einstein add-on). Extends STDM with moment clustering and LLM-driven tag annotations. See: https://help.salesforce.com/s/articleView?id=ai.generative_ai_optimize_data_model.htm&type=5
ssot__AiAgentMoment__dlm — session-level rollup
Carries agent identity.
| Field | Type | Notes |
|---|---|---|
ssot__Id__c | string (PK) | |
ssot__AiAgentSessionId__c | string (FK) | → Session.Id |
ssot__AiAgentApiName__c | string | e.g. "MyAgent" |
ssot__AiAgentVersionApiName__c | string | e.g. "v5" — observed NOT_SET in some live samples |
ssot__RequestSummaryText__c | string | |
ssot__ResponseSummaryText__c | string | |
ssot__StartTimestamp__c | string | |
ssot__EndTimestamp__c | string | |
ssot__InternalOrganizationId__c | string |
ssot__AiAgentMomentInteraction__dlm — junction: moment ↔ interactions
| Field | Type | Notes |
|---|---|---|
ssot__Id__c | string (PK) | |
ssot__AiAgentMomentId__c | string (FK) | → Moment.Id |
ssot__AiAgentInteractionId__c | string (FK) | → Interaction.Id |
ssot__StartTimestamp__c | datetime | |
ssot__InternalOrganizationId__c | string |
ssot__AiAgentTagDefinition__dlm — tag schema / vocabulary
| Field | Type | Notes |
|---|---|---|
ssot__Id__c | string (PK) | |
ssot__DeveloperName__c | string | |
ssot__Name__c | string | |
ssot__Description__c | string | |
ssot__TagIdentifier__c | string | |
ssot__DataType__c | string | |
ssot__Status__c | string | Live enum observed: Available (verified via live describe; all sampled rows used this value). Help docs say Active/Inactive but no 'Active' rows exist on tested orgs — query by 'Available' or omit filter. |
ssot__VersionNumber__c | number | |
ssot__EngineType__c | string | LLM engine that applies the tag |
ssot__SourceType__c | string | |
ssot__SourceTagReferenceName__c | string | |
ssot__InputScope__c | string | |
ssot__CreatedDate__c | datetime | |
ssot__InternalOrganizationId__c | string |
ssot__AiAgentTagDefinitionAssociation__dlm — definition ↔ agent binding
| Field | Type | Notes |
|---|---|---|
ssot__Id__c | string (PK) | |
ssot__AiAgentTagDefinitionId__c | string (FK) | → TagDefinition.Id |
ssot__AiAgentApiName__c | string | Agent API name this definition binds to |
ssot__AiPromptTemplateId__c | string | |
ssot__IsActive__c | boolean | |
ssot__CreatedDate__c | datetime | |
ssot__InternalOrganizationId__c | string |
ssot__AiAgentTag__dlm — tag instance (a specific value under a definition)
| Field | Type | Notes |
|---|---|---|
ssot__Id__c | string (PK) | |
ssot__AiAgentTagDefinitionId__c | string (FK) | → TagDefinition.Id |
ssot__Value__c | string | Tag value text |
ssot__Description__c | string | |
ssot__OrderNumber__c | number | Display order |
ssot__IsActive__c | boolean | |
ssot__IsFallback__c | boolean | True when this tag is the fallback for the definition |
ssot__CreatedDate__c | datetime | |
ssot__InternalOrganizationId__c | string |
ssot__AiAgentTagAssociation__dlm — applied annotation
One row per tag applied to a target. Target is a moment, session, or interaction — only one of those FKs is populated per row.
| Field | Type | Notes |
|---|---|---|
ssot__Id__c | string (PK) | |
ssot__AiAgentTagId__c | string (FK) | → AiAgentTag.Id — the tag value |
ssot__AiAgentTagDefinitionAssociationId__c | string (FK) | → TagDefinitionAssociation.Id |
ssot__AiAgentMomentId__c | string (FK) | → Moment.Id — populated when target is a moment |
ssot__AiAgentSessionId__c | string (FK) | → Session.Id — populated when target is a session |
ssot__AiAgentInteractionId__c | string (FK) | → Interaction.Id — populated when target is a turn |
ssot__IsPassed__c | boolean | Pass/fail for evaluation-style tags |
ssot__AssociationReasonText__c | string | LLM rationale for applying this tag |
ssot__CreatedDate__c | datetime | |
ssot__InternalOrganizationId__c | string |
---
Agent Platform Tracing DMO (1)
Provisioned only when Agent Platform Tracing is enabled (Setup → Einstein Audit, Analytics, and Monitoring Setup → Agent Platform Tracing toggle; requires Agentforce Session Tracing already on). Captures OpenTelemetry-style spans from Apex, Flows, Prompt Builder, Invocable Actions, Planner, AI Gateway, LLM Gateway, and DC Query Federator. Data collection runs every 5 minutes.
Joined to STDM via ssot__TelemetryTrace__c = Interaction.ssot__TelemetryTraceId__c.
ssot__TelemetryTraceSpan__dlm — one row per span
| Field | Type | Notes |
|---|---|---|
ssot__Id__c | string (PK) | Span id |
ssot__TelemetryTrace__c | string (FK) | → Interaction.ssot__TelemetryTraceId__c |
ssot__TelemetryParentSpanId__c | string | Parent span id (within the same trace); null on root |
ssot__OperationName__c | string | e.g. run.interaction, run.llmstep, run.action.<name>, run.invokeActions.<name>, run.hybridsearch.<index> |
ssot__ServiceName__c | string | e.g. Atlas Reasoning Engine, InvocableAction, PromptTemplate, Einstein AI Gateway, Data Cloud |
ssot__SpanKind__c | string | OpenTelemetry span kind |
ssot__StatusCode__c | string | Execution result |
ssot__StartDateTime__c | datetime | Span start |
ssot__EndDateTime__c | datetime | Span end |
ssot__DurationNumber__c | number | Duration in nanoseconds |
ssot__TelemetrySpanAttributeText__c | string | JSON key/value span attributes (e.g. prompt_template.api.name, retriever.retrievername) |
ssot__InternalOrganizationId__c | string |
---
Known enum values (live-API verified)
| DMO | Field | Values |
|---|---|---|
| Session | AiAgentChannelType__c | E & O, Builder, SCRT2 - EmbeddedMessaging, Voice, NGC |
| Participant | AiAgentType__c | DemoAgentType, AgentforceEmployeeAgent, AgentforceMyAgent |
| Participant | AiAgentSessionParticipantRole__c | USER, AGENT |
| Interaction | AiAgentInteractionType__c | TURN, SESSION_END |
| Message | AiAgentInteractionMessageType__c | Input, Output |
| Step | AiAgentInteractionStepType__c | LLM_STEP, ACTION_STEP, TOPIC_STEP, TRUST_GUARDRAILS_STEP, SESSION_END (verified live) |
| ContentCategory | detectorType__c | TOXICITY, PII, PROMPT_DEFENSE, InstructionAdherence, TaskResolution |
---
Tooling
DMO queries are executable; one CLI wrapper ships with the skill for common operator needs:
| Script | Use case | Args |
|---|---|---|
scripts/discover_sessions.py | Find sessions by time / agent / channel / outcome / grep; newest-first picker. | --org, optional --since / --agent / --channel / --outcome / --grep / --tz / --limit |
# Shared module for the investigating-agentforce-d360 skill.
# Three siblings live in this package: paths, fs_guard, sql.
"""Shared CLI override helper for d360 entry scripts.
D360 has 5 independent entry scripts (``fetch_dc.py``, ``assemble_dc.py``,
``render_dc.py``, ``resolve_session.py``, ``discover_sessions.py``) — each
needs the same ``--data-dir`` / ``--cache-dir`` flags and the same
3-level namespace rebind. This module factors that duplication into one
helper.
Usage in each entry script::
from _shared.cli_override import add_cli_flags, apply_overrides
parser = argparse.ArgumentParser(...)
parser.add_argument(...)
add_cli_flags(parser)
args = parser.parse_args()
apply_overrides(args, caller_globals=globals())
The ``caller_globals=globals()`` parameter is the magic that lets us
rebind the entry script's own ``DATA_ROOT`` / ``CACHE_ROOT`` snapshot —
without it, only ``paths.X`` and ``config.X`` would update, and the
entry script's local ``DATA_ROOT`` would still point at the default.
"""
from __future__ import annotations
import argparse
from pathlib import Path
_SKILL_NAME = "investigating-agentforce-d360"
def add_cli_flags(parser: argparse.ArgumentParser) -> None:
"""Add --data-dir / --cache-dir flags to the given parser.
Defaults are runtime-agnostic (``~/.vibe/...``); other
runtimes (AFV OOTB, Codex, Cursor, OpenCode) override at invocation
time.
"""
parser.add_argument(
"--data-dir",
type=Path,
default=None,
help=(
"Override data root (default: "
"~/.vibe/data/investigating-agentforce-d360)."
),
)
parser.add_argument(
"--cache-dir",
type=Path,
default=None,
help=(
"Override cache root (default: "
"~/.vibe/cache/investigating-agentforce-d360)."
),
)
def apply_overrides(args: argparse.Namespace, caller_globals: dict) -> None:
"""Apply --data-dir / --cache-dir overrides across all 3 namespace levels.
Must be called BEFORE any pipeline code reads ``DATA_ROOT`` or
``CACHE_ROOT``.
Levels rebound:
1. ``paths.DATA_ROOT`` (the source of truth)
2. ``config.DATA_ROOT`` (the re-export)
3. The caller's own local ``DATA_ROOT`` (via ``caller_globals``)
Why 3 levels? Python's ``from X import Y`` snapshots ``Y`` into the
caller's local namespace at import time. Mutating ``X.Y`` later does
NOT update the caller's local binding. Without the 3rd-level rebind,
any function in the caller that references ``DATA_ROOT`` still sees
the default path even after ``paths.DATA_ROOT`` was overridden.
"""
if not (args.data_dir or args.cache_dir):
return
from _shared import runtime, paths
import config
if args.data_dir:
runtime.set_data_root_override(args.data_dir)
new_data = runtime.resolve_data_root(_SKILL_NAME)
paths.DATA_ROOT = new_data
config.DATA_ROOT = new_data
# Rebind caller's local snapshot, if it captured DATA_ROOT.
if "DATA_ROOT" in caller_globals:
caller_globals["DATA_ROOT"] = new_data
if args.cache_dir:
runtime.set_cache_root_override(args.cache_dir)
new_cache = runtime.resolve_cache_root(_SKILL_NAME)
paths.CACHE_ROOT = new_cache
if hasattr(config, "CACHE_ROOT"):
config.CACHE_ROOT = new_cache
if "CACHE_ROOT" in caller_globals:
caller_globals["CACHE_ROOT"] = new_cache
#!/usr/bin/env python3
r"""Filesystem-safety and input-validation guard.
One script, six check types. Emits a STATUS=INVALID_INPUT RESULT block on
failure + tees to $ERROR_TEE on disk + exits 1.
Guiding principle: the agent MUST suffix every call with `|| exit 1` — Python
`sys.exit(1)` only terminates this subprocess, not the parent bash. A bare
call without `|| exit 1` silently continues past a failed guard, which is
the worst possible failure mode for a security check.
Check types:
symlink — path must NOT be a symlink (rejects pre-planted attacker bait)
owned — path must be owned by current UID (rejects foreign-owned dirs)
uuid — value must match ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
org_id_15 — value must match ^[A-Za-z0-9]{15}$ (Salesforce org ID slice)
api_name — value must match ^[A-Za-z0-9_]+$ (Salesforce API identifier)
api_version — value must match ^v[0-9]+\.[0-9]+$ (e.g. v60.0)
agent_version — value must match ^v[0-9]+$ (e.g. v5 — Agentforce agent version, no dot-minor)
not_empty — value must be non-empty string
Python-importable API:
validate_api_name(value, label="...") — raises ValidationError on bad input
validate_api_version(value, label="...") — raises ValidationError on bad input
validate_agent_version(value, label="...") — raises ValidationError on bad input
validate_org_id_15(value, label="...") — raises ValidationError on bad input
ValidationError — carries (label, reason)
The Python API is used by in-process callers (SOQL loader, path builders in
config.py) that need a raise-based boundary rather than the process-exit
CLI behavior. Regex is shared — do NOT duplicate API_NAME_RE / API_VERSION_RE.
Usage:
python3 fs_guard.py <value> <label> <check> || exit 1
# Input validation
python3 fs_guard.py "$AGENT_API_NAME" agent_api_name api_name || exit 1
python3 fs_guard.py "$ORG_ALIAS" org_alias not_empty || exit 1
# Filesystem safety
python3 fs_guard.py "$WORK_DIR" WORK_DIR symlink || exit 1
python3 fs_guard.py "$WORK_DIR" WORK_DIR owned || exit 1
python3 fs_guard.py "$ORG_ID_15" ORG_ID_15 org_id_15 || exit 1
Inputs:
argv[1] value or path to check
argv[2] label (appears in ERROR_DETAIL; used to identify which guard tripped)
argv[3] check type (one of the 6 above)
env $ERROR_TEE (optional): path to disk-tee file. Defaults to
$HOME/.vibe/data/investigating-agentforce-d360/_last_error_result.txt.
Also reads $AGENT_API_NAME / $ORG_ID_18 / $ORG_ID_15 for
RESULT-block context if set.
Outputs:
on failure: STATUS=INVALID_INPUT RESULT block on stdout + tee to disk, exit 1
on success: silent, exit 0
on bad argv: exit 1 with a minimal ERROR_DETAIL ("fs_guard internal: ...")
"""
import os
import pathlib
import re
import sys
# Anchored \A...\Z (not ^...$) — Python's `$` matches before a trailing
# newline, so "00DTESTORG12345\n" passes ^...$ + .match(). Every call
# site below MUST use .fullmatch() for the same reason; .match() is
# unsafe even with \A...\Z anchors. Mirrors the pattern in
# _shared/paths.py SESSION_ID_RE.
UUID_RE = re.compile(r"\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\Z") # @rule-suppress starter-sec-002 — re.compile, not eval/exec
ORG_ID_15_RE = re.compile(r"\A[A-Za-z0-9]{15}\Z") # @rule-suppress starter-sec-002 — re.compile, not eval/exec
API_NAME_RE = re.compile(r"\A[A-Za-z0-9_]+\Z") # @rule-suppress starter-sec-002 — re.compile, not eval/exec
# P0-5: api_version check — matches `v60.0`, `v66.0`, etc. Used before any
# Path composition that embeds the api_version segment.
API_VERSION_RE = re.compile(r"\Av[0-9]+\.[0-9]+\Z") # @rule-suppress starter-sec-002 — re.compile, not eval/exec
# Agent-version check — matches Agentforce version identifiers (`v1`, `v5`,
# `v12`). Deliberately stricter than api_name (rejects free-form names like
# `release_1`) and looser than api_version (rejects `v66.0` which is a REST
# API version, never an agent version). Lives on its own so callers that
# embed agent_version in a filesystem path get the tight regex, not the
# permissive api_name fallback.
AGENT_VERSION_RE = re.compile(r"\Av[0-9]+\Z") # @rule-suppress starter-sec-002 — re.compile, not eval/exec
VALID_CHECKS = {"symlink", "owned", "uuid", "org_id_15", "api_name", "api_version", "agent_version", "not_empty"}
# -----------------------------------------------------------------------------
# Python-importable API (P0-1, P0-5)
# -----------------------------------------------------------------------------
# In-process callers (soql_loader.load_soql, config.build_*_dir) need a
# raise-based validation boundary, distinct from the CLI script's exit-1
# behavior. The regexes are shared with the CLI checks above — do NOT
# duplicate. If a regex changes, both surfaces update in lockstep.
class ValidationError(ValueError):
"""Raised by the Python-importable validators on bad input.
Carries the label (which field failed) + reason. Callers (e.g.
soql_loader.load_soql) convert this into `_unresolved[]` entries or
RESULT-level INVALID_INPUT responses as appropriate.
"""
def __init__(self, label: str, reason: str) -> None:
self.label = label
self.reason = reason
super().__init__(f"{label}: {reason}")
def validate_api_name(value, label: str = "value") -> None:
"""Validate `value` against ^[A-Za-z0-9_]+$ (Salesforce API identifier).
P0-1: used by soql_loader.load_soql at the substitution boundary to catch
injection attempts in names pulled from Bot XML, Flow.Metadata, ApexClass
SymbolTable, etc.
P0-5: used by config.build_*_dir helpers for path components that must
not contain `..`, `/`, or other traversal characters.
Raises ValidationError on any of: None, non-string, empty, regex miss.
"""
if value is None:
raise ValidationError(label, "is None")
if not isinstance(value, str):
raise ValidationError(label, f"must be str, got {type(value).__name__}")
if not value:
raise ValidationError(label, "must not be empty")
if not API_NAME_RE.fullmatch(value):
# Preview first 20 chars so logs/_unresolved entries carry enough
# context to debug, without dumping an unbounded attacker-controlled
# string into output.
preview = value[:20]
raise ValidationError(
label,
f"does not match [A-Za-z0-9_]+ (preview={preview!r})",
)
def validate_api_version(value, label: str = "api_version") -> None:
"""Validate `value` against ^v[0-9]+\\.[0-9]+$ (Salesforce API version).
P0-5: used by config.build_*_dir helpers. api_version is returned by
`sf org display` and later embedded in cache paths; reject anything that
could escape the cache subtree.
"""
if value is None:
raise ValidationError(label, "is None")
if not isinstance(value, str):
raise ValidationError(label, f"must be str, got {type(value).__name__}")
if not value:
raise ValidationError(label, "must not be empty")
if not API_VERSION_RE.fullmatch(value):
preview = value[:20]
raise ValidationError(
label,
f"does not match v<major>.<minor> (preview={preview!r})",
)
def validate_agent_version(value, label: str = "agent_version") -> None:
"""Validate `value` against ^v[0-9]+$ (Agentforce agent version).
Strictly `v<digits>` with no dot-minor — matches the shape Agentforce
actually uses (`v1`, `v5`, `v12`). Rejects `release_1`, `v66.0`, `FOO`,
and anything else that could silently slip past a permissive api_name
check and land in a filesystem path.
P0-5: used by path-builder helpers for the agent_version segment.
"""
if value is None:
raise ValidationError(label, "is None")
if not isinstance(value, str):
raise ValidationError(label, f"must be str, got {type(value).__name__}")
if not value:
raise ValidationError(label, "must not be empty")
if not AGENT_VERSION_RE.fullmatch(value):
preview = value[:20]
raise ValidationError(
label,
f"does not match v<digits> (preview={preview!r})",
)
def validate_org_id_15(value, label: str = "org_id_15") -> None:
"""Validate `value` against ^[A-Za-z0-9]{15}$ (Salesforce 15-char org ID).
P0-5: stricter than validate_api_name — enforces exact 15 chars AND
no underscores. `org_id_15` always comes from a Salesforce-generated
field, never from free-form input.
"""
if value is None:
raise ValidationError(label, "is None")
if not isinstance(value, str):
raise ValidationError(label, f"must be str, got {type(value).__name__}")
if not value:
raise ValidationError(label, "must not be empty")
if not ORG_ID_15_RE.fullmatch(value):
preview = value[:20]
raise ValidationError(
label,
f"must be exactly 15 alphanumeric chars (preview={preview!r})",
)
def scrub(s: str) -> str:
# Same rules as sanitize.py; duplicated locally so this script has no
# intra-skill imports. Keeps the agent-script boundary clean.
bad = set("`$\"\\\r\t\0\n")
return "".join(c for c in (s or "") if c not in bad)
def emit_failure(reason: str, label: str) -> None:
agent_api_name = scrub(os.environ.get("AGENT_API_NAME", ""))
org_id_18 = scrub(os.environ.get("ORG_ID_18", ""))
org_id_15 = scrub(os.environ.get("ORG_ID_15", ""))
label_safe = scrub(label)
reason_safe = scrub(reason)
block = (
"=== RESULT ===\n"
"STATUS=INVALID_INPUT\n"
f"ERROR_DETAIL={label_safe}: {reason_safe}\n"
f"AGENT_API_NAME={agent_api_name}\n"
f"ORG_ID_18={org_id_18}\n"
f"ORG_ID_15={org_id_15}\n"
)
# Default ERROR_TEE lives under the skill-scoped data root. Runtime-agnostic
# default mirrors runtime.resolve_data_root() in the sibling runtime module
# (duplicated rather than imported because fs_guard.py is also invoked
# standalone from SKILL.md bash, where the override hook isn't active).
tee_default = str(
pathlib.Path.home()
/ ".vibe"
/ "data"
/ "investigating-agentforce-d360"
/ "_last_error_result.txt"
)
tee_path = os.environ.get("ERROR_TEE") or tee_default
try:
p = pathlib.Path(tee_path)
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(block)
except OSError:
pass
sys.stdout.write(block)
sys.exit(1)
def check_symlink(value: str, label: str) -> None:
if pathlib.Path(value).is_symlink():
emit_failure(f"path is a symlink (refusing to follow): {value}", label)
def check_owned(value: str, label: str) -> None:
p = pathlib.Path(value)
if not p.exists():
return
try:
st = p.stat()
except OSError as e:
emit_failure(f"stat failed: {e}", label)
return
if st.st_uid != os.getuid():
emit_failure(f"foreign-owned (uid {st.st_uid} != current {os.getuid()}): {value}", label)
def check_uuid(value: str, label: str) -> None:
if not UUID_RE.fullmatch(value):
emit_failure("does not match UUID pattern (8-4-4-4-12 lowercase hex)", label)
def check_org_id_15(value: str, label: str) -> None:
if not ORG_ID_15_RE.fullmatch(value):
emit_failure("must be exactly 15 alphanumeric characters", label)
def check_api_name(value: str, label: str) -> None:
if not API_NAME_RE.fullmatch(value):
emit_failure("does not match [A-Za-z0-9_]+ (Salesforce API name rules)", label)
def check_api_version(value: str, label: str) -> None:
# P0-5: api_version must be `vNN.N` — no slashes, no dots beyond the one,
# no path-traversal sequences.
if not API_VERSION_RE.fullmatch(value):
emit_failure("does not match v<major>.<minor> (e.g. v60.0)", label)
def check_agent_version(value: str, label: str) -> None:
# Agent version must be `v<digits>` — no dots, no slashes.
if not AGENT_VERSION_RE.fullmatch(value):
emit_failure("does not match v<digits> (e.g. v5)", label)
def check_not_empty(value: str, label: str) -> None:
if not value:
emit_failure("must not be empty", label)
CHECKS = {
"symlink": check_symlink,
"owned": check_owned,
"uuid": check_uuid,
"org_id_15": check_org_id_15,
"api_name": check_api_name,
"api_version": check_api_version,
"agent_version": check_agent_version,
"not_empty": check_not_empty,
}
def main() -> int:
if len(sys.argv) != 4:
sys.stdout.write(
"=== RESULT ===\n"
"STATUS=INVALID_INPUT\n"
"ERROR_DETAIL=fs_guard internal: wrong argv count (need value, label, check)\n"
)
return 1
value, label, check = sys.argv[1], sys.argv[2], sys.argv[3]
for arg in (value, label, check):
if any(c == "\0" or (ord(c) < 0x20 and c not in "\t\n") for c in arg):
emit_failure("argv contains control characters", label or "argv")
if check not in VALID_CHECKS:
emit_failure(f"unknown check type '{check}' (valid: {', '.join(sorted(VALID_CHECKS))})", label)
CHECKS[check](value, label)
return 0
if __name__ == "__main__":
sys.exit(main())
"""Canonical SQL-escaping helpers for investigating-agentforce-d360.
Single source of truth for DC-SQL string-literal escaping. Behavioral contract
is fixed — do not change the escape strategy without updating every caller and
its tests.
"""
from __future__ import annotations
def _escape_sql_literal(s: str) -> str:
"""Double single quotes per DC SQL escaping rule. Handles O'Brien →
O''Brien, `'; DROP --` → `''; DROP --` (still harmless because it's
wrapped in surrounding single quotes)."""
return s.replace("'", "''")
"""Test bootstrap — adds sibling ``scripts/`` to sys.path.
Every ``test_*.py`` in this directory imports this module first
(as ``from . import _bootstrap``) so the sibling modules
(`config`, `storage`, `paths`, etc.) resolve.
"""
from __future__ import annotations
import sys
from pathlib import Path
# scripts/tests/_bootstrap.py → scripts/
_SCRIPTS_DIR = Path(__file__).resolve().parent.parent
if str(_SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(_SCRIPTS_DIR))