data_claim_next
Atomically claim the NEXT unprocessed row of a datastore (work queue).
Claim with empties="sentinel" when you will re-send a list: an assumed
empty (written @empty, its reason in comment) then comes back
"@empty" and, sent back as is, stays one; sent back as "" it becomes an
ordinary empty, refused on a required field. Never copy @empty into a
deliverable.
⚠️ Your filter MUST name a column your processing WRITES. That is the
one condition which makes the queue EMPTY, and nothing enforces it. The
order: rows never served first, then the least recently served — a
released row goes behind every fresh row, but it comes back once they are
served, for as long as it still matches your filter.
⚠️ How to see a queue running empty: the row carries _claims. Above 1
it means « you have already been served this row and it was not written »
— that is the signature. Stop and re-read your filter;
claiming again will not help.
⚠️ A claim lives in a run. Call run_start first and pass its
run_id as _run_id= on this call: without an active run the claim is
REFUSED and nothing is reserved — a row held outside a run could not be
written, so you would research it for nothing.
Write your result and release it by the _id of the returned row.
⚠️ Address the table by its NUMBER, not its name: the reply carries
ns_id (e.g. 174), the form to pass as datastore in every following
call — data_write(datastore=174, id=…), data_release(datastore=174, …).
The primitive for draining a table with N parallel (sub-)agents without
collisions: picks, among rows whose claim lease is free or expired, the one
served least recently (never-served rows first, then creation order),
stamps _claimed_by/_claimed_until and returns it — two concurrent
workers never get the same row. Returns {row: null} when nothing is
left to claim.
Filter on column A, write into column B, and the queue never empties: once the fresh rows are served, your own finished rows come back, round after round. Measured on a 3 766-row table, when the order was still « oldest first »: three workers filtered on a column the processing never touched. 834 fresh rows were never reached, and one worker claimed the SAME row seven times. Every call succeeded and no error was raised — a livelock, not a failure, and you cannot see it from inside. Rows claimed again and again without a write end up set aside (the ceiling below) — LOST to the pass, without being at fault.
A name still resolves as datastore until 2026-11-08 — then it is refused —
but the number is what to carry: it survives a rename, it is unique where
a name is only unique per owner, and it is what the platform records.
datastore in the reply is the table's REAL name whatever form you passed
in, so reading "600" back from a claim on 600 no longer happens.
worker is a label YOU choose and REUSE verbatim on data_release — the
guard so one agent cannot release another's claim.
filter (exact {col: val}, e.g. {"status": "nouveau"}) selects what counts
as claimable; it narrows the table's declared lifecycle.claimable, never
widens it. The claim does NOT change the row: write your progress via
data_write (id=…), then release it — writing a "final" status does not
free it (#317). Release the row with data_release if you have it;
otherwise finishing your run (run_finish) releases it. Never write your
intent into the row (no _action/_liberation columns). The lease
(lease_s, default 900s) only covers a worker that died. While you hold a
row, nobody else can write it.
Multi-pass table (lifecycle.advance declared): claim by the state of YOUR
pass (filter={"statut": "societe"}), write your findings, release — the
platform moves the row to the next state. Do not write that state yourself.
The row carries _claims = how many times it has been claimed since the
last successful write. A row claimed over and over WITHOUT a write is a
queue running empty: past the ceiling — lifecycle.max_claims declared on
the table, else a platform default of 3; max_claims here can only RAISE
it for this pass — the server sets it aside: it stamps _abandon with the
reason, ending with what the LAST attempt hit (the job's error, a refused
write, the tools in error, or no write attempted) and _abandon_run with
that attempt's run, moves it to lifecycle.abandon_state when the table declares a
ceiling (with the default one its status is left untouched), and STOPS
serving it — whatever your filter says. It stays readable and repairable:
an explicit data_write puts it back in the queue and resets the counter.
⚠️ layers="nested" gives the row back in the SHAPE YOU WRITE — a cell
that carries layers comes as {"valeur": …, "comment": …} instead of the
flat pair champ + champ.comment. ⚠️ It stays a READ: do NOT send this
row back. Write only the fields you established — and never
<field>.origine, which is read here and set by the platform, never by an
agent. The flat form shows a key with a DOT, and a dot does not look
like a field name: agents turn effectif.comment into effectif_comment,
which creates a ghost column — or, on a table that refuses unknown columns,
loses the whole row. This is the only read that feeds a WRITE loop, so it is
the one where the shape matters.
Every column DECLARED in the schema is on the row, null when no value is in
place — null means "nothing here yet": find it if your task needs it, never
make it up. Sending such a null back changes nothing (a null only erases a
value that is in place); still, write only the fields you established.
filters is the LIST form — [{field, op, value}], ANDed with filter.
Use it when one column needs TWO bounds: filter accepts a single
operator per column, so a range (score >= 10 AND score <= 20) can only
be expressed here. Same grammar as data_rows.
datastore also accepts slot:<name> (table bound by the active project).
Tool call template
Replace each placeholder with a value matching the input schema. Only required parameters are included; add optional parameters as needed.
{
"id": 1,
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"arguments": {
"datastore": "<datastore>",
"worker": "<worker>"
},
"name": "data_claim_next"
}
}Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| _org | No | Org (id) de CET APPEL. `oto_list_orgs()`. | |
| _group | No | Équipe (id) de CET APPEL, sous son org parente. | |
| filter | No | ||
| layers | No | shape of a cell that carries layers. `flat` (default): each filled layer beside the value (`email.comment`). `nested`: `{"valeur": …, "comment": …}`, the shape you write. Any other value is refused. | flat |
| worker | Yes | ||
| _run_id | No | OBLIGATOIRE à chaque appel dès `run_start` ou `data_claim_next` : sans lui, l'écriture sur une ligne réservée est refusée. Hérité seulement si le serveur tient un run actif — n'y compte pas. | |
| empties | No | how an assumed empty (written `@empty`) comes back. `plain` (default): `""`, like any empty cell. `sentinel`: `"@empty"`, the word that writes it. Any other value is refused. | plain |
| filters | No | ||
| lease_s | No | ||
| _project | No | Projet (id) de CET APPEL — jeton PRIMAIRE. `oto_project(op='list')`. | |
| datastore | Yes | the table's NUMBER (`ns_id`, e.g. 174) — the form to use. Its name still resolves until 2026-11-08, then is refused. `slot:<name>` also works. It must already exist. | |
| max_claims | No |