---
name: gdocs-tables
version: 2.0.0
domain: productivity
description: Inserisce + popola tabelle native Google Docs con pattern verificato live. Bypassa il bug populate del tool create_table_with_data.
trigger-keywords: ['tabella google docs', 'table google docs', 'inserisci tabella', 'google docs table', 'report con tabelle']
user-invocable: true
allowed-tools:
  - google_workspace_*
  - markitdown-mcp_*
max-tokens: 4000
estimated-cost-eur: 0.02
---

# Skill: gdocs-tables

## Problema

Il tool `google_workspace_create_table_with_data(index=N, table_data=[...])`
crea la struttura della tabella, ma il populate cells fallisce
silenziosamente o con HTTP 400 (`deleteContentRange: cannot include the
newline character`, `Invalid deletion range`). Risultato: tabella vuota.

Il pattern alternativo `batch_update_doc + insertText` su cell `startIndex`
fallisce anch'esso con `insertion index must be inside the bounds of an
existing paragraph` perché il `startIndex` ritornato dall'inspect è il
BORDER strutturale della cella, NON il content area.

Pattern delimiter-text (`▸`, `|`, `\t`) NON va usato — non sono tabelle
native, non si formattano via API, non si esportano in PDF correttamente.

## Pattern corretto (verificato live 2026-05-14)

### Step 1: Inserisci tabella vuota

```
google_workspace_batch_update_doc(
  document_id="<doc_id>",
  requests=[
    {
      "insertTable": {
        "location": {"index": <paragraph_start_index>},
        "rows": <N>,
        "columns": <M>
      }
    }
  ]
)
```

`<paragraph_start_index>` deve essere INIZIO di un paragrafo (non
middle-of-paragraph). Usa `inspect_doc_structure(detailed=true)` per
trovare un paragrafo vuoto o l'indice subito dopo un header.

### Step 2: Estrai PARAGRAPH startIndex DENTRO ogni cella

Dopo `insertTable`, fai `inspect_doc_structure(detailed=true)` e cerca la
tabella. Per ogni `tableCell` osserva:

```
tableCell.startIndex = X        ← BORDER della cella (NON usare per insertText)
tableCell.content[0].startIndex = X+1  ← paragraph DENTRO la cella (USA QUESTO)
tableCell.content[0].paragraph.elements[0].textRun.content = "\n"
```

Esempio reale (tabella 3×2 inserita a index=1):

```
cell[0][0] cell=4..6  para=5..6   content='\n'   ← usa 5
cell[0][1] cell=6..8  para=7..8   content='\n'   ← usa 7
cell[1][0] cell=9..11 para=10..11 content='\n'   ← usa 10
cell[1][1] cell=11..13 para=12..13 content='\n'  ← usa 12
cell[2][0] cell=14..16 para=15..16 content='\n'  ← usa 15
cell[2][1] cell=16..18 para=17..18 content='\n'  ← usa 17
```

Regola pratica: `paragraph_start = cell.startIndex + 1`.

### Step 3: Popola TUTTE le celle in UN SOLO batch_update_doc, ordine REVERSE

CRITICAL: ordina le inserzioni per `paragraph_start` DECRESCENTE. Inserire
dall'indice più ALTO al più BASSO evita lo shift cumulativo che invaliderebbe
gli indici più alti dopo ogni insert al più basso.

```
# Costruisci lista (paragraph_start, text) per ogni cella
cells = [
  (5,  "Header1"),
  (7,  "Header2"),
  (10, "row1col1"),
  (12, "row1col2"),
  (15, "row2col1"),
  (17, "row2col2"),
]
cells.sort(key=lambda x: -x[0])  # DESC

google_workspace_batch_update_doc(
  document_id="<doc_id>",
  requests=[
    {"insertText": {"location": {"index": idx}, "text": txt}}
    for idx, txt in cells
  ]
)
```

Risultato: tabella popolata atomicamente in 1 chiamata API.

### Step 4: Header bold (optional)

Dopo populate, per bold sui header (riga 0):

```
google_workspace_batch_update_doc(
  document_id="<doc_id>",
  requests=[
    {
      "updateTextStyle": {
        "range": {"startIndex": <header_cell_start>, "endIndex": <header_cell_end>},
        "textStyle": {"bold": true},
        "fields": "bold"
      }
    }
    for each header cell
  ]
)
```

`<header_cell_start>` e `<endIndex>` sono i bound della stringa header
appena inserita. Calcolabili dopo populate via re-inspect, oppure
direttamente: `start = paragraph_start`, `end = start + len(text)`.

## Sequenza completa (pseudo-code)

```python
def insert_populated_table(doc_id, index, header_row, data_rows):
    rows = 1 + len(data_rows)
    cols = len(header_row)
    all_rows = [header_row] + data_rows

    # Step 1: empty table
    batch_update_doc(doc_id, [
        {"insertTable": {"location": {"index": index}, "rows": rows, "columns": cols}}
    ])

    # Step 2: re-inspect → find table cells
    doc = inspect_doc_structure(doc_id, detailed=True)
    table = find_table_at_or_after(doc, index)

    # Step 3: collect (paragraph_start, text) per cella
    cells = []
    for r, row in enumerate(table.rows):
        for c, cell in enumerate(row.cells):
            cells.append((cell.startIndex + 1, all_rows[r][c]))

    # Step 4: populate reverse
    cells.sort(key=lambda x: -x[0])
    batch_update_doc(doc_id, [
        {"insertText": {"location": {"index": idx}, "text": txt}}
        for idx, txt in cells
    ])
```

## Anti-pattern (vietati)

| Anti-pattern | Problema |
|---|---|
| `create_table_with_data(table_data=[...])` | Populate fallisce con HTTP 400 deleteContentRange |
| `insertText` su `cell.startIndex` (cell border) | `insertion index must be inside paragraph` |
| Multiple `batch_update_doc` calls one-per-cell | Ogni call può schifare indici, fragile, lento |
| Inserimento ordine forward (basso → alto) | Shift cumulativo invalida indici già calcolati |
| Delimiter-text (`▸`, `|`, `\t`) | Non è tabella nativa, no API formatting |
| `deleteContentRange` su celle con solo `\n` | HTTP 400 newline-end-of-segment |

## Diagnostic checklist

| Sintomo | Causa | Azione |
|---|---|---|
| `Invalid requests[*].deleteContentRange: cannot include newline` | tentativo populate cells con only-`\n` content | NON usare deleteContentRange, usa insertText puro |
| `Invalid requests[*].deleteContentRange: Invalid deletion range` | range out-of-bounds dopo shift indici | re-inspect prima di delete; OR usa solo insertText |
| `insertion index must be inside the bounds of an existing paragraph` | usato cell.startIndex invece di paragraph.startIndex | sostituisci con `cell.startIndex + 1` |
| `Could not find table after creation` | indice middle-of-paragraph | inserisci `\n\n` prima e usa nuovo paragraph-start |
| `Extra inputs are not permitted [type=extra_forbidden]` | parametri tool wrapper invalidi | rimuovi argomenti non-spec dalla call MCP |
| 429 quota | write/min limit eToro/Google | backoff 30s + retry |

## Regola operativa

NON usare delimiter-text come fallback. Se il pattern sopra fallisce:
1. Re-inspect doc completo
2. Verifica indici manualmente
3. Riporta al conductor `{"status": "table_skipped", "reason": "...",
   "doc_id": "...", "missing": "table 10×2 at index N"}`. Conductor decide
   se ri-tentare o procedere senza tabella.
