SObjectActions

SObjectActions

Enables object-agnostic Salesforce operations through 27 invocable Apex MCP tools, covering discovery, reading, writing, intent shortcuts, and flow execution. It allows any standard or custom object to be managed from MCP clients or called from Flow, Agentforce, and REST.

Category
访问服务器

README

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).

Documentation map

Document What it is for
README.md (this file) Usage guide: deploy, enable, conventions, per-tool reference, security, limits, errors, testing
docs/TOOLS.md Generated tool manifest: every tool with its inputs, outputs, annotations (scripts/gen-tool-manifest.py)
docs/tools.json Same manifest as machine-readable JSON (for agent prompts, docs sites, CI diffing)
docs/ARCHITECTURE.md How the classes fit together, shared helpers, design rules, how to add a tool
docs/CLIENT_AUTH.md How external clients authenticate (ECA, PKCE, refresh tokens, Postman, headless fallback)
docs/postman/SObjectActions.postman_collection.json Generated Postman collection: OAuth pre-set, MCP + REST request per tool
docs/DEMO_SCRIPT.md 30-minute presentation script, slide by slide against the 23-slide deck
docs/DEMO_SCRIPT_15MIN.md 15-minute cut of the same talk: 9 slides, one demo moment
docs/DEMO_NOTES.md Reference behind the talk: the catalog explained, question bank, claims not to make
CHANGELOG.md Release history

Contents

  1. Components
  2. Deploy
  3. Enable the MCP server
  4. Common conventions
  5. Tool reference
  6. Calling from Flow
  7. Calling from REST
  8. Security model
  9. Limits and bulk behavior
  10. Error catalog
  11. Testing
  12. Extending

Tool families

Family Tools
Discover checkAccess, describeObject, picklistValues, listFlows, recordSummary
Read readRecords, findRecords, countRecords, aggregateRecords, searchRecords, relatedRecords
Write validateRecords (dry run), createRecords, upsertRecords, updateRecords, cloneRecords, assignOwner, changeRecordType, deleteRecords, undeleteRecords
Intent shortcuts logActivity, closeCase, convertLead, postChatter, addNote, attachFile
Automation runFlow

Full input/output reference for each: docs/TOOLS.md.

Components

All source lives under force-app/main/default/.

Path Purpose
classes/SObjectCreateAction.cls Invocable Create Records (Generic) -> MCP tool createRecords
classes/SObjectReadAction.cls Invocable Read Records (Generic) -> MCP tool readRecords
classes/SObjectUpdateAction.cls Invocable Update Records (Generic) -> MCP tool updateRecords
classes/SObjectDeleteAction.cls Invocable Delete Records (Generic) -> MCP tool deleteRecords
classes/SObjectUpsertAction.cls Invocable Upsert Records (Generic) -> MCP tool upsertRecords
classes/SObjectFindAction.cls Invocable Find Records (Generic) -> MCP tool findRecords
classes/SObjectDescribeAction.cls Invocable Describe Object (Generic) -> MCP tool describeObject
classes/SObjectSearchAction.cls Invocable Search Records (Generic) -> MCP tool searchRecords (SOSL)
classes/SObjectRelatedAction.cls Invocable Get Related Records (Generic) -> MCP tool relatedRecords
classes/SObjectCountAction.cls Invocable Count Records (Generic) -> MCP tool countRecords
classes/SObjectUndeleteAction.cls Invocable Undelete Records (Generic) -> MCP tool undeleteRecords
classes/SObjectAccessAction.cls Invocable Check Access (Generic) -> MCP tool checkAccess
classes/SObjectCloneAction.cls Invocable Clone Records (Generic) -> MCP tool cloneRecords
classes/SObjectValidateAction.cls Invocable Validate Records (Generic, dry run) -> MCP tool validateRecords
classes/SObjectAggregateAction.cls Invocable Aggregate Records (Generic) -> MCP tool aggregateRecords
classes/SObjectRunFlowAction.cls Invocable Run Flow (Generic) -> MCP tool runFlow
classes/SObjectAssignOwnerAction.cls Assign Owner (Generic) -> assignOwner
classes/SObjectRecordTypeAction.cls Change Record Type (Generic) -> changeRecordType
classes/SObjectPicklistAction.cls Get Picklist Values (Generic) -> picklistValues
classes/SObjectLogActivityAction.cls Log Activity -> logActivity
classes/SObjectCloseCaseAction.cls Close Case -> closeCase
classes/SObjectConvertLeadAction.cls Convert Lead -> convertLead
classes/SObjectPostChatterAction.cls Post to Chatter -> postChatter
classes/SObjectAddNoteAction.cls Add Note -> addNote
classes/SObjectAttachFileAction.cls Attach File -> attachFile
classes/SObjectListFlowsAction.cls List Flows -> listFlows
classes/SObjectSummaryAction.cls Get Record Summary (Generic) -> recordSummary
flows/SObjectActions_EchoFlow.flow-meta.xml Tiny autolaunched flow used as the runFlow test fixture
test-fixtures/ (separate package dir) Optional: SObjectActions_Fixture__c with 2 record types + dependent picklist + permission set; only used by tests, referenced dynamically
classes/SObjectActionUtil.cls Shared helpers: type resolution, JSON -> SObject, field validation, label lookup, duplicate-safe DML
classes/SObjectActionsTest.cls 14 tests for create/read/update/delete
classes/SObjectActionsExtTest.cls 6 tests for upsert/find/describe
classes/SObjectActionsExt2Test.cls 7 tests for search/related/count/undelete/access
classes/SObjectActionsExt3Test.cls 7 tests for clone/validate/aggregate/runFlow/urls
classes/SObjectActionsExt4Test.cls 11 tests for the intent/utility tools (~97.5% total coverage)
mcp/main/default/mcpServerDefinitions/SObjectActions.mcpServerDefinition-meta.xml MCP server definition wiring all 27 Apex actions to tools (own package dir: not packageable)
scripts/smoke-test.sh REST end-to-end smoke test (45 checks)
scripts/apex/smoke.apex Anonymous Apex smoke script
scripts/gen-tool-manifest.py Regenerates docs/TOOLS.md and docs/tools.json from source
scripts/gen-postman.py Regenerates the Postman collection from docs/tools.json
scripts/setup.sh One-command install + verify into any org
scripts/scratch.sh, scripts/package.sh Scratch org bootstrap; unlocked package create/version/install
permissionsets/SObjectActions_User Execute access to all action classes (no object CRUD)
eca/main/default/externalClientApps/SObjectActionsClient (+ oauth settings, global oauth, policies) The OAuth client for MCP: MCP + refresh scopes, PKCE, JWT tokens, common callbacks (own package dir: global OAuth settings are not packageable)
.github/workflows/validate.yml 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 SObjectActionsExt4Test

Notes:

  • The McpServerDefinition developer name must be alphanumeric, start with a letter, 2-40 chars (underscores are rejected). It is SObjectActions.
  • apiIdentifier values are aa: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

  1. Setup > MCP Servers > SObject Actions > link External Client App SObject Actions MCP Client (deployed by this repo) > Activate.
  2. Client authentication details: docs/CLIENT_AUTH.md.
  3. 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.
  4. Point your MCP client (Claude, Agentforce, Cursor, etc.) at the server URL shown in Setup. tools/list returns checkAccess, 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
record / records (SObject) From the SObject's actual type; anything other than Task/Event is rejected
recordsJson Each element must carry {"attributes":{"type":"Task"}} or "Event"; missing -> error
recordId / recordIds From the Id prefix (00T = Task, 00U = Event); other prefixes rejected

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
record SObject Flow Type must match objectApiName
records List<SObject> Flow Same
recordsJson String MCP / REST JSON object or array of {field: value} maps

recordsJson rules:

  • Field names must exist on the object (direct field API names or relationship names such as Account for external-Id upserts). Unknown names cause the whole request to fail with Unknown 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 as true/false, numbers unquoted.
  • attributes.type, if present, must equal objectApiName (or Task/Event for Activity).

Id inputs (read / delete)

  • recordId (read only) and/or recordIds (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 when Activity).

allOrNone (create / update / delete)

  • Default false: partial success; each failed row is reported in errors as row <n>: <message>.
  • true: the entire request rolls back on any failure; failureCount = number of rows, errors holds the DML exception message.

Labels

recordLabels is aligned index-for-index with recordIds. Label source:

Object Label
Case CaseNumber - Subject (Subject omitted if blank)
Everything else The object's describe name field (Name, Subject for Task/Event, CaseNumber... )
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
isSuccess Boolean true only if every row in the request succeeded (read: every Id found)
successCount / failureCount (CUD) Integer Row counts
foundCount / notFoundCount (read) Integer Row counts
recordIds List<String> Affected/found Ids, in input order
recordLabels List<String> Aligned with recordIds
errors List<String> row <n>: <msg> per failed row, or a single request-level error
message String One-line summary, e.g. 2 created, 1 failed.
recordUrls List<String> Lightning record URLs aligned with recordIds (create/read/update/upsert/clone/find/search/related)

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
objectApiName String (req) Activity allowed for mixed Task/Event Ids
recordId String Single Id
recordIds List<String> Bulk; duplicates collapsed
fields List<String> Optional. Field API names, relationship paths allowed (Owner.Name, Account.Name). Empty = all fields accessible to the running user. Id and the label fields are always included.

Outputs:

Field Notes
isSuccess true if every requested Id was found and visible
foundCount, notFoundCount
recordIds, recordLabels Found records, input order
recordsJson JSON array of found records (serialized SObjects, includes attributes.type) - MCP-facing
record, records First / all found records as SObjects - Flow-facing
notFoundIds Ids that do not exist or are not visible under sharing
errors, message

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
objectApiName (req) Must be queryable. Use Task/Event, not Activity.
filtersJson JSON array (or single object) of {"field","op","value"}. Ops: =, !=, <, <=, >, >=, LIKE, IN, NOT IN. value may be null with =/!=. IN/NOT IN need an array. Filter fields must be direct, filterable fields (no relationship paths).
filterLogic AND (default) or OR, across all filters.
fields Extra fields to return; relationship paths allowed (Owner.Name). Id + label fields always included.
orderBy "Field" or `"Field ASC
limitCount 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
objectApiName Activity describes Task.
includeFields default true
fieldNameContains 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
objectNameContains case-insensitive filter on API name or label
customOnly 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
searchTerm (req) min 2 chars; * and ? wildcards allowed
objectApiNames default Account, Contact, Lead, Opportunity, Case; each must be searchable; Activity not allowed (use Task/Event)
searchIn ALL (default), NAME, EMAIL, PHONE, SIDEBAR
fields extra fields, applied only where the field exists on that object
limitCount 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
parentRecordId (req) parent object inferred from the Id
relationshipName (req) child relationship name on the parent (Contacts, Cases, Opportunities, Tasks, Events, My_Children__r); case-insensitive; see describeObject.childRelationshipsJson
fields, orderBy, limitCount 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
objectApiName (req) Task/Event, not Activity
filtersJson, filterLogic identical to findRecords
groupByField optional groupable field; up to 200 groups, ordered by count desc; null group reported as null

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
objectApiName CRUD check (Activity -> Task)
fields requires objectApiName; each reported as {apiName, exists, readable, editable, createable}
recordIds up to 200; uses UserRecordAccess -> {recordId, hasRead, hasEdit, hasDelete, hasTransfer, maxAccessLevel} (None when not visible / nonexistent)

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
objectApiName (req) Activity for Task/Event mix
recordIds (req) sources; not-found / invisible Ids fail per row
overridesJson JSON object applied to every clone (field names validated)
excludeFields fields not to copy (e.g. OwnerId, external Ids)
allOrNone

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
objectApiName (req) Task/Event, not Activity
aggregationsJson (req) [{"function","field"}]; functions COUNT, COUNT_DISTINCT, SUM, AVG, MIN, MAX; SUM/AVG need numeric fields; field must be aggregatable
filtersJson, filterLogic as findRecords
groupByFields 0-3 groupable fields
limitCount 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
flowApiName (req) active autolaunched flow
inputsJson JSON object of input variable -> value
outputVariableNames 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 with Activity).
  • Read the outputs Created Record Ids / Record / Records / Errors into 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 -> notFoundIds on read, row error on update/delete.
  • The classes are global with sharing (required for MCP discovery).
  • No Database.query string is built from raw caller input: object names are validated through describe, field names through the field map, Ids through Id.valueOf; relationship paths in readRecords.fields are 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 deleteRecords from 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 recordsJson payloads or readRecords with no fields on wide objects and hundreds of Ids can approach heap/CPU limits; pass an explicit fields list 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
objectApiName is required. Missing input Supply the API name
Unknown object API name: X Typo / not visible Check spelling and object access
Activity is polymorphic. ... Used Activity without a way to resolve Task vs Event Use Task/Event, or add attributes.type per JSON row
Record of type X does not match objectApiName Y. SObject input of the wrong type Align inputs
attributes.type X does not match objectApiName Y. JSON row typed differently Remove or fix attributes.type
recordsJson is not valid JSON: ... Malformed string Ensure it is a JSON object or array, properly escaped
recordsJson must be a JSON object or array of objects. Scalar/other JSON Wrap in {} or []
Unknown field(s) on X: a, b Field names not on the object Fix API names (__c suffix, namespace)
Could not build X from JSON: ... Value type coercion failed (bad date format, etc.) Use ISO-8601 dates/datetimes, correct types
No records supplied. ... Nothing to act on Provide record, records, recordsJson, or Ids
Invalid record Id: ... Not a valid 15/18-char Id Fix Id
Id X is a T and does not match objectApiName Y. Wrong prefix Fix objectApiName or Id
row n: Id is required for update. Update row without Id Add Id
Every record must include Id for delete. SObject delete input without Id Add Id
Unknown field on X: f (read/find) Bad entry in fields / orderBy / externalIdField Fix API name
Field X on Y is not an external Id / idLookup field ... Bad externalIdField Use an external Id field or Id
Unknown filter field on X: f (relationship paths are not allowed in filters). Bad/dotted filter field Use a direct field
Field X is not filterable. Long text / encrypted etc. Filter on another field
Unsupported op "X" Bad op Use =, !=, <, <=, >, >=, LIKE, IN, NOT IN
Value "x" is not valid for field F (TYPE) Coercion failed Match the field type / ISO date format
limitCount must be between 1 and 200. Out of range Adjust
row n: <platform message> [Field] 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 to mcp/main/default/mcpServerDefinitions/SObjectActions.mcpServerDefinition-meta.xml with apiIdentifier = aa:apex-<ClassName> and redeploy; re-activate the server in Setup if needed.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选