Apex API
Developer
ctxl.Entity is the package’s Apex entry point. The agent actions and the scoped tools you generate both call it. New options arrive as new request fields, never as new overloads, so build against the request form shown here.
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.
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 |
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 object 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. Returns 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. A truncated response says so |
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 on its own: 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 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 note |
| Per-record failure | A note names each record that can’t load. 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. → JSON schema |
Keep the level lean: detailed multiplies a full 360 by the record count. When you need totals over a set, use Entity.find.
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 or 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 |
Every level has to allow the write, 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 |
The domain decides whether a commit needs confirmation:
| 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. ContextWorks refuses a commit whose digest 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 or 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. An org can allow creating while refusing editing.
The digest is also an idempotency key. Repeat a confirmed create with the same digest inside five minutes and you get back the record already made.
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 to 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. A refused write or create throws it, and so do an unknown entity, an object with no entity, and a malformed request.
try {
String diff = ctxl.Entity.write(req);
} catch (ctxl.Entity.EntityException e) {
// e.getMessage() names what was wrong, and often what to call instead
}
Reads can also throw a type internal to the package, for an object the domain doesn’t serve or a filter on a field the entity doesn’t offer. Your code can’t name those types, so catch Exception around load, find and loadMany when you need to handle them.
Two outcomes return normally. 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 you 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.
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.