conductor/docs/response-mapping-model.md
2026-07-18 10:23:13 -04:00

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

  1. Why project.bindings is canonical
  2. Why actions own runtime responses
  3. Why actions.<id>.response is a virtual namespace
  4. Source path grammar
  5. Target path grammar
  6. Trigger semantics
  7. Label runtime-value behaviour
  8. Compatibility — existing onClick bindings
  9. Deprecation of action.responseMapping
  10. Step 17.1 scope
  11. Deferred capabilities
  12. 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 id for 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 ProxyResponse envelope directly in componentState[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:

  1. project.actions is an array in the canonical document, not an object keyed by ID. At design time, actions.action_httpbin does not exist as a JSON key path.

  2. The runtime resolves <actionId> via Array.find((a) => a.id === actionId), not by property access.

  3. 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.1
  • actions.<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 match component.name (not component.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>.value is 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 deferred
  • components.<name>.label — Label runtime-value override is deferred to Step 17.1 (see §7)
  • Per-property routing for non-.value segments

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 (not undefined), it overrides properties.label in the Preview renderer.
  • A binding of the form:
    {
      "source": "actions.<id>.response.body.<field>",
      "target": "components.<labelName>.value",
      "trigger": "onSuccess"
    }
    
    writes the resolved field value (coerced to string for display) into componentState[labelId].value.
  • Runtime mutations must not modify canonical project JSON. The override is ephemeral; it lives only in useState for 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:

  1. ActionRuntimeStateMap — introduce alongside the existing componentState map in usePreviewRuntime. After each proxy call, write the ProxyResponse into actionState[actionId].response.

  2. Source path resolution — implement dot-path traversal for actions.<id>.response, .response.body, and .response.body.<field...>.

  3. Binding execution loop — after a successful proxy call, iterate project.bindings whose trigger is "onSuccess" (or "onClick" for legacy compatibility) and whose source parses to the completed action ID. Resolve the source path against actionState[actionId].response. Write the resolved value to componentState[targetId].value.

  4. Label renderer update — make LabelRenderer read runtimeState?.value as an override over properties.label, consistent with JsonViewerRenderer.

  5. 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
  6. 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