Skip to main content
Glama

data_write

Write one row, or a BATCH of rows in a single call.

⚠️ Provenance goes in comment, never in origine. Put WHAT you established and WHERE it came from in <field>.comment, and the page in <field>.link. origine is the platform's layer — an agent never writes it. Writing <field>.origine is REFUSED unless the call declares origine_override=true — which belongs to an import, not to a write of your own.

Layers: valeur/comment/link are yours to write, origine is read only — write nested, {"field": {"valeur": …, "comment": …, "link": …}}, never these as top-level keys of your own row. Provenance (WHAT you established, WHERE it came from) goes in <field>.comment, the page in <field>.link — never in origine, the platform's layer. A misspelled layer name next to a known layer ({"valeur": …, "comnent": …}) is REFUSED; alone ({"field": {"comnent": "x"}}), it is no layer: that dict IS the value. What a write destroys, what readonly and the business key protect, and where the REST face differs: guide datastore-semantics (oto_guide op=read slug=datastore-semantics).

⚠️ Writing a value DROPS the comment and link that came with it — they described the OLD value. To keep one while the value changes, send it back as it is.

Two gestures on a cell, and only two: null = ERASE the cell: {"field": null}. The erased value is kept nowhere: the response hands it back once, in valeurs_effacees. "@empty" = "searched, nothing found", its reason in comment: {"valeur": "@empty", "comment": "registry and imprint: none"}. It satisfies required (a plain "" does not) and reads back "" — or "@empty" with empties="sentinel". On a layer (comment, link), it only empties that layer.

⚠️ On a cell that holds a value: keep it and write the layers alone ({"field": {"comment": "…"}}), erase it with null, or discard it with @empty and the reason in comment. A field you leave out is "not mine" — its value stays. A comment alone is a note beside the value, never "searched, nothing found".

⚠️ "@keep" and "@clear" are not accepted: a write carrying them anywhere is REFUSED whole. Instead of @clear, write null. Instead of @keep, leave the sub-field out — or, for a comment or link that must survive a value that changes, send it back as it is.

"" and [] are values: they REPLACE the value in place, like any other value, and the replaced value comes back in valeurs_effacees. To keep a value, leave the field out; to erase it, write null.

⚠️ Business key: ADDING a row whose key value already exists is REFUSED (business_key_exists, naming the existing row) unless you pass upsert=true, which merges onto it. To DESIGNATE a row, pass its id=, or name the business key with key= (a batch, or a single row naming the declared key): an existing key value then MODIFIES its row and a new one creates it, no upsert needed. Without id or key=, a write ADDS a row. Two rows of one call with the same key value refuse the WHOLE batch, before anything is written, unless upsert=true. A batch reports every merge in fusions — {rang, dans_rang, id, cle}, dans_rang being the earlier row of the SAME batch that made the target row, null for a row already in the table — and ids stays aligned row for row. On a CLOSED table (new_rows: "reject") every write without id designates by the key.

⚠️ A write into a column the schema does NOT declare is REFUSED (unknown_column), on every table — nothing is written, a batch is refused WHOLE. To write a NEW column, declare it first — data_patch_schema(datastore=…, fields=[{"key": "<column>", "type": …}]) — then write. ⚠️ The declared format is a CONTRACT on every table: top-level options are enforced, the SHAPE of text/url/object/list/enum values is checked, an unknown layer is refused and a declared sub-record refuses an attribute it does not declare. A violation is refused (row_invalid; an off-options value alone in its fault is set aside and the rest of the row is written). Only what a write SETS is judged: a row already off-format still accepts a write to its OTHER columns (the stale value is reported in hors_type). No head setting changes this.

⚠️ @empty must be the ENTIRE sub-field, alone. Mixed into a sentence it is just text and gets stored as such — "@empty ; nothing on the imprint" lands in the cell verbatim, and a client reads it in their deliverable. The reason goes in comment.

In a list, the word goes on the element's field: {"contacts": [{"nom": "Alice", "fonction": "@empty"}]}. A list whose schema declares no of.key is REPLACED by what you send: read the row with empties="sentinel" and send each element back as it came, so an assumed empty stays "@empty". Refused on an element's identity (of.key), in a list of plain values, and inside an object or a json column.

ONE element of a list is written at its RANK — the address you read and filter by: {"contacts[1].email": "d@x.fr"} (or {"valeur": …, "comment": …}; contacts[1].email.comment alone annotates it, null erases it). {"contacts[+]": {…a whole record…}} appends, {"contacts[0]": null} removes. Ranks start at 0 and point at the list AS YOU READ IT; the append comes last. To add or remove WITHOUT reading the row first: {"tags[+]": ["x", "y"]} appends one element or a list of them, in order, duplicates kept; {"tags[-]": ["x"]} removes every occurrence of each value (plain values only — a record is removed at its rank; an absent value is refused). Both are applied under the row lock: two concurrent appends both land. On a text column, {"journal[+]": "…"} appends a line to the cell (a list = several lines) without resending it; a log kept by many is better as a table — past 64 000 characters (unless the column declares max_length) an append is refused: move the log to a table, one row per entry. Refused, with the form that works: a rank that does not exist (append with contacts[+]), contacts[0]: {…} (write its fields), contacts[].x, and the whole column together with one of its ranks.

A requirement declared on a sub-field (required, options, max_length… on an element's nom) is enforced like one on a column. In a list with of.key, only the elements your write CHANGES are judged: an element sent back unchanged never blocks yours — its defect comes back in hors_type. Written at its rank, only that element is judged, with or without of.key.

Dates: a date/datetime column reads ISO in any variant, 04/09/2026 (day first) and Unix timestamps, and STORES one form — 2026-09-04T10:00:00Z (UTC) for an instant, 2026-09-04 for a day; known only to the month or the year, it stays 2026-09 / 2026. An instant without a time zone is taken as UTC, and notices says so: write its offset (+02:00) if it is local time.

A schema refusal ends with the payload to send back: only the fields to fix, a <…> template in place of each value, | @empty where that is allowed — {"contacts": [{"role": "RH", "nom": "<text> | @empty"}]}. Fill the templates and write again; a list goes back WHOLE, that element fixed in its place (by its of.key, else its rank), the others as they were — or write only that element's fields at its rank (contacts[1].nom).

⚠️ A write DESTROYS what is in the column. On an open column there is no undo: the previous value leaves the row the moment yours lands. It survives only in the row's revision journal — data_row_history serves each write's before and after, kept 90 days by default — and nothing puts it back for you. If the value was supplied by the table's owner, announce what you are about to change on a column you did not fill yourself.

⚠️ There is NO automatic safety net. origine: "system" was REMOVED on 2026-09-08. It captured the previous value lazily, on the first write that changed it — which required having been declared BEFORE the row existed. Declared after the fact it caught nothing: 837 cells out of 846 lost on one campaign table, the owner's values already overwritten by agents.

What replaces it is a DECLARED gesture, carried by the call that brings the data in: donnees_d_origine=true. It sets the FIRST version of the data (the origin) and marks the write as an import in the journal — the current value and the origin both go in at the moment the value enters, so there is no "before" and no "after" to get wrong. Three rules: an origin ALREADY set is never touched (a re-import updates the current value, never the origin) ; an EMPTY column receives nothing (supplying nothing is not supplying blank) ; the call's layers go into BOTH versions.

So on a column whose origin was never captured, overwriting is FINAL in the row: only data_row_history still shows the previous value. The 28 799 origine layers already in the base are untouched — they are still read, served and protected; it is the mechanism that went, not the data.

Re-writing the SAME value changes nothing and captures nothing, by design. And it does not depend on how the row was created: a row appended through the MCP tool and one created through the REST face behave identically (measured 2026-09-04, both faces call the same store).

SINGLE (row): WITHOUT id = ADD a NEW row (new JSON keys auto-create columns, unless the table is CLOSED — see below); if the table declares a business key and your row carries a value that already exists, the business-key rule above applies. WITH key=<the declared key> = DESIGNATE the row by its key value: an existing value modifies that row and returns its _id, a new one creates it. WITH id = PARTIAL update of that row (only provided fields change). Returns the row (with _id/_created_at/_updated_at/_revision).

expected_revision (with id only) — Only when what you write was COMPUTED from a row you read (a list or text you re-send whole, a status chosen from the current one): pass that read's _revision. If the row changed since — any column, or its reservation — nothing is written and the call is refused with revision_conflict and the current revision: read again, recompute, write again. Omit it otherwise: writes to different columns never overwrite each other.

On a row you CLAIMED, address it by the _id of the row data_claim_next returned.

BATCH (rows = list of dicts): write them all at once — for importing a dataset without round-tripping each row through your context. If a business KEY is in effect (the key arg, else the datastore's declared schema.key): with key= the batch DESIGNATES — a row whose key value exists modifies that row, a new value creates one; without key= it ADDS, and the business-key rule above applies to values already in the table. Two rows of the batch with the same key value follow that rule either way; rows without a key are appended. Returns a summary {inserted, updated, count, key, ids, fusions?} — ids holds one _id per row sent, rank for rank. Use data_set_schema to declare a persistent key. For a FILE, never retype its rows here: oto_import loads it from where it is (link, Drive, project file), oto_upload_url takes it from your disk.

⚠️ An error on the way BACK (expired session, dropped connection, timeout) does NOT mean the write failed: it may have committed before the error. Read the row back (data_rows, by id or filtered on its key) before re-sending. Re-sending is safe with id, or with key= (or upsert=true) on a table with a business key: the row is designated, or merged; a keyless append re-sent creates a DUPLICATE.

⚠️ A table can be CLOSED by its schema (new_rows: "reject", next to its business key) — data_get_schema says whether it is. On such a table there is NO append at all: a write designating no existing row (no id, and no key value the table already carries) is REFUSED, single row and batch alike, and nothing is created — including a key value that is simply NEW. That is a deliberate setting of that table, not a platform rule. To make a row EXIST there, it is a schema move and not a write: data_patch_schema(datastore=…, new_rows="create"), your write, then data_patch_schema(datastore=…, new_rows="reject") to close it back.

origine_override=true belongs to an IMPORT, not to a write of your own: it declares that this call knowingly sets the origine layer — refused without it. ⚠️ For a real import, prefer donnees_d_origine=true, which sets the first version (origin) in one gesture; the override only says "I know what I am doing on this layer".

⚠️ A COLUMN can be LOCKED by the schema (readonly: true) — it holds a value someone put there, and an ordinary write that CHANGES it is refused by name (writing the same value again is fine, and <column>.comment always stays open for what another source says). The lock holds what is SET, not what is missing: a cell with no value (key absent, null or "") can be filled once; @empty ("searched, nothing") IS a value, and clearing one (null) is a change. To REPLACE it anyway, pass readonly_override=true ON THIS CALL. It is open to the OWNER of the table (you, your org or your team) or to whoever GOVERNS it — a table merely SHARED with you in write is refused, by design. It applies to this one call and nothing else: there is no schema setting to reopen and therefore none to close back. Every forced replacement is written to the call journal (row, column, replaced value) next to who called. ⚠️ That journal entry is BOUNDED: at most 25 replacements per call, each value cut at 120 characters (…, with was_len/now_len giving the original length). When it is cut short it says so (readonly_forced_bilan: "25 of N") — the count you read there is then a floor. The full history, whole values, is the row history: data_row_history.

⚠️ The datastore must EXIST first (create it with data_create_datastore); writing to an unknown datastore raises "datastore inconnu" — it is NOT auto-created.

⚠️ Address the table by its NUMBER. The reply carries ns_id — the table's number — and that is the form to pass as datastore: data_write(datastore=174, id=…). A name still resolves until 2026-11-08, then it is REFUSED.

datastore also accepts slot:<name> = the table BOUND under that slot name by the ACTIVE project (procedures reference tables as slot:name; the project maps the name via its links). Requires an active project + the binding — otherwise an actionable error, never a fallback.

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>"
    },
    "name": "data_write"
  }
}

Input Schema

NameRequiredDescriptionDefault
idNoomit = append a new row ; provided = partial update of that `_id` (the one data_write / data_claim_next returned for that row).
keyNoDESIGNATES rows by this business key column: an existing value modifies its row, a new one creates it — no `upsert` needed. BATCH (with `rows=[…]`): any column (default `schema.key`, but then the batch ADDS). Alongside a single `row`, accepted only when it names the table's DECLARED business key, `row` carries its value and no `id` is given; refused otherwise — wrap the row in `rows=[…]` to designate by another column.
rowNosingle-row content as a dict (JSON-encoded automatically).
_orgNoOrg (id) de CET APPEL. `oto_list_orgs()`.
rowsNoBATCH mode — a list of row dicts written in one call.
forceNoforce the NAMED columns on this call instead of everything it carries — `["raison_sociale", "raison_sociale.origine"]`. Naming them is enough; no need for `readonly_override` as well. ⚠️ It changes the SCOPE, not the right: forcing stays reserved to the table's owner or whoever governs it. A locked column absent from the list is refused normally, and the refusal says so.
_groupNoÉquipe (id) de CET APPEL, sous son org parente.
layersNoshape of the ROW this write returns (`flat` default, `nested`) — same as `data_rows`. Single row only.flat
upsertNowithout `id` only. `true` = MERGE when the key value already exists — for a write that ADDS (neither `id` nor `key=`), and for rows of one call that share a key value. A write that designates by `key=` needs no `upsert`. Every merge of a batch comes back in `fusions`. Without it, an adding write on an existing key, or two rows of one call on the same key, is REFUSED (`business_key_exists`). Refused on a table with no business key: there is nothing to merge on.
_run_idNoOBLIGATOIRE à 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.
emptiesNohow the returned row serves an assumed empty (`plain` default, `sentinel`) — same as `data_rows`. Single row only.plain
_projectNoProjet (id) de CET APPEL — jeton PRIMAIRE. `oto_project(op='list')`.
versionsNowhich versions of each cell the returned row carries. Default: the current value only; the origin (the FIRST version of the data) is asked for with `versions=["current","origine"]`. Single row only; the reply states what it served in `versions_servies`.
datastoreYesthe 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.
origine_overrideNoIMPORT path only — declares that this call sets the `origine` layer knowingly (without it, such a write is refused). ⚠️ `origine: "system"` was removed on 2026-09-08: there is no longer a "formatted column". For a real import, prefer `donnees_d_origine`. This call only.
donnees_d_origineNosets the FIRST version of the data (the origin) and marks the write as an import in the journal: this call brings data AS THE CLIENT HANDED IT OVER. Each cell gets its `origine` version frozen at the same time as its current value, carrying the same layers, so `comment` says where the data came from. Use it for the import itself, NOT for enrichment: an agent's findings are the current version. An origin already set is never overwritten (a re-import updates the current version and leaves the origin alone), and an empty cell gets nothing — the client handed over nothing there, which is not the same as handing over an empty value.
expected_revisionNowith `id` only — the `_revision` of that row as you read it, when what you write was computed from that read (see above).
readonly_overrideNo`true` = overwrite the `readonly` columns THIS CALL writes, instead of being refused. Owner or governor of the table only ; valid for this call alone ; journaled.

Schema Changelog

Changes observed during successful MCP inspections.

No schema history has been recorded yet.

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are supplied, so the description carries the full behavioral burden and does so: it discloses that a write destroys the prior value with no undo, that the prior value survives only in `data_row_history` (90 days), that `readonly`, business keys and `@empty` rules can refuse whole batches, and that a return-path error may still have committed. It also documents override scopes (`readonly_override`, `origine_override`, `donnees_d_origine`) and the 25-entry/120-char bound on the forced-replacement journal.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness2/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the right idea, but the provenance/comment/link rule is stated three times in the opening block, and the text runs to roughly two thousand words with narrative asides (the 2026-09-08 removal, '837 cells out of 846 lost', '28 799 origine layers') that an agent does not need to invoke the tool. Critical refusal rules are buried mid-paragraph under a pile of warnings.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For an 18-parameter mutation tool with no annotations and no output schema, it covers everything: modes, key/upsert semantics, layer write rules, sentinel values, list rank addressing, date formats, closed tables, revision conflicts, and inline description of the return payload (ids, fusions, valeurs_effacees). Little an agent needs to call it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3; the description still adds rule-level meaning the schema only gestures at — `key` designating vs `upsert` merging, `expected_revision` only when the write was computed from a read, `force` changing scope but not right, `layers`/`empties`/`versions` governing the returned row. Some schema descriptions explicitly defer ('see above'), so the prose is load-bearing rather than redundant.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Opens with a specific verb and resource: 'Write one row, or a BATCH of rows in a single call', and immediately separates SINGLE (`row`) from BATCH (`rows`) modes later in the text. It is unambiguously distinct from read siblings like data_rows and from data_update_where.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly routes the agent elsewhere where appropriate: `oto_import`/`oto_upload_url` for files ('never retype its rows here'), `data_patch_schema` to declare a new column or reopen a closed table, `data_set_schema` for a persistent key, `data_rows` to read back after an ambiguous error. It also states when-not (closed tables, readonly columns, unknown datastore) with the exact remedy for each.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Try in Browser

Glama MCP Gateway

Add one secure layer between your agents and this server.

Resources