# Built-in function reference

The complete callable surface: **32 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. `body` may be `""` for GET. The connector throws on any 4xx/5xx, so
a returned value always means success.

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

send_to_bm("v2", "PATCH", "/cards/" + cardId, ["title": "Renamed"]);
```

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 = "")`
Ends the script as an **error**, which means an SA board card and an execution row marked failed.
Use it for genuinely unexpected states, not for ordinary "nothing to do" exits — use `exit()` for
those.

### `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, ["column_id": columnId, "lane_id": laneId]);
```
