# Sf Event Monitoring Canvas

> Creates an executive-readable Cursor Canvas from Salesforce Event Monitoring logs. Use when the user asks to audit Salesforce org activity, summarize EventLogFile data, identify API/RestApi/Login/Apex/Lightning activity, or create an Event Monitoring Canvas.

- Skill: `dylandersen/sf-event-monitoring-canvas` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add dylandersen/sf-event-monitoring-canvas`
- Raw SKILL.md: https://api.skillmd.com/api/skills/dylandersen/sf-event-monitoring-canvas/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: dylandersen (https://skillmd.com/u/dylandersen)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/dylandersen/sf-event-monitoring-canvas

---


# Salesforce Event Monitoring Canvas

## When Invoked

Create a privacy-conscious Cursor Canvas summarizing Salesforce Event Monitoring activity for a target org. The Canvas explains, for a non-technical executive audience, what happened, who did it, where it came from, and what Salesforce objects or features were accessed.

Treat this as a complete workflow: collect inputs, query Salesforce with `sf`, download log CSVs, aggregate locally, resolve users and connected apps where possible, then write the final `.canvas.tsx`.

## Required Inputs

Before running commands, determine:

- Target org alias
- Org display name for the Canvas
- Lookback window in days, defaulting to `7`
- Target username, optional and only needed if the user provided it for clarity

If the org alias or org display name is missing, ask the user before querying Salesforce. Do not assume a default org. If the lookback window is missing, use 7 days and state that default.

Useful question:

```text
Which Salesforce org alias and display name should I use for the Event Monitoring Canvas? The lookback defaults to 7 days unless you want a different window.
```

## Hard Rules

- Use Salesforce CLI (`sf`) only for Salesforce org access.
- Do not use MCP for Salesforce queries, downloads, user resolution, or org operations.
- Use local bash/Python scripts for CSV parsing and aggregation.
- Cursor file tools are allowed for creating the final `.canvas.tsx` artifact.
- Never print raw CSV contents.
- Do not expose raw CSV rows, session identifiers, login keys, request IDs, tokens, or unnecessary full URLs.
- Do not deploy anything.
- Keep the raw CSVs and sanitized summary in `/tmp`; only the Canvas should be written to the Cursor canvas directory.

## Workflow

### 1. Prepare Names

Create a filesystem-safe alias by lowercasing the org alias and replacing non-alphanumeric characters with `-`. Use:

```text
/tmp/sf-event-activity-<safe-org-alias>-csv/
/tmp/sf-event-activity-<safe-org-alias>-summary.json
```

Canvas filename:

```text
salesforce-org-activity-<safe-org-alias>.canvas.tsx
```

### 2. Query EventLogFile Rows

Use the target org alias and lookback window:

```bash
sf data query -o <ORG_ALIAS> --json -q "SELECT Id, EventType, LogDate, LogFileLength FROM EventLogFile WHERE LogDate = LAST_N_DAYS:<LOOKBACK_DAYS> AND EventType IN ('Login','API','RestApi','ApexExecution','LightningInteraction') ORDER BY LogDate DESC"
```

If no rows are returned, stop and tell the user that no matching Event Monitoring logs were available for the window.

### 3. Download Raw Log Bodies

Download each returned log body to a temp directory:

```bash
/tmp/sf-event-activity-<safe-org-alias>-csv/<EventType>-<Id>.csv
```

First read the org's API version once (don't hardcode an old version — a stale version can 404):

```bash
sf org display -o <ORG_ALIAS> --json   # use result.apiVersion, e.g. 67.0
```

Then download each body with that version:

```bash
sf api request rest "/services/data/v<API_VERSION>/sobjects/EventLogFile/<Id>/LogFile" -o <ORG_ALIAS> > "/tmp/sf-event-activity-<safe-org-alias>-csv/<EventType>-<Id>.csv"
```

Use a bash array with `EventType:Id` pairs or a Python script so file names and IDs are parsed safely. Some files can be several MB (RestApi bodies are often the largest); aggregate them in code.

After download, verify that files are larger than tiny error placeholders and that the file count matches the EventLogFile row count. If any download failed, retry only the failed file.

### 4. Parse CSVs And Resolve Users

Parse with Python's `csv.DictReader`. Collect every distinct `USER_ID_DERIVED`.

Resolve users with chunked SOQL:

```bash
sf data query -o <ORG_ALIAS> --json -q "SELECT Id, Name, Username, Profile.Name FROM User WHERE Id IN (<chunked quoted ids>)"
```

If a `USER_ID_DERIVED` no longer resolves, keep its aggregate activity and label it `Unknown or inactive user`.

### 5. Detect Connected Apps And MCP Patterns

For API and RestApi logs, extract these fields when present:

- `CONNECTED_APP_ID`
- `CLIENT_NAME`
- `USER_AGENT`
- `METHOD_NAME`
- `ENTITY_NAME`
- `URI`
- `CLIENT_IP`

Group API activity by connected app ID, client name, user agent, method, object, URI pattern, and source IP. If `CONNECTED_APP_ID` is populated, attempt to resolve it:

```bash
sf data query -o <ORG_ALIAS> --use-tooling-api --json -q "SELECT Id, Name, DeveloperName FROM ConnectedApplication WHERE Id IN (<chunked quoted ids>)"
```

If that fails, try available CLI-supported setup/tooling objects only if you can do so with `sf` and without MCP. If connected app resolution remains unavailable, keep summarized counts by connected app ID internally and present them as unresolved OAuth clients only when useful.

#### Client Name Resolution

Treat `CLIENT_NAME` as evidence, not as a guaranteed human-readable application name. Event Monitoring can populate `CLIENT_NAME` with opaque numeric or internal client codes such as `5234` or `9999`; these are not readable app names and must not be shown as "Top client apps" without qualification.

Before rendering client-app summaries:

- Detect opaque client values (the same numeric/opaque codes also show up in `USER_AGENT`, e.g. `5234`, `9999`, `5400` — treat them identically):
  - Numeric-only values, for example `5234`, `9999`
  - Very short all-caps or code-like values that are not recognizable product names
  - Placeholder values such as `unknown`, `null`, `n/a`, empty strings, or generic `API client`
- Build a display label from the strongest available evidence, in this order:
  1. Resolved connected app name from `CONNECTED_APP_ID`
  2. Clear product/client name from `CLIENT_NAME`
  3. Clear tooling or product identity from `USER_AGENT`
  4. Clear API pattern from `URI` plus method/object context, for example `Data Cloud event ingestion API`, `Salesforce query API`, `Metadata deploy API`
  5. Fallback label `Unresolved client code <CLIENT_NAME>` or `Unresolved API client`
- Preserve the original opaque value only as supporting context, not as the primary app name. Example presentation: `Unresolved client code 5234 · mostly /services/data/v67.0/a360/events`.
- Keep unresolved numeric/code clients in a separate "Unresolved client codes" or "Unresolved API clients" summary instead of mixing them into "Top client apps" as if they are app names.
- Do not infer Cursor, MCP, Salesforce CLI, SFDX, Slack, Data Cloud, or another product from a numeric client code alone. Require supporting evidence from `USER_AGENT`, `CONNECTED_APP_ID`, connected app name, `METHOD_NAME`, `URI`, or object patterns.
- When numeric client codes dominate the counts, add a short note in the Canvas explaining that Salesforce logged opaque client identifiers and that the Canvas grouped them by supporting URI/method/object evidence where possible.

Only label activity as Cursor, MCP, Salesforce CLI, SFDX, or related tooling when `CLIENT_NAME`, `USER_AGENT`, `METHOD_NAME`, `URI`, connected app name, or `CONNECTED_APP_ID` clearly supports that attribution. If unclear, call it `API client` or `integration activity`.

If Salesforce-hosted MCP activity is detected, explain that Event Monitoring captures it as Salesforce API activity under the authenticated user, connected app, IP, method, and object touched.

### 6. Aggregate Safely

Normalize values before summarizing:

- Strip wrapping quotes from CSV string fields.
- Convert missing clients to `API client`.
- Convert numeric-only or opaque `CLIENT_NAME` values to unresolved client labels unless another field clearly resolves the client identity. Do not display raw codes like `5234` or `9999` as app names.
- Convert missing objects to `Unspecified object`, but keep a separate count instead of making it the main "top object."
- Simplify browser/user-agent strings to human-readable labels.
- Count distinct source IPs from `SOURCE_IP` and `CLIENT_IP`, excluding placeholders like `0.0.0.0` and `Salesforce.com IP`.
- Separate real people from integration/system users using name, username, profile, browser, login type, client name, and user-agent patterns.

### 7. Write Sanitized Aggregate JSON

Write:

```bash
/tmp/sf-event-activity-<safe-org-alias>-summary.json
```

The aggregate may include:

- total events
- distinct users
- distinct source IPs
- event counts by type and day
- top users
- top client apps
- top connected apps / OAuth clients
- top Salesforce objects or features touched
- top methods
- summarized Login, ApexExecution, and LightningInteraction table rows
- notes about unresolved users or unclear clients

The aggregate must not include raw CSV rows, session keys, login keys, request IDs, tokens, or full sensitive URLs.

## Event Columns To Use

- Login: `USER_ID_DERIVED`, `LOGIN_STATUS`, `SOURCE_IP`, `COUNTRY_CODE`, `BROWSER_TYPE`, `LOGIN_TYPE`
- API / RestApi: `USER_ID_DERIVED`, `CLIENT_NAME`, `METHOD_NAME`, `ENTITY_NAME`, `URI`, `ROWS_PROCESSED`, `CLIENT_IP`, `CONNECTED_APP_ID`, `USER_AGENT`
- ApexExecution: `ENTRY_POINT`, `RUN_TIME`, `NUMBER_SOQL_QUERIES`, `BOT_ID`, `PLANNER_ID`, `USER_ID_DERIVED`
- LightningInteraction: `USER_ID_DERIVED`, `UI_EVENT_SOURCE`, `COMPONENT_NAME`, `RECORD_TYPE`, `PAGE_URL`, `CLIENT_IP`, `BROWSER_NAME`

## Field & Data Quirks (verified)

Confirmed EventLogFile behaviors — handle each explicitly:

- **`LOGIN_STATUS` is a code, not the word "success".** Success is `LOGIN_NO_ERROR`; failures are codes like `LOGIN_ERROR_INVALID_PASSWORD`. Count success = `LOGIN_NO_ERROR` (or empty), everything else = failure. (Naively testing for `"success"` marks every login as failed.)
- **`API` vs `RestApi` are different event types:** `API` = SOAP / legacy API, `RestApi` = REST. Label them distinctly. `RestApi` row counts usually dwarf every other type (tens of thousands), so a stacked daily chart is RestApi-dominated — expected; note it in the caption.
- **Opaque codes appear in `USER_AGENT`, not only `CLIENT_NAME`.** Bulk internal traffic frequently has an empty `CLIENT_NAME` (→ `API client`) plus a numeric `USER_AGENT` like `5234` / `9999`. Group this as "internal Salesforce platform traffic"; never present the code as an app name.
- **`CONNECTED_APP_ID` key prefixes matter:** only `0H4`-prefixed IDs are `ConnectedApplication` records resolvable via the Tooling API query. Other prefixes (e.g. `888B…`, `8881…`) are not ConnectedApplication rows and will not resolve — present them as unresolved OAuth clients (don't keep retrying the query).
- **`CLIENT_IP` placeholders:** values like `0.0.0.0` and `Salesforce.com IP` are not real addresses — exclude them from distinct-IP counts.
- **`BROWSER_TYPE` (Login) is a full user-agent string** (e.g. `Go-http-client/1.1`, `Jetty/12.0.34`, a full `Mozilla/… Chrome … Safari`) and may contain an `IP_ADDRESS_REMOVED` token. Simplify to a short human label; server-to-server clients (Go/Jetty/node-fetch) are a strong integration-vs-person signal.
- **Numeric fields can be empty strings:** wrap `RUN_TIME`, `ROWS_PROCESSED`, and `NUMBER_SOQL_QUERIES` in `try/except` float/int coercion.
- **Per-day bucketing:** group rows by `TIMESTAMP_DERIVED` (ISO 8601; first 10 chars = day), not the file's `LogDate`.
- **User resolution is often partial:** the `User` SOQL can return fewer rows than the distinct `USER_ID_DERIVED` set (deleted/special users) — keep their activity as `Unknown or inactive user`. Common resolvable system/integration accounts to separate from real people: `Automated Process`, `Platform Integration User`, analytics `Integration User`, `EinsteinServiceAgent User`.
- **`BOT_ID` / `PLANNER_ID` populated on ApexExecution = Agentforce / automation actions** running as governed Apex (verified with a Deal Desk policy-check action) — badge these as agent actions.

## Canvas Requirements

Before writing a Canvas, use the Cursor canvas skill or Canvas SDK declarations if available. Write exactly one `.canvas.tsx` file in the current workspace's Cursor canvas directory:

```text
/Users/<user>/.cursor/projects/<workspace-id>/canvases/salesforce-org-activity-<safe-org-alias>.canvas.tsx
```

Use inline aggregate data only. Do not fetch from the Canvas.

Canvas title:

```text
Salesforce Org Activity: <ORG_DISPLAY_NAME>
```

Subtitle:

```text
Salesforce Event Monitoring · org <ORG_DISPLAY_NAME> · last <LOOKBACK_DAYS> days
```

Include:

- 3 stat tiles: total events, distinct users, distinct source IPs
- This explainer sentence: `Event Monitoring captured every sign-in, API request, automation run, agent action, and UI interaction below — with the user, time, IP, and object touched.`
- A card per non-empty event type

### Card 1: Sign-ins (Login)

Show:

```text
User | Result | Source IP | Country | Browser
```

Separate real people from integration/system users based on username, profile, browser, login type, and client patterns.

### Card 2: Data & Metadata Access (API + RestApi)

Show:

- top users
- top client apps
- unresolved client codes/API clients when opaque numeric `CLIENT_NAME` values appear
- top connected apps / OAuth clients when available
- Salesforce objects touched, such as Account, Opportunity, Case, Lead, Contact, custom objects, metadata, Agentforce metadata, or Data Cloud objects
- common methods, such as query, insert, update, delete, read, meta_deploy, meta_list
- number of distinct IPs

Do not title a table "Top client apps" if the rows are mostly unresolved numeric client codes. Use a clearer label such as "Client apps and API identifiers", and separate real app names from unresolved codes.

Only attribute activity to Cursor, MCP, Salesforce CLI, SFDX, or related tooling when the logs clearly support it.

### Card 3: Apex Executions

Show:

```text
Entry point | Runs | Total ms | SOQL
```

Add an `Agent action` badge to any row where `BOT_ID` or `PLANNER_ID` is populated. Explain that these are governed Salesforce automation or Agentforce actions captured as real Apex execution.

### Card 4: UI Interactions (LightningInteraction)

Show:

```text
User | Action | Component | Browser | IP
```

Describe this as human activity inside Salesforce.

Footer:

```text
Every API read, automation run, agent action, and UI interaction is captured as an auditable Salesforce Event Monitoring record — with the user, time, IP, and object touched.
```

Keep the Canvas clean, executive-readable, and privacy-conscious. Summarize patterns and highlight notable activity.

## Verification

Before responding:

- Confirm the EventLogFile count, downloaded CSV count, total parsed events, distinct users, and distinct source IPs.
- Confirm user resolution completed, including any unresolved users.
- Confirm connected-app detection was attempted when `CONNECTED_APP_ID` appeared.
- Confirm the Canvas TypeScript check reports no errors.
- Return a markdown link to the Canvas file and a short summary of the key counts.


