8.6 KiB
Example: Nested-object payload field (Apex-class-typed response field)
A complete walkthrough of the flow when one @InvocableVariable field on the response class is
itself another Apex class, not a primitive (source = apex, nested-object branch). This is the
second, additive payload shape alongside the flat-primitive shape in apex-invocable-source-prompt.md
— read references/mcp-tool-output-discovery.md ("Nested-object payload fields") and
references/two-clt-modeling.md ("Nested-object payload fields (a second, additive case)") first.
The prompt
I have an MCP server tool backed by the
GetFlightDetailsActionApex invocable action. Build a widget that renders its output as a flight details card.
Phase 1 — Input selection
- Source:
apex(the prompt names an Apex Invocable class; no org describe needed for this walkthrough). - Tool API name:
getFlightDetails(from the class nameGetFlightDetailsAction, stripping the trailingActionsuffix and lower-camelCasing — see the naming convention inreferences/two-clt-modeling.md).
Phase 2 — Payload discovery
Read references/mcp-tool-output-discovery.md, then locate .../classes/GetFlightDetailsAction.cls.
The invocable method:
@InvocableMethod(label='Get Flight Details' description='Returns flight details based on input ID, Origin, and Destination')
public static List<FlightDetailsResponse> getFlightDetails(List<FlightDetailsRequest> requests) { ... }
public class FlightDetailsResponse {
@InvocableVariable(label='Flight Details')
public SearchFlightsAction.Flight flightInfo;
}
- Response class =
FlightDetailsResponse(theList<...>element type) → payload source. - Request class =
FlightDetailsRequest→ excluded (tool input). - The single
@InvocableVariablefield,flightInfo, is declaredSearchFlightsAction.Flight— another Apex class, notString/Integer/etc. This is the nested-object branch: it does not go in the primitive Apex→CLT mapping table.
Enumerate the referenced class (SearchFlightsAction.Flight) for its own @InvocableVariable
leaf fields — these become the widget's flat properties:
public class Flight {
@InvocableVariable public String flightId;
@InvocableVariable public String origin;
@InvocableVariable public String destination;
@InvocableVariable public String departureTime;
@InvocableVariable public String arrivalTime;
@InvocableVariable public Long price;
}
| Leaf field | Apex type | CLT / widget lightning:type |
|---|---|---|
| flightId | String | lightning__textType |
| origin | String | lightning__textType |
| destination | String | lightning__textType |
| departureTime | String | lightning__textType |
| arrivalTime | String | lightning__textType |
| price | Long | lightning__numberType (widget uses lightning__numberType for all numerics; the response CLT would use lightning__integerType if this leaf were typed directly on the response CLT — but it isn't, since it's flattened through @apexClassType, see below) |
payloadFields (top-level) = 1 field: flightInfo, typed as an Apex class → nested-object branch.
payloadFields (flattened, for the widget) = the 6 leaf rows above.
Phase 3 — Build plan (abridged)
MCP Tool Widget Build Plan: getFlightDetailsWidget
PLAN: Render the GetFlightDetails MCP tool output as a flight-details card.
TOOL / SOURCE:
Tool API name: getFlightDetails
Payload source: Apex Invocable class GetFlightDetailsAction
Response class FQN: GetFlightDetailsAction.FlightDetailsResponse
LIGHTNING TYPES:
Response CLT: getFlightDetailsResponse
Properties: flightInfo → "@apexClassType/c__SearchFlightsAction$Flight" # nested-object field, NOT {"type":"object"}
Envelope CLT: getFlightDetails
Renderer (default, bundle root): .../lightningTypes/getFlightDetails/renderer.json
Envelope: actionName (text), isSuccess (boolean), outputValues (c__getFlightDetailsResponse)
WIDGET: getFlightDetailsWidget
Schema: flattened to flightInfo's 6 leaf fields (flightId, origin, destination, departureTime, arrivalTime, price)
Renderer binding: each attribute → {!$attrs.outputValues.flightInfo.<leaf>} # two levels deep, not one
Properties omitted: none
GENERATION ORDER: response CLT → widget → envelope CLT
Proceed unless the next reply pushes back.
Phase 4 — Generation order
-
Response CLT
getFlightDetailsResponse:{ "title": "Get Flight Details Response", "description": "Response fields from the GetFlightDetailsAction invocable-action (FlightDetailsResponse)", "type": "object", "lightning:type": "lightning__objectType", "lightning:tags": ["mcp"], "unevaluatedProperties": false, "properties": { "flightInfo": { "title": "Flight Info", "lightning:type": "@apexClassType/c__SearchFlightsAction$Flight" } } } -
Widget
getFlightDetailsWidget— flat schema over the 6 leaf fields (not the singleflightInfofield); body binds{!$attrs.flightId},{!$attrs.origin}, etc. -
Envelope CLT
getFlightDetails:{ "title": "Get Flight Details", "description": "Invocable-action result envelope for the GetFlightDetailsAction MCP tool", "type": "object", "lightning:type": "lightning__objectType", "lightning:tags": ["mcp"], "unevaluatedProperties": false, "properties": { "actionName": { "title": "Action Name", "lightning:type": "lightning__textType" }, "isSuccess": { "title": "Is Success", "lightning:type": "lightning__booleanType" }, "outputValues": { "title": "Output Values", "lightning:type": "c__getFlightDetailsResponse" } } } -
Default renderer at
lightningTypes/getFlightDetails/renderer.json—definition: @widget/c/getFlightDetailsWidget, each attribute bound two levels deep through the nested object:{ "renderer": { "componentOverrides": { "$": { "definition": "@widget/c/getFlightDetailsWidget", "attributes": { "flightId": "{!$attrs.outputValues.flightInfo.flightId}", "origin": "{!$attrs.outputValues.flightInfo.origin}", "destination": "{!$attrs.outputValues.flightInfo.destination}", "departureTime": "{!$attrs.outputValues.flightInfo.departureTime}", "arrivalTime": "{!$attrs.outputValues.flightInfo.arrivalTime}", "price": "{!$attrs.outputValues.flightInfo.price}" } } } } }
Phase 5 — Validation
clt-reference-integrity: envelopeoutputValues→c__getFlightDetailsResponse, response CLT exists,flightInfouses@apexClassType/c__SearchFlightsAction$Flight(not a bare{"type":"object"}and not an inlinedlightning__objectType), no$schema/items→ pass.renderer-wires-widget: bundle-root renderer present, definition@widget/c/getFlightDetailsWidget, every widget property bound two levels deep as{!$attrs.outputValues.flightInfo.<property>}(not one level, which would bind to an unresolvable object) → pass.field-trace: INVOCABLE_FIELDS (top-level, per the gate's literal definition) =flightInfo— a single nested-object field, not a primitive. Because it resolves to@apexClassType/...(seeclt-reference-integrityabove), it expands to its referenced class's own leaf fields before comparing against the widget:flightId, origin, destination, departureTime, arrivalTime, price. Widget properties match this expanded leaf set exactly; INVENTED empty; OMITTED empty → pass.
Notes
- Why not a single-level CLT. Typing
flightInfoas{"type":"object"}on the response CLT produces an opaque blob with no leaf fields — not renderable, and rejected by intent even where the CLT metaschema would technically accept a generic object. The@apexClassType/<ns>__<OuterClass>$<InnerClass>reference is what actually deploys (verified against a hand-fixed, successfully deployed bundle). - This is additive, not a replacement. A response class with only primitive
@InvocableVariablefields (theAccountSummary/GetOrderStatusexamples) still uses the flat mapping tables unchanged. Check each field independently — a response class can mix primitive and Apex-class-typed fields. List<ApexClass>fields (e.g.SearchFlightsAction.FlightSearchResponse.availableFlights, aList<Flight>) are out of scope for this beta single-response flow — surface them in the build plan the same way amaxOccurs > 1scalar is surfaced, rather than emitting a schema for them.