apply_data_operations Atomically edit data with expected field/cell values and named versions. Prefer patch_metadata for small metadata edits. update_table needs metaVersion; set_cells needs column.version. Migrations require exact revision and reject invalid references. Never silently remove external game IDs. Reuse revisionAfter/dataVersions; returnVersions=none omits hashes. Retry lost responses identically; conflicts need focused rereads and a new requestId. Details: /docs/mcp/apply_data_operations. Edit Structured Data atomically, with merge_non_overlapping by default. For descriptions/names/width/unit/display-field changes use patch_metadata: up to 1,000 edits, each with tableId, optional columnId and patches[{field,expected,value}]. Field tags (board tags) also fits records (recordId); [] removes. This checks only touched metadata fields and merges with unrelated row/schema edits; null represents an absent optional metadata value. Prefer one metadata batch, not full update_column replacements. update_table uses expectedMetaVersion from table.metaVersion, NOT table.version. set_cells uses expectedValue and expectedColumnVersion from column.version. Structural operations use their explicitly named versions. preconditionMode=batch_start checks version hashes against the initial snapshot so dependent schema changes can share one atomic batch; expected field/cell values still check in operation order. migrate_reference switches an existing reference to another unique scalar key IN THE SAME target table, or referenceColumnId=null for internal record identity. It converts all populated values and the default atomically, preserves linked records, and rejects missing/ambiguous links. Migrations require the exact current expectedRevision even under merge_non_overlapping. Order migrations before deleting old keys; newly broken schema dependencies refuse the entire batch with source columns and affected counts. Do not silently remove numeric game IDs; they may be needed externally. Record deletion can still leave flagged draft links. No implicit cascading or guessed conversions. New table/column/record IDs are stable across rename/type edits; never reuse deleted IDs. Auto-number game keys are allocated server-side. Optional returnVersions=none omits the dataVersions map from this mutation response (stored receipts retain it); default changed returns only changed entity hashes. Reuse revisionAfter and returned dataVersions; tableId:meta is table metadata, tableId:schema is columns. Warnings group validation errors by column and count. dryRun validates without saving. Retry lost responses with identical input/requestId; after a conflict reread only affected targets and use a new requestId. telemetry reports server duration/phases, payload bytes and conflicts, not connector/model latency. OPERATION REFERENCE These are object shapes, not literal JSON: supply values for identifiers/versions; ? means optional. Unknown fields and operation names are rejected. Use add_record (singular), with a nested record object. Add multiple records as separate operations; each requires its own expectedSchemaVersion. Commands accept 1–100 operations. {type:"add_table", table:{id,name,columns:[Column],records:[Record]}} {type:"update_table", tableId,expectedMetaVersion,name,description} {type:"delete_table", tableId,expectedVersion} {type:"add_column", tableId,expectedSchemaVersion,column:Column} {type:"update_column", tableId,expectedVersion,column:Column} {type:"delete_column", tableId,columnId,expectedTableVersion} {type:"reorder_columns", tableId,expectedSchemaVersion,ids:[columnId,...]} {type:"add_record", tableId,expectedSchemaVersion,record:Record} {type:"delete_record", tableId,recordId,expectedVersion} {type:"set_cells", tableId,cells:[{recordId,columnId,expectedColumnVersion,expectedValue,value}]} {type:"patch_metadata", edits:[{tableId,columnId?,recordId?,patches:[{field,expected,value}]}]} {type:"migrate_reference", tableId,columnId,referenceColumnId} Column: {id,name,type} plus optional description,required,unique,immutable,autoNumber,defaultValue,options,referenceTableId,referenceColumnId,unit,width,tags. type is text|integer|decimal|boolean|select|reference. update_column replaces the column definition: send the complete desired definition (omit the read-only version), or prefer patch_metadata for small metadata edits. Auto-number columns are immutable unique integers; omit their cell values when adding records. Record: {id,values:{columnId:cell,...},tags?}. Values are keyed by column ID, not display name. A cell is string|number|boolean|null, never an object or array. null clears a cell. New IDs must be distinct across tables, columns and records. Table: {id,name,columns:[Column],records:[Record]} plus optional description,primaryDisplayColumnId,nextId,tags. add_table may include its initial columns and records. patch_metadata targets a table, a column, or a record (do not combine columnId and recordId). Tables accept name,description,primaryDisplayColumnId,tags; columns accept name,description,width,unit,tags; records accept tags only. Use expected/value for every patch, null for absent optional metadata, and [] for absent tags. reorder_columns.ids must list every column exactly once. migrate_reference.referenceColumnId is a unique scalar key in the same target table, or null for internal record IDs; it also requires the exact current expectedRevision. GUARDS FROM read_data Read the target table with includeVersions:true (schemaOnly:true if rows are unnecessary). Copy returned hashes; do not calculate them yourself. - add_column, add_record, reorder_columns: expectedSchemaVersion = table.schemaVersion. - update_table: expectedMetaVersion = table.metaVersion (not table.version). - delete_table: expectedVersion = table.version. - delete_column: expectedTableVersion = table.version. - update_column: expectedVersion = column.version. - delete_record: expectedVersion = record.version. - set_cells: each cell carries expectedColumnVersion = column.version and expectedValue = its current value (null when unset). - patch_metadata: each patch carries the current expected field value, not a version hash. ATOMIC ADD-COLUMN / ADD-RECORD EXAMPLE Assume an existing empty table {id:"table_units",name:"Units",columns:[],records:[]} in sheet_data. The concrete hash below is that empty table's schemaVersion. Replace boardId, sheetId, tableId, expectedRevision and BOTH expectedSchemaVersion values with your read_data result; choose fresh column/record IDs and requestId. This is one apply_data_operations call: { "boardId": "board_example", "sheetId": "sheet_data", "requestId": "add_score_and_record_1", "expectedRevision": "g1.r0", "preconditionMode": "batch_start", "operations": [ { "type": "add_column", "tableId": "table_units", "expectedSchemaVersion": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", "column": { "id": "col_score", "name": "Score", "type": "integer" } }, { "type": "add_record", "tableId": "table_units", "expectedSchemaVersion": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", "record": { "id": "record_unit_1", "values": { "col_score": 10 } } } ] } Both guards intentionally use the SAME initial table.schemaVersion: preconditionMode=batch_start checks version hashes against the pre-batch snapshot, while operations still execute in order, so the new record can use col_score. Default sequential mode would reject the second operation's stale schema hash. Expected field/cell values still check in operation order even under batch_start. dryRun:true validates without saving; it does not reserve IDs or versions. Reuse revisionAfter/dataVersions after a committed response.