SObjectActions
Provides tools for interacting with Salesforce records and objects, including discovery, reads, writes, searching, aggregating, intent shortcuts, and flow execution across standard and custom objects.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@SObjectActionsFind all open high-priority cases and give me a summary of each."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
sfka-invocableMCP
Generic, object-agnostic invocable Apex actions (27 MCP tools) covering discovery (describe, picklists,
list flows, access check, record summary), reads (read, find, count, aggregate, search, related), writes
(create, upsert, update, clone, delete, undelete, validate dry-run, assign owner, change record type),
intent shortcuts (log activity, close case, convert lead, post to Chatter, add note, attach file) and
run flow,
exposed as tools on a hosted Salesforce MCP server (McpServerDefinition = SObjectActions).
The same actions are callable from Flow, Agentforce agent actions, and REST
(/services/data/vXX.X/actions/custom/apex/<ClassName>).
Every action takes an objectApiName string plus records or Ids, so one deployment
covers any standard or custom object without further code.
Quick start (repeatable)
scripts/setup.sh <org-alias> [--no-fixtures] [--no-eca] [--no-tests] [--no-smoke] [--assign user@example.com]Deploys test fixtures, the 27 actions + echo flow + permission set, the MCP server definition and the External
Client App; assigns SObjectActions_User; runs all 45 unit tests and the 45-check REST smoke test; prints the
consumer key and the two remaining manual steps (activate the server in Setup, grant object CRUD/FLS).
Idempotent; verified on orgs with and without Enhanced Notes / record types.
Alternative: unlocked package SObject Actions MCP (Apex + flow + permission set; scripts/package.sh install <org> <04t>
also deploys mcp/ and eca/, which Salesforce does not allow in packages). Also scripts/scratch.sh (fresh scratch org,
full install) and .github/workflows/validate.yml (check-only deploy + tests + doc drift on PRs; needs secret SF_AUTH_URL).
Repo layout: force-app/ (packageable: classes, flows, permissionsets), mcp/ (McpServerDefinition), eca/ (External Client App),
test-fixtures/ (optional).
Related MCP server: Salesforce-Hosted-Custom-Mcp-Server
Documentation map
Document | What it is for |
| Usage guide: deploy, enable, conventions, per-tool reference, security, limits, errors, testing |
| Generated tool manifest: every tool with its inputs, outputs, annotations ( |
| Same manifest as machine-readable JSON (for agent prompts, docs sites, CI diffing) |
| How the classes fit together, shared helpers, design rules, how to add a tool |
| How external clients authenticate (ECA, PKCE, refresh tokens, Postman, headless fallback) |
| Generated Postman collection: OAuth pre-set, MCP + REST request per tool |
| 30-minute presentation script, slide by slide against the 23-slide deck |
| 15-minute cut of the same talk: 9 slides, one demo moment |
| Reference behind the talk: the catalog explained, question bank, claims not to make |
| Release history |
Contents
Tool families
Family | Tools |
Discover |
|
Read |
|
Write |
|
Intent shortcuts |
|
Automation |
|
Full input/output reference for each: docs/TOOLS.md.
Components
All source lives under force-app/main/default/.
Path | Purpose |
| Invocable |
| Invocable |
| Invocable |
| Invocable |
| Invocable |
| Invocable |
| Invocable |
| Invocable |
| Invocable |
| Invocable |
| Invocable |
| Invocable |
| Invocable |
| Invocable |
| Invocable |
| Invocable |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| Tiny autolaunched flow used as the runFlow test fixture |
| Optional: |
| Shared helpers: type resolution, JSON -> SObject, field validation, label lookup, duplicate-safe DML |
| 14 tests for create/read/update/delete |
| 6 tests for upsert/find/describe |
| 7 tests for search/related/count/undelete/access |
| 7 tests for clone/validate/aggregate/runFlow/urls |
| 11 tests for the intent/utility tools (~97.5% total coverage) |
| MCP server definition wiring all 27 Apex actions to tools (own package dir: not packageable) |
| REST end-to-end smoke test (45 checks) |
| Anonymous Apex smoke script |
| Regenerates |
| Regenerates the Postman collection from |
| One-command install + verify into any org |
| Scratch org bootstrap; unlocked package create/version/install |
| Execute access to all action classes (no object CRUD) |
| The OAuth client for MCP: |
| CI: check-only deploy with tests, docs drift check |
API version: 67.0 (sfdx-project.json). Requires an org where hosted MCP servers and
McpServerDefinition metadata are available (Summer '26 or later; verify in current release notes).
Deploy
# deploy everything and run the test class
sf project deploy start -o <alias> -d force-app/main/default \
-l RunSpecifiedTests -t SObjectActionsTest -t SObjectActionsExtTest -t SObjectActionsExt2Test -t SObjectActionsExt3Test -t SObjectActionsExt4Test
# Test fixtures (record types + dependent picklist used by two tests; deploy first for full per-class coverage)
sf project deploy start -o <alias> -d test-fixtures && sf org assign permset -n SObjectActions_Fixture -o <alias>
# MCP server definition and External Client App (separate dirs)
sf project deploy start -o <alias> -d mcp -d eca
# Apex only (skip the MCP definition)
sf project deploy start -o <alias> -d force-app/main/default/classes \
-l RunSpecifiedTests -t SObjectActionsTest -t SObjectActionsExtTest -t SObjectActionsExt2Test -t SObjectActionsExt3Test -t SObjectActionsExt4TestNotes:
The
McpServerDefinitiondeveloper name must be alphanumeric, start with a letter, 2-40 chars (underscores are rejected). It isSObjectActions.apiIdentifiervalues areaa:apex-<ClassName>and resolve automatically from the API Catalog once the class is deployed. Deploy classes and the definition together or classes first.
Enable the MCP server
Setup > MCP Servers >
SObject Actions> link External Client App SObject Actions MCP Client (deployed by this repo) > Activate.Client authentication details:
docs/CLIENT_AUTH.md.Assign users a permission set with access to the Apex classes plus CRUD/FLS on the objects they will operate on. The tools run as the authenticated user.
Point your MCP client (Claude, Agentforce, Cursor, etc.) at the server URL shown in Setup.
tools/listreturnscheckAccess,describeObject,searchRecords,findRecords,countRecords,aggregateRecords,readRecords,relatedRecords,recordSummary,picklistValues,validateRecords,createRecords,upsertRecords,updateRecords,cloneRecords,assignOwner,changeRecordType,deleteRecords,undeleteRecords,logActivity,closeCase,convertLead,postChatter,addNote,attachFile,listFlows,runFlow.
Tool annotations set in the definition:
Tool | readOnly | destructive | idempotent |
createRecords | false | false | false |
readRecords | true | false | true |
updateRecords | false | false | true |
deleteRecords | false | true | false |
upsertRecords | false | false | true |
findRecords | true | false | true |
describeObject | true | false | true |
searchRecords | true | false | true |
relatedRecords | true | false | true |
countRecords | true | false | true |
undeleteRecords | false | false | true |
checkAccess | true | false | true |
cloneRecords | false | false | false |
validateRecords | true | false | true |
aggregateRecords | true | false | true |
runFlow | false | false | false |
assignOwner | false | false | true |
changeRecordType | false | false | true |
picklistValues | true | false | true |
logActivity | false | false | false |
closeCase | false | false | true |
convertLead | false | false | false |
postChatter | false | false | false |
addNote | false | false | false |
attachFile | false | false | false |
listFlows | true | false | true |
recordSummary | true | false | true |
Typical agent sequence: describeObject (get exact API names) -> findRecords / readRecords
-> createRecords / upsertRecords / updateRecords -> deleteRecords.
Common conventions
objectApiName
Required on every action. Case-insensitive API name: Account, Case, Task, My_Object__c, ...
Activity (polymorphic Task/Event)
Activity is not creatable/queryable directly, so it is treated as an alias for "Task or Event":
Input style | How the concrete type is determined |
| From the SObject's actual type; anything other than Task/Event is rejected |
| Each element must carry |
| From the Id prefix ( |
A single request may therefore mix Tasks and Events when objectApiName = "Activity".
Record inputs (create / update)
Provide any combination; they are merged in this order:
Field | Type | Audience | Notes |
| SObject | Flow | Type must match |
| List<SObject> | Flow | Same |
| String | MCP / REST | JSON object or array of |
recordsJson rules:
Field names must exist on the object (direct field API names or relationship names such as
Accountfor external-Id upserts). Unknown names cause the whole request to fail withUnknown field(s) on <Object>: <names>rather than being silently dropped.Values are coerced by the platform JSON deserializer: dates as
YYYY-MM-DD, datetimes as ISO-8601 (2030-01-01T10:00:00.000Z), booleans astrue/false, numbers unquoted.attributes.type, if present, must equalobjectApiName(or Task/Event for Activity).
Id inputs (read / delete)
recordId(read only) and/orrecordIds(read + delete). Blank entries are skipped.15- or 18-character Ids accepted; invalid strings fail the request (
Invalid record Id: ...).Each Id's type must match
objectApiName(Task/Event whenActivity).
allOrNone (create / update / delete)
Default
false: partial success; each failed row is reported inerrorsasrow <n>: <message>.true: the entire request rolls back on any failure;failureCount= number of rows,errorsholds the DML exception message.
Labels
recordLabels is aligned index-for-index with recordIds. Label source:
Object | Label |
Case |
|
Everything else | The object's describe name field ( |
No name field / no read access | The record Id |
Create/update labels are re-queried after DML (so auto-numbers like CaseNumber are populated).
Delete captures labels before deleting.
Common outputs
Field | Type | Meaning |
| Boolean | true only if every row in the request succeeded (read: every Id found) |
| Integer | Row counts |
| Integer | Row counts |
| List<String> | Affected/found Ids, in input order |
| List<String> | Aligned with |
| List<String> |
|
| String | One-line summary, e.g. |
| List<String> | Lightning record URLs aligned with |
Request-level validation errors (bad object name, malformed JSON, ...) set isSuccess=false,
failureCount=1 and one entry in errors; no DML is performed for that request.
Other requests in the same call are unaffected.
Tool reference
createRecords / SObjectCreateAction
Invocable label: Create Records (Generic), category SObject Actions.
Inputs: objectApiName (req), record, records, recordsJson, allOrNone.
Outputs: isSuccess, successCount, failureCount, recordIds, recordLabels, errors, message.
MCP call examples:
{ "objectApiName": "Account",
"recordsJson": "[{\"Name\":\"Acme\",\"Industry\":\"Energy\"},{\"Name\":\"Globex\"}]" }{ "objectApiName": "Case",
"recordsJson": "{\"Subject\":\"Printer on fire\",\"Status\":\"New\",\"Origin\":\"Web\",\"AccountId\":\"001...\"}" }-> recordLabels: ["00001234 - Printer on fire"]
{ "objectApiName": "Activity",
"recordsJson": "[{\"attributes\":{\"type\":\"Task\"},\"Subject\":\"Call\",\"WhatId\":\"001...\"},{\"attributes\":{\"type\":\"Event\"},\"Subject\":\"Meet\",\"DurationInMinutes\":30,\"ActivityDateTime\":\"2030-01-01T10:00:00.000Z\"}]" }Sample result:
{ "isSuccess": true, "successCount": 2, "failureCount": 0,
"recordIds": ["00T...", "00U..."], "recordLabels": ["Call", "Meet"],
"errors": [], "message": "2 created, 0 failed." }Partial failure (allOrNone=false):
{ "isSuccess": false, "successCount": 1, "failureCount": 1,
"recordIds": ["003..."], "recordLabels": ["Good"],
"errors": ["row 2: Required fields are missing: [LastName] [LastName]"],
"message": "1 created, 1 failed." }readRecords / SObjectReadAction
Invocable label: Read Records (Generic).
Inputs:
Field | Type | Notes |
| String (req) |
|
| String | Single Id |
| List<String> | Bulk; duplicates collapsed |
| List<String> | Optional. Field API names, relationship paths allowed ( |
Outputs:
Field | Notes |
| true if every requested Id was found and visible |
| |
| Found records, input order |
| JSON array of found records (serialized SObjects, includes |
| First / all found records as SObjects - Flow-facing |
| Ids that do not exist or are not visible under sharing |
|
Examples:
{ "objectApiName": "Account", "recordId": "001..." }{ "objectApiName": "Case", "recordIds": ["500...","500..."], "fields": ["Status","Priority","Account.Name"] }{ "objectApiName": "Activity", "recordIds": ["00T...","00U..."], "fields": ["Subject","ActivityDate"] }Sample result:
{ "isSuccess": true, "foundCount": 1, "notFoundCount": 0,
"recordIds": ["500..."], "recordLabels": ["00001234 - Printer on fire"],
"recordsJson": "[{\"attributes\":{\"type\":\"Case\"},\"Id\":\"500...\",\"CaseNumber\":\"00001234\",\"Subject\":\"Printer on fire\",\"Status\":\"New\",\"Account\":{\"attributes\":{\"type\":\"Account\"},\"Name\":\"Acme\"}}]",
"notFoundIds": [], "errors": [], "message": "1 found, 0 not found." }Direct field names are validated before the query (Unknown field on Account: Foo__c).
Relationship paths are validated by SOQL itself; a bad path surfaces as a QueryException in errors.
Records the user cannot see are simply reported in notFoundIds (no error).
updateRecords / SObjectUpdateAction
Invocable label: Update Records (Generic).
Inputs: objectApiName (req), record, records, recordsJson, allOrNone.
Every row must include Id; a row without one fails the whole request
(row <n>: Id is required for update.).
Outputs: same shape as create (successCount, recordIds, recordLabels, ...).
Labels reflect the record's state after all updates in the call are applied.
Examples:
{ "objectApiName": "Account",
"recordsJson": "[{\"Id\":\"001...\",\"Name\":\"Acme Corp\",\"Phone\":\"555-0100\"}]" }{ "objectApiName": "Case", "recordsJson": "{\"Id\":\"500...\",\"Status\":\"Closed\"}" }{ "objectApiName": "Activity",
"recordsJson": "[{\"attributes\":{\"type\":\"Task\"},\"Id\":\"00T...\",\"Status\":\"Completed\"}]" }To clear a field pass null: {"Id":"001...","Description":null}.
deleteRecords / SObjectDeleteAction
Invocable label: Delete Records (Generic). Tool is flagged destructive.
Inputs: objectApiName (req), recordIds, record, records, allOrNone.
Records passed as SObjects must include Id.
Outputs: isSuccess, successCount, failureCount, recordIds, recordLabels
(captured before deletion), errors, message.
Examples:
{ "objectApiName": "Account", "recordIds": ["001...", "001..."] }{ "objectApiName": "Activity", "recordIds": ["00T...", "00U..."] }Deleted records go to the Recycle Bin (standard Database.delete semantics). Cascade
deletes follow the platform's master-detail / lookup rules.
upsertRecords / SObjectUpsertAction
Invocable label: Upsert Records (Generic).
Inputs: objectApiName (req), externalIdField (optional, default Id), record, records,
recordsJson, allOrNone. externalIdField must be an external Id / idLookup field on the object.
Activity is supported (Task/Event via attributes.type); the external Id field must exist on
the concrete type.
Outputs: isSuccess, successCount, failureCount, createdCount, updatedCount,
recordIds, recordLabels, wasCreated[] (aligned with recordIds), errors, message.
Examples:
{ "objectApiName": "Account", "externalIdField": "External_Key__c",
"recordsJson": "[{\"External_Key__c\":\"ERP-1001\",\"Name\":\"Acme\"},{\"External_Key__c\":\"ERP-1002\",\"Name\":\"Globex\"}]" }{ "objectApiName": "Contact",
"recordsJson": "[{\"Id\":\"003...\",\"Email\":\"a@b.com\"},{\"LastName\":\"New Person\"}]" }-> wasCreated: [false, true], message: "1 created, 1 updated, 0 failed."
Rows are grouped per concrete object type; each group is one Database.upsert.
allOrNone=true uses a savepoint so a failure in any group rolls back the whole request.
findRecords / SObjectFindAction
Invocable label: Find Records (Generic). Structured filters only; no raw SOQL/WHERE is accepted.
Inputs:
Field | Notes |
| Must be queryable. Use |
| JSON array (or single object) of |
|
|
| Extra fields to return; relationship paths allowed ( |
|
|
| 1-200, default 50. |
Values are coerced to the field type before binding (dates YYYY-MM-DD, datetimes ISO-8601,
numbers, booleans, Ids). All values go through bind variables (Database.queryWithBinds, USER_MODE).
Outputs: isSuccess, resultCount, recordIds, recordLabels, recordsJson, records (Flow),
soql (the query that ran, values redacted as :b0, :b1...), errors, message
("... (limit reached)" when resultCount == limitCount).
Examples:
{ "objectApiName": "Case",
"filtersJson": "[{\"field\":\"Status\",\"op\":\"IN\",\"value\":[\"New\",\"Working\"]},{\"field\":\"CreatedDate\",\"op\":\">=\",\"value\":\"2026-08-01T00:00:00Z\"}]",
"fields": ["Status","Priority","Account.Name"], "orderBy": "CreatedDate DESC", "limitCount": 25 }{ "objectApiName": "Account",
"filtersJson": "[{\"field\":\"Name\",\"op\":\"LIKE\",\"value\":\"Acme%\"},{\"field\":\"Industry\",\"op\":\"=\",\"value\":\"Energy\"}]",
"filterLogic": "OR" }{ "objectApiName": "Task",
"filtersJson": "[{\"field\":\"WhatId\",\"op\":\"=\",\"value\":\"001...\"},{\"field\":\"IsClosed\",\"op\":\"=\",\"value\":false}]" }describeObject / SObjectDescribeAction
Invocable label: Describe Object (Generic). Two modes in one tool.
Describe mode (objectApiName set):
Input | Notes |
|
|
| default true |
| case-insensitive filter on field API name or label |
Outputs: objectApiName, objectLabel, keyPrefix, isCustom, isCreateable/Updateable/Deletable/Queryable,
labelFields (e.g. ["CaseNumber","Subject"] for Case), requiredFields (createable, non-nillable,
no default), fieldsJson (accessible fields only: apiName, label, type, required, createable, updateable, externalId, nameField, length, referenceTo[], relationshipName, picklistValues[]), recordTypesJson
(active, available, non-master: id, developerName, name, isDefault), childRelationshipsJson
(relationshipName, childObject, field), resultCount (fields returned).
List mode (objectApiName blank):
Input | Notes |
| case-insensitive filter on API name or label |
| default false |
Outputs: objectsJson (apiName, label, keyPrefix, isCustom, createable, queryable for accessible
objects; custom settings and prefix-less system objects excluded), resultCount.
Examples:
{ "objectApiName": "Case", "fieldNameContains": "status" }{ "objectApiName": "My_Object__c" }{ "objectNameContains": "invoice", "customOnly": true }searchRecords / SObjectSearchAction
Invocable label: Search Records (Generic). SOSL free-text search; the term is bound (FIND :term).
Input | Notes |
| min 2 chars; |
| default |
|
|
| extra fields, applied only where the field exists on that object |
| per object, 1-200, default 20 |
Outputs: resultCount, recordIds, recordLabels, recordObjectNames (aligned), recordsJson, records.
{ "searchTerm": "acme*", "objectApiNames": ["Account","Contact"], "searchIn": "NAME", "fields": ["Phone","Email"] }relatedRecords / SObjectRelatedAction
Invocable label: Get Related Records (Generic).
Input | Notes |
| parent object inferred from the Id |
| child relationship name on the parent ( |
| as in findRecords (limit default 50, max 200) |
Outputs: parentObjectApiName, childObjectApiName, resultCount, recordIds, recordLabels, recordsJson, records.
{ "parentRecordId": "001...", "relationshipName": "Cases", "fields": ["Status","Priority"], "orderBy": "CreatedDate DESC", "limitCount": 10 }countRecords / SObjectCountAction
Invocable label: Count Records (Generic).
Input | Notes |
| Task/Event, not Activity |
| identical to findRecords |
| optional groupable field; up to 200 groups, ordered by count desc; null group reported as |
Outputs: totalCount, groupValues[], groupCounts[] (aligned), groupsJson ([{value,count}]), soql.
{ "objectApiName": "Case", "filtersJson": "[{\"field\":\"IsClosed\",\"op\":\"=\",\"value\":false}]", "groupByField": "Priority" }undeleteRecords / SObjectUndeleteAction
Invocable label: Undelete Records (Generic). Restores from the Recycle Bin.
Inputs: objectApiName (req; Activity for Task/Event mix), recordIds (req), allOrNone.
Outputs: successCount, failureCount, recordIds, recordLabels (post-restore), errors, message.
Duplicate Ids in one request are collapsed. Records already restored or purged fail per row.
{ "objectApiName": "Account", "recordIds": ["001..."] }checkAccess / SObjectAccessAction
Invocable label: Check Access (Generic). All inputs optional; with none it is a "who am I".
Input | Notes |
| CRUD check ( |
| requires |
| up to 200; uses |
Outputs: userId, userName, loginUsername, profileId, profileName, userType, organizationId, timeZone,
objectApiName, canCreate/canRead/canUpdate/canDelete, fieldAccessJson, recordAccessJson, message.
{ "objectApiName": "Opportunity", "fields": ["Amount","StageName"], "recordIds": ["006..."] }cloneRecords / SObjectCloneAction
Invocable label: Clone Records (Generic).
Input | Notes |
|
|
| sources; not-found / invisible Ids fail per row |
| JSON object applied to every clone (field names validated) |
| fields not to copy (e.g. |
|
Copies every field that is createable, readable, not auto-number and not a formula. Child records
are not cloned. Outputs: sourceRecordIds (aligned), recordIds, recordLabels, recordUrls, counts, errors.
{ "objectApiName": "Opportunity", "recordIds": ["006..."], "overridesJson": "{\"Name\":\"Renewal 2027\",\"StageName\":\"Prospecting\"}", "excludeFields": ["OwnerId"] }validateRecords / SObjectValidateAction
Invocable label: Validate Records (Generic, dry run). Performs the insert/update inside a savepoint and always rolls back; triggers, validation rules, required fields, FLS and sharing all run for real.
Inputs: objectApiName (req), operation (CREATE default | UPDATE), record, records, recordsJson.
Outputs: isSuccess (would all rows save), successCount, failureCount, rowResultsJson
([{row, valid, errors[]}]), errors, message (... Nothing was saved.).
Note: the DML still counts toward governor limits and any auto-numbers consumed in the transaction are not reused.
{ "objectApiName": "Contact", "recordsJson": "[{\"LastName\":\"Ok\"},{\"FirstName\":\"No last name\"}]" }aggregateRecords / SObjectAggregateAction
Invocable label: Aggregate Records (Generic).
Input | Notes |
| Task/Event, not Activity |
|
|
| as findRecords |
| 0-3 groupable fields |
| groups only, 1-2000, default 200 (ungrouped queries cannot take LIMIT) |
Outputs: resultCount, rowsJson (each row: group field values + FUNCTION_Field keys, e.g. SUM_Amount), soql.
{ "objectApiName": "Opportunity",
"aggregationsJson": "[{\"function\":\"SUM\",\"field\":\"Amount\"},{\"function\":\"COUNT\",\"field\":\"Id\"}]",
"filtersJson": "[{\"field\":\"IsClosed\",\"op\":\"=\",\"value\":false}]",
"groupByFields": ["StageName"] }runFlow / SObjectRunFlowAction
Invocable label: Run Flow (Generic).
Input | Notes |
| active autolaunched flow |
| JSON object of input variable -> value |
| variables marked "available for output" to return |
Outputs: interviewId, outputsJson, errors (flow faults surface as Could not start flow ... or the
fault message), message. The flow runs in the same transaction, in the flow's declared run mode.
{ "flowApiName": "SObjectActions_EchoFlow", "inputsJson": "{\"inputText\":\"hi\",\"inputNumber\":1}", "outputVariableNames": ["outputText","outputNumber"] }assignOwner / SObjectAssignOwnerAction
Inputs: objectApiName, recordIds, newOwnerId (005/00G) or newOwnerName (exact user full name / username, or queue name / developer name; ambiguous names are rejected), allOrNone.
Outputs: resolved ownerId/ownerName, per-record counts, recordIds, recordLabels, errors.
{ "objectApiName": "Case", "recordIds": ["500..."], "newOwnerName": "Tier 2 Support" }changeRecordType / SObjectRecordTypeAction
Inputs: objectApiName, recordIds, recordType (Id, DeveloperName or Name), allOrNone. Errors list the available developer names. Outputs: resolved recordTypeId/recordTypeName, per-record outcomes.
picklistValues / SObjectPicklistAction
Inputs: objectApiName, fieldApiName, includeInactive. Outputs: values[], labels[], valuesJson
({value,label,active,default,validFor[]}), isRestricted, isDependent, controllingField, defaultValue.
Values reflect the org-wide field definition (record-type value sets are not applied).
logActivity / SObjectLogActivityAction
Logs a completed Task. Inputs: subject (req), relatedRecordId (WhatId), personRecordId (Contact/Lead WhoId), description, activityDate (default today), activityType, status (default first closed status), priority, ownerId, extraFieldsJson. Outputs: recordId, recordUrl, status.
{ "subject": "Call with CFO", "relatedRecordId": "001...", "personRecordId": "003...", "description": "Discussed renewal", "activityType": "Call" }closeCase / SObjectCloseCaseAction
Inputs: caseIds, status (must be a closed status; default first closed status), comment (+ commentIsPublic), extraFieldsJson, allOrNone. Outputs: status used, per-case counts, recordLabels (CaseNumber - Subject).
convertLead / SObjectConvertLeadAction
Inputs: leadId, convertedStatus (default first converted status), accountId / contactId (merge into existing), createOpportunity (default true), opportunityName, ownerId, sendEmailToOwner. Outputs: accountId, contactId, opportunityId + URLs.
postChatter / SObjectPostChatterAction
Inputs: recordId (any feed-enabled record or User), text, mentionUserIds. Outputs: feedItemId. Fails with a clear message when the object has no feed tracking.
addNote / SObjectAddNoteAction
Inputs: title, body (plain text or simple HTML), recordIds, shareType (V default / I / C). Creates a ContentNote and ContentDocumentLinks; if Enhanced Notes is disabled in the org, stores the note as an .html file instead (storedAsFile=true). Outputs: noteId, linked recordIds, storedAsFile.
attachFile / SObjectAttachFileAction
Inputs: fileName, textContent or base64Content, title, recordIds, shareType. Outputs: contentVersionId, contentDocumentId, linked recordIds. Heap limit applies to large base64 payloads (~6 MB sync).
listFlows / SObjectListFlowsAction
List mode: nameContains, processType (default AutoLaunchedFlow; ALL), includeInactive, limitCount -> flowsJson.
Detail mode: flowApiName -> variablesJson (apiName, dataType, isInput, isOutput, isCollection, objectType, description). Pair with runFlow.
recordSummary / SObjectSummaryAction
Inputs: recordId, fields (default all accessible), relationshipNames (max 10; default common ones that exist), recentActivityLimit (0-20, default 5).
Outputs: objectApiName, recordLabel, recordUrl, ownerName, recordJson, relatedCountsJson ({Contacts: 3, Cases: 1, ...}), recentActivityJson ([{id,type,subject,date,status,ownerName}]).
Costs 1 SOQL for the record + 1 per relationship + 2 for activity.
Calling from Flow
Add an Action element, search the SObject Actions category, pick the action.
Set Object API Name to a literal or a text variable.
For create/update, assign a record variable to Record or a record collection to Records. Flow's generic SObject inputs require you to pick the object type on the action element; it must match
objectApiName(or be Task/Event withActivity).Read the outputs
Created Record Ids/Record/Records/Errorsinto Flow variables.Leave All Or None empty for partial success, or set
{!$GlobalConstant.True}to roll back.
Fault paths: request-level problems do not throw; check Success and Errors.
Only unexpected platform exceptions (limits, etc.) reach a Flow fault connector.
Calling from REST
POST /services/data/v67.0/actions/custom/apex/SObjectCreateAction
Authorization: Bearer <token>
Content-Type: application/json
{ "inputs": [
{ "objectApiName": "Account", "recordsJson": "[{\"Name\":\"Acme\"}]" },
{ "objectApiName": "Contact", "recordsJson": "[{\"LastName\":\"Smith\",\"AccountId\":\"001...\"}]" }
] }Same shape for SObjectReadAction, SObjectUpdateAction, SObjectDeleteAction.
Each element of inputs maps to one element of the response outputValues.
Security model
All DML and SOQL run with
AccessLevel.USER_MODE: object CRUD, field-level security and sharing of the running user are enforced by the platform.Create/update of a non-writable field -> row error from the platform.
Read of an inaccessible object/field -> not returned (default field list only includes accessible fields; explicit inaccessible fields raise a query error).
Records outside the user's sharing scope ->
notFoundIdson read, row error on update/delete.
The classes are
global with sharing(required for MCP discovery).No
Database.querystring is built from raw caller input: object names are validated through describe, field names through the field map, Ids throughId.valueOf; relationship paths inreadRecords.fieldsare only ever placed in the SELECT list, never in a WHERE clause.Because the tools are generic, gate access with permission sets: who can call the Apex classes, and CRUD/FLS on the objects. Consider excluding
deleteRecordsfrom the server for agent use cases that should never delete.
Limits and bulk behavior
Invocable inputs are bulkified: all records across all requests in one call are combined into the fewest DML statements possible (one insert / update / delete per call in the normal case).
Duplicate Ids across requests (same record updated or deleted twice in one call) are split into sequential DML batches instead of failing with
Duplicate id in list.Label lookups: one SOQL per concrete object type per call (create/update/delete); read issues one SOQL per concrete type per request.
Governor limits that apply per transaction: 100 SOQL, 150 DML statements, 10,000 rows DML, 6 MB heap (sync). Very large
recordsJsonpayloads orreadRecordswith nofieldson wide objects and hundreds of Ids can approach heap/CPU limits; pass an explicitfieldslist for bulk reads.Mixed object types in one call are fine, but mixing setup and non-setup objects (e.g. User + Account) in a single call is subject to the platform's mixed-DML rule.
Records per DML chunk of differing SObject types is limited to 10 distinct types per DML statement by the platform.
Error catalog
Message | Cause | Fix |
| Missing input | Supply the API name |
| Typo / not visible | Check spelling and object access |
| Used | Use |
| SObject input of the wrong type | Align inputs |
| JSON row typed differently | Remove or fix |
| Malformed string | Ensure it is a JSON object or array, properly escaped |
| Scalar/other JSON | Wrap in |
| Field names not on the object | Fix API names ( |
| Value type coercion failed (bad date format, etc.) | Use ISO-8601 dates/datetimes, correct types |
| Nothing to act on | Provide |
| Not a valid 15/18-char Id | Fix Id |
| Wrong prefix | Fix objectApiName or Id |
| Update row without Id | Add |
| SObject delete input without Id | Add |
| Bad entry in | Fix API name |
| Bad | Use an external Id field or |
| Bad/dotted filter field | Use a direct field |
| Long text / encrypted etc. | Filter on another field |
| Bad | Use =, !=, <, <=, >, >=, LIKE, IN, NOT IN |
| Coercion failed | Match the field type / ISO date format |
| Out of range | Adjust |
| DML failure (validation rule, required field, FLS, sharing) | Fix data or permissions |
Testing
Three layers, cheapest first:
# 1. Unit tests (Apex, ~98% coverage)
sf apex run test -o <alias> -n SObjectActionsTest -n SObjectActionsExtTest -n SObjectActionsExt2Test -n SObjectActionsExt3Test -n SObjectActionsExt4Test -r human -w 10 -c
# 2. Anonymous Apex smoke: create -> read -> update -> delete, prints every result
sf apex run -o <alias> -f scripts/apex/smoke.apex
# 3. REST smoke through the Invocable Actions API (the exact path MCP tools use).
# 45 assertions across all 27 tools incl. Case labels, Activity Task/Event mix,
# validation errors. Creates and removes its own records. Needs jq.
scripts/smoke-test.sh <alias> [apiVersion]To exercise the MCP layer itself, activate the server (see above), connect an MCP client with
the External Client App, run tools/list and call e.g.
readRecords {"objectApiName":"Account","recordId":"001..."}.
Test coverage: create (SObject + JSON inputs, Case label, Activity resolution, validation matrix, partial and all-or-none), read (all/explicit fields, relationship path, Activity mix, not-found, validation), update (JSON/SObject/Activity, missing Id, ghost Id, all-or-none rollback, duplicate Id across requests), delete (Ids/SObjects/Activity mix, labels, validation, partial, all-or-none, duplicate Id), util fallbacks.
Tests assert by record Id rather than table counts so they pass in orgs with existing triggers/automation on Account.
Extending
Everything from the original design list is implemented. Natural next additions: record-type-aware picklist values (UI API callout via Named Credential), sendEmail (single email / template), approval submit/recall, and share/unshare (manual sharing) actions.
Custom label rules: extend
SObjectActionUtil.labelFields()(Case is the current special case).New tool on the server: add a
<tools>block tomcp/main/default/mcpServerDefinitions/SObjectActions.mcpServerDefinition-meta.xmlwithapiIdentifier = aa:apex-<ClassName>and redeploy; re-activate the server in Setup if needed.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables AI agents to interact with Salesforce organizations through natural language by exposing Salesforce APIs (REST, Bulk v2, GraphQL, Tooling, Auth) as MCP tools for querying data, managing records, and executing SOQL queries.1219MIT
- FlicenseNot gradedqualityBmaintenanceEnables interaction with Salesforce data and services via custom MCP tools, including account analytics, opportunity queries, case creation, and AI agent invocation.4
- AlicenseNot gradedqualityBmaintenanceProvides live-org context for AI assistants with tools to search skills, agents, templates, decision trees, and query Salesforce metadata (Apex, LWC, objects, fields, etc.) via MCP.15Apache 2.0
- AlicenseNot gradedqualityCmaintenanceEnables AI agents to interact with Salesforce through MCP, supporting queries, records, metadata, and bulk operations with flexible OAuth authentication.MIT
Related MCP Connectors
Search, document and execute authenticated API calls across 700+ apps via one MCP server
An AI concierge that turns static forms into adaptive AI conversations. From any MCP client.
Operator-as-agent MCP hub. 6 tools. First $5 free, then $0.001/call.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/Kelley-Austin/sfka-mcp-demo'
If you have feedback or need assistance with the MCP directory API, please join our Discord server