{
  "schema": "leumas.docs.page/1",
  "id": "skill:leumas-studio#domains/bulkforge",
  "slug": "skills/leumas-studio/domains/bulkforge",
  "kind": "tools",
  "bucket": "skill",
  "title": "BulkForge — what you can do",
  "name": "BulkForge",
  "eyebrow": "what you can do",
  "chip": null,
  "summary": "<!-- GENERATED by ops/tools/skill-sync — do not edit by hand. Run pnpm skills:sync. -->",
  "keywords": [
    "leumas-studio",
    "fetch remember",
    "tabs focus",
    "skill-sync",
    "pnpm",
    "skills",
    "edit",
    "hand"
  ],
  "audience": "both",
  "funnel": {
    "product": null,
    "cta": null
  },
  "body": "<!-- GENERATED by ops/tools/skill-sync — do not edit by hand. Run `pnpm skills:sync`. -->\n\n# BulkForge — what you can do\n\n`/admin/d/bulkforge` · pillar `apps`\n\nRun one function, or a chain of them, over a list of items, and get a table back.\n\n## Fetch this, never remember it\n\nThese change per install and per release. Call for them - a list written here would be a future lie.\n\n| What | Call | Why |\n|---|---|---|\n| 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 }`. |\n| 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. |\n| 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. |\n| 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. |\n| 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. |\n\n## Tabs\n\nEvery one is a real URL. A tab with its own section below carries a real job.\n\n| Tab | Path | Group | What it is for |\n|---|---|---|---|\n| `forge` | `/admin/d/bulkforge` | - | Pick a preset, pick a list, preview, then run — inline for a try, queued for real work. |\n| `lists` | `/admin/d/bulkforge/lists` | - | Build and keep the item sets batches run over. |\n| `runs` | `/admin/d/bulkforge/runs` | - | The queue: watch a batch, cancel it, and read the table it produced. |\n\n## Tabs in focus\n\n### `forge` - Forge\n\nPick a preset, pick a list, preview, then run — inline for a try, queued for real work.\n\nAddress: `/admin/d/bulkforge`\n\n**Read with:** `POST /api/adapters/bulkforge/presets` | `GET /db/bulkforge_list`\n\n**Write with:** `POST /api/adapters/bulkforge/run` | `POST /api/jobs`\n\n1. 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.\n2. Choose the items: a saved list by id, or pasted text split one per line.\n3. Preview. Read the resolved args, not just the status.\n4. Run now for under 200 items; queue anything bigger.\n\n**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.\n\n### `lists` - Lists\n\nBuild and keep the item sets batches run over.\n\nAddress: `/admin/d/bulkforge/lists`\n\n**Read with:** `GET /db/bulkforge_list`\n\n**Write with:** `POST /db/bulkforge_list` | `PUT /db/bulkforge_list/:id` | `DELETE /db/bulkforge_list/:id`\n\n**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.\n\n### `runs` - Runs\n\nThe queue: watch a batch, cancel it, and read the table it produced.\n\nAddress: `/admin/d/bulkforge/runs`\n\n**Read with:** `GET /api/jobs?kind=bulkforge.batch` | `GET /api/jobs/:id`\n\n**Write with:** `POST /api/jobs/:id/cancel`\n\n**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`.\n\n## What this domain owns\n\nIts verb is **Open**.\n\nEntity kinds: `bulkforge.list` | `bulkforge.recipe`\n\n## Traps\n\nTHERE 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.\n\n",
  "source": {
    "path": ".claude/skills/leumas-studio/reference/domains/bulkforge.md",
    "blobSha": "",
    "commit": "",
    "committedAt": "",
    "provenance": "no-git",
    "bytes": 6779,
    "hash": "d3d19221f0c3db5adc9e9021ae3e87c376b16605"
  },
  "urls": {
    "html": "/p/skills/leumas-studio/domains/bulkforge",
    "json": "/docs/skills/leumas-studio/domains/bulkforge.json",
    "md": "/docs/skills/leumas-studio/domains/bulkforge.md"
  },
  "links": {
    "composes": [],
    "usedBy": [],
    "product": [],
    "howTo": [],
    "skills": []
  }
}
