# Debugging a script

## The debugger is the only place you can see output

Because production runs are asynchronous and their response is discarded, the **internal
debugger** is the real development loop:

**`https://solutions.businessmap.io/internal/codeRunner`** — staff SSO, requires the
`manageIntegrations` privilege. Tabs: Debugger, Documentation, Logs.

Fill in the subdomain and API key (or a `multipart` JSON blob to test that path), paste the script,
press Send. The debugger posts with `debug=1`, which runs the script **synchronously in the same
request**, so the Result pane shows:

- everything `print_value()` printed,
- the full error with its DSL line number, as JSON: `{"job_id": …, "code": …, "err": …}`,
- the job ID on success.

Debug mode also probes the API before running, so a bad key or unreachable account fails fast with
code 105 instead of failing halfway through.

In debug mode no error email is sent and no SA board card is created — the error comes straight
back to you.

## Do not use `/codeRunner/test` as a syntax checker

There is a `POST /codeRunner/test` endpoint that lexes and parses `source` and prints the AST. It
looks like a convenient syntax check. Avoid it:

- **A parse error files a card on the live Solution Architecture board** (error code 444), tagged
  with an empty subdomain and no job ID, so it is noise nobody can trace.
- It does **not** run the `esc()` preprocessor, so any script using `esc("…")` is reported as
  broken when it is fine.

Use `python3 "$SKILL/scripts/crlint.py" <file>` for a local check, and the debugger for the authoritative
answer.

## Reading the execution log

Every run gets a row, including runs that failed before executing a single statement.

| Log | Who | URL |
|---|---|---|
| Tenant | The customer, with subdomain + API key | `https://solutions.businessmap.io/codeRunner/logs` |
| Staff | SSO + `manageIntegrations`, all accounts | `https://solutions.businessmap.io/internal/codeRunner/logs` |

Both take a date range of at most **30 days**. A row carries `status` (`running` / `success` /
`error`), `error_code`, `error_message`, `started`/`finished`, the `identifier`
(`business_rule_id`), the `job_id`, and — **on failure** — the full source that ran, behind the
"View code" toggle. That last point matters: to see what a failing rule actually sent, read the
stored source rather than the rule setup, because rule parameters are interpolated before sending.

Deep-link straight to one run with `?execution=<id>`; the parameter survives the login round-trip.

`job_id` links to the per-API-call detail, so a script that made several requests can be traced
call by call.

## Error codes

| Code | Meaning |
|---|---|
| 100 | `apikey` header missing |
| 101 | `subdomain` header missing |
| 102 | `source` missing or empty |
| 103 | `business_rule_id` missing |
| 105 | Connection/auth probe failed (debug runs only) |
| 106 | The background execution could not be dispatched |
| 107 | `multipart` header is not a valid JSON object |
| 108 | `apikey` property missing from the `multipart` header — **or** the run timed out (see below) |
| 109 / 110 | Internal async key missing / mismatched |
| 111 | Internal job ID missing |
| 112 / 113 | Internal single-use nonce missing / invalid, expired or already used |
| 201 | Any error thrown on the synchronous path — lexer, parser, interpreter or API |
| 301 | Any error thrown during the real (async) execution — the usual code for a script bug |
| 302 | Fatal error, typically out of memory |
| 303 | Never finalised; reaped 15 minutes later by cleanup — the worker died mid-run |

> **Code 108 is used for two unrelated failures**: a missing `apikey` property in the multipart
> header, and an execution that hit the PHP time limit. Read `error_message` to tell them apart —
> the timeout one begins `Execution timed out`. Known wrinkle in the backend, not something the
> script author can influence.

Codes 109–113 are internal plumbing. If you see them, the problem is the platform, not the script.

## Where a failure surfaces

| Failure | Email | SA board card | Execution row |
|---|---|---|---|
| Businessmap API **4xx** | yes, to the API key's user, with a deep link | no | yes |
| Businessmap API **5xx** | no — treated as transient | no | yes |
| Anything else | no | yes | yes |
| Any error in debug mode | no | no | yes |

So a customer reporting "I got an email about a 403" is holding a permissions problem with the API
key, and a card on the SA board is a script or platform problem.

## Symptoms and causes

| Symptom | Cause |
|---|---|
| Rule succeeds, nothing happens | Normal — the rule always sees `OK`. Check the execution log for the real outcome. |
| `Undefined variable 'x'` | Read before assignment. There is no implicit null. |
| A guard never fires | The value is the string `"undefined"`, which is truthy. Compare `!= "undefined"`. |
| A `contains()` branch is inverted | Not found returns the truthy string `"false"`; a hit at position 0 returns falsy `0`. Compare `!= "false"`. |
| `Expected ';' after statement but got: [` | Multi-dimensional access `a[0][1]` — unsupported. Split into two steps. |
| `Cannot assign to non-variable object base` | `a->b->c = …` — only one level of property assignment works. |
| `Unexpected token: 'if' Expected: LBRACE` | `else if` instead of `elseif`. |
| `Unexpected token in primary expression: /` | A `//` comment. Only `/* … */` exists. |
| `Foreach can only iterate over arrays` | The API returned a scalar or an error shape. Check the response before iterating. |
| `…exceeded the maximum of 10000 iterations` | Runaway loop, or one API call per card where a batch endpoint exists. |
| `Script exceeded the maximum execution time of 180 seconds` | Too many sequential API calls. Use ID-list filters and batch endpoints. |
| Status stuck at `running`, then 303 | The worker died — usually memory. Reduce how much is held at once. |
| Nothing in the log at all | The rule never fired, is pointed at the wrong URL, or **its method is not POST** — with GET the parameters are discarded and the request lands on the tool's landing page, so the rule records a `200` and reports success. Check the rule, not the script. |
| `JSON decoding error` on `Links` | The pill was quoted. Use it bare, or `get_json(esc("[Links]"))` — see SETUP.md. |

## Reproducing a customer failure

1. Find the run in the staff log by subdomain and time.
2. Open "View code" and copy the **stored** source — that is the text after parameter
   interpolation, which is what actually ran.
3. Paste it into the debugger with that account's subdomain and a key of equivalent permission.
4. Iterate there, where errors and `print_value()` are visible.
5. Re-check any endpoint involved with `python3 "$SKILL/scripts/bmapi.py" show <METHOD> <path>` — an API
   that changed shape is a routine cause of a script that used to work.
