16 KiB
Response Mapping Model
Step 17.0 design document — established 2025
This document records the canonical decisions about how action responses flow into component state in the Conductor Preview runtime. It is the authoritative reference for Steps 17.x implementation.
Table of Contents
- Why
project.bindingsis canonical - Why actions own runtime responses
- Why
actions.<id>.responseis a virtual namespace - Source path grammar
- Target path grammar
- Trigger semantics
- Label runtime-value behaviour
- Compatibility — existing
onClickbindings - Deprecation of
action.responseMapping - Step 17.1 scope
- Deferred capabilities
- Required diagnostics
1. Why project.bindings is canonical
Two mechanisms could in principle move action response data into components:
| Mechanism | Location | Status |
|---|---|---|
action.responseMapping[] |
Inside each Action object |
Deprecated |
project.bindings[] |
Top-level project array | Canonical |
project.bindings was chosen as the sole canonical mechanism for the following
reasons:
Single locus of data-flow declarations. All data movement in a Conductor project — between components, from action responses, from variables — is expressed in one flat array. Keeping response routing there avoids a split where "inbound response routing" lives inside actions while all other data movement lives at the project level.
Symmetry with component-sourced bindings. A binding from a TextInput value to a Label uses the same structure as a binding from an action response to a JsonViewer. Same schema, same Inspector rendering, same future transform field.
Decoupled from action definition. An action definition describes how to call an endpoint (URL, method, auth). Where the response goes is a layout concern, not an API concern. Separating them means a single action can feed multiple targets without duplicating the action.
Better Inspector and diagnostics surface. A flat array of bindings is easy to enumerate, validate cross-references for, and display in a panel. Distributing response mapping rules across individual action objects makes completeness checks harder.
Incremental migration path. Existing documents that carry
action.responseMapping values remain valid under the schema; they are simply
ignored at runtime. New documents use only project.bindings.
2. Why actions own runtime responses
Although project.actions is a design-time array of REST action definitions,
the Preview runtime must store ephemeral per-action state: the most recent
response, loading flag, and error. This state is:
- Not stored in canonical project JSON. The project document is the design-time source of truth. Mutating it with runtime values would corrupt the save/load round-trip.
- Keyed by action ID. Even though actions are stored as an ordered array
in the project document, the runtime resolves them by
idfor O(1) lookup. - Discarded on navigation or refresh. Preview state is ephemeral React
state (
useState).
The proposed runtime shape (to be implemented in Step 17.1) is:
type ActionRuntimeState = {
response?: ProxyResponse; // the full ProxyResponse envelope
loading?: boolean;
error?: string;
};
type ActionRuntimeStateMap = Record<string, ActionRuntimeState>;
This is a parallel structure to the existing componentState map in
usePreviewRuntime. In Step 17.1 it will be introduced alongside it.
Step 17.0 note: This runtime shape is defined here for design purposes. It is not implemented in Step 17.0. The current runtime continues to store the full
ProxyResponseenvelope directly incomponentState[id].value.
3. Why actions.<id>.response is a virtual namespace
In binding source and target expressions, paths of the form:
actions.<actionId>.response
actions.<actionId>.response.body
actions.<actionId>.response.body.<field>
refer to runtime action response data. This is a virtual namespace because:
-
project.actionsis an array in the canonical document, not an object keyed by ID. At design time,actions.action_httpbindoes not exist as a JSON key path. -
The runtime resolves
<actionId>viaArray.find((a) => a.id === actionId), not by property access. -
The path is only meaningful during Preview execution. It has no value in the persisted project document.
The path syntax is chosen for readability and symmetry with components.<name>.*
target paths. It is not JSONPath; it is a Conductor runtime dot-path with
a fixed actions.<id>.response[.body[.<fields>]] prefix.
4. Source path grammar
Step 17.1 will support the following source path forms for action-response bindings:
actions.<actionId>.response
actions.<actionId>.response.body
actions.<actionId>.response.body.<field>
actions.<actionId>.response.body.<nested>.<field>
Resolution rules
| Path | Resolved value |
|---|---|
actions.<id>.response |
The full ProxyResponse envelope { ok, status, statusText, headers, body, durationMs } |
actions.<id>.response.body |
ProxyResponse.body — parsed JSON object or text string |
actions.<id>.response.body.<field> |
ProxyResponse.body.<field> where body is an object |
actions.<id>.response.body.<nested>.<field> |
Deep dot-path traversal on body |
If a field in the path does not exist on the response body, the resolved value
is undefined. Step 17.1 will surface a runtime warning in that case; it
will not throw.
Deferred source path forms
The following are explicitly deferred and must not be inferred from the above:
actions.<id>.response.headers.<header>— reserved; not in Step 17.1actions.<id>.response.status— reserved; not in Step 17.1- Wildcard
*segments - Filter expressions
[?(...)] - Array index syntax
[0],[-1] - Keys containing literal
.characters
5. Target path grammar
Step 17.1 will support only:
components.<componentName>.value
Semantic enforcement
<componentName>must matchcomponent.name(notcomponent.id) in the project's page components, consistent with the existing binding model.- The property segment must be
.value. The Inspector must emit a warning for targets with any other property segment (e.g..data,.message,.label). The runtime must not silently remap these to.value.
Property segment rationale
.value is the single runtime-mutable property exposed by ComponentRuntimeState
for display purposes. Until per-property runtime targeting is designed:
components.<name>.valueis valid.components.<name>.data,.message,.label,.text, etc. are unsupported. They must produce an Inspector diagnostic and a runtime warning.
Deferred target forms
variables.<variableName>— variable runtime support is deferredcomponents.<name>.label— Label runtime-value override is deferred to Step 17.1 (see §7)- Per-property routing for non-
.valuesegments
6. Trigger semantics
Canonical trigger for action-response bindings: onSuccess
{ "trigger": "onSuccess" }
onSuccess fires when the action completes and ProxyResponse.ok === true.
This is the trigger all new action-response bindings should use.
Why not onChange (the schema default)?
The schema's binding.trigger default is "onChange". This default is
appropriate for component-to-component data flow (e.g. a TextInput value driving
a downstream Label). It is not appropriate for action-response bindings because:
- Action responses are not "change" events on a component; they are completion events on an async operation.
"onChange"implies reactive/continuous behaviour; action execution is discrete and user-triggered (or lifecycle-triggered).
The "onChange" default is therefore not the correct default for
action-response bindings. The Inspector should surface a warning when
"onChange" is found on a binding whose source is actions.*.
Deferred triggers
| Trigger | Status |
|---|---|
onSuccess |
Step 17.1 |
onError |
Deferred |
onLoad |
Deferred |
onChange |
Component-change bindings only; deferred |
| Chained actions | Deferred |
7. Label runtime-value behaviour
Current (Step 17.0): Label renders properties.label only. It has no
runtimeState wiring. Runtime action responses cannot update a Label.
Planned (Step 17.1):
properties.label— design-time / configured text. Displayed when no runtime value is set.componentState[id].value— runtime display value. If set (notundefined), it overridesproperties.labelin the Preview renderer.- A binding of the form:
writes the resolved field value (coerced to string for display) into{ "source": "actions.<id>.response.body.<field>", "target": "components.<labelName>.value", "trigger": "onSuccess" }componentState[labelId].value. - Runtime mutations must not modify canonical project JSON. The override
is ephemeral; it lives only in
useStatefor the Preview session.
This makes Label consistent with JsonViewer, which already reads
runtimeState.value in PreviewComponent.tsx.
8. Compatibility — existing onClick bindings
Steps 15 and 16 produced examples and docs using:
{ "trigger": "onClick" }
on action-response bindings. These bindings fire when the button's onClick
event causes the action to execute.
Step 17.0 treatment: "onClick" bindings continue to work exactly as they
do today. The runtime (usePreviewRuntime) fires all bindings whose source
matches the action ID, regardless of their trigger value.
Recommended migration: New bindings should use "onSuccess" rather than
"onClick". The Inspector may surface an info-level diagnostic recommending
migration on bindings that use "onClick" as the trigger on an
actions.* source. This is not yet implemented.
Important: "onClick" on an action-response binding is legacy, not
invalid. Documents using it will not fail schema validation and will not be
broken by Step 17.1.
9. Deprecation of action.responseMapping
action.responseMapping is deprecated as of Step 17.0.
| Property | Status |
|---|---|
| Present in schema | Yes — retained for backward compatibility |
x-deprecated: true annotation |
Added in Step 17.0 |
Schema description |
Updated to say project.bindings is canonical |
TypeScript @deprecated JSDoc |
Added in Step 17.0 |
| TS type narrowed | Yes — changed from unknown[] to ResponseMappingRule[] |
| Executed by Preview runtime | No — never was, never will be |
| Executed by backend proxy | No — never was, never will be |
| Shown by Inspector | No |
| Removal timeline | Not scheduled; retained for backward compatibility only |
No new examples should use action.responseMapping. Existing examples
(valid-full.json) that carry it are not updated; they remain valid because
the field is optional and the schema accepts (but ignores) its contents.
10. Step 17.1 scope
Step 17.1 will implement execution of project.bindings for
action-response-to-component data movement. Specifically:
-
ActionRuntimeStateMap— introduce alongside the existingcomponentStatemap inusePreviewRuntime. After each proxy call, write theProxyResponseintoactionState[actionId].response. -
Source path resolution — implement dot-path traversal for
actions.<id>.response,.response.body, and.response.body.<field...>. -
Binding execution loop — after a successful proxy call, iterate
project.bindingswhosetriggeris"onSuccess"(or"onClick"for legacy compatibility) and whosesourceparses to the completed action ID. Resolve the source path againstactionState[actionId].response. Write the resolved value tocomponentState[targetId].value. -
Label renderer update — make
LabelRendererreadruntimeState?.valueas an override overproperties.label, consistent withJsonViewerRenderer. -
Inspector diagnostics — add warnings for:
- Unsupported target property segments (anything other than
.value) "onChange"trigger on action-response bindings (recommend"onSuccess")- Bindings whose source action ID does not exist
- Bindings whose target component name does not exist
- Unsupported target property segments (anything other than
-
valid-response-mapping-basic.json— the example created in Step 17.0 becomes the primary Step 17.1 integration test document.
11. Deferred capabilities
The following are explicitly out of scope until separately designed and scheduled:
| Capability | Reason deferred |
|---|---|
onError trigger |
Requires error-state routing design |
onLoad trigger |
Requires page lifecycle event system |
Component-change data movement (onChange) |
Requires reactive binding engine |
| Chained actions | Requires action dependency graph |
| Variable runtime support | Requires variable state store |
| Variable template interpolation in binding paths | Depends on variable runtime |
Wildcard * in source paths |
JSONPath-style engine not scoped |
| Filter expressions in source paths | Same |
Array-index syntax [0] |
Same |
Keys with literal . in name |
Requires path quoting syntax |
components.<name>.label as writable target |
Deferred to Step 17.1 label work |
Any target property other than .value |
Deferred to per-property routing design |
| Dropdown or Table runtime population | Separate step |
ComponentType / FullComponentType cleanup |
Separate step |
Component-level bindings[] array |
Schema field exists but is not wired |
| Duplicate component-name detection at save time | Future validation step |
| Transform expression execution | Deferred (binding.transform field retained) |
| Authentication credential injection | Separate step |
action.responseMapping execution |
Permanently deferred; field deprecated |
12. Required diagnostics
The following diagnostics must be implemented by Step 17.1. They are listed here so the Inspector and runtime can be built consistently against this spec.
Inspector (static, design-time)
| Condition | Severity | Message guidance |
|---|---|---|
Binding source references action ID that does not exist in project.actions |
Warning | "Source references action <id> which does not exist." |
Binding target references component name that does not exist on any page |
Warning | "Target references component <name> which does not exist." |
Binding target uses property segment other than .value |
Warning | "Target property <segment> is not supported. Use .value." |
Two or more components share the same name on a page |
Warning | "Duplicate component name <name>. Binding resolution is ambiguous." |
trigger: "onChange" on an actions.* source binding |
Info | "Consider using onSuccess for action-response bindings." |
trigger: "onClick" on an actions.* source binding |
Info | "Legacy trigger. Consider migrating to onSuccess." |
action.responseMapping is non-empty |
Info | "Use project.bindings instead. responseMapping is deprecated and not executed." |
Runtime (execution-time, Preview only)
| Condition | Behaviour |
|---|---|
Source path resolves to undefined (field not on body) |
Write undefined to target; emit console warning |
| Target component not found at runtime | Skip write; emit console warning |
ProxyResponse.ok === false and trigger is onSuccess |
Do not fire binding; optionally fire onError bindings (deferred) |
| Action ID in binding source does not match any executing action | Skip silently |