---
name: businessmap-code-runner
description: Write, wire up and debug Businessmap Code Runner scripts - the DSL and its built-ins, Business Rule setup, and the live Businessmap API spec. Use for any Code Runner or Businessmap automation task.
---

# Businessmap Code Runner

Code Runner executes custom scripts inside [Businessmap](https://businessmap.io) accounts. A
Business Rule fires an **Invoke Web Service** action, which POSTs a script to the Code Runner
backend; the backend parses it and runs it against that account's API. It exists so customers can
get bespoke automation without anyone running arbitrary PHP.

The language is a small JavaScript-flavoured DSL — not JavaScript, not PHP. It has no functions, no
`while`, no `try`/`catch`, no `//` comments, and a set of return-value conventions that silently
invert ordinary guards. Writing it from intuition produces scripts that parse and then misbehave.

## Reference map

Read the file you need; do not work from memory.

| File | Contents |
|---|---|
| `LANGUAGE.md` | Grammar, semantics, execution limits, the trap list |
| `FUNCTIONS.md` | The complete callable surface, signatures reflected from the registry |
| `PLATFORM.md` | What cards, boards, workflows, custom fields and OKRs actually are |
| `BUSINESSRULES.md` | Which triggers can call a script, conditions, native actions, the form |
| `SETUP.md` | Business Rule wiring, headers, pills and their quoting, the multi-secret header |
| `DEBUGGING.md` | The debugger, the execution log, error codes, symptom table |
| `EXAMPLES.md` | Worked scripts — all verified to parse |
| `scripts/bmapi.py` | Query the live Businessmap API spec |
| `scripts/crlint.py` | Static checker for the traps below |

All of these live **next to this file**, in the skill's own directory — which is not the directory
you are working in. Set `SKILL` to the folder you read this SKILL.md from and use it for every
command below:

```bash
SKILL=~/.claude/skills/businessmap-code-runner    # Claude Code; elsewhere, wherever this file is
python3 "$SKILL/scripts/bmapi.py" tags            # check it resolves
```

Both scripts need Python 3.8+ and nothing else — no packages, no repo checkout, no network for
`crlint.py`. They work from any working directory as long as the path is right.

## Always deliver the script as a file

**Never leave a script only in the chat transcript.** Write it to a file so the user can reopen,
edit and re-upload it. Chat text is not an artifact they can work with.

- **Filename:** `<purpose>.coderunner`, e.g. `close-parent-when-children-done.coderunner`. The
  extension keeps editors and formatters from reformatting it — this is not JavaScript, and a
  formatter would corrupt `foreach (x as k => v)`. If a surface refuses an unknown extension, use
  `.txt`.
- **Where it goes — in Claude Code, or any surface with a working directory:** a
  `codeRunner scripts/` folder, created if it is not there yet. **Never write a script loose in
  the working directory itself** — in a repository checkout that is the project's own root, and a
  delivered script does not belong there. Everything Code Runner-related goes in that one folder:
  scripts you generate, and the verbatim copies of scripts the user hands over.
  **Do not ask where to put it.** The convention is the answer, and it is one question fewer for
  someone who wants a script rather than a filesystem discussion. It is a default, not a rule: a
  location the user names, or one set in the project's `CLAUDE.md`, wins. Either way, **give the
  full path in your reply** — a file nobody can find is barely better than one in the chat.
- **On claude.ai:** write it as a single file wherever that session returns downloads from, and
  **do not create the folder there.** The sandbox is per-conversation and thrown away with it, so
  there is no shared directory to keep tidy — what the person receives is a download link in the
  chat, and burying the file one level down only makes that link harder to produce and to find.
  The filename carries the tidiness on this surface; the chat reply carries everything else.
- **Start every generated file with a header comment** recording what it does, which rule drives
  it, and which inputs it expects:

```
/*
 * Close the parent card once every child is Done.
 * Business Rule: <id> on board <id>, trigger "Card moved".
 * Inputs: cardId — the card the rule fired for.
 */
```

- **In chat, give a short summary plus the Business Rule settings** — not a second copy of the
  script.
- **On revision, edit the existing file in place** and keep the filename, so the user's copy stays
  the current one.

## How to work

1. **Establish the trigger.** Which Business Rule fires this, on what event, and what values does
   it pass in? A script is the tail of a rule; without that you are guessing at its inputs.
   Check in `BUSINESSRULES.md` that the trigger they want can call a script at all — only nine can
   — and that a native action would not do the job without one.
2. **Read `LANGUAGE.md` and `FUNCTIONS.md`** before writing a line, and `PLATFORM.md` if the domain
   objects are not already clear.
3. **Look up every endpoint** you intend to call with `bmapi.py` — path, method, parameters
   and body shape. Never write an API call from memory; the spec changes.
4. **Write the script to a file**, applying the rules below.
5. **Lint it:** `python3 "$SKILL/scripts/crlint.py" <file>`. Fix every ERROR; justify or fix every WARN.
6. **Check it against the checklist** at the end of this file.
7. **Hand back the full file path together with its Business Rule settings** — trigger, conditions, URL,
   method, headers, the `business_rule_id` reminder, and **which pill goes where**. A script alone
   is half the deliverable; the person configuring the rule cannot infer the pills from the code.

## Working with a script the user already has

When someone pastes an existing script, or uploads one, and asks for a change:

1. **Save it to a file verbatim first** — in `codeRunner scripts/` where there is a working
   directory — before changing anything. That is the baseline they can diff against and fall back
   to.
2. **Lint the original:** `python3 "$SKILL/scripts/crlint.py" <file>`. Old scripts routinely carry
   `move_card()`, unguarded `contains()`, or hardcoded secrets.
3. **Report what you found before editing**, separating *the change they asked for* from *problems
   that were already there*. Do not silently rewrite unrelated code.
4. **Make targeted edits.** Preserve their formatting, comments and variable names — this is
   someone's working script, not a draft to replace. Rewrite wholesale only if asked.
5. **Re-verify any endpoint the script touches** with `bmapi.py`. A script that used to work and
   now fails is often an API whose shape moved.
6. **Re-lint, then hand back the updated file** and a summary of what changed.

Two things to watch for in pasted scripts:

- **A hardcoded credential.** Flag it and offer the masked `multipart` header (see `SETUP.md`)
  rather than leaving a key in text that gets pasted around.
- **Source copied from the execution log** is post-interpolation: the Business Rule's parameters
  have already been substituted with literal values. Ask which values are rule parameters before
  treating a literal as a constant.

## Checking the API

The spec is fetched live, because it changes. It is far too large to read directly — 439 paths,
754 operations, 565 schemas — so query it:

```bash
python3 "$SKILL/scripts/bmapi.py" tags                              # browse the 231 areas
python3 "$SKILL/scripts/bmapi.py" search card comment               # find operations
python3 "$SKILL/scripts/bmapi.py" search --tag Outcomes             # everything OKR-related
python3 "$SKILL/scripts/bmapi.py" show PATCH /cards/{card_id}       # params, body, responses
python3 "$SKILL/scripts/bmapi.py" schema CardCreateRequest          # one schema, refs resolved
```

It caches the spec for six hours; add `--refresh` after a known API change.

The spec is served from one Businessmap account, so its `servers` entry names that host. It is a
**shape reference only** — paths, parameters and payloads are the same for every account, and
`send_to_bm()` already targets the customer's own subdomain. Never copy the host out of the spec
into a script.

If it reports that it cannot reach the spec, the sandbox has no network access to that host.
Recover as the error message describes — fetch the URL with your own web-fetch tool and pass
`--spec <file>`, or ask the user for the JSON. **Do not fall back to guessing endpoints.**

## Rules that are not negotiable

**Card data arrives as pills, substituted raw.** A Business Rule pill is replaced by the bare value
before the script is sent — no quotes, no escaping. Numbers and IDs stand bare, **free text must be
wrapped in `esc("[Pill]")`** or the first apostrophe in a card title breaks the parse, and an unset
field substitutes nothing at all. `Links` carries JSON and must **never** be quoted. Never use the
`Kanbanize Payload` pill. `SETUP.md` has the per-pill table — use it rather than guessing.

**`[Card Id]` is notation, not syntax.** Pills are chips inserted from the rule editor's picker.
Writing `[Card Id]` in a delivered script marks the spot for whoever configures the rule; say so
when you hand it over.

**Every setting and every pill goes in a block at the top of the script.** A host, a form or table
name, a board / column / lane id, a status string, a field name, a threshold — anything the person
installing the script has to change — and every pill, assigned to a named variable. Put them under
a banner comment before the first line of logic, with a one-line comment each saying what to put
there, and close the block with a line saying nothing below it needs editing. Then use the
variables further down; never repeat a literal id or URL in the body. Whoever wires the rule up is
configuring a script, not reading one, and a value buried at the line that uses it gets missed or
half-replaced. Normalise and validate below the block (`trim()`, the empty-value guards) so the top
stays a plain list of settings.

**Only nine triggers can call a script.** Check `BUSINESSRULES.md` before designing around one, and
do not write a script for something a native rule action already does.

**A missing key or property is the string `"undefined"`, not null.** It is truthy, so `if (value)`
does not guard anything. Compare `!= "undefined"`.

**`contains()` returns the string `"false"` when there is no match, and `0` when the match is at
position 0.** A bare `if (contains(a, b))` is wrong in both directions. Compare `!= "false"`.

**Comments are `/* … */` only.** A `//` is a syntax error.

**`elseif` is one word.** `else if` does not parse.

**No `var` in a `for` initialiser.** `for (var i = 0; …)` does not parse; write `for (i = 0; …)`.

**`foreach` requires both key and value:** `foreach (items as k => v)`. Iterating a non-array
throws, so guard the array first.

**No multi-dimensional access.** `a[0][1]` is a syntax error. Assign the intermediate value.

**Property assignment goes one level deep.** `a->b->c = x` parses but throws at runtime.

**Reading an unassigned variable throws.** There is no implicit null.

**`print_value()` is invisible in production.** Output is discarded on the async path; it only
shows in the debugger.

**Budget the API calls.** 10 000 iterations per loop, 180 s wall-clock, 60 s PHP limit — and every
API round-trip spends it. Use ID-list filters and batch endpoints instead of one call per card.

**A write body for `send_to_bm()` must be a string.** On POST, PUT and PATCH, wrap it in
`make_json()`. An array or object is not encoded for you — it reaches cURL as an array and is sent
as a multipart form under the `application/json` header, and the API answers
`{"errCode":400,"errText":"The request body is not a valid json."}`. `send_json()` does encode, so
the two are not interchangeable.

**Never write `move_card()`.** It is deprecated. Move cards by PATCHing `column_id` / `lane_id`.

**Never put a secret in the source.** Send it in the masked `multipart` header and read it with
`get_header_param()`.

## Before you hand it over

- [ ] The script is in a **file**, not just in the chat — under `codeRunner scripts/` on a
      surface with a working directory, and as a plain download on claude.ai
- [ ] Every pill written in the form `SETUP.md`'s table gives for it, and guarded for an empty value
- [ ] Every setting and every pill declared in one block at the top, nothing configurable below it
- [ ] `Links` unquoted; no `Kanbanize Payload`
- [ ] The chosen trigger actually supports Invoke Web Service
- [ ] `python3 "$SKILL/scripts/crlint.py" <file>` reports no errors
- [ ] Every API path, method and body field verified with `bmapi.py`
- [ ] Every value read from a response guarded against `"undefined"`
- [ ] Every `contains()` compared against `"false"`
- [ ] Every `foreach` target confirmed to be an array
- [ ] No `//`, no `else if`, no `a[0][1]`, no nested property assignment
- [ ] Loops bounded; batch endpoints used where they exist
- [ ] Comment and description bodies built as HTML, with interpolated content escaped
- [ ] Business Rule settings included in the handover

## Testing a script

Use the internal debugger at `https://solutions.businessmap.io/internal/codeRunner` (staff SSO,
`manageIntegrations`). It runs the script synchronously, so errors and `print_value()` output come
straight back. Prefer a sandbox account over a customer's. See `DEBUGGING.md`.

## Installing this skill

This folder is self-contained: the reference files and both scripts are everything it needs. Zip it
with the folder itself at the zip root (`businessmap-code-runner/SKILL.md`, not `SKILL.md` loose at
the top) and it installs in three places:

- **Org-wide on claude.ai** — Organization settings → Skills → **+ Add** → upload the zip. Team and
  Enterprise plans, with Skills and code execution both enabled. Everyone in the org gets it.
- **Per user on claude.ai** — Settings → Capabilities → Skills → upload the same zip.
- **Claude Code** — copy the folder into `~/.claude/skills/`.

On claude.ai the code-execution sandbox defaults to package managers only, so an admin must
allowlist `rock.kanbanize.com` under Organization settings → Capabilities → network access for
`bmapi.py` to fetch the spec. Without it the script still works, via the `--spec` fallback above.
