# Worked examples

Every plain snippet below parses as written. Blocks tagged `pill` are shown in their
**pre-substitution** form — they are what you type into the rule editor, and they only become valid
source once Businessmap replaces the pills. Endpoint paths and payload keys still need checking
with `bmapi.py` against the live spec before you ship them.

## 0. Taking card data from the rule

The first lines of a real script capture the pills, quote them correctly, and guard them. See
SETUP.md for why each rule matters.

```pill
/* Each [Name] below marks where a pill is inserted from the rule editor's
   picker. They are chips, not text — see SETUP.md. */
var cardId = [Card Id];              /* digits: bare               */
var state  = '[Column Name]';        /* constrained text: quoted   */
var title  = esc("[Title]");         /* free text: esc()           */
var links  = [Links];                /* JSON: bare, NEVER quoted   */

if (trim(title) == "") { exit("card has no title"); }
```

The examples below use literals in place of pills so they can be checked by the linter and the
parser; substitute the pill forms above when you build the real thing.

## 1. The standard preamble

Read the card the rule fired for and refuse to continue if anything is missing. Almost every script
starts like this.

```
var cardId = 12345;   /* stands in for the [Card Id] pill */

var response = send_to_bm("v2", "GET", "/cards/" + cardId, "");
var payload = get_json(response);
var card = payload->data;

if (card == "undefined") {
    throw_custom_error("Card " + cardId + " returned no data");
}
```

`send_to_bm` throws on any 4xx/5xx, so reaching the next line already means the request succeeded.
The guard covers the other case: a 200 whose body is not shaped the way you expected.

## 2. Reading a custom field safely

There is no lookup by name — read the array and match on `field_id`. Guard the array itself first,
because an absent property is the string `"undefined"` and `foreach` throws on non-arrays.

```
var targetFieldId = 42;
var fieldValue = "undefined";

var fields = card->custom_fields;
if (fields == "undefined") {
    fields = [];
}

foreach (fields as index => field) {
    if (field->field_id == targetFieldId) {
        fieldValue = field->value;
    }
}

if (fieldValue == "undefined") {
    exit("Field " + targetFieldId + " is not set on card " + cardId);
}
```

`exit()` here rather than `throw_custom_error()` — "the field isn't set" is a normal outcome, not a
failure worth an error card.

## 3. Updating and moving a card

Both are a `PATCH` on the card. There is no separate move endpoint.

```
var updates = [
    "title": "[Reviewed] " + card->title,
    "column_id": 987,
    "position": 0
];

send_to_bm("v2", "PATCH", "/cards/" + cardId, updates);
```

## 4. Commenting, in HTML

Comment and description bodies are HTML. Raw newlines and Markdown render literally, and anything
interpolated from user data must be escaped.

```
var owner = card->owner_user_id;
var body = "<p>Closed on " + today("date", "Y-m-d") + "</p>";
body = body + "<ul><li>Owner: " + owner + "</li>";
body = body + "<li>Size: " + if_empty_return_zero(card->size) + "</li></ul>";

send_to_bm("v2", "POST", "/cards/" + cardId + "/comments", ["text": body]);
```

Use `esc("…")` when a literal contains quotes:

```
var note = esc("Marked "done" by automation");
```

## 4b. Reaching a related card from the `Links` pill

The linked-card triggers cannot call a script (see BUSINESSRULES.md), so the usual pattern is to
trigger on the card you *can* reach and walk the link yourself. The `Links` pill hands you the
relationships without an API call.

The literal below stands in for the bare `[Links]` pill — same shape the pill substitutes.

```
var links = [["card_id": 4321, "link_type": "parent"], ["card_id": 8765, "link_type": "child"]];

var parentId = 0;
foreach (links as index => link) {
    if (link->link_type == "parent") {
        parentId = link->card_id;
    }
}

if (parentId == 0) {
    exit("card has no parent");
}

var parent = get_json(send_to_bm("v2", "GET", "/cards/" + parentId, ""));
var parentCard = parent->data;
if (parentCard == "undefined") {
    throw_custom_error("parent " + parentId + " returned no data");
}
```

`link_type` is one of `parent`, `child`, `relative`, `predecessor`, `successor`.

## 5. Batch instead of a call per card

One request per card burns the 180 s budget fast. Collect the IDs, then fetch them in a single
filtered call.

```
var childIds = [];

var links = card->linked_cards;
if (links == "undefined") {
    links = [];
}

foreach (links as index => link) {
    if (link->link_type == "child") {
        childIds[] = link->card_id;
    }
}

if (get_count(childIds) == 0) {
    exit("No child cards to process");
}

var idList = implode(",", childIds);
var childResponse = send_to_bm("v2", "GET", "/cards?card_ids=" + idList, "");
var children = get_json(childResponse);

var unfinished = 0;
foreach (children->data as index => child) {
    if (child->section != 4) {
        unfinished = unfinished + 1;
    }
}

if (unfinished == 0) {
    send_to_bm("v2", "PATCH", "/cards/" + cardId, ["column_id": 987]);
}
```

## 6. Calling an external service with a second secret

The credential arrives in the masked `multipart` header, never as a literal in the source. See
SETUP.md.

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

var message = ["text": "Card " + cardId + " (" + card->title + ") is done"];
send_json("POST", hookUrl, message);
```

For a service that wants form fields rather than JSON:

```
var fields = ["ticket": card->custom_id, "status": "closed"];
send_form("POST", "https://example.com/api/update", fields);
```

## 7. Substring matching without the `contains()` trap

`contains()` returns the position when found and the **string** `"false"` when not. `"false"` is
truthy and position `0` is falsy, so a bare `if` is wrong in both directions.

```
var title = strtolower(card->title);

if (contains(title, "urgent") != "false") {
    /* priority 1 = Critical. The scale is inverted: 1 Critical … 4 Low. */
    send_to_bm("v2", "PATCH", "/cards/" + cardId, ["priority": 1]);
}
```

## 8. Dates

```
var due = add_working_days(today("date", "Y-m-d"), 5);
send_to_bm("v2", "PATCH", "/cards/" + cardId, ["deadline": due]);

var age = date_diff(today("date", "Y-m-d"), card->created_at, "days");
if (age > 30) {
    send_email("warning", "<p>Card " + cardId + " has been open " + int(age) + " days.</p>");
}
```

`date_diff` returns a float and goes negative if the arguments are the wrong way round — newer
date first.

## 9. Walking nested data one step at a time

`a[0][1]` and `a->b->c = …` do not work. Split them.

```
var rows = payload->data;
var firstRow = rows[0];
var cellValue = firstRow["name"];

var settings = card->settings;
if (settings == "undefined") {
    settings = [];
}
settings->notified = true;
card->settings = settings;
```
