# Wiring a script into a Business Rule

A Code Runner script never runs on its own. It is invoked by a Business Rule's **Invoke Web
Service** action, which POSTs the script's source to the Code Runner endpoint. Handing over a
script without these settings is handing over half the job.

## HTTP settings

| Setting | Value |
|---|---|
| URL | `https://solutions.businessmap.io/codeRunner` |
| Method | `POST` |
| Authentication | Type **API Key**, header name `apikey`, value = the account's API key |
| Additional header | `subdomain` = the account subdomain, e.g. `acme` |

The API key decides what the script may do — it runs as that user, and every call carries that
user's permissions. Use a service account with exactly the access the script needs.

### Accounts on the businessmap.io domain

The backend defaults to `kanbanize.com`. An account on the newer domain needs one more header:

| Additional header | Value |
|---|---|
| `domain` | `businessmap.io` |

Send it without a leading dot. Getting this wrong makes every API call fail against a host that
does not exist for that tenant.

## Body parameters

| Field | Required | Meaning |
|---|---|---|
| `business_rule_id` | yes | The rule's ID from the Business Rules table |
| `source` | yes | The script itself |
| `debug` | no | `1` runs the script synchronously — never set it on a live rule |

`business_rule_id` is not decoration: errors are filed under *subdomain + rule ID*, and a wrong or
missing value means an error card nobody can trace back. It is also the value shown in the
execution log, so it is how anyone finds this script's runs later.

## Passing card data in — pills

This is where most scripts go wrong, so read it before writing one.

Card data reaches the script through **pills**: card fields inserted from the picker in the rule
editor. A pill is a **chip**, not text — the editor stores it as a positional placeholder and keeps
the field binding beside it. You cannot type a pill; it has to be inserted from the picker.

> **Notation.** These docs write a pill as `[Card Id]` so it can be shown in plain text. That is a
> convention for writing *about* pills and for marking the spot in a draft script — it is not what
> you type into the rule. When you hand a script over, every `[Something]` is an instruction to the
> person configuring the rule: *put the real pill here, from the picker*.

Pills are available in **parameter values** (which is where `source` lives) and, in a much reduced
form, in the URL. **Header values do not accept pills at all.** See BUSINESSRULES.md.

When the rule fires, each pill is replaced by **the bare value, with no quoting and no escaping**,
and the resulting text is POSTed as `source`. Code Runner never sees a pill — only the substituted
result. Two consequences decide how you write every line that touches one.

### Quoting is your job

The pill contributes a raw value, so it lands in the middle of your source as if you had typed it.
A number can stand alone; **text must be quoted by you**:

```
var cardId  = [Card Id];         /* numeric — bare is correct */
var state   = '[State TCR]';     /* text — YOU supply the quotes */
```

Leave the quotes off a text field and the value is lexed as code: `var state = In Progress;` is a
syntax error, and a single-word value becomes an undefined-variable error instead.

### A value containing a quote breaks the script

`'[Card Title]'` looks safe until a card is called `Bob's card`. The literal terminates early and
the script fails to parse — for every card with an apostrophe, and no others. This is exactly what
`esc()` is for: it is a preprocessor that scans to the closing `")`, so the value may contain raw
quotes.

```
var title = esc("[Card Title]");     /* survives quotes in the value */
```

**Use `esc("[Pill]")` for every free-text field** — titles, descriptions, comments, custom-field
text. Reserve bare `'[Pill]'` for values you know are constrained, like a column or state name.

### Which form to use, by pill

Businessmap types some pills and not others. Use the type where it exists, and assume free text
everywhere else — the untyped group is mostly the free-text fields, so guessing costs you a parse
error on one card in a hundred.

| Pill | Write it as |
|---|---|
| Block Count · Block Time · Child Cards Progress · Cycle Time · Finished Subtasks Count · Logged Time · Subtasks Progress · Times Moved to · Total Subtasks Count · Unfinished Subtasks Count | bare — declared numeric |
| First Blocked Date · First Date Moved to · Last Blocked Date · Last Date Moved out of · Last Date Moved to · Last Modified · Last Moved | `"[Pill]"` — declared date/time, safe in double quotes |
| Card Id · Board Id · Custom Card Id · Internal Card Id | bare — the value is digits, so it needs no quotes |
| **Links** | bare, or `get_json(esc("[Links]"))` — **never quoted**, see below |
| Author · Board Name · Column Path · Comments · Last Comment · Stickers · Trigger · Type Name · Workflow Name · Workspace Name | `esc("[Pill]")` — declared text |
| Assignee · Attachments · Block Reason · Blocked state · Color · Column Name · Created At · Deadline · Description · Lane Name · Milestones · Priority · Reporter · Section · Size · Tags · Title · Watched | `esc("[Pill]")` — **no declared type**, so treat as free text |
| Any **custom field** | `esc("[Field Name]")` unless you know it is a Number field |

`Card Id` and `Board Id` are declared *text*, not numeric — they are safe bare because the value is
digits, not because the pill is a number. Do not generalise that to other "ID-looking" fields.

### `Links` carries JSON — do not quote it

The `Links` pill substitutes a JSON array of the card's links. Quoting it breaks, in two different
ways:

| | |
|---|---|
| `var links = "[Links]";` | **always** breaks — the JSON's own `"` ends the literal |
| `var links = '[Links]';` | breaks **as soon as any linked card's text contains an apostrophe** — so it works until it doesn't, on specific cards only |

Both safe forms:

```
var links = [Links];                      /* parsed natively as an array of objects */
var links = get_json(esc("[Links]"));     /* explicit parse, same result */
```

Bare works because the DSL's array and object literals are a superset of JSON here — quoted keys
are strings, and `true` / `false` / `null` are already keywords. Guard for an empty list, which can
substitute to nothing and leave `var links = ;`.

```
var links = [Links];
var parentId = 0;
foreach (links as i => link) {
    if (link->link_type == "parent") { parentId = link->card_id; }
}
```

### Never use the `Kanbanize Payload` pill

It exists in the picker and carries the whole card, but **do not put it in a script**. It is
declared as text, so it arrives as one large string that has to be parsed before anything can be
read out of it, and it drags the entire card through the rule for the sake of a field or two.

Take the specific pills you need, and fetch anything else with `send_to_bm()`. If you meet it in an
existing script, replace it the same way.

### An empty value is a syntax error

An unset field substitutes nothing at all, so `var cardId = [Card Id];` becomes `var cardId = ;`.
Quoting turns that into a harmless empty string, which is another reason to quote and then guard:

```
var owner = esc("[Owner Email]");
if (trim(owner) == "") { exit("no owner set"); }
```

### How these failures look

Substitution happens before the request, so a broken pill is a **parse** error: the rule still
receives `OK`, and the failure appears in the execution log as code 201 with the *interpolated*
source under "View code". That stored text is what actually ran — read it rather than the rule
setup, because it shows the values, not the pills.

### Declare defaults before branching

A related trap, visible in the reference setup: assigning a variable only inside `if` branches.

```
columnId = 0;                                    /* declare first */
if (state == "in progress") { columnId = 4978; }
elseif (state == "development") { columnId = 4986; }
if (columnId == 0) { exit("unmapped state: " + state); }
```

Without the first line, a state matching no branch reaches the API call with `columnId` never
assigned, and the script dies on `Undefined variable`.

## Several secrets in one header

The rule setup masks **only** the Authentication value. A script needing a second credential — a
Jira token, a Slack webhook — would otherwise have to send it as a plain-text header visible to
anyone who can open the rule.

Instead, put everything into the masked field as one JSON object:

| Setting | Value |
|---|---|
| Authentication type | API Key |
| Header name | `multipart` |
| Header value | `{"apikey": "<API key>", "jiraToken": "<…>", "slackHook": "<…>"}` |

- The `apikey` property authenticates the run exactly as the `apikey` header would.
- Every other property is read in the script with `get_header_param("name")`.
- `get_header_param("apikey")` returns `"disallowed"` — the API key is stripped before the script
  can see it, so a script cannot exfiltrate the credential it runs under.
- The header is consulted **only when no `apikey` header is present**. Use one form or the other,
  never both.

```
var jiraToken = get_header_param("jiraToken");
if (jiraToken == "undefined") {
    throw_custom_error("jiraToken is missing from the multipart header");
}
```

## What happens when the rule fires

Business Rules time out after **5 seconds**, which shapes everything:

1. The request arrives. Headers and body are validated, an execution row is opened, and the script
   is tokenised and parsed — a syntax error is caught here and nothing else runs.
2. The endpoint answers **`OK`** and closes the connection.
3. The script is then executed in a **separate background request**, with a 60 s PHP limit and a
   180 s wall-clock cap.

Three consequences worth stating to whoever asked for the script:

- **A script cannot return anything to the rule.** The rule always sees `OK`, including when the
  script later fails. Results must be written back through the API, emailed, or read from the log.
- **Nothing printed is visible.** `print_value()` output is discarded on this path.
- **Failures are asynchronous.** They surface in the execution log, by email for API 4xx errors,
  or as a card on the SA board — never in the rule's own history.

## Before handing a script over

- [ ] Every API path, method and body field checked with `bmapi.py`
- [ ] Every value read from an API response guarded against `"undefined"`
- [ ] No `print_value()` load-bearing in production logic
- [ ] Loops bounded, and batch endpoints used rather than one call per card
- [ ] Deliver: the script, the URL, method, headers (including `domain` if relevant), and the
      `business_rule_id` reminder
