# BulkForge — what you can do

<!-- GENERATED by ops/tools/skill-sync — do not edit by hand. Run pnpm skills:sync. -->

<!-- GENERATED by ops/tools/skill-sync — do not edit by hand. Run `pnpm skills:sync`. -->

# BulkForge — what you can do

`/admin/d/bulkforge` · pillar `apps`

Run one function, or a chain of them, over a list of items, and get a table back.

## Fetch this, never remember it

These change per install and per release. Call for them - a list written here would be a future lie.

| What | Call | Why |
|---|---|---|
| the ready-made batches this install ships | `POST /api/adapters/bulkforge/presets` | POST, not GET — /api/adapters/:system/:fn is POST-only because running an adapter is an action with side effects, and the route is gated and metered on that basis. The answer is wrapped in `{ result }`. |
| exactly what a batch WOULD call, before running anything | `POST /api/adapters/bulkforge/preview` | it resolves every template against the first few items and calls nothing. This is how a wrong token is caught for the price of one request instead of five thousand failures. |
| one item end to end, for debugging | `POST /api/adapters/bulkforge/runOne` | preview shows what would be called; this actually calls it, once. The fastest way to find out why a batch is failing without paying for the batch. |
| what is callable at all — the thing a step points at | `GET /api/function-index/entries?q=<text>` | the catalogue is per-install and grows as the operator wires things up; a remembered list is wrong the day after it is written. GET /api/function-index/entry/:id gives its declared inputs. |
| the batches that have run, and what came out | `GET /api/jobs?kind=bulkforge.batch` | a run IS a job row; there is no second record of it. |

## Tabs

Every one is a real URL. A tab with its own section below carries a real job.

| Tab | Path | Group | What it is for |
|---|---|---|---|
| `forge` | `/admin/d/bulkforge` | - | Pick a preset, pick a list, preview, then run — inline for a try, queued for real work. |
| `lists` | `/admin/d/bulkforge/lists` | - | Build and keep the item sets batches run over. |
| `runs` | `/admin/d/bulkforge/runs` | - | The queue: watch a batch, cancel it, and read the table it produced. |

## Tabs in focus

### `forge` - Forge

Pick a preset, pick a list, preview, then run — inline for a try, queued for real work.

Address: `/admin/d/bulkforge`

**Read with:** `POST /api/adapters/bulkforge/presets` | `GET /db/bulkforge_list`

**Write with:** `POST /api/adapters/bulkforge/run` | `POST /api/jobs`

1. Choose the callable. A preset is a starting point, not a fixed menu — the executor is a { kind, ref } descriptor and can point at anything in the function index.
2. Choose the items: a saved list by id, or pasted text split one per line.
3. Preview. Read the resolved args, not just the status.
4. Run now for under 200 items; queue anything bigger.

**Trap:** Run now is CAPPED at 200 items and answers 413 past that, on purpose: an HTTP request cannot promise a five-thousand-row batch anything — nothing survives a restart, nothing can be cancelled, and the client that disconnects takes the answer with it. When a saved list is picked the job is sent its `listId`, not the expanded items, so the batch reflects the list as it is when it RUNS.

### `lists` - Lists

Build and keep the item sets batches run over.

Address: `/admin/d/bulkforge/lists`

**Read with:** `GET /db/bulkforge_list`

**Write with:** `POST /db/bulkforge_list` | `PUT /db/bulkforge_list/:id` | `DELETE /db/bulkforge_list/:id`

**Trap:** A list is a separate record from a batch on purpose — the same list runs through several setups and the same setup points at several lists. Blank lines and duplicates are dropped on save, order is kept.

### `runs` - Runs

The queue: watch a batch, cancel it, and read the table it produced.

Address: `/admin/d/bulkforge/runs`

**Read with:** `GET /api/jobs?kind=bulkforge.batch` | `GET /api/jobs/:id`

**Write with:** `POST /api/jobs/:id/cancel`

**Trap:** Cancellation is COOPERATIVE — the runner stops dispatching new items and lets in-flight ones finish, so the partial table stays coherent. A cancelled batch reports `cancelled`, never `failed`.

## What this domain owns

Its verb is **Open**.

Entity kinds: `bulkforge.list` | `bulkforge.recipe`

## Traps

THERE IS NO /api/bulkforge ROUTE. Presets, preview and a small run go through `POST /api/adapters/bulkforge/<fn>`; a real batch is `POST /api/jobs { kind: "bulkforge.batch" }`; lists and saved batches are `/db/bulkforge_list` and `/db/bulkforge_recipe`. Each of those already decides who may do it, and a fourth door would be a second, weaker copy of that decision. STEPS RUN IN ORDER PER ITEM. `{{steps.<id>.output}}` in step 2 is step 1's answer FOR THE SAME ITEM. This is the one thing a @leumas/workflows `loop` node cannot express — its body fans every item onto the frontier at once, so a downstream node reads the LAST item's result and the run still finishes green. If a workflow needs per-item chaining, it wants a single `bulkforge.batch` node, not a loop. The tokens are `{{item}}`, `{{item.field}}`, `{{index}}`, `{{input.key}}` and `{{steps.<id>.output}}`. A token that resolves to nothing becomes an EMPTY STRING, not undefined — so a `{ n = 5 }` default never fires and `''` reaches arithmetic as 0. Every number in a policy is coerced back to its default for exactly this reason; anything else a step is sent must do the same. A FAILED ITEM IS A ROW, NOT AN EXCEPTION. A batch that processed 4,998 of 5,000 reports `done` with `failed: 2`, and the summary names the failures out loud. Never read `status: "done"` alone as "it all worked". A queued batch survives a restart: it checkpoints its cursor onto the job row and `resumePending()` re-dispatches it at boot, restarting at the cursor rather than at zero. Result rows are kept on the row only while they fit a 256KB budget; past that the run sets `truncated: true` and reports `carried` vs `total` rather than silently dropping them. Two legacy BulkForge modes are deliberately absent. `command-line` ran an interpolated shell command per item — on a public subdomain that is remote code execution with a list attached. `fileProcessor` did `fs.unlink`, rename and copy on arbitrary absolute paths from the request body. Neither is a preset, and neither belongs on the member lane. An adapter executor ref splits at the LAST dot, so it must be exactly `system.fn`. A dotted tool name like `a-transformation.json.parse` addresses a system that does not exist and fails at run time for a ref that looks correct. `pnpm check:bulkforge-presets` holds that line for the shipped presets. The subdomain `bulkforge.leumas.tech` and Studio's `/admin/d/bulkforge` render the SAME components, from `@leumas/features/bulkforge`. A change to one is a change to both.



---
Source: .claude/skills/leumas-studio/reference/domains/bulkforge.md
Canonical: https://docs.leumas.tech/p/skills/leumas-studio/domains/bulkforge
