| name | sf-event-monitoring-canvas |
| description | 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. |
| disable-model-invocation | true |
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:
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:
/tmp/sf-event-activity-<safe-org-alias>-csv/
/tmp/sf-event-activity-<safe-org-alias>-summary.json
Canvas filename:
salesforce-org-activity-<safe-org-alias>.canvas.tsx
2. Query EventLogFile Rows
Use the target org alias and lookback window:
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:
/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):
sf org display -o <ORG_ALIAS> --json
Then download each body with that version:
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:
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:
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:
- Resolved connected app name from
CONNECTED_APP_ID
- Clear product/client name from
CLIENT_NAME
- Clear tooling or product identity from
USER_AGENT
- Clear API pattern from
URI plus method/object context, for example Data Cloud event ingestion API, Salesforce query API, Metadata deploy API
- 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:
/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:
/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:
Salesforce Org Activity: <ORG_DISPLAY_NAME>
Subtitle:
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:
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:
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:
User | Action | Component | Browser | IP
Describe this as human activity inside Salesforce.
Footer:
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.