Skip to main content
Glama
Kelley-Austin

SObjectActions

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

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"] }

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.

F
license - not found
Not graded
quality - not tested
B
maintenance

Maintenance

–Maintainers
–Response time
–Release cycle
–Releases (12mo)
Commit activity

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

  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables 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.
    12
    19
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables AI agents to interact with Salesforce through MCP, supporting queries, records, metadata, and bulk operations with flexible OAuth authentication.
    MIT

View all related MCP servers

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.

View all MCP Connectors

Latest Blog Posts

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