# MCP Tool Output Discovery Phase 2 resolves the payload shape that defines the response CLT and the widget schema. There are three sources, in order of preference: an invocable action API name (`action`), a pasted tool-output sample (`sample`), or an Apex Invocable class (`apex`). --- | Source | Use when | Authority | |---|---|---| | `action` | An **invocable action API name** is known — directly, or from a class name that resolves to one. **Preferred.** | The org's Actions REST API describes the action's typed outputs directly — it already excludes the request wrapper and private helpers. | | `sample` | The describe 404s or no org is reachable, but a pasted tool-output JSON is available. | Infers types from example values. | | `apex` | Only the Apex **class** name is known, and no org and no sample are available — fallback only, may be stale relative to what's deployed. | Parses `@InvocableVariable` fields from source. | Prefer `action` whenever an action name (directly given, or derived from a class name that resolves to a single invocable action) and an authenticated org are available — it is the same schema the platform itself exposes, so it needs no request/helper filtering and gives real field types. Fall back to `sample`, then `apex`. --- ## `action` — resolve from the invocable action name (preferred) The Salesforce **Actions REST API** describes any custom Apex invocable action, including its output variables and their types. This is authoritative: the `outputs` it returns are exactly the `@InvocableVariable` fields on the response class — the request wrapper and private helper classes never appear. ### 1. Confirm the action API name For an Apex invocable action the action API name is the **Apex class name** that declares the `@InvocableMethod` (e.g. `GetAccountSummaryTest`), not the method label. If the user gave a label ("Get Account Summary") resolve it to the class name — the class name is what the endpoint path uses. ### 2. Describe the action ```bash sf api request rest '/services/data/v63.0/actions/custom/apex/' -o ``` `` is the Apex class name. Use the org's API version (`v63.0` here — match `sourceApiVersion` in `sfdx-project.json` or the org's max). If the org alias is the default, `-o ` may be omitted. (`sf api request rest ...` is the form used elsewhere in this repo; `sf org api request rest ...` is an equivalent alias.) To list all custom Apex actions first (when the exact name is unknown) — the action names are under `.actions[].name`: ```bash sf api request rest '/services/data/v63.0/actions/custom/apex' -o | jq -r '.actions[].name' ``` If the describe returns 404 / `NOT_FOUND`, the action is not deployed or is not exposed as a custom Apex action — fall back to `sample` (a pasted tool-output JSON) if one is available, else `apex` (parse the class), and surface the miss. ### 3. Read the `outputs` array The describe response has an `outputs` array. Each entry describes one payload field: ```json { "outputs": [ { "name": "accountName", "label": "Account Name", "type": "STRING", "maxOccurs": 1 }, { "name": "contactCount", "label": "Contact Count", "type": "INTEGER", "maxOccurs": 1 }, { "name": "totalOpportunityAmount", "label": "Total Opportunity Amount", "type": "DOUBLE", "maxOccurs": 1 } ], "inputs": [ { "name": "accountId", "type": "ID", "required": true } ] } ``` - **Use `outputs` only.** `inputs` is the tool input (the request wrapper) — exclude it, exactly as `apex` excludes the request class. - `name` → the CLT/widget property key. `label` → the property `title`. - A field with `maxOccurs > 1` is a **collection** (list). For the beta, surface it to the user in the build plan — the widget renders a single response, and list payloads need a nested item CLT (out of scope for the default flow). - An entry with **`"type": null` and an `"apexClass"` key instead** (e.g. `{ "name": "flightInfo", "type": null, "apexClass": "SearchFlightsAction$Flight", "maxOccurs": 1 }`) is **not missing data to default to text** — it is the describe's encoding for a field typed as another Apex class. `$` separates the outer class from the inner class, matching the `@apexClassType/__$` convention used elsewhere in this skill. This is the same case as a `List`/nested-object field from the other two sources — see "Nested-object payload fields" below; the Actions REST API describe never exposes that class's own leaf fields, so retrieving/reading the Apex class named in `apexClass` (via `apex` §1/§3) is the correct next step, not a fallback away from `action`. Extract the field list with `jq` (do NOT wrap in `$()`): ```bash sf api request rest '/services/data/v63.0/actions/custom/apex/' -o \ | jq -r '.outputs[] | "\(.name)\t\(.type)"' ``` ### 4. Map Actions-API `type` → CLT `lightning:type` | Actions API `type` | CLT `lightning:type` | |---|---| | `STRING`, `TEXTAREA`, `PICKLIST`, `ID`, `REFERENCE`, `EMAIL`, `PHONE`, `URL` | `lightning__textType` | | `INTEGER`, `INT`, `LONG` | `lightning__integerType` | | `DOUBLE`, `DECIMAL`, `CURRENCY`, `PERCENT` | `lightning__numberType` | | `BOOLEAN` | `lightning__booleanType` | | `DATE` | `lightning__dateType` | | `DATETIME` | `lightning__dateTimeType` | Casing varies by API version (some return `Int`/`Double`, some `INTEGER`/`DOUBLE`) — match case-insensitively. An unrecognized `type` defaults to `lightning__textType`; note the assumption in the build plan. > **Widget schema vs CLT schema type vocabulary** (applies to every source). The response CLT uses `lightning__integerType` for integers. The widget `schema.json` (per `platform-widget-generate`) has no integer type — all numerics are `lightning__numberType`. So integer fields are `lightning__integerType` in the *response CLT* but `lightning__numberType` in the *widget* schema. Do not copy CLT leaf types verbatim into the widget schema. Renderer bindings are strings and type-agnostic. --- ## `sample` — parse from a pasted tool-output JSON (fallback) Use when the action describe 404s or no org is reachable, but a pasted tool-output sample is available. Given a pasted envelope sample, read the `outputValues` object and infer each field's type from its value: | JSON value | CLT `lightning:type` | |---|---| | string | `lightning__textType` | | integer (no fraction) | `lightning__integerType` | | number (fractional) | `lightning__numberType` | | boolean | `lightning__booleanType` | Confirm the envelope keys (`actionName`, `isSuccess`, `outputValues`) against the sample. If the sample nests the payload further (e.g. `outputValues.data.`), the response CLT and the renderer bindings must reflect that extra level (`{!$attrs.outputValues.data.}`) — surface this in the build plan, because it changes every binding. --- ## `apex` — parse the Invocable class (fallback) Use when no org is reachable and no sample is available, but the `.cls` is in the project. ### 1. Locate the class Search the local project first: `/classes/.cls` where `` = `/main/default`. If absent, retrieve from the org: ```bash sf project retrieve start --metadata ApexClass: ``` If it exists nowhere, STOP and surface — the payload shape cannot be enumerated. ### 2. Identify the response class The Invocable method is annotated `@InvocableMethod` and returns `List`. The **response class** is that element type. ```apex @InvocableMethod(label='Get Account Summary') global static List getAccountSummary( List requests ) { ... } ``` - Response class = `GetAccountSummaryResponse` (the `List<...>` element type). **This is the payload source.** - Request class = `GetAccountSummaryRequest` (the parameter element type). **Excluded** — it is the tool input, not output. - Private helper classes (e.g. `private class OpportunityMetrics`) are **excluded** — not part of the invocable's externally visible schema. The Apex author signals this with `private`; respect it. ### 3. Enumerate `@InvocableVariable` fields Read the response class block and list every field annotated `@InvocableVariable`. These become the response CLT `properties` and the widget schema properties (1:1). ```bash # Print the response class field declarations. Do NOT wrap in $(). echo "INVOCABLE_FIELDS:" grep -A1 '@InvocableVariable' /classes/.cls \ | grep -oE '(public|global)\s+[A-Za-z0-9_<>,\s]+\s+[a-zA-Z_][a-zA-Z0-9_]*\s*;' \ | sed -E 's/.*\s([a-zA-Z_][a-zA-Z0-9_]*)\s*;/\1/' \ | sort -u ``` If the grep misses multi-line annotations, read the `.cls` with the Read tool and list fields manually — but do not skip enumeration. Scope the enumeration to the **response class block only**; do not pick up `@InvocableVariable` fields from the request class. ### 4. Map Apex → CLT `lightning:type` | Apex type | CLT `lightning:type` | |---|---| | `String`, `Id` | `lightning__textType` | | `Integer`, `Long` | `lightning__integerType` | | `Decimal`, `Double` | `lightning__numberType` | | `Boolean` | `lightning__booleanType` | | `Date` | `lightning__dateType` | | `Datetime` | `lightning__dateTimeType` | --- ## Nested-object payload fields (applies to every source above) The mapping tables above (Actions-API `type`, Apex type, JSON value/schema `type`) cover **primitive** fields. A field can also be typed as **another Apex class** instead of a primitive — e.g. `GetFlightDetailsAction.FlightDetailsResponse.flightInfo`, typed `SearchFlightsAction.Flight`. This is a second, additive branch — check every payload field against it, regardless of which of the three sources produced the field list: - **Actions API**: an output entry with `"type": null` and an `"apexClass": "$"` key, rather than a primitive `STRING`/`INTEGER`/etc. `type` (see step 3 above). - **`apex`**: an `@InvocableVariable` field whose declared type is not `String`/`Id`/`Integer`/`Long`/`Decimal`/`Double`/`Boolean`/`Date`/`Datetime` but another Apex class. - **`sample`**: a property whose JSON value/schema `type` is `"object"` (not a primitive). **Never** model such a field as `{"type":"object"}` (opaque, unrenderable) or inline it as a nested `lightning__objectType` (rejected by the CLT metaschema — same rule as the envelope↔response relationship). Instead: 1. Type the response CLT property as `"@apexClassType/__$"` (e.g. `"@apexClassType/c__SearchFlightsAction$Flight"`) — the same convention `platform-custom-lightning-type-generate` documents for Apex-backed CLTs. 2. Enumerate the referenced Apex class's own fields (its own `@InvocableVariable`/public members) — these become the widget's leaf properties. The object field itself never appears on the widget. 3. Bind the renderer one level deeper: `{!$attrs.outputValues..}` (e.g. `{!$attrs.outputValues.flightInfo.flightId}`), not `{!$attrs.outputValues.}`. 4. A field typed `List` (a list of nested objects, not a single one) is out of scope for the beta single-response flow — surface it in the build plan like a `maxOccurs > 1` scalar rather than emitting a schema for it. See `references/two-clt-modeling.md` ("Nested-object payload fields") for the worked JSON, and `examples/nested-object-source-prompt.md` for the full end-to-end walkthrough. ## Output of Phase 2 `payloadFields` — an ordered list of `{ name, title, lightning:type }` describing the response payload (primitive fields), plus any nested-object fields resolved to `@apexClassType/...` per above. This drives: - the **response CLT** `properties` (CLT type vocabulary, `lightning__integerType` allowed, `@apexClassType/...` for nested-object fields), - the **widget schema** `properties.attributes.properties` (widget type vocabulary, numerics → `lightning__numberType`; nested-object fields flattened to their leaf properties), - the **renderer** attribute bindings (`{!$attrs.outputValues.}` for primitives, `{!$attrs.outputValues..}` for nested-object leaves). Whichever source produced the fields, record it in the build plan so the reviewer knows whether the schema came from the live org (`action`), source (`apex`), or an example (`sample`).