Apex API
Developer
ctxl.Entity is the package’s Apex entry point: the agent actions and the scoped tools you generate both call it. Its members don’t change shape once released — new options arrive as new fields.
Entity.load — one record
Returns the assembled payload as a String.
// Default entity for the object, markdown
String context = ctxl.Entity.load(recordId);
The object comes from the record ID. The level is the one the entity pins, or the org’s Default detail level when it pins none (shipped: detailed). Every load serves the entity’s active version; an entity never activated serves its draft.
There are no positional overloads. Everything beyond the one-argument form goes through EntityRequest:
ctxl.EntityRequest req = new ctxl.EntityRequest();
req.recordId = recordId;
req.domainApiName = 'sales_desk';
req.entityApiName = 'account_360'; // optional: narrows within the domain
req.level = 'standard';
req.format = 'json';
req.toolName = 'QuarterlyDigest';
String payload = ctxl.Entity.load(req);
| Field | Type | Notes |
|---|---|---|
recordId | Id | Required. The record to assemble |
recordIds | List<Id> | For loadMany. Objects may differ unless entityApiName or objectApiName pins one |
entityApiName | String | A specific entity. Alongside domainApiName it narrows within that domain, which has to carry the entity |
objectApiName | String | Optional. Comes from the record ID when omitted, and has to match it |
level | String | list · standard · detailed. Default: the entity’s pinned level, then the org’s |
format | String | markdown (default) · json · both. both is the JSON payload with the markdown rendering under markdown: one assembly, one metered load |
domainApiName | String | The domain to read through. It picks the entity for the object, and adds the domain’s briefs and Info to the payload |
toolName | String | Your caller’s name, recorded as the Surface in Usage. Without it, loads land under apex |
includeBriefs | Boolean | Domain calls only. false leaves the domain’s briefs out of the payload; on by default |
New call-time options arrive as new EntityRequest fields, never as new overloads. Build against this form.
Entity.find — a set of records
ctxl.EntityFindRequest req = new ctxl.EntityFindRequest();
req.objectApiName = 'Opportunity';
req.filtersJson = '[{"fieldName":"IsClosed","operator":"=","values":["false"]},' +
' {"fieldName":"CloseDate","operator":"<=","values":["THIS_QUARTER"]}]';
req.sortBy = 'Amount DESC'; // ascending without a direction
req.maxRows = 10;
req.format = 'markdown';
req.toolName = 'PipelineReview';
String results = ctxl.Entity.find(req);
| Field | Type | Notes |
|---|---|---|
objectApiName | String | The collection to query. Required unless entityApiName names it |
entityApiName | String | A specific entity, which supplies its own object. Alongside domainApiName it narrows within that domain, which has to carry the entity |
domainApiName | String | The domain to read through. It picks the entity for the object, so it needs objectApiName unless an entity is named |
filtersJson | String | Structured filter list. → Filters |
filterNames | String | Comma-separated named filters from the catalog (open,committed). They combine by AND, with each other and with filtersJson |
sortBy | String | Field to sort by, ascending unless followed by a direction: Amount DESC. Filterable fields only. Default: last modified, newest first |
level | String | list (default) · standard — how much of each row. detailed is refused: pass the IDs to loadMany |
maxRows | Integer | Default and cap are org settings (shipped: 20/50). A capped response says so |
format | String | markdown (default) · json · both. both is the JSON payload with the markdown rendering under markdown: one query, one metered load |
scope | String | Whose records: all (default) · me · user:<id|name> · team:<id|name> · rollup:<id|name>. → Find Records |
groupBy | String | One groupable field — one aggregate row per distinct value instead of records |
aggregate | String | count, or sum(F) · avg(F) · min(F) · max(F). With groupBy, one value per group; alone, one summary |
maxGroups | Integer | Groups returned, largest first. Org-capped; truncation is stated |
sampleSize | Integer | Records per group. Requires groupBy |
sampleLevel | String | list (default) · standard — depth of those sample rows |
maxSampledGroups | Integer | How many top groups get samples. Org-capped |
toolName | String | Recorded as the Surface in Usage |
includeBriefs | Boolean | Domain calls only. false leaves the domain’s briefs out |
Rows come back in the entity’s curated list columns, or its standard columns when you ask for them. Totals cover the full matching set: a query capped at 10 rows still reports the true count and sums. → JSON schema for the result and grouped-result shapes.
Entity.loadMany — several records
ctxl.EntityRequest req = new ctxl.EntityRequest();
req.recordIds = recordIds;
req.level = 'list';
String context = ctxl.Entity.loadMany(req);
Assembles each record through the same path as load.
| Behavior | Detail |
|---|---|
| Mixed objects | Each record resolves independently: object from the ID, then that object’s default entity — or the domain’s entity for it when domainApiName is set. An entityApiName alongside a domain narrows within it and therefore names one object |
| Pinned address | entityApiName or objectApiName alongside mixed IDs throws. Either one names a single object |
| Cap | 5 records at detailed, 10 otherwise, both org settings. Past the cap it assembles what fits and appends a visible note |
| Per-record failure | A record that can’t load is named in a trailing note; the rest come back |
| Markdown | The record payloads joined by ---, with briefs, Info and the computed line once at the top |
| JSON | One envelope — requested, returned, truncated, records, errors — not a joined string. → JSON schema |
Use a lean level: detailed multiplies a full 360 by N. For sets, prefer Entity.find — it returns totals.
Entity.write — a curated update
ctxl.EntityWriteRequest req = new ctxl.EntityWriteRequest();
req.recordId = oppId;
req.domainApiName = 'sales_desk';
req.changesJson = '{"StageName": "Negotiation/Review", "CloseDate": "2026-09-30"}';
req.confirm = false; // preview first — changes nothing
String diff = ctxl.Entity.write(req);
// show the diff, get approval, then the same request with confirm = true
| Field | Type | Notes |
|---|---|---|
recordId | Id | Required. The record to update |
changesJson | String | JSON object of field API name → new value. Picklists take the API value; dates are yyyy-MM-dd |
confirm | Boolean | false/omitted returns the diff and changes nothing; true commits |
objectApiName | String | Optional; has to match the ID’s type |
entityApiName | String | The entity whose write declarations apply. Default: the object’s default entity, or the domain’s entity for it when domainApiName is set. Alongside domainApiName it narrows within that domain, which has to carry the entity |
domainApiName | String | The domain to write through. That domain has to allow writes |
changeDigest | String | The digest the preview returned, passed back with confirm = true. Required wherever confirmation applies |
toolName | String | Recorded as the Surface, in Usage and on every write-log row |
A write has to clear every level in turn, and any one of them can refuse it:
| Level | What allows it |
|---|---|
| Org | Allow writes in Settings |
| Domain | Allow writes on the domain, when the call names one |
| Entity | Write Access on the entity the call resolves to |
| Field | The field picker’s Write column |
| Platform | FLS, sharing and validation rules — the DML runs in user mode |
Confirmation is a separate question, and the domain answers it:
| The call | Confirmation |
|---|---|
| Names a domain | Allow unattended writes on that domain decides |
| Names no domain | Always confirms |
Where confirmation applies, a commit needs the preview’s changeDigest, and ContextWorks refuses one that is missing or no longer matches the record’s current values. Every commit, failure and refused attempt writes per-field rows to the write log before the call throws. → Write access
Entity.create — a new record
ctxl.EntityCreateRequest req = new ctxl.EntityCreateRequest();
req.objectApiName = 'Task';
req.domainApiName = 'sales_desk';
req.valuesJson = '{"Subject": "Follow up", "WhatId": "' + oppId + '"}';
req.confirm = false; // preview the proposed record
String preview = ctxl.Entity.create(req);
| Field | Type | Notes |
|---|---|---|
objectApiName | String | Required. The object to create |
valuesJson | String | JSON object of field API name → value |
confirm | Boolean | false/omitted returns the proposed record and creates nothing; true commits |
entityApiName | String | The entity whose create declarations apply. Alongside domainApiName it narrows within that domain, which has to carry the entity |
domainApiName | String | The domain to create through |
changeDigest | String | The preview’s digest, passed back with confirm = true |
toolName | String | Recorded as the Surface |
Creating clears its own chain: Allow record creation at the org, then the domain, the entity, and the field picker’s Create column. So an org can allow creating while refusing editing.
The digest doubles as an idempotency key. Repeat a confirmed create with the same digest inside five minutes and you get back the record already made, not a second one.
Governor limits
Assembly is query-bound: one query for the record, one batched query for all metrics, one per related list and reference, plus whatever each enrichment source runs. A detailed assembly runs 6–8 queries, which is why loadMany caps at 5 of them. Calling load in a loop will hit limits; Entity.find is one query at any level it serves.
Error handling
ctxl.Entity.EntityException is the only exception type the package exposes globally. An unknown entity, an object with no entity and a malformed request throw it. Everything else — an object the domain doesn’t serve, a filter on a field the entity doesn’t offer, a refused write — throws a type internal to the package. Catch Exception:
try {
String context = ctxl.Entity.load(req);
} catch (Exception e) {
// e.getMessage() names what was wrong, and often what to call instead
}
Two outcomes are not exceptions. A record the caller can’t see comes back as a not-found payload. An element that fails degrades to an issue, and the rest of the payload arrives.
Calling out after a load
Assembly leaves no uncommitted DML, so a caller can load context and then make a callout in the same transaction — an LLM, a Models API request, any HTTP call. Usage telemetry publishes as a Publish Immediately platform event, which the platform’s callout rule ignores. An embedded orchestrator can assemble context and call a model in one go.
Sharing
Every query runs as the calling user, in user mode. Calling Entity.load from a without sharing context does not widen what comes back. See Security.