Invocable actions
The package ships six runtime actions and two administration actions. Register them as Agentforce agent actions or MCP tools, or call them from Flow. All sit in the Akoonu category and return the same response shape. Update Records is optional: an agent without it registered cannot change anything at all. The administration pair belongs on its own topic and MCP server, never mixed in with the runtime actions.
How it works
The actions form a discovery sequence:
An agent that calls Get Catalog first learns your real object and field names instead of guessing them.
Get Catalog
Discovery in two depths. With no inputs it returns a version-stamped index, grouped by domain: one section per domain the caller holds, carrying that domain’s purpose, synonyms, and instruction, then a table of the entities it serves. Pass entity names for each one’s full contract: fields with the smallest level that includes each, filterable fields with their live picklist values and sampled example values, writable fields, synonyms, and instructions.
Grouping is what makes a domain selectable: a merged list would have to be re-partitioned before an agent could choose from it. An entity in several domains appears in each section, and the API name is its identity.
The index carries what you choose on. Entity instructions are not in it — they can be long, and they tell you how to read data you have not selected yet. They arrive with the detail call and with every payload.
Briefs are listed too, and a brief bound at the in catalog tier has its body printed in full here rather than only a line — the material an agent should simply know, delivered in the call it was already making instead of a second round trip. → Delivery
| Input | Type | Notes |
|---|---|---|
domainApiName | String | Optional. Only the entities this domain serves — live coverage for a scoped tool |
objectApiName | String | Optional. Only the entity for this object |
entityApiNames | String | Optional. Comma-separated names from the index — returns each entity’s full contract, up to 5 per call |
knownVersion | String | Optional. The stamp from a previous call; an unchanged catalog answers with one line instead of repeating itself |
format | String | markdown (default) · json |
Every action resolves what the calling user may discover before anything else. A domain they have not been granted is refused; naming none serves whichever domains they hold. An entity with no active version is served to nobody. → Domain access
JSON carries the same grouping by reference, and the map reads both ways: entities stays a flat array where each entity lists the domains serving it, and domains[] gives each domain {apiName, label, description, instruction, synonyms, isDefault, entities[]}. Markdown gets the association from position; JSON needs it stated, or a consumer walking entities would have to build the reverse index itself.
The detail call carries it too — each entity contract lists its domains, and the markdown says Available through:, since a contract has no domain heading to sit under.
The detail’s fields arrive as a markdown table — one row per field, with a single legend line above it expanding the Lvl (L/S/D, or — for filter-only), F (filterable) and W (writable) codes rather than repeating the words on every row.
Picklist values in the detail are the complete valid set for filters and writes — the API values, not labels. An agent working through a scoped tool that already names its entity can skip the index entirely.
Find Records
Returns a filtered set in the entity’s curated list columns, with totals over the full matching set — a query capped at 10 rows still reports the true count and sums. The same action also aggregates (a grain change: groups instead of rows) and scopes (whose records).
| Input | Type | Notes |
|---|---|---|
objectApiName | String | The object to query. Optional when entityApiName is given |
entityApiName | String | Optional named entity — its curated columns are returned |
filters | String | JSON filter list, on the entity’s filterable fields only. → Filters |
filterNames | String | Comma-separated named filters from the catalog (open,committed). They compose by AND, with each other and with filters. → Named filters |
scope | String | Whose records: all (the default — every record visible to the caller), me, user:<id|name>, team:<id|name> (that user plus direct reports), rollup:<id|name> (their full subtree). A name or part of one works — exact match first, contains as backup — and an ambiguous match is refused with the candidates and their ids, so the resolver never picks a person for the caller; me works as the id (rollup:me). Plain hierarchy walks — no forecasting awareness — and sharing still applies on top |
sortBy | String | Field to sort by, ascending unless followed by a direction: Amount DESC. Filterable fields only. Default: last modified, newest first |
maxRows | Integer | Default and cap are org settings (shipped: 20/50); capped requests are noted in the payload |
groupBy | String | One groupable field (the catalog lists them per entity) — returns one aggregate row per distinct value instead of records |
aggregate | String | count, or sum(F) · avg(F) · min(F) · max(F) over an aggregatable field. With groupBy, one value per group, largest first; alone, one summary. When set, level does not apply |
maxGroups | Integer | Groups returned, largest first by the measure; org-capped (default 10). A truncated response says so and still totals the full set |
sampleSize | Integer | Optional records per group — the 3 biggest deals in each stage. Hard-capped at 10; requires groupBy |
sampleLevel | String | list (default) · standard — depth of sample rows |
maxSampledGroups | Integer | How many of the top groups get samples (org-capped, default 5). The remainder are summarised, and the response declares the edge |
format | String | markdown (default) · json · both |
domainApiName | String | Optional: route through a domain. Mutually exclusive with entityApiName; requires objectApiName |
Example filter:
[{"fieldName":"IsClosed","operator":"=","values":["false"]},
{"fieldName":"CloseDate","operator":"<=","values":["THIS_QUARTER"]}]
One call answers “what is my pipeline this quarter by stage, with the biggest deals in each”:
filterNames: open,this_period scope: me
groupBy: StageName aggregate: sum(Amount)
sampleSize: 3 maxSampledGroups: 8
Sorting takes a direction. sortBy is ascending unless you add one — Amount DESC is how “the ten biggest deals” is expressed, with maxRows: 10. The field must still be filterable on the entity; a direction is not a way around that gate, and anything after the field that isn’t ASC or DESC is refused with the grammar named. Each entity’s contract states this in its own terms — which of its fields can be sorted on is the filterable list, so an agent that read the contract already knows what is legal here.
Three behaviors worth knowing. Truncation declares itself: group caps, sample caps, and row caps all state what they cut, and totals always cover the full matching set. Currency converts: in multi-currency orgs the platform aggregates in corporate currency, and measures convert to the caller’s display currency — the same number in your own currency, whoever asks. Ownership narrows, sharing permits: scope narrows to whose records; the user-mode query still narrows to what you may see — ask about someone whose records you can’t access and you get nothing back, correctly, in totals as in rows.
Get Records
One or more records in depth. Pass comma-separated IDs and each comes back in full, so a set found through Find Records can be read without a call per record — its first column is the record ID for exactly this.
| Input | Type | Notes |
|---|---|---|
recordIds | String | Required. One or more record IDs, comma-separated. Objects may differ. me is the calling user’s own User record |
objectApiName | String | Optional — resolved from the record ID when omitted |
entityApiName | String | Optional named entity. When omitted, the object’s default applies |
level | String | list · standard · detailed (default) |
format | String | markdown (default) · json · both |
domainApiName | String | Optional: route through a domain. Mutually exclusive with entityApiName |
Records need not share an object. Each resolves independently — object from the ID, then a view for that object:
| You pass | Each record uses |
|---|---|
| neither | its object’s default entity |
entityApiName | that entity — one object only, since an entity belongs to one |
domainApiName | the domain’s entity for that record’s object |
A pinned address is single-object: entityApiName or objectApiName alongside mixed IDs is refused, and the error names the fix. For non-default views across mixed objects, use a domain — the only address that varies per object.
| Behavior | Detail |
|---|---|
| Cap | 5 records at detailed, 10 otherwise. Past the cap it returns what fits and says how many were asked for |
| Per-record failure | A record that can’t load — most often a domain-scoped call handed an object the domain doesn’t serve — is named in a note; the rest come back |
| Output | markdown payloads are separated by ---. json returns one envelope: {schemaVersion, requested, returned, truncated, records[], errors[]} |
me | recordIds: me loads the calling user’s own User record. An agent asked about the person it is talking to has no way to produce their ID — no runtime action looks users up — so this is the only expression of the commonest question about the commonest record. It composes: me,006… reads you and a deal in one call |
Structure and prose in one call
format takes a third value on Find Records and Get Records: both. The response is the json payload with the markdown rendering inside it, under a markdown key.
It exists for a caller that needs both halves — IDs, names and field values to build a link or a datatable, and the prose to ground a model. Asking twice assembles twice: same queries, same metrics, same related lists, all run again, and two loads on your usage. both renders twice from one assembly, and meters as one load, recorded under its own both format dimension so a usage rollup can still tell the two apart.
Everything the JSON carries, it still carries; the markdown is additive. Injected briefs ride both halves — a domain’s frame reaching the JSON but not the prose handed to the model would break the promise that injection is unconditional. Aggregate queries (groupBy / aggregate) support it too, so the option doesn’t depend on the shape of the question.
Update Records
Changes declared-writable fields on one or more records, through each entity’s write access chain. Two-phase by design: the first call changes nothing and returns a current-vs-proposed diff (with the fields’ instructions) for the user to approve; the same call with confirm=true commits. Every commit, failed commit, and refused attempt is durably logged per field.
A set found through Find Records can be pushed in a single call, so eight deals cost two calls and one approval instead of sixteen calls and eight of them. Every record carries its own changes — the set does not have to be homogeneous.
There are two ways to name records, because the agent surfaces afford different shapes:
| Input | Type | Notes |
|---|---|---|
recordId | String | The record to update, when updating one. Pair with changes |
changes | String | That record’s changes: JSON object of field API name → new value, e.g. {"StageName": "Negotiation/Review"} |
records | String | Several records, each with its own changes: a JSON array of {"recordId": …, "changes": {…}}. Use instead of recordId/changes — naming both is refused |
confirm | Boolean | Omit for the preview diff; true commits. Applies to every record in the call |
changeDigest | String | The digest the preview printed, echoed back with confirm=true. Required wherever the domain requires confirmation. In records, each entry carries its own |
objectApiName | String | Optional — validated against each record ID’s type |
entityApiName | String | Optional named entity whose write contract applies to every record. Default: each object’s default entity |
domainApiName | String | Optional: route through a domain — the domain must allow writes. Mutually exclusive with entityApiName |
records exists because an Agentforce function schema is a flat object of scalars and cannot carry a list — so the list travels as text, the same way Get Records takes comma-separated IDs. MCP and Flow callers may also repeat the whole input; the two axes compose.
{ "records": "[{\"recordId\": \"006…\", \"changes\": {\"StageName\": \"Closed Won\"}}, {\"recordId\": \"006…\", \"changes\": {\"CloseDate\": \"2027-01-01\"}}]" }
| Behavior | |
|---|---|
| Cap | entity_write_max_records (default 10) counts records, not inputs — summed across records entries and repeated inputs. Over it the call is refused whole: nothing previewed, nothing saved, and the refusal names the limit. Get Catalog publishes the number so an agent can size the call |
| One domain, one phase | An input is one entity/domain and one confirm applied to every record in it. A set needing different domains is different inputs |
| Per record | Each record resolves, diffs, and saves on its own. Diffs come back joined by ---, the same convention Get Records uses |
| Not a transaction | The set is not atomic: one record failing validation leaves the others as they were. errorMessage names the records that were not updated; the ones that were are still in result |
| Confirmation | Decided by the domain, not by this action: Allow unattended writes when a domain is named, and always required when none is. Get Catalog reports it as writes.confirmationRequired. Where required, a commit without a matching changeDigest is refused — including one whose record moved since the preview |
A validation-rule failure comes back as data in the rule’s own words — the agent can adjust and retry, or relay the reason. An invalid picklist value is refused with the valid values named. A field outside the writable set refuses that record’s whole write; nothing partially applies within a record.
Response shape
All four return:
| Output | Type | Notes |
|---|---|---|
result | String | The payload — markdown or JSON |
success | Boolean | False when the request failed |
errorMessage | String | Why it failed, with corrective guidance |
errorMessage is written for an agent to read — a domain coverage miss names what the domain currently serves, so the agent can re-plan.
Input descriptions
Every input carries a description written for an agent: date literals like THIS_QUARTER, the 50-row cap, full-set totals, entity/domain exclusivity. The descriptions carry through registration and are the agent’s only documentation — change them only deliberately.
Registering
Name each registration exactly as its class — GetCatalogAction, FindRecordsAction, GetRecordsAction, UpdateRecordsAction, or the generated class names for scoped tools. Register UpdateRecordsAction only where writes belong: an absent tool cannot be misused.
Installed classes carry the package namespace: in a subscriber org an AgentScript target reads apex://ctxl__GetRecordsAction, not apex://GetRecordsAction. One exception in the package’s own namespaced dev org: the apex:// target resolves local classes bare, so it drops the prefix there — while source: still names the function as ctxl__Get_Catalog in both org kinds. The Deploy page generates the right form for the org you’re in; either mistake fails at runtime, not on save.
See Agentforce and MCP.
Calling from Flow
Add an Action element and pick from the Akoonu category — to attach a briefing to a record on a schedule, or to ground a Prompt Builder template. For anything beyond the exposed inputs, call the Apex API from your own invocable.
What’s next
Get Briefs
The retrieve tier of ambient context: briefs — the frame the data should be read in — requested by name from the catalog index, or resolved by domain in its declared order, injected briefs first. A brief bound at the in catalog tier needs no call here at all: Get Catalog already printed its body. Version-stamped: echo the stamp back as knownVersion and an unchanged set answers with one line. The stamp folds in the fiscal period, so a quarter rollover always refreshes, and the unchanged response still carries a fresh computed-context line.
| Input | Type | Notes |
|---|---|---|
briefApiNames | String | Optional. Comma-separated names from the index; capped per call by an org setting |
domainApiName | String | Optional. Everything that domain serves, in its declared order |
knownVersion | String | Optional. The stamp from a previous call |
format | String | Optional. markdown (default) or json |
A brief that none of the caller’s domains carries is absent from every list and unknown by name — enforcement, not filtering.
Get Skill
Progressive disclosure for procedural context: the catalog carries names and one-line descriptions; this returns one skill’s full procedure and output shape when a task matches. Runtime skills require domain routing; admin skills serve on the administration surface.
| Input | Type | Notes |
|---|---|---|
skillApiName | String | Required. From the catalog’s Skills section |
format | String | Optional. markdown (default) or json |
Administration: Get Full Config
The admin read surface: the configuration graph — entities with their instructions and resolved writability, briefs with the domains carrying them, skills, domains — in one call, so an analysis skill reads instead of orchestrating retrieval. Two shapes from one source: analysis (org-resolved: activation status, effective injection, computed writability; deep per-artifact resolution requires a scope) and export (the round-trip bundle). Unscoped reads require an explicit flag. Runs as the calling user: delegated managers receive only their domains’ slice.
| Input | Type | Notes |
|---|---|---|
domainApiName / objectApiName / entityApiNames | String | Scope — pick one |
include | String | Optional. Comma-separated sections: entities, briefs, skills, domains, elements |
shape | String | Optional. analysis (default) or export |
unscoped | Boolean | Optional. Deliberate whole-org read |
Administration: Save Draft
The administration surface’s only write, and it writes brief drafts only — never a version, never the active pointer. Combined with unpublished-means-undelivered serving, nothing an agent composes reaches a running agent until a person reviews it in Studio and activates with a comment; the draft carries a “composed via” attribution the reviewer sees. Delegated managers draft into domains they manage.
| Input | Type | Notes |
|---|---|---|
briefApiName | String | Required. Upserts by name |
label, briefType, description, meaning, refusalPolicy | String | The draft’s identity and content (markdown) |
customerSafe | Boolean | Optional. Default false |
reviewBy, homeDomainApiName | String | Optional |