# Troubleshooting

Common failure modes when authoring, running, packaging, or publishing API workflows. Organized by category — each entry: symptom → cause → fix.

## Structure Pitfalls

### Missing `#Wrapper` on If activity
- **Symptom:** Validation error about invalid If structure or missing container
- **Cause:** If activity placed directly without the `If_N#Wrapper` outer container
- **Fix:** Wrap with `If_N#Wrapper` containing the switch `If_N`, `If_N#Then`, and `If_N#Else` as children

### Missing `#Body` on loop
- **Symptom:** ForEach or DoWhile validation error; loop body not recognized
- **Cause:** Loop `do` array contains activities directly instead of inside a `#Body` element
- **Fix:** Wrap loop contents in `For_Each_N#Body` or `Do_While_N#Body` with proper export pattern

### Missing `then: "exit"` on If branches
- **Symptom:** Activities after If execute twice or unexpected fall-through
- **Cause:** `#Then` and `#Else` branches missing `"then": "exit"`
- **Fix:** Add `"then": "exit"` to both `If_N#Then` and `If_N#Else`

### Duplicate activity keys
- **Symptom:** Validation error about duplicate keys; only one activity rendered in designer
- **Cause:** Two activities share the same key (e.g., two `Assign_1`)
- **Fix:** Every key must be globally unique. Increment the suffix number.

### Missing `WorkflowStart`
- **Symptom:** Variables not initialized; `$context.variables` is undefined
- **Cause:** `WorkflowStart` activity removed or not included as first activity in `Sequence_1`
- **Fix:** Always include `WorkflowStart` as the first activity with `isTransparent: true`. See [workflow-file-format.md](workflow-file-format.md#workflowstart--system-activity).

### Missing `evaluate` block
- **Symptom:** Expressions not evaluated; workflow behaves unexpectedly
- **Cause:** `"evaluate": { "mode": "strict", "language": "javascript" }` block missing from root
- **Fix:** Add the evaluate block at the root level

### Activities outside the root sequence
- **Symptom:** Activities not visible in designer or not executing
- **Cause:** Activities placed at wrong nesting level (not inside the root sequence's `do` array)
- **Fix:** All user activities go inside the root sequence, after `WorkflowStart`

---

## Export Pattern Pitfalls

### Wrong export for Assign (using outputs instead of variables)
- **Symptom:** Assigned variable not accessible via `$context.variables.X`
- **Cause:** Using outputs export pattern (`...$context?.outputs, "Assign_1": $output`) instead of variables pattern
- **Fix:** Assign must use: `{ ...$context, variables: { ...$context.variables, ...$output } }`

### Losing existing context in export
- **Symptom:** Previous activity outputs disappear after an activity runs
- **Cause:** Export does not spread existing context (`$context`) or outputs (`$context?.outputs`)
- **Fix:** Always spread: `{ ...$context, outputs: { ...$context?.outputs, "Key": $output } }`

### Missing `?.` on `$context.outputs` in export
- **Symptom:** Error when activity is the first to write to outputs (outputs is undefined)
- **Cause:** Using `$context.outputs` without optional chaining when no outputs exist yet
- **Fix:** Always use `$context?.outputs` in export patterns

### ForEach body export missing index reset
- **Symptom:** Results array grows incorrectly across loop iterations
- **Cause:** Using the simpler DoWhile accumulation pattern instead of the ForEach index-aware pattern
- **Fix:** ForEach body must use: `...($currentItemIndex == 0 ? [] : ($context?.outputs?.For_Each_N?.results ?? []))`

---

## Expression Pitfalls

### Missing `${}` wrapper
- **Symptom:** Expression treated as literal string instead of evaluated
- **Cause:** Expression written as `$context.variables.x` instead of `${$context.variables.x}`
- **Fix:** Always wrap expressions in `${...}`

### Accessing property on undefined (strict mode)
- **Symptom:** Runtime error: "Cannot read property 'X' of undefined"
- **Cause:** Accessing nested property without null checks in strict evaluation mode
- **Fix:** Use optional chaining: `${$context.outputs?.Javascript_1?.items ?? []}`

### Wrong variable reference for loop iterator
- **Symptom:** `currentItem is not defined` inside the ForEach body, or `$context.variables.currentItem` returns `undefined`.
- **Cause:** Two related mistakes. (1) Reading the iterator from `$context.variables.<name>` — the executor does NOT put it there. (2) Reading it as `${currentItem}` (no `$` prefix) — the executor binds the global with a literal `$` *in the identifier name*, so the unprefixed name has no binding.
- **Fix:** Reference the iterator with the `$` prefix: `${$currentItem}` and `${$currentItemIndex}` (and same for whatever names you passed to `for.each` / `for.at`). The `$` is part of the identifier, not expression syntax. Example: `"when": "${$currentItem.priority === 'high'}"`. See critical rule 11 in `SKILL.md` and the `for.each` binding source at `dist/handlers/for-task-handler.js:67-70`.

### Type coercion errors
- **Symptom:** String concatenation instead of arithmetic, or comparison fails
- **Cause:** Values stored as strings (from input arguments or upstream); strict mode does not auto-coerce
- **Fix:** Explicit coercion: `${Number($context.variables.countStr) + 1}`

### Using `${}` in `for.each` and `for.at`
- **Symptom:** Validation error on ForEach loop definition
- **Cause:** `"each": "${item}"` instead of `"each": "item"` — these declare variable names, not expressions
- **Fix:** `for.each` and `for.at` take plain strings. Only `for.in` takes an expression.

### `arguments[0]` is undefined inside JsInvoke
- **Symptom:** Script throws `Cannot read properties of undefined (reading '$context')` on a line like `const ctx = arguments[0]; ctx.$context.X`
- **Cause:** Trying to read context through `arguments[0]`. The runtime does NOT pass arguments to the script. `arguments.length === 0` inside the body. The task's `arguments` field exists for designer roundtrip but is ignored by the runtime.
- **Fix:** Drop the `arguments[0]` indirection. Reference `$context`, `$workflow`, `$input` directly — they are globals. `return $context.variables.userName.toUpperCase();` instead of `return arguments[0].$context.variables.userName.toUpperCase();`

### `$workflow.input.<name>` is undefined (or `$input.<name>` returns the wrong thing)
- **Symptom:** Read returns `undefined` or wrong value (the previous task's output)
- **Cause:** Either the input was not declared in `input.schema`, was not passed via `--input-arguments`, no default exists, or the agent used `$input.<name>` instead of `$workflow.input.<name>`
- **Fix:**
  - Use `$workflow.input.<name>` everywhere — `$input.<name>` is the previous task's output for any non-first task
  - Confirm the input is declared in `input.schema` or has a default in `document.metadata.variables.schema.document.properties`
  - Pass the value at runtime via `--input-arguments`

---

## Loop Pitfalls

### Infinite DoWhile loop
- **Symptom:** Workflow never completes; timeout or resource exhaustion
- **Cause:** Condition variable not updated inside `#Body`, so `doWhile` condition never becomes false
- **Fix:** Always update the condition variable inside `Do_While_N#Body`

### Wrong `for.in` on DoWhile
- **Symptom:** DoWhile executes wrong number of times or fails to start
- **Cause:** Using an actual collection for `for.in` instead of `"${ [1] }"`
- **Fix:** DoWhile always uses `"for": { "in": "${ [1] }" }`. The `doWhile` condition controls repetition.

### Break outside loop body
- **Symptom:** Validation error; Break activity not recognized
- **Cause:** Break placed outside a `#Body` element (e.g., directly in Sequence)
- **Fix:** Break must be inside `For_Each_N#Body` or `Do_While_N#Body`

### Break with boolean instead of string
- **Symptom:** Break does not exit loop; validation error
- **Cause:** `"break": true` (boolean) instead of `"break": "true"` (string)
- **Fix:** Must be `"break": "true"` (string literal)

### ForEach on non-array
- **Symptom:** Runtime error; loop fails to iterate
- **Cause:** `for.in` expression resolves to a non-array value (object, string, null)
- **Fix:** Ensure `for.in` resolves to an array. Guard with: `${$context.variables?.items ?? []}` or `${$workflow.input.items ?? []}`

---

## Nesting Pitfalls

### Reusing keys across nested scopes
- **Symptom:** "Duplicate key" validation error, or designer renders only one instance
- **Cause:** Two activities share a key (e.g., two `If_1#Then` blocks at different nesting levels)
- **Fix:** Keys are globally unique across the WHOLE workflow, regardless of nesting depth. Increment numbers monotonically: outer If is `If_1`, inner If is `If_2`. See [control-flow-patterns.md](control-flow-patterns.md#key-numbering-convention).

### Reusing iteration variable names across nested loops
- **Symptom:** Inner loop's iterator value leaks into outer scope or vice versa
- **Cause:** Outer ForEach uses `for.each: "currentItem"` and inner ForEach also uses `for.each: "currentItem"` — the inner shadows the outer
- **Fix:** Use distinct names per nesting level: `outerItem` / `innerItem`, `customer` / `order`, etc.

### Break exits the wrong loop
- **Symptom:** You expected Break to exit BOTH nested loops, but only the inner exits
- **Cause:** Break exits only the innermost enclosing loop — that's the spec
- **Fix:** Set a flag variable before Break, then check it in the outer loop and Break again. See [control-flow-patterns.md](control-flow-patterns.md) pattern #5.

### `then: "exit"` confused with `then: "end"`
- **Symptom:** Workflow terminates unexpectedly when an If branch finishes
- **Cause:** Used `then: "end"` on a `#Then` or `#Else` branch — that ends the WORKFLOW, not the branch
- **Fix:** Use `then: "exit"` to exit the current container; use `then: "end"` only on Response activities

---

## Response Pitfalls

### Missing `then: "end"`
- **Symptom:** Workflow does not terminate properly; subsequent activities run
- **Cause:** Response activity missing `"then": "end"`
- **Fix:** Always include `"then": "end"`

### `markJobAsFailed` nested inside `response`
- **Symptom:** Job runs even though intent was to mark it failed
- **Cause:** `markJobAsFailed` placed inside `response` object instead of as a sibling
- **Fix:** `markJobAsFailed` is a SIBLING of `response`:
  ```json
  "response": "${expression}",
  "markJobAsFailed": false,
  "then": "end"
  ```

### Workflow returns `Data: { "message": "(no output)" }`
- **Symptom:** Run succeeds but no output value
- **Cause:** No Response task in the workflow, OR Response uses legacy `set:` form without `then: "end"`
- **Fix:** Use `response: <value>` + `markJobAsFailed: false` + `then: "end"`

---

## StudioWeb Roundtrip Pitfalls

These are issues that surface only when a workflow is opened or run in **StudioWeb** (cloud.uipath.com). Workflows that pass `uip api-workflow run --no-auth` may still fail in cloud for these reasons.

### `ReferenceError: <literal> is not defined` after opening in StudioWeb

- **Symptom:** Workflow runs cleanly under `uip api-workflow run`. Open it in StudioWeb's designer, run from there, get `Worker operation failed: PASS is not defined` (or `FAIL`, `INVALID`, `done`, etc. — whatever literal string you used).
- **Cause:** StudioWeb's designer normalizes Assign `set` values and Response `response` literals when it parses or saves the JSON. It treats unwrapped strings (e.g. `"grade": "PASS"`) as expressions typed into the property panel and rewrites them to `"grade": "${PASS}"` — turning the literal into a bare identifier reference. At run time `PASS` has no binding, so the expression evaluator throws `<name> is not defined`.
- **Fix:** Pre-wrap every string literal in `Assign.set` and `Response.response` (and similar expression-typed slots) as a JS string inside an expression: `"${'literal'}"`. The single-quoted form avoids JSON escaping. Examples:
  ```json
  "set": { "tier": "${'PLATINUM'}" }              // ✓ roundtrips cleanly
  "set": { "tier": "PLATINUM" }                   // ✗ becomes ${PLATINUM} → ReferenceError

  "response": "${'done'}"                          // ✓
  "response": { "status": "${'ok'}", "code": 200 } // ✓ — numbers/booleans need no wrap

  "response": { "status": "ok" }                   // ✗ — gets rewritten on save
  ```
- **What does NOT need wrapping:** numbers (`0`, `42`), booleans (`true`/`false`), values that already evaluate expressions (`"${$workflow.input.x}"`, `"${$context.variables.tier}"`), and the activity-control strings `then: "exit"` / `then: "end"`.
- **Heuristic:** any time you'd write `"foo"` as a *literal value* you intend the workflow to use, wrap it as `"${'foo'}"`. The CLI evaluates the expression and gets the string `'foo'`; StudioWeb leaves the already-wrapped form alone.

### Object-valued Response gets corrupted; fields evaluate to literal expression text

- **Symptom:** Workflow runs correctly under `uip api-workflow run` and Response returns the expected object (e.g. `{ tier: "GOLD", count: 3 }`). After opening + saving in StudioWeb, the same Response now returns each field's value as the **literal text of its expression** rather than the evaluated value — `tier` becomes the string `"${$context.variables.tier}"` (one long string, often 100+ chars), not `"GOLD"`. StudioWeb's own output-schema validator may flag the mismatch ("Output-ul nu corespunde schemei de output configurate").
- **Cause:** StudioWeb's designer rewrites Response object payloads on save. Authored `{ "response": { "tier": "${...}", "count": "${...}" } }` is collapsed into a single stringified expression: `"response": "${{\"tier\":\"${...}\",\"count\":\"${...}\"}}"`. The outer `${{ ... }}` is a JS object-literal expression form, but inside it the keys/values are inside JS **double-quoted** strings (`"tier":"${...}"`) — and JS double-quoted strings don't interpolate `${...}`, only template literals do. So each field's value resolves to the literal characters `${...}`, not the evaluated expression.
- **Fix:** Pre-author the Response in the single-expression `${{ ... }}` form yourself, with raw context references inside (no inner `${...}` wrapping):
  ```json
  // ✗ Wrong — CLI runs fine, designer corrupts on save
  {
    "Response_1": {
      "response": {
        "tier": "${$context.variables.tier}",
        "count": "${$context.variables.count}"
      },
      "markJobAsFailed": false,
      "then": "end",
      "metadata": { "...": "..." }
    }
  }

  // ✓ Correct — roundtrips cleanly through the designer
  {
    "Response_1": {
      "response": "${{ tier: $context.variables.tier, count: $context.variables.count }}",
      "markJobAsFailed": false,
      "then": "end",
      "metadata": { "...": "..." }
    }
  }
  ```
- **Why it works:** Inside the outer `${{ ... }}` you're already in JS expression scope. The body is a JS object literal where unquoted keys are identifiers (`tier:`, `count:`), references like `$context.variables.tier` evaluate directly, string literals use single quotes (`status: 'ok'`), and numbers/booleans are bare (`count: 0`, `flag: true`). The designer recognizes the whole thing as a single expression and leaves it alone — it doesn't try to reinterpret each field.
- **Either expression-form works:** `"${ { ... } }"` (single-brace expression containing a JS object literal) and `"${{ ... }}"` (double-brace object-literal-expression form) evaluate to the same value. Pick one convention; this skill standardizes on the double-brace form, but you may see single-brace in the wild and they are interchangeable.
- **Single-value responses are fine as-is:** `"response": "${$context.outputs.Javascript_1}"` or `"response": "${'done'}"` — the designer only mangles object payloads, not single expressions.
- **On-disk is authoritative — re-validate after every designer save.** Even with the single-expression workaround, every StudioWeb designer save may re-trigger normalization passes that corrupt the Response shape. Treat the file on disk as the source of truth: after any designer roundtrip, re-run `uip api-workflow run --no-auth --output json` and inspect the Response output. If a field has become the literal text of its expression (a long string instead of the evaluated value), the file was re-corrupted — re-apply the single-expression workaround in the file directly, and consider keeping CLI-authored workflows out of designer save cycles until the designer fix ships.
- **Upstream:** designer-side StudioWeb bug. Fix lives in the api-workflows translator for Response tasks (needs to preserve object payloads losslessly). Until that ships, the single-expression workaround is required and may need re-applying after each designer roundtrip.

### Multi-key `Assign.set` silently drops all but one variable

- **Symptom:** Workflow runs correctly under `uip api-workflow run` and updates several variables in one Assign. Open it in StudioWeb, run from the designer (or after a save+reload), and now only one variable is being updated each iteration. The others stay at their schema default. Loops produce results like `{sum: 10, count: 0, max: 0}` when all three should have been computed.
- **Cause:** **StudioWeb's designer collapses multi-key `Assign.set` blocks to a single key on save.** The Assign activity card in the designer represents one variable assignment, and the persistence layer normalizes the JSON to match. After a roundtrip: `"set": { "sum": "${...}", "count": "${...}", "max": "${...}" }` becomes `"set": { "sum": "${...}" }`. The other keys are gone from the file; the runtime executes what's left.
- **Fix:** Use one Assign per variable. Place them sequentially in the same `do` array. Each Assign has a single-key `set` that StudioWeb's designer leaves intact. Example:
  ```json
  // ✗ Multi-key — loses count and max after StudioWeb save
  {
    "Assign_1": {
      "set": {
        "sum": "${$context.variables.sum + $currentItem}",
        "count": "${$context.variables.count + 1}",
        "max": "${Math.max($context.variables.max, $currentItem)}"
      },
      ...
    }
  }

  // ✓ Single-key per Assign — roundtrips cleanly
  { "Assign_Sum":   { "set": { "sum":   "${$context.variables.sum + $currentItem}" }, "export": {...}, "metadata": {...} } },
  { "Assign_Count": { "set": { "count": "${$context.variables.count + 1}" },         "export": {...}, "metadata": {...} } },
  { "Assign_Max":   { "set": { "max":   "${Math.max($context.variables.max, $currentItem)}" }, "export": {...}, "metadata": {...} } }
  ```
- **Cost:** N Assigns instead of 1. The variables export pattern (`{ ...$context, variables: { ...$context.variables, ...$output } }`) on each one merges its single key into `$context.variables` cleanly — the next Assign sees the previous one's update.

### `TS2708: Cannot use namespace '$workflow' as a value` (and `$context`, `$input`)

- **Symptom:** StudioWeb's expression editor shows a warning marker on conditions like `${$workflow.input.score >= 50}`.
- **Cause:** The editor's ambient TypeScript typings declare `$workflow` (and `$context`, `$input`) as **namespaces**, which in TypeScript are type-only constructs erased at compile time and cannot be used as values. The TS checker flags any expression that reads them like values.
- **Status:** Cosmetic, ignore. At run time the executor binds `$workflow`/`$context`/`$input` as real values on `globalThis` via `setVariables` — the TS check has no relationship to runtime behavior. Same warning fires for any `when`, `set`, `response`, or `for.in` expression that touches these names. Workflows containing this warning still execute correctly.
- **What NOT to do:** do not "fix" by rewriting the expression. There is no syntax that satisfies the TS check without breaking runtime — `$workflow` IS the binding name. Workarounds like `(globalThis as any).$workflow` are nonsensical in expression strings.
- **Proper fix is on StudioWeb's side:** ship `declare const $workflow: WorkflowRuntime` instead of `declare namespace $workflow { ... }`. That's not a skill-level concern.

### Activity card renders with a "block" / "forbidden" icon in the designer

- **Symptom:** StudioWeb shows the activity as blocked; you can only delete it. Run-time behavior depends — sometimes the activity is silently skipped, leading to downstream `$context.outputs.<missing>` errors.
- **Cause:** StudioWeb's designer doesn't recognize the activity type. For HTTP-style cards specifically, the designer's `restoreFromTaskItem` (`connector-translator.ts:113`) requires `call: "UiPath.Http"` (or `"UiPath.IntSvc"`, `"UiPath.IntSvcEvent"`) AND a `metadata.configuration` blob containing at minimum `instanceParameters` (`connector-translator.ts:121-136`). Plain `call: "http"` and missing/empty configurations both produce the block icon.
- **Fix:** Run the discovery flow in [connector-activity-discovery.md](connector-activity-discovery.md) to get a stub with the right `uiPathActivityTypeId` and `metadata.configuration` already filled in. The [connector-call-example.json](../assets/templates/connector-call-example.json) template shows the correct shape verified end-to-end. Common mistakes that produce block / legacy icons:
  - Used `call: "http"` (deprecated simple form). **Fix:** switch to `call: "UiPath.Http"` (Http kind) or `call: "UiPath.IntSvc"` (IntSvc kind) — re-stub via `uip api-workflow registry stub <guid>` and replace the activity.
  - Used `call: "UiPath.Http"` but `metadata.configuration` is missing or `"{}"`. **Fix:** re-stub via `uip api-workflow registry stub <guid>` — the stub builds `essentialConfiguration` (with `unifiedTypesCompatible: true` + `savedJitInputFieldId`) automatically.
  - Invented a `uiPathActivityTypeId` value or used the default fallback (`111d59b7-...`). **Fix:** look up the real GUID via `uip api-workflow registry resolve "<keyword>"`.

### `uip is connections ping` returns 404 `"Connection [<uuid>] is invalid or you do not have access to it"`

- **Symptom:** A connection appeared in `uip is connections list <connectorKey> --output json` with `State: "Enabled"`. Pinging it returns HTTP 404 with the message above (sometimes also `Code: "ConnectionNotEnabled"`). The agent is tempted to proceed anyway because the listing said the connection was enabled.
- **Cause:** The filtered `uip is connections list <connectorKey>` listing is not authoritative. It can return **stale or orphaned records** — connections where the underlying element instance was deleted upstream, or that were created in a different org/tenant. The unfiltered `uip is connections list` (no connector argument) often shows a different, working UUID for the same `ConnectorKey`.
- **Fix:** Run the unfiltered listing and find the working UUID:
  ```bash
  uip is connections list --output json
  # Search Data[] for entries with ConnectorKey == "<connector-key>"
  # Take a different Id, then ping it:
  uip is connections ping <alternate-uuid> --output json
  # If Code: "ConnectionPing" — use this UUID in the workflow
  ```
  If the unfiltered listing is also empty, the connection may live in another folder — both listings are folder-scoped. Search every folder before giving up:
  ```bash
  uip is connections list --all-folders --output json
  # Each row carries Folder / FolderKey. Take an Enabled Id, then ping it.
  ```
- **What NOT to do:** do NOT proceed with the failing UUID and "flag for follow-up." A workflow authored against a non-pinging connection will 401 in cloud regardless of how correct the JSON is. Do NOT conclude "no connection exists" from an empty filtered or unfiltered listing — run `--all-folders` first. Only if the filtered, unfiltered, AND `--all-folders` listings all fail to yield a working UUID should you abort and tell the user to re-authenticate (`uip is connections edit <uuid>` opens an OAuth browser flow) or create a fresh connection in the StudioWeb UI.
- **See also:** [connector-activity-discovery.md — Step 2](connector-activity-discovery.md#step-2--verify-a-vendor-connection-intsvc-kind-only) for the full discovery+fallback flow.

### IntSvc kind activity output read at the root returns `undefined`

- **Symptom:** A `UiPath.IntSvc` activity (Outlook GetNewestEmail, Gmail Send Email, GitHub Search Issues, …) ran successfully — the run output shows the vendor data in the activity result. Downstream code that reads `$context.outputs.<Activity>.<field>` gets `undefined`. If conditions like `${$context.outputs.getNewestEmail_1?.subject?.length > 15}` always evaluate false. JsInvoke scripts return empty values.
- **Cause:** IntSvc kind (`call: "UiPath.IntSvc"`) wraps the vendor payload in `.content`. The full activity output is `{ statusCode, statusText, headers, ok, request, content: { <vendor fields> }, vendorProcessingTimeMs }` — the actual data is one level deeper than the root, under `.content`. The other keys carry HTTP-level metadata you usually don't need.
- **Fix:** Read through `.content`:
  ```javascript
  // ✗ Wrong — always undefined
  "${$context.outputs.getNewestEmail_1?.subject}"

  // ✓ Correct
  "${$context.outputs.getNewestEmail_1?.content?.subject}"
  ```
  For list-shaped operations, `.content` itself holds the result — usually as a bare array (the IS proxy strips vendor-native envelopes like M365 Graph's `{ value: [...] }` before it reaches you), but the exact shape is per-operation. **Read the stub's `optionalConfiguration.fieldsContainer.outputJsonSchema.type` to know which:**
  - `type: "array"` → `.content` IS the array. Read `.content[0].<field>`.
  - `type: "object"` → `.content` is a single object. Read `.content.<field>`.

  Verified shapes: Outlook `ListEmails` → `.content[0].subject` (bare array). Slack `send_message_to_user_v2` → `.content.ok`, `.content.ts` (single object). HTTP Request → `.content.<field>` (single object). For unverified vendors, log the activity output once and inspect.
- **Defensive form for JsInvoke** (handles the local-CLI quirk where `.content` is sometimes a JSON string):
  ```javascript
  const out = $context.outputs.getNewestEmail_1;
  const raw = out && (out.content !== undefined ? out.content : out);
  const body = (typeof raw === 'string') ? JSON.parse(raw) : raw;
  const item = Array.isArray(body) ? body[0] : body;
  return { subject: (item && (item.subject || item.Subject)) || '' };
  ```
- **Use the stub's `Data.ExportBucketKey`** for downstream reads — for Outlook `getNewestEmail` it's `getNewestEmail_1`, not the slot's `GetNewestEmail_1`. Connector activities can have a slot/export divergence — see "Connector slot key and export-bucket key can differ — use the stub's values" below.
- **Http kind (`UiPath.Http`) is the same shape:** `{ statusCode, statusText, headers, ok, request, content, vendorProcessingTimeMs }`. The `.content` field carries the parsed response body. The wrapping is universal across both kinds.

### Connector `bodyParameters` fields disappear after StudioWeb save (nested objects dropped)

- **Symptom:** Authored a connector activity with nested body — `{ message: { toRecipients: "...", subject: "...", body: { content: "..." } }, saveToSentItems: true }`. Workflow runs locally. Open it in StudioWeb (or save via the designer); the `message` block is gone from the file. Only fields whose names appear at the top level of `bodyParameters` survive (e.g. `saveToSentItems`). The vendor receives a payload missing the actual message data.
- **Cause:** StudioWeb's connector deserializer (`buildConnectorProperties` in `connector-translator-utils.ts`) scans `bodyParameters` for keys that match the connector's input-field names verbatim. Field names like `message.toRecipients` are flat dotted strings — the dot is a literal character, not a path separator. The deserializer does NOT recurse into nested objects. Nested-object values aren't found, the field model shows them empty, and the next save persists the empty model.
- **Fix:** Use flat dotted keys matching the schema's `requestFields[].name` verbatim:
  ```json
  // ✗ Wrong — dropped on save
  "bodyParameters": {
    "message": {
      "toRecipients": "andrei.hodoroaga@uipath.com",
      "subject": "test",
      "body": { "content": "<p>hi</p>", "contentType": "Html" }
    },
    "saveToSentItems": true
  }

  // ✓ Correct — survives roundtrip
  "bodyParameters": {
    "message.toRecipients": "andrei.hodoroaga@uipath.com",
    "message.subject": "test",
    "message.body.content": "<p>hi</p>",
    "message.body.contentType": "Html",
    "saveToSentItems": true
  }
  ```
- **Same rule** applies to `queryParameters` and `pathParameters`. The IS proxy unflattens the dotted keys into a nested wire payload before calling the vendor — so the over-the-wire JSON ends up identical, but the on-disk shape must be flat. See [connector-activity-discovery.md#rule-a--bodyparameters--queryparameters--pathparameters-use-flat-dotted-keys](connector-activity-discovery.md#rule-a--bodyparameters--queryparameters--pathparameters-use-flat-dotted-keys).

### Connector `bodyParameters` literal cleared after StudioWeb save (`${'literal'}` read as expression)

- **Symptom:** Authored connector body with literals wrapped per the Assign rule — `"message.toRecipients": "${'andrei.hodoroaga@uipath.com'}"`. Workflow runs locally. After StudioWeb save, the field becomes empty (or shows a non-literal expression marker in the designer); the email goes out with no recipient.
- **Cause:** SKILL.md rule 5 (literal-wrap as `${'foo'}`) applies to **Assign / Response / If `when`**, NOT to connector params. StudioWeb's connector field detector treats `${...}` as a non-literal expression — it's looking for either a bare literal value or a real reference. `${'foo'}` looks like neither (it's a literal-disguised-as-expression), so the value isn't bound as a field literal and is dropped.
- **Fix:** Bare literals in connector params:
  ```json
  // ✗ Wrong — cleared on save
  "bodyParameters": {
    "message.toRecipients": "${'andrei.hodoroaga@uipath.com'}",
    "message.subject": "${'this is a claude skill test'}"
  }

  // ✓ Correct — bare literals
  "bodyParameters": {
    "message.toRecipients": "andrei.hodoroaga@uipath.com",
    "message.subject": "this is a claude skill test"
  }
  ```
  References (`${$context.variables.X}`, `${$workflow.input.Y}`) stay wrapped because they're real expressions — the rule applies to literal *values*, not to references. See [connector-activity-discovery.md#rule-b--literals-in-connector-params-are-bare-not-literal-wrapped](connector-activity-discovery.md#rule-b--literals-in-connector-params-are-bare-not-literal-wrapped).

### Connector slot key and export-bucket key can differ — use the stub's values

- **Symptom:** Authored a connector activity using the slot key for the export — `"GetNewestEmail_1"` in the `do` array AND in the `export.as`. Workflow runs locally; downstream `$context.outputs.GetNewestEmail_1.content.subject` returns the correct value. After opening or saving in StudioWeb, downstream reads return `undefined`. The TypeScript linter shows `TS2551: Property 'GetNewestEmail_1' does not exist on type 'typeof outputs'. Did you mean 'getNewestEmail_1'?`. Diff of the file shows the export bucket key was rewritten to `getNewestEmail_1` — the slot key in the `do` array stayed `GetNewestEmail_1`.
- **Cause:** Connector activities are the only activity type where the slot key (in the `do` array) and the export-bucket key (what `$context.outputs.<X>` reads as) can differ. Every other type (Assign, JsInvoke, If, ForEach, DoWhile, TryCatch, Wait, Response) keeps "slot key === export key." For connector activities, StudioWeb's serializer normalizes the export bucket to a stub-computed form. The stub returns both keys correctly in `Data.SlotKey` and `Data.ExportBucketKey` — they are sometimes identical (Outlook `ListEmails` → both `ListEmails_1`) and sometimes different (Outlook `getNewestEmail` → slot `GetNewestEmail_1` / bucket `getNewestEmail_1`; HTTP `http-request` → slot `HttpRequest_1` / bucket `http_request_1`). Reconstructing either key from `objectName` by hand is what produces the mismatch.
- **Fix:** Use `Data.SlotKey` and `Data.ExportBucketKey` from the stub verbatim. The slot key goes in the `do` array; the export-bucket key goes in `export.as` AND in every downstream `$context.outputs.<X>` reference:
  ```json
  // ✓ Correct — both keys taken from the stub output
  {
    "GetNewestEmail_1": {                          // Data.SlotKey
      "call": "UiPath.IntSvc",
      "with": { ... },
      "export": { "as": "{ ...$context, outputs: { ...$context?.outputs, \"getNewestEmail_1\": $output } }" }
                                                   // Data.ExportBucketKey
    }
  }
  ```
  And every downstream consumer:
  ```json
  "when": "${$context.outputs.getNewestEmail_1?.content?.subject?.length > 15}"
  "response": "${{ subject: $context.outputs.getNewestEmail_1.content.subject }}"
  ```
- **Rule:** Read `Data.SlotKey` and `Data.ExportBucketKey` from `uip api-workflow registry stub` output. Use both verbatim. Never derive either from `objectName` by hand. See [connector-activity-discovery.md — Rule (c)](connector-activity-discovery.md#rule-c--use-dataslotkey-and-dataexportbucketkey-from-the-stub-verbatim).

### `400 "Unable to parse multipart body"` from a curated send-email-style endpoint

- **Symptom:** Workflow runs against a vendor curated activity (Outlook `send-mail-v2`, Gmail `sendEmail` with attachments, etc.) and the vendor returns `400 "Unable to parse multipart body"`. The request `Content-Type` header was `application/json`. The body looked structurally correct (recipient, subject, body all present).
- **Cause:** The endpoint expects `multipart/form-data`, not `application/json`. Multipart-only endpoints typically support file attachments — they encode the email JSON as one form part and any attached files as other parts. The activity needs a `multipartParameters` declaration alongside `bodyParameters`. Without it, the executor sends `application/json` with the raw `bodyParameters` payload, and the vendor's multipart parser rejects it.
- **Detection:** Re-stub the activity (`uip api-workflow registry stub <guid> --connection-id <uuid> --output json`) — the CLI calls IS Elements internally and declares `multipartParameters` automatically when it sees `parameters[].type === "multipart"`. If `Data.Activity.with.multipartParameters` is missing from the stub output, the endpoint isn't multipart. (Manual fallback if the stub's enrichment failed: `uip is resources describe <connector-key> <object-name> --output json` and read the `parameters` section.)
- **Fix:** Mirror the parameters on the activity. For Outlook `send-mail-v2`:
  ```json
  "with": {
    "endpoint": "/hubs/productivity/send-mail-v2",
    "bodyParameters": {
      "message.toRecipients": "andrei.hodoroaga@uipath.com",
      "message.subject": "...",
      "message.body.content": "...",
      "message.body.contentType": "Text",
      "saveToSentItems": true
    },
    "queryParameters": { "saveAsDraft": false },
    "multipartParameters": [
      { "name": "file", "dataType": "file" },
      { "name": "body", "dataType": "string" }
    ]
  }
  ```
- **What the executor does with `multipartParameters`:** `is-utils.js:constructMultipartFormData` walks the array. For `dataType: "string"` parts, it JSON-stringifies the **entire `bodyParameters` object** and stuffs the resulting string into the multipart part with that name. So `bodyParameters` (with its flat-dotted keys) becomes the JSON content of the multipart `body` part. For `dataType: "file"` parts, the part is left empty unless the activity supplies a file reference (rarely needed for the no-attachment case — Outlook accepts an empty `file` part). See [connector-activity-discovery.md#multipart-endpoints--multipartparameters-declaration](connector-activity-discovery.md#multipart-endpoints--multipartparameters-declaration).

### Properties panel: "to debug this resource, select a connection for it from the resource definition page"

- **Symptom:** The workflow runs locally with `uip api-workflow run` AND from "Run" in StudioWeb. Open the activity card in the designer, click the connection field, and the properties panel renders the connection as invalid with the message **"to debug this resource, select a connection for it from the resource definition page."** `bindings_v2.json` contains a correct `Connection` resource entry; `Workflow.json` has `connectionId` / `connectionResourceId` set to the same pinged UUID.
- **Cause:** **In a Solutions-mode project, every connection used by an activity must ALSO be declared in two Solution-level artefacts.** The catalogue resource file (`Solution/resources/solution_folder/connection/<connector-key>/<connection-name>.json`) declares that the Solution uses the connection; the per-user debug overwrites file (`Solution/userProfile/<guid>/debug_overwrites.json`) maps the connection's UUID to a runtime folder+key. StudioWeb's properties panel resolves connections by calling `getResourceDebugReference({ key: <uuid> })` — when that returns null (because either file is missing), `activityState.realConnectionId` becomes null, and the workflow serializer writes `with.connectionId: null` to `Workflow.json` on the next save. The runtime resolves connections from `Workflow.json` directly (which is why "Run" works against a freshly-authored workflow), but the panel resolves via the Solution resource tree (which is why the click breaks). `uip api-workflow registry stub` does not write either file — both are created by the post-authoring sync flow described in the Fix below. Distinct from `bindings_v2.json`: that file is the *input* to the sync (StudioWeb computes it in-memory; the CLI's `bindings sync` emits the same content offline); the catalogue + debug-overwrite files are the *outputs*.
- **Detection:** Look for the file:
  ```bash
  ls Solution/resources/solution_folder/connection/<connector-key>/ 2>/dev/null
  # → ENOENT or empty → file missing → this is the bug
  ```
  Also check that `bindings_v2.json`'s `"key"` matches `Workflow.json`'s `connectionId` matches the missing resource file's intended `"key"` — same UUID across all three.
- **Fix (preferred — two CLI commands):**
  ```bash
  # 1. Generate bindings_v2.json from Workflow.json (local for connections; queries IS for resource picker fields):
  uip api-workflow bindings sync --workflow <path-to-Workflow.json> --output json

  # 2. Sync catalogue + debug overwrites via @uipath/resource-builder-sdk (requires uip login):
  uip solution resources refresh --solution-folder <path-to-solution-root> --output json
  ```
  Step 1 walks the workflow, extracts IntSvc connector activities, dedupes by connection UUID, and writes the canonical `bindings_v2.json` next to the workflow — what StudioWeb normally produces in-memory on workflow open. Step 2 reads that file, uses the SDK's `addOrUpdateResourceToSolutionAsync` to write the catalogue file, and `editOverwritesAsync` to write the per-user debug overwrites. Both commands are idempotent and safe to re-run.

  Reload the workflow in StudioWeb. The connection pill should resolve to the connection name, the "resource definition page" error should be gone, and clicking the activity should no longer null `with.connectionId`.

- **Fix (hand-authored fallback — when the CLI is unavailable):** Start from [assets/templates/solution-connection-resource-template.json](../assets/templates/solution-connection-resource-template.json) and fill in the placeholders:
  ```json
  {
    "docVersion": "1.0.0",
    "resource": {
      "name": "<connection Name from `uip is connections list`>",
      "kind": "connection",
      "type": "<connector key, e.g. uipath-microsoft-outlook365>",
      "apiVersion": "integrationservice.uipath.com/v1",
      "isOverridable": true,
      "dependencies": [],
      "runtimeDependencies": [],
      "folders": [{ "fullyQualifiedName": "solution_folder" }],
      "spec": {
        "connectorName": "<ConnectorName from `uip is connections list`, e.g. Microsoft Outlook 365>",
        "name": "<same connection Name>",
        "authenticationType": "AuthenticateAfterDeployment",
        "connectorVersion": "<from stub's metadata.configuration.essentialConfiguration.connectorVersion, fallback \"1.0.0\">",
        "connectorKey": "<connector key>",
        "pollingInterval": 5
      },
      "locks": [],
      "key": "<connection UUID — MUST equal Workflow.json's connectionId>",
      "files": []
    }
  }
  ```
  Place it at `Solution/resources/solution_folder/connection/<connector-key>/<connection-name>.json`. Reuse the `solution_folder` name from any existing `Solution/resources/<folder>/package/<workflow>.json` (`folders[0].fullyQualifiedName`); default is `"solution_folder"`. One file per unique connection UUID — if the workflow has two activities reusing one connection, write one file; two distinct connections → two files.
- **Note on the user-profile debug overwrite.** StudioWeb additionally writes `Solution/userProfile/<guid>/debug_overwrites.json` mapping `solutionResourceKey` to a concrete folder + connection at debug time. That file is per-user state, written by the designer the first time you assign a debug connection. The agent does not author it; if it's missing, debug runs from the StudioWeb UI will prompt for a connection but won't 401.
- **See also:** [connector-activity-discovery.md — Step 5](connector-activity-discovery.md#step-5--solutions-mode-intsvc-kind-declare-the-connection-as-a-solution-resource) for the full flow, including where each field value comes from.

### Required request field dropped by `registry stub`

- **Symptom:** A vendor curated activity (Outlook `getNewestEmail`, Gmail `searchMessages`, …) runs locally — sometimes returning unexpected results (wrong folder, no filter applied) — and fails in cloud with a 4xx, OR the StudioWeb properties panel marks a field with a red border and an "invalid" badge but no clear error text. `registry stub`'s output shows `queryParameters: {}`, `pathParameters: {}`, or `bodyParameters: {}` for an endpoint that obviously needs inputs.
- **Cause:** **`uip api-workflow registry stub` only populates `<location>Parameters` from `--inputs`** — a `required: true` field not passed there stays out of the activity. The stub does flag it: `Data.Parameters` / `Data.RequestFields` carry the IS schema's `required` flags, and a missing required field raises a `Data.Warnings` entry (`"Required field(s) not provided via --inputs: parentFolderId"`). The failure mode arises when that warning is ignored.
- **Prevention:** Follow the documented order — `uip is resources describe <connector-key> <object-name> --operation <Op> --connection-id <pinged-uuid>` BEFORE stubbing, then pass the required values via `--inputs` so the stub is complete on the first run.
- **Detection:** Read `Data.Warnings` and `Data.Parameters` / `Data.RequestFields` in the stub output — a `"Required field(s) not provided"` entry means the describe step was skipped or a value was missed.
- **Fix:** Either re-run the stub with the missing fields included via `--inputs`:
  ```bash
  uip api-workflow registry stub <activity-type-id> \
    --connection-id <uuid> \
    --inputs '{"parentFolderId": "inbox"}' \
    --output json
  ```
  Or hand-edit the activity to insert the missing field — **bare literal** (rule 16(b)), **flat dotted key** if a body/nested field (rule 16(a)). Example:
  ```json
  "with": {
    ...
    "queryParameters": {
      "parentFolderId": "inbox"
    }
  }
  ```
- **Well-known shortcuts.** MS Graph accepts well-known folder names (`"inbox"`, `"sentitems"`, `"drafts"`) as `parentFolderId`. They run, but StudioWeb's FolderPicker only displays the friendly folder name when the value matches an ID from its lookup cache. For exact UI fidelity, fetch the real ID via `uip is resources run list <connector-key> <object-name> --connection-id <uuid>` against the field's `lookup.path` (often `/MailFolders`, `/Folders`, etc.).
- **Heuristic:** when the stub returns empty `queryParameters` / `pathParameters` / `bodyParameters` for a non-trivial vendor operation, treat it as the bug. Real endpoints (CRUD on real objects, list-with-filters operations) almost never have zero required inputs.
- **Upstream:** the stub IS surfacing the metadata it has — `metadata.configuration` contains the full `inputFields` list — so this is a CLI-side fix where the stub should populate defaults/placeholders from `required: true` fields, not a missing-data issue. Until that ships, the cross-check is mandatory per skill rule 16 step 4.
- **See also:** [connector-activity-discovery.md — Required-field cross-check](connector-activity-discovery.md#required-field-cross-check--the-stub-drops-required-true-request-fields).

### `401 — Failed to execute IS call to /<endpoint>: Invalid Organization or User secret, or invalid Element token provided`

- **Symptom:** The workflow runs locally with `uip api-workflow run` but fails in StudioWeb cloud (or against the real IS proxy) with a 401 status. The error detail says `"Invalid Organization or User secret, or invalid Element token provided."` — sounds like a credential / auth-token issue but is often something else.
- **Cause:** The IS proxy's auth flow for `/elements_/v3/element/instances/{connectionId}/{operationName}` rejected the call. There are two distinct sub-cases — diagnose by looking at the URL the proxy hit:

  **Sub-case A — Wrong endpoint on the connection's element.** The endpoint in the proxy URL doesn't exist on the target connection's connector. Most common when an agent uses Http kind (`call: "UiPath.Http"` with `endpoint: "/http-request"`) but `connectionId` points at a vendor connection (Outlook, Gmail, etc.) instead of a `uipath-uipath-http` connection. The Outlook connector has no `/http-request` operation, only its curated ones (`/getNewestEmail`, `/sendEmail`, …) — so the proxy returns 401 as a generic "I can't service this request" rather than "operation not found."
  - **Fix:** Switch to IntSvc kind (`call: "UiPath.IntSvc"`, `with.connector` = the vendor key, `with.endpoint` = `"/<curated-operation-name>"`). See [connector-activity-discovery.md — IntSvc kind](connector-activity-discovery.md#intsvc-kind--call-uipathintsvc-vendor-curated-activity) — IntSvc kind is for vendor activities, Http kind is only for the `uipath-uipath-http` HTTP Request activity.

  **Sub-case B — Connection is in a broken state.** The endpoint is right (e.g. `/getNewestEmail` on an Outlook connection), but the connection's upstream OAuth token is expired, never properly authorized, or the running identity doesn't have access to the connection.
  - **Fix:** Run `uip is connections ping <connection-uuid> --output json`. If it returns `Code: "ConnectionNotEnabled"`, re-authenticate via `uip is connections edit <connection-uuid>` (opens browser for OAuth) or fix in the StudioWeb UI. If it returns `Code: "ConnectionPing"` (success) but the cloud still 401s, check that your CLI login (`uip login status`) is in the same org+tenant your browser is using at cloud.uipath.com — a tenant mismatch will reject the connection ID at the proxy layer.

- **Prevention:** The discovery flow's Step 4b (`uip is connections ping`) is mandatory specifically to catch sub-case B before authoring. Don't author against a connection that doesn't ping — the workflow shape will look right and even run locally, but fail at deployment time.

---

## Run-Time Errors (CLI)

### `"File not found: <path>"`
- **Cause:** The workflow file path passed to `uip api-workflow run` does not resolve
- **Fix:** Use an absolute path or run from the directory containing the workflow

### `"Invalid JSON in workflow file"`
- **Cause:** Malformed JSON — trailing comma, unquoted key, mismatched brace, comment
- **Fix:** Validate before running:
  ```bash
  node -e "JSON.parse(require('fs').readFileSync('./wf.json','utf8'))"
  ```
  JSON does NOT permit comments. Strip them.

### `"Invalid JSON in --input-arguments"`
- **Cause:** The string passed to `--input-arguments` is not valid JSON
- **Fix:** Wrap the entire JSON in single quotes; double-quote all keys and string values:
  ```bash
  --input-arguments '{"name":"Alice","count":3}'
  ```

### `Workflow status is not "Successful"` (executor returns failure)
- **Cause:** A task threw during execution
- **Fix:** Read `Message` and `Instructions` in the failure output. Common patterns:
  - JS_Invoke: missing `return` statement, runtime error in script body, undefined `$context.outputs.<Activity>` (prior activity did not run or did not `export`)
  - Assign expression: invalid `${...}` syntax, referencing an undefined variable in strict mode
  - Loop body: condition variable not updated (DoWhile infinite loop), missing `#Body` suffix, wrong export pattern

### `$context.outputs.<Activity>` is undefined
- **Cause:** The prior activity did not `export` its output back into context
- **Fix:** Add the standard export to the prior task — see [expressions-and-context.md](expressions-and-context.md)

### Strict-mode JS error inside a JS_Invoke
- **Cause:** Implicit globals, `var` hoisting, unsafe property access, duplicate object keys
- **Fix:**
  - Replace `var` with `const` / `let`
  - Use optional chaining: `$context?.outputs?.Javascript_1?.items`
  - Ensure object literals have unique keys

### `"Script execution failed timed out"`

- **Symptom:** A JavaScript activity that works on small inputs fails on larger ones with `Execution error: Script execution failed timed out: Script execution failed timed out`. The workflow validates; only the run fails.
- **Cause:** The activity exceeded the runner's per-script budget. In the executor the CLI actually ships — `@uipath/api-workflow-executor` **12.10.2**, exact-pinned by `packages/api-workflow-tool/package.json` on `uipcli` main — the budget is a **flat 10 seconds**, passed as a bare literal:

  ```js
  // dist/handlers/script-task-handler.js
  await this.expressionHandler.evaluateScript(code, taskName, 10000);
  ```

  Measured against that build: an 8-second script passes, a 12-second script fails.

  **Check the pinned executor before trusting any number here** — it is a literal, not a documented contract, and it has already changed: executor 12.23.2 (newer, NOT pinned by the CLI as of 2026-08-20) replaces it with `SCRIPT_TIMEOUT_MS = 10000` plus a second budget of ~100s for scripts whose source contains `$helpers`, selected by a literal `code.includes('$helpers')` substring test. If your CLI pins 12.23.x or later, the 10s figure applies only to scripts with no `$helpers` call.

  All of the above are **local** figures. The **cloud** cap is different and is documented: *"JavaScript code execution has a timeout of 30 seconds"* — [Script activity, Known limitations](https://docs.uipath.com/studio-web/automation-cloud/latest/user-guide/script).

  So the local runner is **3x stricter than cloud**, and each side misleads on its own:
  - A local `timed out` failure does **not** mean the workflow fails in cloud — anything under 30s runs there.
  - Passing locally does **not** prove you clear the cloud cap. Local runs use small fixtures; a script that takes 3s over 10 test rows can exceed 30s over 10,000 production rows.

- **Fix:**
  - Move bulk work out of one script: page the data and process a batch per loop iteration, so each script invocation is short.
  - Split one long script into several JavaScript activities chained by `export`.
  - Never busy-wait inside a script (`while (Date.now() < end) {}`) — use a `Wait` activity, which is not charged against the script budget.
  - If the work genuinely cannot be split, it does not belong in an API workflow; a connector call or a Coded Function is the right home.

### Failed cloud run after publish (job faulted in Orchestrator)

- **Symptom:** The workflow passed `validate` and `run --no-auth` locally, packed, published, and deployed — but a triggered cloud run faults. Local re-runs still pass.
- **Cause:** Faults that only surface in cloud — real vendor responses, connection auth/token state, trigger payload shape, tenant/folder scoping — none of which the local runtime exercises.
- **Fix:** Diagnose from the deployed job, not the local file:
  ```bash
  uip or jobs get <jobId> --output json   # THE diagnostic: Data.State + Data.Info
  ```
  `Data.State` reads `Faulted` and `Data.Info` carries the runtime message — for an API workflow that is usually the whole answer.

  **Two surfaces that look useful and are not, for API-workflow jobs:**

  | Command | What it actually returns |
  |---------|--------------------------|
  | `uip or jobs logs <jobId>` | Lifecycle lines only — `"Workflow started"` / `"Workflow completed"`, both at level `Info`. It reports **`Workflow completed` even for a Faulted job** and never carries the error. Never read "completed" here as success. |
  | `uip traces spans get --job-key <jobKey>` | Returned `"Error retrieving trace ID for job"` on every API-workflow job probed. The CLI emits that same message for any trace-ID lookup failure (a malformed GUID included), so read it as "no trace resolved for this job", not as proof the surface is absent. Either way it yields no fault detail — use `jobs get`. |

  **Diagnose before you tear down:** after uninstalling the deployment, `uip or jobs get` on its jobs returns `Result: Failure` with an empty `State`. Jobs themselves are immutable audit records (`uip or jobs --help`: they "cannot be deleted -- they age out per the binding process's retention period"), so the likely cause is that the folder/process context needed to resolve the job is gone, not the records. Either way, read the fault before you uninstall.

  (Folder scoping differs: `uip or jobs list` accepts `--folder-path`/`--folder-key`/`--all-folders`; `uip or triggers list` accepts only `--folder-path`/`--folder-key`; `uip or jobs start <process-key>` infers the folder.)
  Map the surfaced error back to a fix with the category order below (Structure > Expression > Activity Config > Logic). If the fault is a 401 / `ConnectionNotEnabled`, `uip is connections ping <uuid>` the bound connection first. Full operate + diagnose command map: [operating-published-workflows.md](operating-published-workflows.md). For deep, multi-signal root-cause (what changed, cross-run comparison, incident correlation), hand off to **uipath-troubleshoot**.

### Outbound call to a third-party API works locally, times out or is refused in cloud

- **Symptom:** An `HTTP Request` or connector call to a customer/vendor endpoint succeeds when the workflow is executed on your own machine and fails only from the deployed copy — connection refused, or a hang ending in a timeout. Same URL, same payload.
- **Cause:** Local runs egress from your machine's IP; cloud runs egress from UiPath infrastructure. If the target sits behind an IP allowlist, the cloud source addresses have to be on it — and **which** addresses depends on how the call is made.

  Per [About API workflows](https://docs.uipath.com/studio-web/automation-cloud/latest/user-guide/about-api-workflows): *"Which outbound path applies depends on how the external call is made"* — an HTTP Request with manual authentication egresses via **serverless robots**, a connector-based call via **Integration Service** — and allowlisting is required *"only when API workflows communicate externally"*, covering both **Serverless static IPs** and **Integration Service IPs**.

  The two are not interchangeable, and the reason is structural. Per [Configuring the firewall for Automation Cloud](https://docs.uipath.com/automation-cloud/automation-cloud/latest/admin-guide/configuring-the-firewall-for-cloud), most services now share one **unified** set of outbound ranges per region — *"a single set of IP ranges covers Automation Cloud Portal, Orchestrator, Integration Service, Apps, Automation Ops, Test Manager, AI Trust Layer, and Notification Service simultaneously"* — but four services are carved out: *"Document Understanding, Insights, IXP, and Automation Cloud Robots - Serverless"* keep their own service-specific ranges.

  So **Integration Service sits in the unified regional set and serverless robots do not.** A customer who allowlisted the unified ranges has covered your connector activities and *not* your manual-auth HTTP calls. Always read the current ranges off those pages before asking anyone to change firewall rules — they are per-region and they change.
- **Fix:** Establish which path the call takes (connector activity → Integration Service; `UiPath.Http` with *connector-based* authentication → also Integration Service; `UiPath.Http` with *manual* authentication → serverless robot), then have the endpoint owner allowlist that service's published outbound ranges. This is a network-configuration fix, not a workflow fix — no edit to `Workflow.json` will resolve it, so stop editing and escalate once the symptom matches.
- **Distinguishing it from an auth fault:** a 401/403 with a response body is an auth or connection problem (`uip is connections ping <uuid>` first). A refused connection or a timeout with no HTTP response at all points at the network path.

---

## Packaging Errors

### `"No CLI tool mapping found for project type 'X'"`
- **Cause:** The solution `.uipx` declares a project type the packager has not loaded
- **Fix:** For API workflows, ensure `Type: "Api"` exactly (case-sensitive)

### `Failed to parse <solution>.uipx`
- **Cause:** Solution file is malformed JSON
- **Fix:** Re-create with `uip solution init <name>` and re-add projects via `uip solution projects add`

### Generated `operate.json` or `package-descriptor.json` mismatch
- **Cause:** Stale files committed by hand or from an older CLI version
- **Fix:** Delete both files from the project directory and re-run `uip solution pack`. The packager regenerates them.

### `.nupkg` produced but missing workflow files
- **Cause:** Workflow JSON not located in the project directory the packager scanned
- **Fix:** Verify `Workflow.json` is in the project folder whose `project.uiproj` is declared in the solution `.uipx`

### API workflow runs/deploys fine but does NOT appear or open in Studio Web
- **Symptom:** The workflow runs under `uip api-workflow run`, the solution packs, publishes, and deploys as an API process — but after uploading the solution to Studio Web the API project is invisible / not editable. Importing it directly fails with `Failed to import new projects at the overwrite operations`.
- **Cause:** The project uses the **runtime-only** shape — `project.json` + `workflows/WF_*.json`, no `.uiproj`. Studio Web's import (`isProjectFolder`) only recognizes a folder as a project when it contains a `.uiproj` file; a `project.json`-only folder is rejected as `invalid_project_folder`. Every runtime gate (validate / run / pack / publish / deploy) passes on this shape, so the defect surfaces only when a human opens Studio Web. This was the Woolworths private-preview RCA root cause. Root reason it happened: the project was hand-assembled instead of scaffolded with `uip api-workflow init`, which always produces the correct shape.
- **Fix (preferred — re-scaffold):** For each broken `Type: "Api"` project, run `uip api-workflow init <newName>` inside the solution directory and copy the old main workflow's `document`/`do` content into the new `Workflow.json`. `init` writes the correct `project.uiproj` / `entry-points.json` / `bindings_v2.json` and registers the project in the `.uipx`. Then delete the old `project.json` folder (and its `.uipx` entry).
- **Fix (in-place conversion)** when you must keep the existing folder/`Id` (SKILL.md rule 19a, [workflow-file-format.md](workflow-file-format.md#project-structure-studio-web-editable-contract)):
  - Add `project.uiproj` (`ProjectType: "Api"`, `MainFile: "Workflow.json"`) and `entry-points.json` (`filePath: "content/Workflow.json"`, no leading slash, `type: "Api"`). Copy `bindings_v2.json` if present.
  - Rename the main workflow to `Workflow.json` at the project root.
  - Edit the `.uipx` `ProjectRelativePath` from `<folder>/project.json` → `<folder>/project.uiproj`, **preserving the project `Id` and `Type`**. Do NOT use `uip solution projects remove`+`add` — it mints a new `Id`.
  - Remove the stray `project.json` / `workflows/` (a mismatched `project.json` triggers `ProjectMetadataMismatchError`).
  - Re-pack, then confirm the project opens in Studio Web (runtime/pack success alone does not prove it).

---

## Publish Errors

### `"Invalid file type. Expected a .zip file"`
- **Cause:** Passing a `.nupkg` directly instead of the wrapping `.zip`
- **Fix:** Publish the `.zip` produced by `uip solution pack`, not its contents

### Publish 401 / 403
- **Cause:** Not logged in, wrong tenant, or insufficient role
- **Fix:** `uip login`, confirm `--tenant` matches deployment target

### Publish 409 / "name conflict"
- **Cause:** A package with the same name and version already exists
- **Fix:** Bump version with `--version <newVersion>` and re-pack/publish

---

## Validation Pitfalls

### Not re-validating after a fix
- **Symptom:** Reported "fixed" but errors remain
- **Cause:** Skipped re-running the validators after applying a fix
- **Fix:** ALWAYS re-run after every edit. Two validators: `uip api-workflow validate <Workflow.json>` (offline static — schema + semantic checks, autonomous) then `uip api-workflow run --no-auth` (runtime — catches expression/connection errors static analysis can't). See SKILL.md rules 20–21.

### `Unknown activityType '<Name>'`

- **Symptom:** `validate` rejects an activity — `Unknown activityType 'X'. Valid types: ...`
- **Cause:** The authorable set is closed and mirrors the Studio Web palette. It is also versioned: `CustomLog` was added 2026-08-11, so older CLIs list 13 types and newer ones 14. **Take the list from the error message — never memorise one.**
- **Fix:** Stay inside the list. Two task types the executor runs but `validate` refuses — do not author them:

  | Instead of | Use |
  |---|---|
  | `raise` | `throw` inside a JavaScript activity, or `Response` with `markJobAsFailed: true` |
  | `while` (pre-condition loop) | `DoWhile` + an `If` whose `#Else` exits via `Break` |

  Both execute under `run` (verified on executor 12.10.2), so **a passing local run is not proof a workflow can ship.** `validate` is the gate.
- **Error-count tell:** a small count naming the activityType — one error per offending task — → unknown **name**, caught by the allowlist. A large avalanche starting `Missing required property 'call'` → the schema could not match the task **shape**, which has two causes and you must check both:
  1. **Unknown task key** — the schema models no such task (e.g. `raise`). Nothing about the fields will help.
  2. **An unexpected field on a KNOWN task** — one stray key makes the whole task unmatchable, and the error text still says `Missing required property 'call'`. `set` on a `Break` is the documented instance: it produced **7797 errors** in this skill's own `nested-control-flow-example.json`, and deleting that one key made it Valid.

  So read the avalanche as "the schema cannot match this task", not "the task key is wrong". Diff the task against the shape in [task-types.md](task-types.md) field by field before concluding the type is unsupported.
- **Logging:** `console.log` / `console.warn` inside a JavaScript activity are captured and emitted as `[Script <TaskName>]: ...`. Whether they reach Orchestrator job logs in cloud is unverified — a probe showed the Orchestrator job-log surface carrying only lifecycle lines — so put anything you must read after a run in the `Response`. `CustomLog` is on the list but no executor ships a handler for it; do not author one yet.
- **Do not** mislabel `metadata.activityType` to slip a type past the check — the validator cross-checks the label against the task's own keys (`has activityType 'DoWhile' but must contain 'for' with 'doWhile'`).

### Fixing in wrong order
- **Symptom:** Fixing one error creates more errors; thrashing
- **Cause:** Fixing logic errors before structure errors; lower-priority fixes destabilize higher-priority structure
- **Fix:** Fix in order: Structure > Expression > Activity Config > Logic. Higher categories often resolve lower ones automatically.

### Assuming an edit succeeded
- **Symptom:** File appears unchanged after edit
- **Cause:** Edit's `old_string` did not exactly match file content (whitespace, escaping)
- **Fix:** Always read the file before editing. After edit, re-run the workflow.

## File & Base64 Pitfalls

### Run refused: "This workflow uses the file helpers … cannot run with --no-auth"
- **Symptom:** the local run in no-auth mode exits 1 before the engine starts, naming File to Base64 / Base64 to File
- **Cause:** `$helpers.file.*` reads and writes Orchestrator blob storage; there is no local file store
- **Fix:** run signed in (sign in first, drop `--no-auth`) — still only on the user's explicit "yes" (rule 21). Same for `--input-file` / `--output-dir`.

### `$helpers.fileToBase64 is not a function` / validate: "does not call $helpers.file.fileToBase64("
- **Symptom:** `validate` reports one error at `…/run/script/code`; or a signed-in run fails inside the task
- **Cause:** the helper was written without the `.file.` namespace (or with another name). The activity is recognised by that exact call
- **Fix:** `return { output: await $helpers.file.fileToBase64(<ref>) }` / `$helpers.file.base64ToFile({ base64: … })`; keep `metadata.activityType` `FileToBase64` / `Base64ToFile`

### File to Base64 / Base64 to File script lost code after a Studio Web save (validate warned "rebuilds the script … and drops the rest", or did not warn at all)
- **Symptom:** after a designer roundtrip the task's `run.script.code` is back to the bare `return { output: await $helpers.file.*(…) }` — a preceding `const`, a trailing statement, a second argument (`fileToBase64(ref, { extra: 1 })`) or an extra option key is gone; downstream logic that relied on it now fails or the wrong reference is converted
- **Cause:** Studio Web parses only the `$helpers.file.*` call and rebuilds the script from the property panel on save. `validate` warns about extra statements but passes an extra argument / option key as `Valid`
- **Fix:** keep the script to the single `return` expression with exactly one argument (see [files-and-base64.md §2](files-and-base64.md#2-the-two-activities)); move pre-processing into a JavaScript activity before the conversion and pass its output as the argument

### File to Base64 output used as a string (`.length`, `+`, `JSON.stringify` shows an object)
- **Symptom:** downstream expression gets `[object Object]`, `undefined`, or an `{ ID, FullName, MimeType }` object where base64 text was expected
- **Cause:** the activity returns a base64 **file reference**, not the base64 string — bytes never enter `$context`
- **Fix:** inline the content where it is sent: `"${{ content: $context.outputs.FileToBase64_1.output.serializeData() }}"` in the HTTP body / Response

### `{ "__uipathFileRead": { "ref": … } }` shows up in a variable or the output
- **Symptom:** a marker object instead of file content
- **Cause:** `serializeData()` was stored in a variable / returned from a script instead of being called inline in a body or Response field
- **Fix:** call it only inside the HTTP body or `response` expression

### Decoded file has no extension (`hello` instead of `hello.txt`)
- **Symptom:** Base64 to File returns `FullName: "hello"`, `MimeType: application/octet-stream`
- **Cause:** for a *reference* input the engine strips `.base64` and sniffs the MIME type from the bytes; plain text has no signature, and `fileName` / `mimeType` are ignored for references
- **Fix:** acceptable for opaque payloads; when the name matters, decode a raw base64 **string** with `fileName` / `mimeType` (e.g. `{ base64: $context.variables.payload, fileName: 'notes.txt', mimeType: 'text/plain' }`)

### `The provided value is not a valid base64 string: base64ToFile`
- **Symptom:** Base64 to File fails on a string input. The suffix is always the literal helper name `base64ToFile` — it does not name the failing task, so with several Base64 to File tasks locate the culprit by the failing task in the run output, not by the message
- **Cause:** URL-safe alphabet (`-` / `_`), non-base64 characters, bad padding, or an empty string. A `data:…;base64,` prefix and whitespace are NOT the problem — the engine strips both before decoding
- **Fix:** convert URL-safe base64 to the standard alphabet (`-`→`+`, `_`→`/`) in a JavaScript activity; make sure the field really holds the payload and is not empty

### `Raw bytes cannot be embedded in JSON` / `A bare file reference cannot be embedded in a nested field`
- **Symptom:** an HTTP Request fails at send time with one of these messages
- **Cause:** a JSON body field holds `serializeData()` of a *binary* reference (e.g. `$workflow.input.document.serializeData()`), or a bare reference object. Only a base64 reference's marker can be inlined in a nested field
- **Fix:** run File to Base64 first and inline `$context.outputs.FileToBase64_1.output.serializeData()`; or, to send the raw bytes, make the bare reference the *whole* request body

### File too large
- **Symptom:** the helper fails with a size error
- **Cause:** the in-memory conversion path is capped at 50 MB — it applies only to raw base64 strings and to references with a small declared `Metadata.Size`; references above 1 MB or with no declared size stream instead, with no size cap
- **Fix:** pass the file as a reference (streamed) rather than as an inline string; keep inline strings under the cap

### Upload / download 403 or "folder" errors with `--input-file` / `--output-dir`
- **Symptom:** `Failed to upload input file … 403` or an Attachments API folder error
- **Cause:** the tenant's Attachments API requires a folder context
- **Fix:** pass `--folder-key <guid>` (the Orchestrator folder's key, visible in the folder's settings or via the platform CLI's folder listing)

---

## Debugging Strategy

1. **Always run with `--output json`** so failures are machine-parseable
2. **Run `--no-auth` first** to confirm structural validity. If structure passes but the real run fails, the issue is auth, network, or input data — not the workflow shape
3. **Reduce to minimal repro** — comment out (delete + restore via git) downstream tasks to isolate which task fails
4. **Check exit code** — `0` = success, `1` = failure
5. **Read `Instructions` first** — the executor often suggests the fix directly
