# Built-in function reference

The complete callable surface: **33 registered functions**, plus `unset` and the two array methods
`push` / `pull`, which the interpreter handles as special forms rather than registry entries.

Signatures below were extracted by reflecting the live registry and its target methods — not
copied from the HTML documentation, which is incomplete. Where the two disagree, this is right.
Every public method on the `Functions` class is reachable from the DSL; there are no hidden extras.

A parameter shown without a default is **required**; supplying the wrong number of arguments throws
`Function \`name()\` was called with the wrong number of arguments.`

## Businessmap API

### `send_to_bm(version, method, url, body, customHeaders = [])`

The main way to talk to the customer's account. Authentication, subdomain and domain are already
applied — pass a **path**, not an absolute URL.

| `version` | Base | Notes |
|---|---|---|
| `"v2"` | `/api/v2` | The modern API. Use this unless you have a reason not to. |
| `"v1"` | legacy kanbanize API | Method is forced to POST and `/format/json` is appended. |
| `"v3"` | `/apiv3` | |
| `"reporting"` | `/reportingApi/v1` | |
| `"files"` | account root | For file paths. |

Returns the decoded response. The connector throws on almost every 4xx/5xx, so a returned value
nearly always means success — the exceptions are a 409, whose body is returned as-is, and the
`C101`/`C111`/`C121`/`C131` "card not found or discarded" codes on POST/PUT, which are swallowed.

> **On POST, PUT and PATCH the body must be a STRING — wrap it in `make_json()`.** Unlike
> `send_json()`, this function does not encode an array or object for you. It hands the value
> straight to cURL, which then builds a multipart form body underneath the `application/json`
> header the connector already set, and the API rejects it:
>
> ```
> {"errCode":400,"errText":"The request body is not a valid json.","status":"error"}
> ```
>
> That 400 is the signature of this mistake. The request is well-formed in every other way, so
> nothing in the path or the payload looks wrong.

```
var response = send_to_bm("v2", "GET", "/cards/" + cardId, "");
var card = get_json(response);

var body = make_json({"title": "Renamed"});
send_to_bm("v2", "PATCH", "/cards/" + cardId, body);
```

The same call written the way that fails, kept apart so it cannot be copied by accident:
`send_to_bm("v2", "PATCH", "/cards/" + cardId, ["title": "Renamed"])`.

On GET and DELETE pass `""`. An array there is *not* a body — it becomes the query string — so
`[]` is a harmless way to say "nothing". `"v1"` is always POST, so it needs the string form too,
and so does `"files"`: it goes through the same `application/json` header as everything else, and
there is no exception. File uploads are not reachable from the DSL at all — the connector's upload
path sets its own multipart header and `send_to_bm()` never reaches it.

Verify the path, method and body shape with `bmapi.py` before writing the call.

## External HTTP

Both of these vet the URL first: http/https only, public hosts only, no redirects, and DNS is
pinned to the vetted addresses. Requests to localhost, RFC1918 ranges or cloud metadata are
rejected. Both throw when the response code is ≥ 300.

### `send_json(method, url, json, headers = [])`

`json` may be a JSON string, or an array/object which is encoded for you. Default headers are
`Content-Type: application/json`, `Accept: application/json`, `Cache-Control: no-cache`. Returns
the raw response body as a string.

### `send_form(method, url, fields, type = "urlencoded", headers = [])`

For endpoints that want form fields rather than a JSON body. `method` must be `POST`, `PUT` or
`PATCH`. `type` is `"urlencoded"` (nested arrays become `name[key]=value`) or `"multipart"` (every
field must be a single value). Any `Content-Type` you pass is dropped — it is set for you, and
multipart must carry cURL's own boundary. No file uploads.

## JSON

### `get_json(data)`
Decodes a JSON string to an array. Passes arrays through unchanged. Throws on invalid JSON.

> **An empty input returns the string `"{}"`, not an empty array.** So an endpoint that answers with
> an empty body hands you a string, and both `get_count()` and `foreach` then throw. Guard it:
>
> ```
> var data = get_json(response);
> if (data == "{}") { exit("empty response"); }
> ```

### `make_json(value)`
Encodes any value to a JSON string.

## Arrays

### `get_count(array)`
Element count. **Throws** if the argument is not an array — so it doubles as an assertion.

### `in_array(item, array)`
True/false membership test on values.

### `implode(glue, items)` · `split(separator, text)`
Join and explode. `split` takes the separator first.

### `usort(items, column)`
Sorts an array of associative arrays ascending by `column`. Returns the sorted array.

### `unset(variable)` · `unset(arr[key])`
Removes a variable or one element. Silent when the target does not exist. The argument must be the
reference itself, not a value. `unset(arr[])` is an error.

### `array.push(key, value)` · `array.pull(key)`
Method syntax, not free functions. `pull` returns `"undefined"` for a missing key.

## Strings

### `trim(value)` · `strtolower(string)` · `url_encode(text)`
As in PHP.

### `substr(text, offset, length)`
All three arguments required.

### `contains(text, searched)`

> **Read this before using it.** It returns the **position** when found and the **string
> `"false"`** when not. Both results break a naive `if`, in opposite directions: `"false"` is a
> non-empty string and therefore truthy, while a match at position `0` is falsy.

```
if (contains(title, "urgent")) { … }                  /* WRONG both ways */
if (contains(title, "urgent") != "false") { … }       /* correct */
```

### `preg_replace(pattern, replacement, input)`
PHP regex, so the pattern needs delimiters: `"/\\s+/"`.

### `number_format(number, decimals, decimalPoint = ".", thousandsSeparator = ",")`
`decimals` is **required** here, unlike PHP's version. Pass `""` as the separator to disable
grouping.

### `esc(text)`
A source preprocessor, not a runtime call — see LANGUAGE.md.

## Dates

### `today(type, format)`
Both arguments required. `type` is `"date"`, `"datetime"` or `"timestamp"`; the first two format
the current time with `format`, the third returns a Unix integer and ignores `format`.

### `date_diff(newDate, oldDate, type)`
`newDate` minus `oldDate` in `"seconds"`, `"minutes"`, `"hours"` or `"days"`. Returns a float and
goes negative when the arguments are the wrong way round.

### `add_date(date, difference, format = "Y-m-d")`
Relative arithmetic: `add_date(today("date","Y-m-d"), "+2 weeks")`.

### `add_working_days(date, difference, isMon = true, isTue = true, isWed = true, isThu = true, isFri = true, isSat = false, isSun = false, format = "Y-m-d")`

Adds working days, skipping the days flagged false. Negative `difference` counts backwards. A
`difference` of `0` rolls a non-working start date forward to the next working day.
**Undocumented in the internal HTML docs** — it exists and works.

### `format_date(dateString, currentFormat, newFormat)`
Reformats. Throws if the input does not match `currentFormat`.

## Numbers

### `int(value)`
Casts to integer, **throws** on non-numeric input.

### `if_empty_return_zero(value = null)`
Returns `0` for null, `""`, `0` or an empty array; otherwise the original value. Useful for
normalising custom-field values before arithmetic.

## Control and diagnostics

### `exit(message = "")`
Ends the script immediately and successfully. The message is recorded on the execution row. The
interpreter special-cases this call to append the DSL line number as a hidden second argument, so
a second argument you pass yourself is ignored.

### `throw_custom_error(message = "", block_card = true)`
Ends the script as an **error**: the execution row is marked failed and, if the script called
`set_error_card()`, that card is blocked with this message as the reason. It deliberately does
**not** reach the SA board — this is how you say "the account's data is wrong", and the SA board
is for scripts that are broken, which is not the same thing. Use it for a condition the script
cannot proceed through; use `exit()` for an ordinary "nothing to do" outcome, which is not a
failure at all.

`block_card` decides whether the card named by `set_error_card()` is blocked for *this* failure.
It defaults to blocking, so a script that opted in stays opted in; pass a no for an error the
board does not need to carry. Without a `set_error_card()` in the script the argument is moot —
nothing is blocked either way.

```
throw_custom_error("no change request id on the card");          /* blocks  */
throw_custom_error("the rule's header is misconfigured", false);  /* does not */
```

Anything the language reads as a refusal counts as a no: `false`, `0`, `"0"`, `""`, `null`, `[]`
and the *string* `"false"` — which matters, because that last one is what `contains()` returns on
a miss, and `throw_custom_error(msg, contains(t, "x"))` would otherwise block by way of the
argument asking it not to.

### `set_error_card(card_id)`

Nominates the card to blame if this run fails. When the script then ends in an error — any error:
a `throw_custom_error()`, an API 4xx, a runtime fault, a parse error, the CPU timeout — CodeRunner
blocks that card and puts the error message in the block reason.

**Opt in deliberately, one script at a time.** A script without this call blocks nothing, which
is what keeps the feature off boards that have not asked for it — so do not add it as a matter of
course. Put it in when the account has agreed their cards should carry automation failures.

Call it **once, before the logic**, so it is already recorded when something unexpected throws:

```
set_error_card([Internal Card Id]);
```

It records the id and nothing else. A successful run never touches the card, and `exit()` is not an
error, so a "nothing to do" exit leaves it alone.

Errors the script never anticipated — an upstream 500, a runtime fault, a parse error, the
execution timeout — carry no `block_card` argument and always block. They are the failures this
exists for: nothing else on the error path reaches the account at all.

Why it exists: a failed script is otherwise invisible to the account. The execution row and the SA
board card are ours, and the API-error email goes to the API key's own user — usually a dedicated
Code Runner user nobody reads. Blocking the card puts the failure in front of the people whose work
it is.

Details worth knowing:

- **Block reasons cap at 250 characters.** A longer message is stripped of the markup a parser
  error carries, collapsed, cut, and ends with a link to its execution row, where the full text
  is. A message that fits is sent whole, with no link.
- **Nothing unblocks the card.** It stays blocked until someone clears it by hand, which is the
  point — an automatic unblock would erase the evidence before anyone saw it.
- **The block is best-effort.** An archived card, or an API key without edit rights on that board,
  is swallowed silently: reporting the original error matters more.
- A non-numeric or non-positive `card_id` throws, so the call fails loudly at the top of the
  script rather than quietly failing to block an hour later.

### `print_value(value)`

> Output goes nowhere on the normal (async) path — the response is discarded. It is only visible in
> the internal debugger, which runs synchronously. Never rely on it in production; write findings
> back through the API or use `send_email()`.

### `send_email(issueType, message, subject = "", from = "no-reply@businessmap.io", to = "")`
`to` defaults to the API key's own user. `from` must stay on the businessmap.io domain. The body is
HTML.

### `sleep(seconds)`
Blocks. Spends the same 60 s / 180 s budget as everything else — avoid it inside loops.

### `get_header_param(name = null)`
Reads one property from the masked `multipart` JSON header (see SETUP.md). Returns `"undefined"`
when the property is absent **or when the request used a plain `apikey` header**, and
`"disallowed"` for `apikey` itself, which is never readable from a script. `name` is technically
optional — calling it with no argument returns `"undefined"` rather than erroring — so a typo'd
call fails silently. Always compare the result.

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

## Deprecated — never emit

### `move_card(card_id, column_id = null, lane_id = null, position = null)`

Still registered so old Business Rules keep working. **Do not write it into a new script, and
replace it when you find it in an old one.** Move a card with the API instead:

```
send_to_bm("v2", "PATCH", "/cards/" + cardId, make_json({"column_id": columnId, "lane_id": laneId}));
```
