# I/O Binding — Implementation

> **Phase split.** Phase 3 only. Input/output binding at Step 9.8; in-expression `vars.$xref` marker resolution at Step 11.5 (after conditions + SLA). Phase 2 writes task shape (schema with empty `value` fields) but does not bind values. See [`../../../phased-execution.md`](../../../phased-execution.md).

Wire task inputs by editing `caseplan.json` directly. Runs after all tasks are created and enriched (Step 9) and after global variable + output wiring is complete.

## Task Input Shape

`task.data.inputs[]` — binding = setting `value`:

```json
{ "name": "in_CustomerId", "type": "string",
  "id": "vA1b2C3d4", "var": "vA1b2C3d4",
  "elementId": "Stage_verify-tKYC001",
  "value": "=vars.customerId" }
```

Inputs are populated with empty `value` from the `tasks describe` schema when the task's `data.inputs[]` are written during the task plugin's impl-json write. Input IDs are random (`v` + 8 chars) — letter-leading, same convention as variable formal slots ([global-vars § Formal-arg slot ID format](../global-vars/impl-json.md#formal-arg-slot-id-format)).

## Task Output Shape

`task.data.outputs[]` — read-only, set at enrichment:

```json
{ "name": "KycResult", "type": "string",
  "id": "kycResult", "var": "kycResult", "value": "kycResult",
  "source": "=KycResult", "target": "=kycResult",
  "elementId": "Stage_verify-tKYC001" }
```

Output IDs are name-based camelCase per [uniqueness rule](../global-vars/impl-json.md#uniqueness-rule). `source` reads from the task response — never changes even when `var` is counter-suffixed.

## Output Binding Shapes

Each task plugin emits `data.outputs[]` entries by combining its Step 0 schema (from `tasks describe` for non-connector plugins, `case spec --input-details` `caseShape.outputs[]` for connector plugins) with the SDD's `->` / `=` rows and any bare items added by schema discovery. Bare is an internal auto-mint form, never an SDD Outputs operator. Apply these rules during the plugin's task-write step.

Resolve the schema descriptor for every SDD field path **before** choosing an output shape:

1. Split `<sdd-field-path>` on `.`. Match the first segment exactly to a top-level Step 0 output (`source` with the leading `=` removed, falling back to `name`).
2. With no remaining segments, the resolved descriptor is that top-level output.
3. With remaining segments, walk them exactly through the top-level output's `Body.Properties` (`body.properties` in normalized case-shape data; `JsonSchema.Properties` is the equivalent CLI mirror). The descriptor after the final segment is the **leaf descriptor**.
4. For a nested path, set the emitted `name` to the leaf's display name when present, otherwise its exact final path segment. Take `type` plus any type-refining attributes from the leaf descriptor only. Never copy the parent object's `jsonSchema` type or body onto a scalar leaf.
5. A nested path references its top-level parent. Do not additionally auto-mint that parent unless the projected output list contains a separate schema-discovered bare item for it. If any segment does not resolve, log `ERROR` and skip the binding; never fall back to the last resolved parent.

For each top-level Step 0 entry, check whether the SDD references it either as a bare name or as the first segment of an explicit `->` path. Only entries with no such reference fall back to schema-driven auto-minting.

- **`<sdd-field-path> -> <sdd-name>`** (extract) → reassign-shape. Let `baseId = camelCase(leaf segment)` and allocate `id` per the global [uniqueness rule](../global-vars/impl-json.md#uniqueness-rule), including its controlled equal-name alias. Emit `{name: <resolved name>, type: <resolved descriptor's type>, id: <allocated id>, var: "<sdd-name>", originalVar: <allocated id>, value: "<sdd-name>", source: "=<sdd-field-path>", target: "=<allocated id>", elementId: "<stage-task>"}`. `<resolved name>` is the top-level schema display name for a top-level path; for a nested path it is the leaf display name when present, otherwise the exact final path segment. **`source` is the SDD's left-side string with `=` prefix, verbatim.** **`type` is required on every emitted output — FE rejects entries without it.** **`originalVar` is load-bearing and mirrors the allocated `id`** — it records the output slot before reassignment and tells FE's `mutateRootVariables` (`VariableMutationUtils.ts:135`) to skip root-mirroring, preserving the case-Variable companion across FE edits. Example: if another task already owns `id: "aPIOutput1"`, `APIOutput1 -> renamedResult` emits `id: "aPIOutput12"`, `target: "=aPIOutput12"`, `var: "renamedResult"`, and `originalVar: "aPIOutput12"`.
- **Bare `<name>`** (no operator) → auto-mint shape: `{name, type: <Step 0 entry's type>, id: <camelCase(name)>, var: <id>, value: <id>, source: <Step 0 entry's source verbatim>, target: "=<id>", elementId}`. No `originalVar`. Used for top-level Step 0 entries the SDD doesn't alias.
- **`<sdd-name> = <expression>`** (set / compute / copy) → Scenario E shape: `{name: "<sdd-name>", custom: true, var: "<sdd-name>", value: "<expression>", source: "<same as value>", target: "", body: "", type: <case var's type>, elementId: "root"}`. **No `id`**, no `originalVar`. **On this shape only, `target` and `body` are present-but-blank: emit `"target": ""` and `"body": ""` literally rather than dropping the keys.** An omitted key is not the same as a blank one — the FE reads a missing `target` as unset rather than deliberately-empty. Blank values belong to Scenario E and nowhere else:

| shape | `target` | `value` | `body` |
|---|---|---|---|
| Scenario E (`=` custom) | `""` | the expression | `""` |
| reassign (`->`) | `"=<allocated id>"` | the SDD name | absent |
| bare auto-mint | `"=<id>"` | the `id` | absent |

Never blank a `target` or `value` on a reassign or auto-mint row to satisfy the rule above — those carry real values, and emptying them is a different defect. NO root mirror — FE's `isUpdateExistingOutput` filter at `VariableMutationUtils.ts:49-64` skips it. Canonicalize `=metadata.X` to `=js:metadata.X` in both `value` and `source`; retain the SDD-natural form in the projected item. For a quoted string literal, treat the quotes as SDD delimiters: `status = "InReview"` emits JSON `"value": "InReview", "source": "InReview"` — never embed the delimiters as payload (`"value": "\"InReview\""`).
- **Schema fields with no SDD reference** → fall back to auto-mint shape (`var` = camelCased schema name). Connector plugins additionally apply the [uniqueness rule](../global-vars/impl-json.md#uniqueness-rule) dedup-suffix on collision (e.g., `response` → `response2`).

**Equal-name extract dispatch.** Dispatch by the explicit operator before comparing names; equal operands select the reassign shape, never the bare auto-mint branch. Apply the global [controlled-alias rule](../global-vars/impl-json.md#uniqueness-rule). With no unrelated collision, `greeting -> greeting` emits `id`, `var`, `originalVar`, and `value` as `"greeting"`, with `source: "=greeting"` and `target: "=greeting"`. `originalVar` distinguishes reassignment from a bare output and keeps the predeclared root companion intact during frontend synchronization; the linked allocator owns any required suffixing.

**Nested extract example.** Given a top-level `Error` output with `type: "jsonSchema"` and `Body.Properties.Message.Type: "string"`, the SDD row `Error.Message -> errorMessage` emits only this reassigned leaf unless schema discovery separately adds a bare `Error` item:

```json
{
  "name": "Message", "type": "string",
  "id": "message", "var": "errorMessage", "originalVar": "message",
  "value": "errorMessage", "source": "=Error.Message", "target": "=message",
  "elementId": "Stage_verify-tCallApi01"
}
```

`type: "jsonSchema"` on this row is wrong: that is the parent `Error` descriptor, not the resolved `Message` leaf.

Cross-cutting rules:

- **Preserve type-refining schema attributes.** The shapes above list the *minimum* fields. Carry over any extra attributes the resolved descriptor defines — most importantly `options` (the enum / picklist value set on choice and decision outputs, e.g. `Action`'s `[{value:"approve",label:"approve"},{value:"reject",label:"reject"}]`) — **verbatim** onto the emitted output entry, alongside the named fields. For a nested extract, copy attributes from the resolved leaf only; do not inherit the parent object's `body`, `jsonSchema`, `options`, or `type`. Applies to reassign (`->`), auto-mint (bare / no-SDD-reference), and connector-rule outputs alike. Dropping `options` strips the decision / choice enum the FE picker and decision widget depend on. (The `=` Scenario E shape is a literal / computed assignment and carries no schema `options`.)
- Expression values for `=`: literal (`"InReview"`, `5`, `true`), computed (`=js:vars.x + 1`), or variable reference (`=vars.X.Y.Z`).
- Dot-paths in `->` paths are supported (e.g., `response.message.ts`, `Error.code`). Array indexing not supported in v1.
- Target case variable on both `->` and `=` MUST exist in Case Variables table (validated at planning time).
- Apply the global [uniqueness rule](../global-vars/impl-json.md#uniqueness-rule), including its controlled equal-name alias, to every `->` source-side `id`; mirror the allocated ID into `target` and `originalVar`. Keep `source` unchanged and `var` pointing to the existing target Case variable.

## Output Binding Shapes for Connector Condition Rules

The Output Binding Shapes above are not task-specific. The SAME shapes (`->` reassign with `originalVar`, `=` Scenario E with `custom: true`, schema-discovered bare-name auto-mint) apply to a `wait-for-connector` **condition rule** in any of the 4 scopes: stage-entry, stage-exit, case-exit, task-entry. The connector-rule dispatch mirrors the connector-task dispatch with three targeting overrides:

| Aspect | Connector task | Connector condition rule |
|---|---|---|
| Target array | `task.data.outputs[]` | `rule.uipath.outputs[]` |
| Step 0 schema source | `tasks describe` / `case spec --input-details` `caseShape.outputs[]` | `case spec --type trigger --input-details` `caseShape.outputs[]` (already minted on `rule.uipath.outputs[]` by the connector-rule recipe — see [connector-trigger-impl.md § Target: connector-bound condition rule](../../../connector-trigger-impl.md#target-connector-bound-condition-rule)) |
| `elementId` on each entry | `<stageId>-<taskId>` | `<ownerNodeId>-<ruleId>` — `<stageId>-<ruleId>` for stage-entry / stage-exit / task-entry; `root-<ruleId>` for case-exit |
| Companion in `root.inputOutputs[]` (for `->` extract) | Required — `elementId: "root"`, `custom: true` | Required — same shape (`elementId: "root"`, `custom: true`) |
| `=` Scenario E (custom output) | Permitted | Permitted — case variable assigned from rule response (`caseVar = response.X`), a literal, or an expression. NO root mirror per `isUpdateExistingOutput` filter. |

**Uniqueness.** The [global pool](../global-vars/impl-json.md#uniqueness-rule) now includes rule outputs across all condition scopes — apply dedup against the union of tasks ∪ triggers ∪ rules ∪ root before minting.

**When invoked.** Each condition plugin's `impl-json.md` invokes this dispatch as the LAST step of its `wait-for-connector` recipe — after writing `rule.uipath` (Step 5 of [connector-trigger-impl.md § Procedure](../../../connector-trigger-impl.md#procedure-phase-3)) and BEFORE running root bindings (Step 6). Iterate the rule's SDD `Outputs:` rows against the already-minted `rule.uipath.outputs[]` entries; rewrite each matched entry per `->` or `=`, then retain any schema-discovered bare items as auto-mints. See the 4 condition `impl-json.md` files for the invocation site.

**Skip guard.** Rules with no `rule.uipath.outputs[]` (stub placeholder — connector configuration unresolved, see [`connector-trigger-impl.md § Placeholder fallback`](../../../connector-trigger-impl.md#placeholder-fallback)) — log `SKIPPED` and move on, same pattern as placeholder tasks (`data:{}`). The stub always carries a `uipath` block, but with empty `outputs[]`, so there is nothing to bind against until the connector resolves.

**Runtime order (KNOWN ISSUE).** The case-backend currently evaluates the gateway BEFORE the rule's output extract populates `vars.caseVar` — gate-first / extract-after, opposite of the intended design contract. Extract-then-gate on a SINGLE rule does NOT work for in-rule event-payload conditioning; the gate sees the pre-extract value of the case var. **Workaround** at the case-design level: place the case-state gate on the DOWNSTREAM stage-entry / task-entry condition that follows the connector rule — by then the extract has populated the case var. Backend disposition pending; treat the in-rule gate against extracted values as undefined behavior until verified.

## Binding Procedure

### Output reference ID (authoritative)

Both whole-value `<-` and in-expression `$xref` resolve through the same runtime variable ID:

```text
# pseudocode — not executed. Realize via Read → reason → Write/Edit.
resolve_output_reference_id(caseplan, src_output):
    if src_output["id"] is a non-empty string:
        return src_output["id"]
    if src_output["custom"] is true and src_output["var"] is a non-empty string:
        companion = exactly one variables.inputOutputs[] entry where
                    id == src_output["var"] and elementId == "root"
        if companion exists:
            return companion["id"]
    ERROR — the output has no runtime-resolvable ID
```

Normal, bare, and reassigned outputs use their own `.id`. This is load-bearing when reassignment collision handling produces `id: "estimatedAge2"` with `var: "estimatedAge"`: downstream references must use `=vars.estimatedAge2`. Only a custom `=` output intentionally lacks `.id`; its `.var` points to an existing Case-variable companion, so resolve through that companion's verified `.id`. Never use a reassigned output's `.var` as its source reference ID.

For each task input row in the SDD's Inputs table:

**Literals/expressions** — write the value string directly to `input.value`. Values shown are POST-rewrite — impl translates the SDD's `=metadata.X` to `=js:metadata.X` per the [canonical-form table](../../../bindings-and-expressions.md#canonical-form-per-sink) (plain `=metadata.X` is not resolved by the lookup-path evaluator). **A quoted string literal's quotes are SDD delimiters, never payload** — same rule as the `=` output side above: the SDD's `APIInput1 = "literal-seed"` emits `"value": "literal-seed"`, NOT `"value": "\"literal-seed\""`. The projected item keeps the SDD cell's own quotes ([planning.md § code spans](planning.md)); `caseplan.json` strips them. Embedded delimiters pass `validate` and ship the quote characters to the resource as payload:
```
"=vars.amount"  |  "=js:metadata.ExternalId"  |  "literal-seed"  |  "50"  |  "=js:new Date()"
```

**Cross-task references** (`input <- "Stage A"."Task X".outputName`) — resolve first:

1. Find Stage A by `data.label`, Task X by `displayName`
2. Find output by `name` in `task.data.outputs[]`
3. Resolve its output reference ID using the authoritative algorithm above
4. Write `=vars.<outputReferenceId>` to target input's `value`

```text
# pseudocode — not executed. Realize via Read → reason → Write/Edit.
src_output = find_output_by_name(src_task, "outputName")
output_reference_id = resolve_output_reference_id(caseplan, src_output)
target_input["value"] = f"=vars.{output_reference_id}"
```

## In-Expression Marker Resolution (Step 11.5)

Whole-value `<-` (above) only resolves an input whose value IS the reference. To reference an upstream output from **inside** a `=js:` expression (composite payload, `conditionExpression`, SLA `expression`, computed `=` output, connector body field), the SDD embeds a `vars.$xref('Stage','Task','output')` marker — see [bindings-and-expressions.md § In-expression references](../../../bindings-and-expressions.md#in-expression-references-varsxref). Resolve all markers in **one pass over the whole `caseplan.json`** at **Step 11.5** — after conditions (Step 10) and SLA (Step 11) are written, and every task/trigger/rule output is minted and deduped (so the marker resolves to the final output reference ID). This is the LAST mutation of Phase 3 before the validator; running it earlier (e.g. right after Step 9.8 input binding) misses markers in conditions / SLA and reads pre-dedup IDs.

This single sink-blind pass replaces per-sink resolution: it walks every string value regardless of which sink holds it, so conditions, SLA, inputs, and connector bodies are all covered in one place.

```text
# pseudocode — not executed. Realize via Read → reason → Write/Edit.
TOKEN = /vars\.\$xref\('([^']+)','([^']+)','([^']+)'\)/   # global, all matches

for each string value V anywhere in caseplan.json:
    for each match (stageLabel, taskName, outputName) of TOKEN in V:
        src_stage  = find_node_by_label(nodes, stageLabel)        # data.label
        src_task   = find_task_by_name(src_stage, taskName)       # displayName
        src_output = find_output_by_name(src_task, outputName)    # data.outputs[].name
        if any lookup fails: leave token unsubstituted — Check 4 (validator) surfaces it via AskUserQuestion
        output_reference_id = resolve_output_reference_id(caseplan, src_output)
        if ID resolution fails: leave token unsubstituted — Check 4 surfaces it
        replace the matched token with "vars." + output_reference_id  # bare, no leading "="
    write V back
```

Resolution semantics are identical to whole-value `<-` (same name-triple and output-reference-ID algorithm), with two differences: the substitution is **bare** `vars.<outputReferenceId>` (the marker already sits inside `=js:`), and it happens in a global string pass rather than against a single input's `value`. Secondary-stage / adhoc scoping (reference any task across any stage) applies unchanged.

After this pass and all bindings, run the end-of-Phase-3 validator. It performs the cross-reference checks below:

### Check 1 — `=vars.X` reference resolution

Verify every bound input has a non-empty `value`, and every `=vars.X` reference resolves to an existing entry in one of:
- Any task `data.outputs[].id` (the resolver match key; mirrors `var` under skill convention)
- Variables `inputOutputs[].id`
- Variables `inputs[].id`

Variables array path is top-level `variables.{inputOutputs,inputs}[].id`.

> **Scan key:** match by `.id`, NOT `.var`. The runtime resolver matches on `Variable.id` (`VariablesService.findVariableByVariableId`). Bare self-declaring outputs commonly have `id === var`, but reassigned outputs can have a collision-safe `.id` whose `.var` points at a different Case variable. Custom `=` outputs resolve through their root companion's `.id` per the authoritative algorithm above.

Also scan `=vars.X` references in:
- Entry / exit condition expressions (stage and task)
- Case-exit and trigger rule expressions
- SLA expressions
- `=js:` expressions anywhere they appear

Same resolution rule applies — these are read-side consumers of the variable namespace.

### Check 1.5 — Custom-output metadata expressions are canonical

For every `custom: true` output, reject `value` or `source` beginning with `=metadata.`; emit `=js:metadata.<field>` in both properties instead.

### Check 2 — Out-arg producer presence

For every entry in top-level `variables.outputs[]` (formal Out-arg entries), the entry's `var` field is a POINTER to the variable slot that should hold the value at case end. Per the always-emit-companion rule, the companion in `variables.inputOutputs[]` is always present; its `default` field is empty when SDD didn't declare a Default.

**The check:** can the Out-arg's slot be populated at runtime? Three populating mechanisms exist:

1. **Companion default** — non-empty `default` field on the companion → always-populated fallback.
2. **Extraction producer** — a task's `outputs: <field> -> <var>` row (extract response field into the Out-arg slot).
3. **Assignment producer** — a task's `outputs: <var> = <expr>` row (`=` operator: set/compute/copy a literal or expression into the Out-arg slot).
4. **Bare-name producer** — a task's `outputs: <var>` row where the bare name matches the Out-arg's var (camelCase of schema field name).

If none of these exist → **pure orphan**, prompt the author.

| Producer status | Validate time action |
|---|---|
| Companion has non-empty `default` | OK — Out-arg always has a value. |
| At least one producer (extraction, assignment, or bare-name) exists in the SDD AND its task is resolved (not Rule 17 placeholder) | OK — producer wires the slot when its task fires. |
| Producer declared but its task is a Rule 17 placeholder (declared-but-unwirable) | **Silent WARN.** Log to `tasks/build-issues.md` under `## Open Items for User`. Rule 17 already prompted the author for this task. |
| NO producer anywhere AND companion default empty | **AskUserQuestion** — pure orphan. 4 options below. |

Pseudocode:

```text
for entry in root.outputs[]:
  var = entry.var
  case_var_row = tasks_md_row_for_out_arg(name=entry.name)
  has_companion_default = (case_var_row.default not empty)

  # Producer scan — three patterns. All operate on the SDD Outputs rows projected per planning.md:
  has_extraction_producer  = exists in the SDD any task with an Outputs row projecting to `<field> -> <var>` (where var matches the Out-arg's var)
  has_assignment_producer  = exists in the SDD any task with an Outputs row projecting to `<var> = <expression>` (where var matches the Out-arg's var)
  has_bare_name_producer   = exists a schema-discovered bare output `<name>` (no operator) where camelCase(name) == var
  has_any_producer         = has_extraction_producer || has_assignment_producer || has_bare_name_producer

  producer_task_unresolved = the SDD-declared producer task is a Rule 17 placeholder (look up the task in caseplan.json by displayName; check `node.data.inputs` is empty `{}`)

  if has_companion_default:
      # Companion default guarantees a value; producer is optional bonus
      OK
  elif has_any_producer and producer_task_unresolved:
      # Declared producer but task is unresolvable — Rule 17 already prompted; just log
      LOG_OPEN_ITEM("Out-arg with declared but unresolvable producer — runtime returns empty until producer is wired")
  elif not has_any_producer:
      # Pure orphan — author never declared a producer AND no Default. Ask.
      AskUserQuestion("pure orphan", options=(a, b, c, d))
```

**On AskUserQuestion ("pure orphan" branch):**

```
Out-argument "<name>" (id v<random8>, var <var>) has no value source:
  SDD row Default: <"" (empty)>
  Companion root.inputOutputs[].id="<var>": exists, default=""
  Producing task in the SDD (extraction or assignment): none found

Pick one:
  (a) Add producer task output — supply the producer task's **display name** as shown in the SDD (e.g., `Send Slack Message`). If the named task doesn't exist, re-prompt. Skill appends `<field> -> <var>` to that task's Outputs.
  (b) Add a Default value to the SDD Case Variables row — supply value inline (literal string).
  (c) Recategorize as Variable (case-internal state) or remove the variable.
  (d) Continue with best-effort emit (case builds; runtime returns empty string for this Out-arg; entry logged under "Open Items for User" in build-issues.md).
```

**Skill response per user pick:**

- **(a)** Edit `sdd.md`: append a `<field> -> <var-name>` row to the named task's Outputs table (use the spec-derived field name if available, else an `<UNKNOWN>` placeholder). Re-run Phase 1 from the modified `sdd.md`, then retry Step 12.
- **(b)** Edit `sdd.md`: set the Out-arg's `Default` cell in the Case Variables table. Re-run Phase 1, then retry Step 12.
- **(c)** Prompt the user inline: `Recategorize as "Variable" or "Remove" the variable?` On `Variable`: edit the SDD's Case Variables row Category → Variable, re-run Phase 1 dispatcher, retry Step 12. On `Remove`: delete the row from `sdd.md`, re-run Phase 1, retry Step 12.
- **(d)** Append the build-issues entry (template below) and continue to Phase 4. No re-run.

Option (d) is the build-with-best escape for cases where the author intends to wire the producer later but wants to keep iterating now — equivalent to the silent-WARN treatment that declared-but-unresolvable producers (T20-style) get automatically.

**Rationale for the split:** real-world authoring is iterative. When an author has already gone through a Rule 17 prompt for the producer task (T20-style), the skill should not pile a second prompt on top — that's the path the author already chose by picking "Skip". But when the author authored a *pure orphan* with no producer declared at all (T14-style — wait-for-timer with no aliasing wire AND no Default), there's no prior signal of intent; the AskUserQuestion is the right surface to ask "did you mean to forget this, or wire it now?" Option (d) preserves the build-with-best escape.

**Build-issues entry template** (both branches log to this, only the AskUserQuestion branch ALSO prompts):

```markdown
## Open Items for User

- **[Out-arg `<name>` has no value source]** — The Out-argument `<name>` is declared in `variables.outputs[]` but {no producer wired AND no Default}. Runtime will return empty string for this Out-argument unless one of:
  - Add a `<field> -> <name>` row to a task's Outputs that produces this value (extraction)
  - Add a `<name> = <expression>` row to a task's Outputs (assignment from literal / computed / variable reference)
  - Add a Default value to the SDD Case Variables row
  - Recategorize the variable as `Variable` or remove it
```

See [implementation.md § Step 12 — End-of-Phase-3 validator pass](../../../implementation.md) for invocation.

### Check 3 — Type and descriptor fidelity

**Input consumers.** Where a `=vars.X` reference resolves to a declaration with a different `type` than the consuming input expects, log WARNING. Proceed (string coercion is common and runtime-tolerant).

**Extract outputs.** For every `->` row, re-resolve its full source path against the Step 0 schema using the algorithm above. Require the emitted output's `name` and `type` to equal the resolved name and leaf type. For a nested path, also require that the top-level parent was not separately auto-minted unless there is a schema-discovered bare item for it. A mismatch is `ERROR`: correct the output from the resolved descriptor and re-run this check before Phase 4. Do not accept a passing `uip maestro case validate` as evidence here; structural validation does not compare a nested binding with its schema leaf.

### Check 4 — No surviving `$xref` markers

Scan every string value in `caseplan.json` for the literal token `$xref(`. The [Step 11.5 pass](#in-expression-marker-resolution-step-115) should have resolved them all; any survivor means its name-triple or output reference ID failed to resolve. This is the same class of failure as a Check 1 unresolved `=vars.X` — so it gets the **same interactive remediation**, NOT a silent ERROR. Never ship a marker to runtime (`vars.$xref(...)` throws — a method call on `vars`).

**On AskUserQuestion** (present the outputs that DO exist on the named task as candidates — same diagnostic shape as a failed whole-value `<-`):

```
In-expression reference $xref('<stage>','<task>','<output>') does not resolve:
  Stage  "<stage>":  <found | NOT FOUND>
  Task   "<task>":   <found | NOT FOUND in that stage>
  Output "<output>": NOT FOUND on task
  Available outputs on "<task>": <name, name, ...>
  Used in: <sink — e.g. entry condition on stage "Approve" | input "payload" on task "Notify">

Pick one:
  (a) Name the intended output — supply the correct output name (or full Stage / Task / output triple).
  (b) Edit the SDD expression — the marker is one term in a larger =js: expression; the upstream output genuinely does not exist.
  (c) Continue with best-effort emit — token left unsubstituted; case builds; the =js: expression throws at runtime until fixed.
```

**Skill response per pick:**

- **(a)** Rewrite the marker's triple in place in `caseplan.json` with the corrected name(s), re-run the [Step 11.5](#in-expression-marker-resolution-step-115) resolution for that token, then re-scan. If it still fails, re-prompt.
- **(b)** Edit the SDD expression as directed, re-run the Phase 1 dispatcher from the modified SDD, then retry Step 11.5 + this check.
- **(c)** Leave the token unsubstituted, append the build-issues entry (template below), continue to Phase 4. No re-run.

**Build-issues entry template:**

```markdown
## Open Items for User

- **[Unresolved `$xref` marker]** — `vars.$xref('<stage>','<task>','<output>')` in <sink> did not resolve (output not found on the named task). The `=js:` expression throws at runtime until fixed. Correct the source output name in the SDD and rebuild.
```

### Check 5 — Resolved-resource I/O completeness

Verifies each resolved task's binding contract **covers** its resource's declared I/O — the build-side re-check of the design-side Resolved-resource I/O completeness rule (case SDD content contract, `uipath-planner`) (Approve-gate item 9 / Finalization step 19). Where Checks 1–4 verify that references which *exist* resolve, Check 5 verifies the *right set of references exists*: required inputs are not silently missing, and extract outputs name real fields.

Read each resolved task's persisted contract from `tasks/registry-resolved.json` (per-input `name` + `required` flag, declared output-field list — written at §Resolve). **Skip** any task with no persisted contract (Rule 17 placeholder / `<UNRESOLVED>`) — same treatment as Check 2's unresolved-producer branch.

```text
# pseudocode — not executed. Realize via Read → reason → Write/Edit.
for task in caseplan.json tasks where contract = registry_resolved[task].contract is present:
    bound_inputs = { inp.name : inp.value for inp in task.data.inputs[] }
    # (a) required-input coverage
    for decl in contract.inputs where decl.required:
        v = bound_inputs.get(decl.name)
        if v is missing or v == "":            # no row, or row with empty value
            ERROR → AskUserQuestion (unbound-required-input)
    # (b) output-field fidelity
    declared_out = set(contract.outputs[].name)
    for out in task.data.outputs[]:            # extract rows: source = "=<path>"
        leaf = top_level_segment(strip_leading_"=", out.source)   # strip envelope prefix: response. / Error. / data.
        if leaf not in declared_out:
            ERROR → AskUserQuestion (phantom-output-field)
```

An **upstream-output-fed** required input is covered like any other — its `value` is `=vars.<outputReferenceId>` (whole-value `<-`) or sits inside a `=js:` (resolved `$xref`); a non-empty `value` passes. Do NOT expect a §1.5 declaration for it.

**On AskUserQuestion — unbound required input:**

```
Required input "<field>" on task "<task>" (resource "<resource>") is not bound:
  Declared by the resource as required; no Inputs row with a value in the case plan.
  Other required inputs on this task: <bound / unbound list>

Pick one:
  (a) Bind it — supply the source: a case variable, a literal, or an upstream task's output ("Stage"."Task".out). Skill writes the Inputs row and binds it.
  (b) Mark <UNRESOLVED> — record a placeholder + a high review item; case builds, this input is runtime-null until wired.
  (c) Continue with best-effort emit — leave it unbound; entry logged under Open Items; the job may fault at runtime.
```

**On AskUserQuestion — phantom output field:**

```
Output field "<field>" extracted by task "<task>" is not in resource "<resource>"'s declared outputs:
  Available outputs on this resource: <name, name, ...>
  Used in: outputs row "<field> -> <caseVar>"

Pick one:
  (a) Name the intended output — pick from the available list; skill rewrites the extract Field + re-resolves.
  (b) Drop the extract row — the case does not consume this output.
  (c) Continue with best-effort emit — left as-is; entry logged under Open Items; the extract resolves to runtime null.
```

**Skill response per pick:**

- Unbound (a) — write the Inputs row to `sdd.md` + `caseplan.json`, run the Step 9.8 binding for that input, retry Check 5. (b) — set the input `value` to a placeholder and append a `high` review item (`rev_unbound_input_<task>_<field>`), continue. (c) — append the build-issues entry, continue. No re-run.
- Phantom (a) — rewrite the output `source`/`Field` in `caseplan.json`, retry Check 5. (b) — delete the output row (and any now-orphaned `=vars.<caseVar>` consumer falls to Check 1). (c) — append the build-issues entry, continue.

Check 5 honors the same **build-with-best** policy as Checks 1, 2, 4: option (c) appends a `## Open Items for User` entry and proceeds to Phase 4. Phase 4 `validate` stays green (a missing input / phantom extract is structurally valid); the runtime concern is surfaced for pre-publish review.

**Build-issues entry templates:**

```markdown
## Open Items for User

- **[Unbound required input]** — task "<task>" (resource "<resource>") input "<field>" is required but unbound; resolves to runtime null. Bind it in the SDD and rebuild.
- **[Phantom output field]** — task "<task>" extracts "<field>", which resource "<resource>" does not emit; resolves to runtime null. Correct the output name in the SDD and rebuild.
```

## Connector Tasks

Connector task input values are written during Step 9.7 (connector detail), not during this I/O binding step. Resolve cross-task output reference IDs with the authoritative algorithm above before constructing the `input-values` body from the resolved entry, then apply the canonical wrap per sink:

```json
{ "body": { "email": "=js:(vars.employeeEmail)", "caseRef": "=js:(metadata.ExternalId)" } }
```

**Connector body sinks require `=js:(...)` wrap for ALL references** — `=vars.X`, `=metadata.X`, `=bindings.X`, and operator expressions (e.g. `=js:(vars.amount > 5000)`). The runtime only evaluates `=js:` prefixed strings inside connector body fields; plain prefix forms arrive at the API as literal strings (silent runtime fault). Full per-sink rule: [bindings-and-expressions.md § Canonical form per sink](../../../bindings-and-expressions.md#canonical-form-per-sink).

See [connector-activity/impl-json.md](../../../plugins/tasks/connector-activity/impl-json.md) for the connector body write path.

## End-to-End: Task A Output → Task B Input

"Validate Expense Data" produces `validationResult`, consumed by "Enrich Employee Details":

```json
// 1. Task A output (auto-enriched) — Stage "Submission", task.data.outputs[]
{ "name": "ValidationResult", "var": "validationResult", "id": "validationResult",
  "value": "validationResult", "source": "=ValidationResult", "target": "=validationResult",
  "type": "string", "elementId": "Stage_submit-tValidate01" }

// 2. Task B input after binding — value set to =vars.<output.id>
{ "name": "in_ValidationResult", "value": "=vars.validationResult",
  "type": "string", "id": "vXr9pQ2mK", "var": "vXr9pQ2mK",
  "elementId": "Stage_submit-tEnrich02" }
```

Two things must exist: output on Task A with a runtime-resolvable reference ID, and bound input on Task B referencing `=vars.<outputReferenceId>`. Root `inputOutputs` companion entries for case Variables produced via `->` are also written for picker visibility — see [global-vars/impl-json.md § Task Output → variable resolution](../global-vars/impl-json.md#task-output--variable-resolution).

## Error Handling

All issues go to the shared issue list per [logging/impl-json.md](../../logging/impl-json.md). No fuzzy matching, no auto-creation, no retries.

| Check | Severity | Action |
|---|---|---|
| Placeholder task (no `data.inputs[]`) | `SKIPPED` | Skip all bindings |
| Placeholder connector rule (no `rule.uipath.outputs[]`) | `SKIPPED` | Skip rule output bindings (nothing minted) |
| Input name not found (exact match) | `ERROR` | Skip binding — log available inputs |
| Source output not found (exact match) | `ERROR` | Skip binding — log available outputs |
| `$xref(...)` marker name-triple or output reference ID fails to resolve (Step 11.5 / Check 4) | `ERROR` | Leave token unsubstituted; AskUserQuestion (Check 4 above) — log unresolved triple + available outputs |
| `=vars.X` not in any task `outputs[].id` or root `inputOutputs[].id` / `inputs[].id` | `ERROR` | Skip binding |
| Out-arg formal entry has NO producer (no extraction, assignment, or bare-name match in any task outputs) AND companion has no `default` | `ERROR` | Log Out-arg pure-orphan issue (Check 2 above); AskUserQuestion |
| Resolved resource's **required** input has no bound `value` in the case plan (Check 5) | `ERROR` | AskUserQuestion (unbound-required-input) — bind / `<UNRESOLVED>`+review-item / best-effort |
| Extract output `Field` absent from resolved output contract (Check 5) | `ERROR` | AskUserQuestion (phantom-output-field) — re-point / drop row / best-effort |
| Resolved task has no persisted contract (placeholder / `<UNRESOLVED>`) | `SKIPPED` | Skip Check 5 for that task |
| Type mismatch (input vs variable) | `WARNING` | Proceed |
| Extract output `name` / `type` differs from its resolved schema descriptor (Check 3) | `ERROR` | Correct from the exact resolved leaf; re-run Check 3 |
| Nested extract also auto-mints its top-level parent without a separate schema-discovered bare item (Check 3) | `ERROR` | Remove the undeclared parent auto-mint; re-run Check 3 |

Example log entry (pseudocode — record in-reasoning, not via subprocess):

```text
# pseudocode — not executed
issues.append({"severity": "ERROR", "step": "9", "plugin": "io-binding",
    "message": f'input "{name}" not found on task "{task}" — available: {available}',
    "context": {"task": task, "stage": stage, "input": name, "available": available}})
```

<!-- END: impl-json.md -->
