Runs
A run is the act of filling cells in, and it is the thing that spends credits. Price it first — the preview is free and uses the same arithmetic the charge does.
Scoping a run: the two axes union
rowIds and columnIds are both optional. Omit both to run the whole table. Given together they union: every cell of those rows, plus every cell of those columns.
# the whole table
-d '{}'
# every cell of three rows
-d '{"rowIds": ["rrow_A…", "rrow_B…", "rrow_C…"]}'
# every cell of one column
-d '{"columnIds": ["rcol_D…"]}'
# BOTH LINES WHOLE — not the nine cells where they cross
-d '{"rowIds": ["rrow_A…", "rrow_B…", "rrow_C…"],
"columnIds": ["rcol_D…", "rcol_E…", "rcol_F…"]}'They do not intersect. Three rows and three columns is three whole rows and three whole columns, not the nine cells where they meet. Reading it the other way is the one mistake on this page that costs money — and it is a mistake a preview catches for free.
What a run skips
Not everything in scope runs, and nothing that is skipped is charged.
- A row whose document is still processing, or whose bundle is empty. Reported by id in
skippedRowIds— poll the job, then run again. - A cell that already carries a human override. A rerun never clobbers a correction. These are not in
skippedRowIds: skipping there is row-granular, and a partly-corrected row runs its other cells perfectly well.
If everything in scope is skipped, the call still succeeds — with run: null and cells: 0. That is not an error. It means the work you asked for is already done or not yet possible.
Two ways a run is refused
Both are 402, both happen before any work, and the order matters:
- The monthly spend cap first. If the run would pass a cap the organisation set, it is refused — and the body carries
cap. Buying credits does not help; the cap is a self-imposed ceiling, raised in the app. - Then the balance. The body carries
costandbalance.
Checking the cap first is deliberate: telling someone to buy credits when the problem is a ceiling they set themselves is the wrong answer to the right error.
When credits actually move
Nothing is charged when a run starts. The extraction engine debits per cell, after that cell has succeeded. Consequences worth building against:
- A cell that fails is not charged. A cell that is cancelled before it runs is not charged.
- There is no refund path, because there is nothing to refund — cancellation is not a reversal.
- The preview's
costis an upper bound, not an invoice.
Run statuses
NOT_STARTED, IN_QUEUE, IN_PROGRESS, SUCCESS, ERROR, CANCELED. Watch pendingCells against totalCells for progress; a cancelled run has pendingCells: 0 and a finishedAt, so a progress bar built on those two never sticks.
Price a run
GET/v2/workspaces/:workspaceId/tables/:tableId/runs/preview
Requires Read on the workspace
Free, and it charges nothing. It exists so a caller can see the price before committing, which is why it needs only read access — gating it at write level would defeat it.
The arithmetic is the same code the run itself uses, so the preview and the charge cannot disagree about scope, skips or the bundle surcharge. cost is credits, and it is an upper bound — cells that fail are not billed.
The scope parameters are comma-separated strings here rather than JSON arrays, because this is a GET.
Query parameters
rowIds | stringComma-separated row ids. Omit both for the whole table. |
|---|---|
columnIds | stringComma-separated column ids. Unions with rowIds — see above. |
curl "https://api.ragextract.com/v2/workspaces/wks_7Kq2mB4nR8vXpL3d/tables/rev_Qm5xC9bV3nK7sAeR/runs/preview?columnIds=rcol_D9sW4kF7nJ2xB6mV" \
-H "x-api-key: $RAGEXTRACT_API_KEY"{
"success": true,
"total": 1,
"data": {
"cost": 184,
"cells": 42,
"skippedRowIds": ["rrow_T1yB7nK4mQ8xV2sD"]
}
}Start a run
POST/v2/workspaces/:workspaceId/tables/:tableId/runs
Requires Read & write on the workspace
Queues the cells and returns immediately — the answers arrive asynchronously. Poll the run for progress, then read the cells when it lands.
Spends credits, per cell as each succeeds.
run is null when nothing was runnable — a success, not a failure.
Parameters
rowIds | string[]Up to 500 row ids. Omit both fields to run the whole table. |
|---|---|
columnIds | string[]Up to 100 column ids. Unions with rowIds. |
curl -X POST https://api.ragextract.com/v2/workspaces/wks_7Kq2mB4nR8vXpL3d/tables/rev_Qm5xC9bV3nK7sAeR/runs \
-H "x-api-key: $RAGEXTRACT_API_KEY" \
-H "content-type: application/json" \
-d '{"columnIds": ["rcol_D9sW4kF7nJ2xB6mV"]}'{
"success": true,
"total": 1,
"data": {
"run": {
"id": "rrun_X8dF2sB6nM4kQ9pL",
"tableId": "rev_Qm5xC9bV3nK7sAeR",
"status": "IN_PROGRESS",
"triggeredBy": "ukey_N5tR8mW3xC7bV1jH",
"totalCells": 42,
"pendingCells": 17,
"startedAt": 1756281600000,
"finishedAt": null,
"createdAt": 1756281600000,
"updatedAt": 1756281800000
},
"cells": 42,
"skippedRowIds": ["rrow_T1yB7nK4mQ8xV2sD"]
}
}List runs
GET/v2/workspaces/:workspaceId/tables/:tableId/runs
Requires Read on the workspace
Every run this table has had. triggeredBy tells them apart: an API key id (ukey_… or wkskey_…), a user id for a click in the app, or standing for the auto-mode sweep.
A table can have several runs in flight at once — each rerun is its own, and a standing table's sweep starts its own alongside whatever a person started.
curl https://api.ragextract.com/v2/workspaces/wks_7Kq2mB4nR8vXpL3d/tables/rev_Qm5xC9bV3nK7sAeR/runs \
-H "x-api-key: $RAGEXTRACT_API_KEY"{
"success": true,
"total": 1,
"data": [ {
"id": "rrun_X8dF2sB6nM4kQ9pL",
"tableId": "rev_Qm5xC9bV3nK7sAeR",
"status": "IN_PROGRESS",
"triggeredBy": "ukey_N5tR8mW3xC7bV1jH",
"totalCells": 42,
"pendingCells": 17,
"startedAt": 1756281600000,
"finishedAt": null,
"createdAt": 1756281600000,
"updatedAt": 1756281800000
}]
}Get a run
GET/v2/workspaces/:workspaceId/tables/:tableId/runs/:runId
Requires Read on the workspace
The endpoint to poll. One small response carrying totalCells and pendingCells — far cheaper than refetching the grid, which is what a naive progress loop reaches for.
curl https://api.ragextract.com/v2/workspaces/wks_7Kq2mB4nR8vXpL3d/tables/rev_Qm5xC9bV3nK7sAeR/runs/rrun_X8dF2sB6nM4kQ9pL \
-H "x-api-key: $RAGEXTRACT_API_KEY"{
"success": true,
"total": 1,
"data": {
"id": "rrun_X8dF2sB6nM4kQ9pL",
"tableId": "rev_Qm5xC9bV3nK7sAeR",
"status": "IN_PROGRESS",
"triggeredBy": "ukey_N5tR8mW3xC7bV1jH",
"totalCells": 42,
"pendingCells": 17,
"startedAt": 1756281600000,
"finishedAt": null,
"createdAt": 1756281600000,
"updatedAt": 1756281800000
}
}Cancel a run
POST/v2/workspaces/:workspaceId/tables/:tableId/runs/:runId/cancel
Requires Read & write on the workspace
POST, not DELETE — DELETE on a table resource in v2 means archive, and a run is history the credit reconciliation reads. Nothing is removed.
Cancellation is cooperative: the engine re-reads the run's status once per cell and skips the cell when it reads CANCELED. A cell already being extracted finishes and is charged; everything behind it goes back to blank, not to error.
An already-finished run answers 409, not a silent 200. A cancel is a race by nature, and answering "done" to a request that did nothing hides the one fact you wanted.
Cancelling needs the same level as starting, and stops runs other people started — including the auto-mode sweep. A run is workspace-shared state, and stopping one is the strictly safer half: it can only ever reduce spend.
curl -X POST https://api.ragextract.com/v2/workspaces/wks_7Kq2mB4nR8vXpL3d/tables/rev_Qm5xC9bV3nK7sAeR/runs/rrun_X8dF2sB6nM4kQ9pL/cancel \
-H "x-api-key: $RAGEXTRACT_API_KEY"{
"success": true,
"total": 1,
"data": {
"id": "rrun_X8dF2sB6nM4kQ9pL",
"tableId": "rev_Qm5xC9bV3nK7sAeR",
"status": "CANCELED",
"triggeredBy": "ukey_N5tR8mW3xC7bV1jH",
"totalCells": 42,
"pendingCells": 0,
"startedAt": 1756281600000,
"finishedAt": 1756281900000,
"createdAt": 1756281600000,
"updatedAt": 1756281800000
}
}Cancel everything in flight
POST/v2/workspaces/:workspaceId/tables/:tableId/runs/cancel
Requires Read & write on the workspace
"Stop this table" as one request. It exists because a table can have several runs going at once, and a client should not have to hold a list of run ids a refresh would lose.
Idempotent. With nothing in flight it cancels nothing and reports canceled: 0 — no 409, unlike the run-scoped route above, because here you never named a specific run.
runs is every run it moved to CANCELED, newest first.
curl -X POST https://api.ragextract.com/v2/workspaces/wks_7Kq2mB4nR8vXpL3d/tables/rev_Qm5xC9bV3nK7sAeR/runs/cancel \
-H "x-api-key: $RAGEXTRACT_API_KEY"{
"success": true,
"total": 1,
"data": {
"canceled": 2,
"runs": [ { … ]
}
}