---
title: Operationen
description: Das Operations-Vokabular, das Editor, CLI, Collaboration-Backends und Templatical Cloud teilen.
---

# Operationen

Eine Operation ist eine Änderung an einem Template: einen Block hinzufügen, verschieben, löschen oder ändern, die Einstellungen ändern oder den ganzen Inhalt ersetzen. Dieselben sechs Operationen sind überall unterwegs, wo sich ein Template außerhalb eines einzelnen Editors ändert:

- **[Zusammenarbeit](/de/backend/collaboration)** — ein Editor mit `init({ collaboration })` schickt jede Änderung als Batch von Operationen an Ihr Backend und wendet die Batches anderer an.
- **Die CLI** — `template-tools edit` wendet sie auf eine Template-Datei an.
- **Templatical Cloud** — speichert jeden Batch mit seiner Revision, und mit seinem MCP-Server verbundene KI-Agenten bearbeiten mit demselben Vokabular über `edit_template`.

`applyOperations` aus `@templatical/types` (MIT) wendet sie an. Der Server von Templatical Cloud verwendet eine Engine mit denselben Regeln, und gemeinsame Testfälle halten beide identisch.

## Die sechs Operationen

```json
{"op": "addBlock",       "block": {"id": "b2", "type": "paragraph", "content": "<p>Hi</p>", "styles": {"padding": {"top": 0, "right": 0, "bottom": 0, "left": 0}}}, "parent": null, "after": "b1"}
{"op": "moveBlock",      "blockId": "b2", "parent": {"sectionId": "s1", "column": 0}, "after": null}
{"op": "deleteBlock",    "blockId": "b2"}
{"op": "updateBlock",    "blockId": "b1", "set": {"content": "<p>Hello</p>"}, "unset": ["visibility"]}
{"op": "updateSettings", "set": {"width": 640}, "unset": ["preheaderText"]}
{"op": "setContent",     "content": {"blocks": [], "settings": {"width": 600, "backgroundColor": "#ffffff", "textColor": "#1a1a1a", "linkUnderline": true, "fontFamily": "Arial", "locale": "en"}}}
```

Die Annahme einer Operation durch die Engine ersetzt keine Schema-Validierung: Führen Sie `template-tools validate` für das resultierende Template aus.

Ein Batch ist eine Liste von Operationen. Er wird der Reihe nach angewendet, jede Operation sieht die vorherigen, und alles oder nichts: Wird eine abgelehnt, gilt keine Operation des Batches.

### Positionen

Positionen nennen einen Nachbarblock, nie einen Index, sodass eine Operation dasselbe bedeutet, auch wenn vorher die Änderung eines anderen ankommt.

- `parent` ist `null` für die oberste Ebene oder `{"sectionId": "…", "column": 0}` für eine Spalte einer Sektion. `column` ist ein 0-basierter Index in die `children` der Sektion.
- `after` ist die ID des Blocks, hinter dem der Block landet, oder `null`, damit er als erster landet. Bei `moveBlock` zählen die Nachbarn im Zielcontainer ohne den verschobenen Block.
- `parent` und `after` sind bei `addBlock` und `moveBlock` Pflicht, auch wenn sie `null` sind.

### Sektionen und Spalten

- Eine Sektion steht nur auf der obersten Ebene. Eine Sektion in eine Spalte hinzuzufügen oder zu verschieben wird abgelehnt, ebenso eine neue Sektion mit einer Sektion unter ihren Kindern.
- `deleteBlock` auf einer Sektion löscht ihre Blöcke mit.
- `updateBlock` ersetzt jeden Schlüssel in `set` vollständig, ein `styles`-Objekt eingeschlossen, und entfernt die Schlüssel in `unset`. `id`, `type` und `children` lassen sich weder setzen noch entfernen, und `columns` einer Sektion lässt sich nicht entfernen.
- `set: {"columns": …}` auf einer Sektion passt ihre `children` an die Spaltenzahl des Layouts an: `"1"` hat eine Spalte, `"3"` hat drei, jedes andere Layout (`"2"`, `"2-1"`, `"1-2"`) hat zwei. Beim Vergrößern kommen leere Spalten hinzu. Beim Verkleinern fallen hintere Spalten nur weg, wenn sie leer sind; verschieben Sie ihre Blöcke also zuerst.
- Ein neuer Block braucht eine nicht leere String-`id` und einen String-`type`, und jede ID darin, die Kinder einer neuen Sektion eingeschlossen, muss im Template neu sein.

### Vorbedingungen

Jede Operation außer `addBlock` und `setContent` kann `expect` mitführen, das gelten muss, bevor sie angewendet wird. Bei einer Abweichung wird die Operation mit `precondition_failed` abgelehnt. Rückgängig nutzt das: Ein Rückgängig-Schritt, den die Änderung eines anderen überholt hat, wird abgelehnt, statt sie zu überschreiben.

- `updateBlock` und `updateSettings`: `expect` ordnet Schlüsseln ihre erwarteten aktuellen Werte zu. `null` passt zu einem `null`-Wert oder einem fehlenden Schlüssel.
- `moveBlock`: `expect.parent` und `expect.after` sind die aktuelle Position des Blocks. Ein fehlender Schlüssel zählt als `null`.
- `deleteBlock`: `expect.block` ist der ganze aktuelle Block, Kinder eingeschlossen, exakt verglichen.
- Werte werden wie JSON verglichen: Die Reihenfolge der Objektschlüssel spielt keine Rolle, Arrays werden der Reihe nach verglichen und Zahlen nach Wert, sodass `1` gleich `1.0` ist. `set`, `unset` und `expect` mit dem Wert `null` zählen als nicht angegeben.

### Sperren

Ein Backend übergibt die Sperren, die es hält, und die handelnde Sitzung: `applyOperations(content, ops, { locks, session })`, mit `locks` als `[{ blockId, sessionId }]`.

- `updateBlock`, `moveBlock` und `deleteBlock` auf einem Block, den eine andere Sitzung hält, werden mit `lock_held` abgelehnt. Beim Verschieben oder Löschen einer Sektion zählt auch jeder Block darin.
- `setContent` wird mit `locked_by_others` abgelehnt, solange eine andere Sitzung eine Sperre hält.
- Mit `session` gleich `null` (eine serverseitige Änderung) gehört jede Sperre jemand anderem.

<!-- locale-operations:start -->
## Locale-Operationen

`addLocale`, `removeLocale`, `setDefaultLocale`, `updateBlockLocale` und `updateSettingsLocale` verwalten Sprachversionen im gemeinsamen Baum des SDK. Aktualisieren Sie Renderer, Validator und Collaboration-Backend, bevor Sie lokalisierte Inhalte speichern.

Der folgende Batch beginnt mit Absatz `b1` mit `<p>Original</p>`, Standardsprache `en`, Richtung `ltr` und ohne Sprachversionen. Er fügt Deutsch hinzu, schreibt einen deutschen Wert, blendet den Block auf Deutsch aus, überschreibt die Richtung und entfernt dann Deutsch samt seinen Ebenen. `sourceHash` ist `sourceHash("<p>Original</p>")`.

```json
{"op":"addLocale","locale":"de"}
{"op":"updateBlockLocale","blockId":"b1","locale":"de","text":{"content":{"value":"<p>Hallo</p>","sourceHash":"f898b751"}},"hidden":true}
{"op":"updateSettingsLocale","locale":"de","set":{"direction":"rtl"}}
{"op":"removeLocale","locale":"de","expect":{"blocks":{"b1":{"text":{"content":{"value":"<p>Hallo</p>","sourceHash":"f898b751"}},"hidden":true}},"settings":{"set":{"direction":"rtl"}}}}
```

| Operation | Vertrag |
|---|---|
| `addLocale` | `locale: string`; `position?: number` fügt in `settings.locales` ein, standardmäßig am Ende. Die Position muss eine Ganzzahl von 0 bis zur aktuellen Länge sein. Standard (`settings.locale ?? "en"`) und vorhandene Versionen werden ohne Beachtung der Groß-/Kleinschreibung abgelehnt. |
| `removeLocale` | `locale: string`; `expect?: { blocks: Record<string, BlockLocaleLayer>; settings?: SettingsLocaleLayer }` vergleicht alle Ebenen exakt. `blocks` enthält Ebenen nach Block-ID; eine fehlende ID bedeutet keine Ebene. Das separate Feld `settings` enthält die Einstellungsebene; fehlt es, gibt es keine. `expect: { blocks: {} }` verlangt keine Block- oder Einstellungsebenen, auch bei einem Block namens `"settings"`. Entfernt die Sprache und alle Block- und Einstellungsebenen, auch in Spalten. |
| `setDefaultLocale` | `locale: string`; ein exakter Tag aus `settings.locales`. Übernimmt Text und Eigenschaften als Basis; die bisherige Standardsprache erhält die Listenposition und eine Ebene für abweichende Werte sowie nicht leeren übersetzbaren Text, einschließlich Lücken. Fehlende Felder behalten ihre Basiswerte und eine aktuelle Textkopie für die bisherige Standardsprache. Kollidiert mit jeder Sperre einer anderen Sitzung (`locked_by_others`). |
| `updateBlockLocale` | `blockId: string`; `locale: string`; `text?: Record<string, LocalizedText>`; `set?: Record<string, unknown>`; `unset?: string[]`; `hidden?: boolean`; `expect?: LocaleLayerExpect`. Mindestens ein Änderungsfeld muss vorhanden sein. |
| `updateSettingsLocale` | `locale: string`; `text?: Record<string, LocalizedText>`; `set?: Record<string, unknown>`; `unset?: string[]`; `expect?: Omit<LocaleLayerExpect, "hidden">`. Mindestens ein Änderungsfeld muss vorhanden sein. Kein Ausblendestatus und keine Sperren. |

`locale` muss `isLocaleTag` erfüllen: `/^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/`. Tags außerhalb dieses Musters, darunter `x-acme` und `i-klingon`, werden abgelehnt. Jede Operation außer dem Hinzufügen verlangt den exakten Tag in `settings.locales`; ungültige Tags und unbekannte Versionen ergeben `invalid_operation`.

Basisupdates entfernen verwaiste Locale-Einträge; ihre Inverse stellt zuerst die Basis und dann die entfernten Ebenenpfade wieder her. Locale-`set`-Overrides lehnen `columns` bei Sektionen, `items` bei Menüs, `rows` und `rows.<id>.cells` bei Tabellen sowie `icons` bei Social-Blöcken mit `invalid_operation` ab. Eigenschaften einzelner Elemente und vollständige benutzerdefinierte `fieldValues.<key>`-Overrides bleiben erlaubt.

`LocalizedText` ist `{ value: string; sourceHash: string }`. Textpfade müssen `TRANSLATABLE_FIELDS` des Blocktyps oder `SETTINGS_TRANSLATABLE_FIELDS` (`preheaderText`) für Einstellungen entsprechen und in der Basis auflösbar sein. Pfade verwenden Element-IDs, etwa `items.i1.text`; IDs von Menüeinträgen, Social-Icons, Tabellenzeilen und Tabellenzellen dürfen weder `.` noch `/` enthalten. Ein `set`-Pfad muss auflösbar sein oder einen vorhandenen Objekt-Elternpfad haben. Prototypsegmente (`__proto__`, `constructor`, `prototype`) und leere Segmente werden abgelehnt. Block-Overrides dürfen nicht mit `id`, `type`, `children` oder `l10n` beginnen; Einstellungs-Overrides nicht mit `locale`, `locales` oder `l10n`.

Die Engine wendet zuerst `unset` an (auf beide Maps), dann `text`, dann `set`, dann `hidden`. Ein Pfad in einer Map wird aus der anderen gelöscht. `hidden: false` entfernt das Flag. `hidden: true` auf dem `slot`-Block eines Layouts oder einem Block, der ihn enthält, wird mit `invalid_operation`, der `blockId` des Blocks und `A layout's slot block, and any block that contains it, can't be hidden.` abgelehnt, weil das Layout diese Sprache sonst nicht zusammensetzen könnte. Aus demselben Grund lehnen `moveBlock` und `addBlock` ab, den Slot oder einen Block, der ihn enthält, unter eine Sektion zu setzen, die eine Sprache ausblendet, und `addBlock` lehnt einen neuen Block ab, dessen eigene Ebenen den enthaltenen Slot ausblenden: `invalid_operation`, die `blockId` des gesetzten Blocks und `A layout's slot block can't be placed inside a block that a language hides.` Leere Maps, Ebenen und Lokalisierungscontainer werden weggelassen; das Entfernen der letzten Version lässt `settings.locales` weg.

`set: { path: null }` speichert einen löschenden Override: Die Eigenschaft fehlt in der `resolveLocale`-Projektion dieser Sprache. `unset: [path]` entfernt den Override, sodass wieder der Wert der Standardsprache gilt. Ebenen speichern niemals `undefined`; undefinierte `text`- oder `set`-Einträge und Texteinträge ohne die Strings `value` und `sourceHash` werden mit `invalid_operation` abgelehnt. Jeder `unset`-Pfad muss nicht leer sein und `isSafePath` erfüllen, auch wenn der Basispfad nicht mehr existiert; sichere verwaiste Pfade bleiben entfernbar.

### Standardsprache

`createSetDefaultLocaleOperation(content: TemplateContent, locale: string, definitions?: readonly CustomBlockDefinition[]): SetDefaultLocaleOperation` erstellt die Operation; `definitions` ist standardmäßig `[]`. Für benutzerdefinierte `text`- und `textarea`-Felder übergeben Sie ihre Definitionen. Die optionale Eigenschaft `customTextFields?: Record<string, string[]>` ordnet IDs benutzerdefinierter Blöcke vorhandene sichere `fieldValues.<key>`-Stringpfade zu. Sie reist mit der Operation, damit jede Replik ohne lokale Definitionen dieselben Kopien erhält. Fehlende oder nicht benutzerdefinierte Blöcke sowie unsichere, fehlende oder nicht als String auflösbare Pfade werden mit `invalid_operation` abgelehnt. Die Map-Schlüssel `__proto__`, `constructor` und `prototype` werden ebenfalls abgelehnt, auch wenn ein benutzerdefinierter Block diese ID hat. Der Builder lehnt eine solche ID mit demselben Fehler und `opIndex: 0` ab, wenn er Textpfade dafür erfassen würde; benennen Sie die Block-ID vor dem Erfassen dieser Pfade um. Ohne Metadaten erkennt die Engine benutzerdefinierte Textpfade nur an vorhandenen `text`-Einträgen; andere benutzerdefinierte Lücken werden nicht erkannt. Der Kern stellt `editor.createSetDefaultLocaleOperation(locale)` mit den registrierten Definitionen bereit; die visuelle Bestätigung verwendet ihn. Im CLI übergeben Sie `edit --definitions definitions.json`.

`{ "op": "setDefaultLocale", "locale": "de" }` macht eine vorhandene deutsche Version zur Standardsprache. Für Blöcke jeder Tiefe, einschließlich Layout-Wrappern, und Einstellungen werden `text`-Werte und `set`-Überschreibungen zur Basis (`null` entfernt eine Eigenschaft). Abweichender Text der bisherigen Standardsprache wird als `text` mit `sourceHash` der neuen Basis gespeichert; andere abweichende Werte als `set`. Gleicher nicht leerer übersetzbarer Text, einschließlich Lücken ohne Text in der übernommenen Version, bleibt als aktueller `text`-Eintrag in der Ebene der bisherigen Standardsprache, gestempelt mit dem `sourceHash` der neuen Basis. Eine spätere Bearbeitung der Basis markiert diese Kopie als veraltet und bewahrt die E-Mail der bisherigen Standardsprache. Unveränderte leere Standardwerte sind keine Lücken und erhalten keinen Eintrag. Lücken behalten die alte Basis; nach der Übernahme weist nichts mehr auf deren unübersetzten Text hin. Übersetzen Sie diese daher zuerst mit Übersetzen oder `strings export` / `strings import`, HTML-Blöcke jedoch von Hand, weil diese Werkzeuge sie ausschließen. Nach der Übernahme von Deutsch statt Englisch listet `validate --locale en` diese kopierten Felder nicht mehr als `localization.untranslated`. `settings.locale` ändert sich; die bisherige Standardsprache übernimmt den Index der gewählten Version in `settings.locales`.

Aktueller Text anderer Sprachen erhält den Hash der neuen Basis. Veralteter Text behält seinen gespeicherten Hash; entspricht dieser dem übernommenen Wert, wird der Text aktuell. Fehlt der neuen Basis ein Textpfad, werden der bisherige Standardwert und Texte anderer Sprachen zu `set`-Einträgen ohne Veraltungsprüfung. Eigenschaftsüberschreibungen, deren übergeordnetes Objekt entfällt, werden am nächsten beschreibbaren Vorfahren zusammengefasst. Der Wert ist der zuvor projizierte Teilbaum dieser Sprache einschließlich geerbter Eigenschaften und Überschreibungen. Ein benutzerdefinierter Block verliert `renderedHtml` nur, wenn sich seine `fieldValues` ändern.

`localeHiddenBlocks(content, locale): string[]` listet in Tiefenreihenfolge die IDs der Blöcke, deren eigenes `l10n[locale].hidden` `true` ist; es sucht auch in ausgeblendeten Blöcken, sodass ein ausgeblendetes Kind eines ausgeblendeten Abschnitts separat erscheint und ein eigenes `hidden: false` braucht. Für Standard- und unbekannte Sprachen ist das Ergebnis `[]`. `defaultLocaleGaps(content, locale, definitions?): string[]` listet übersetzbare Feld-IDs mit nicht leerem Standardwert, die mangels Text oder anwendbarer Eigenschaftsüberschreibung die alte Basis behalten. Veralteter Text wird übernommen und zählt nicht als Lücke, ebenso wenig wie ein leerer Standardwert, wie bei `localeCoverage`. Beide Helfer sind rein; übergeben Sie Definitionen, um Textfelder benutzerdefinierter Blöcke mitzuzählen.

Eine unbekannte Version oder die aktuelle Standardsprache erzeugt `invalid_operation`: `"<locale>" is not a language version of this template.` Ausgeblendete Blöcke erzeugen `invalid_operation`, die erste ausgeblendete `blockId` und `<count> blocks are hidden in "<locale>". Show them before making it the default.` Jede Sperre einer anderen Sitzung lehnt sie wie bei `setContent` mit `locked_by_others` ab; `onRemoteChange.locale` ist für diese Operation `null`. Die Prüfungen laufen in dieser Reihenfolge: Tag, Version, ausgeblendete Blöcke, dann Sperren; ausgeblendete Blöcke gewinnen also vor einer Sperre. KI-Batches in einer Sprachansicht lehnen sie ab.

`invertOperations` stellt mit `setContent` den exakten vorherigen Inhalt einschließlich Hashes, übernommener Ebene und HTML-Cache wieder her. Für Rückgängig gilt daher dieselbe templateweite Sperrregel. Ein Snapshot kann nicht erwarten, was er überschreibt; mit Zusammenarbeit merkt sich der Editor deshalb auch den Inhalt, auf den ein solcher Rückgängig- oder Wiederholen-Schritt angewendet wird, und überspringt den Schritt (`undoSkipped`, `precondition_failed`), sobald dieser Editor danach einen Batch einer anderen Sitzung empfangen hat, gesperrt oder nicht. Einen Batch, den das Backend schon angenommen, aber noch nicht zugestellt hat, fängt das Backend ab: Es lehnt einen Batch mit einem `setContent` ab, dessen `baseRevision` nicht die aktuelle Revision ist ([Zusammenarbeit](/de/backend/collaboration#submit)). Das Rückgängigmachen von `setContent` funktioniert genauso. Ohne Batches anderer Sitzungen stellen Rückgängig und Wiederholen den Inhalt exakt wieder her. Senden Sie die explizite Operation über Collaboration; ein Snapshot-Diff erzeugt für eine Änderung der Standardsprache `setContent`.

### Overrides löschen

Dieser separate Batch beginnt mit Deutsch bereits in `settings.locales`, Block `b1` mit `styles.backgroundColor: "#ffffff"` und Einstellungen mit `linkColor: "#1d4ed8"`. Die ersten beiden Operationen lassen diese Eigenschaften auf Deutsch fehlen. Die letzten beiden entfernen die löschenden Overrides und stellen die Standardwerte wieder her. Die Basiswerte bleiben unverändert.

```json
{"op":"updateBlockLocale","blockId":"b1","locale":"de","set":{"styles.backgroundColor":null}}
{"op":"updateSettingsLocale","locale":"de","set":{"linkColor":null}}
{"op":"updateBlockLocale","blockId":"b1","locale":"de","unset":["styles.backgroundColor"]}
{"op":"updateSettingsLocale","locale":"de","unset":["linkColor"]}
```

`LocaleLayerExpect` ist `{ text?: Record<string, LocalizedText | null>; set?: Record<string, unknown>; hidden?: boolean }`. Text- und Override-Prüfungen vergleichen den aktuellen Ebenenwert, fehlende Werte zählen als `null`. Eine Ausblendeprüfung vergleicht `layer.hidden === true`. Eine Abweichung ergibt `precondition_failed`. Block-Locale-Updates prüfen Form, benannten Block (`block_missing`), Pfade, Sperren, Vorbedingungen und dann die Änderung, zu der die Slot-Regel gehört.

Eine Sperre kann `locale?: string | null` tragen. Eine fehlende oder `null`-Locale sperrt Standard und Struktur. Ein Block-Locale-Update kollidiert mit der Sperre einer anderen Sitzung auf demselben Block und derselben Sprache oder auf der Struktur (`lock_held`, mit `blockId`). Verschiedene Sprachen können gleichzeitig bearbeitet werden. Strukturelle Block-Updates, Verschieben und Löschen kollidieren mit jeder Locale-Sperre auf den betroffenen Blöcken. Hinzufügen und Entfernen von Sprachen sowie Einstellungs-Locale-Updates ignorieren Sperren.

Nur Locale-Operationen ändern bestehende Lokalisierungscontainer: `updateBlock` lehnt `l10n` in `set` oder `unset` ab; `updateSettings` lehnt `locales` und `l10n` ab und verbietet das Setzen oder Entfernen von `locale`, solange Versionen existieren (`invalid_operation`). `addBlock` darf `l10n` mitbringen, `setContent` ersetzt alles.

`invertOperations` schützt das Rückgängigmachen. Die Umkehrung von `addLocale` verlangt leere Ebenen. Die Umkehrung von `removeLocale` stellt die alte Listenposition, dann Block- und Einstellungsebenen mit Prüfungen auf leere Pfade wieder her. Umkehrungen von Locale-Updates stellen beide Maps und den Ausblendestatus wieder her und erwarten die Werte der Vorwärtsoperation.

### Prüfung

Führen Sie `pnpm --filter @templatical/types exec vitest run tests/operations.test.ts tests/invert-operations.test.ts tests/set-default-locale.test.ts tests/locale-hidden-blocks.test.ts` für gemeinsame Fixtures und geschützte Umkehrungen aus. Führen Sie `template-tools validate` für das resultierende Template aus; die Engine-Annahme validiert das Schema nicht.
<!-- locale-operations:end -->

## Fehler

Ein abgelehnter Batch wirft einen `OperationError` mit `code` und `opIndex`, dem 0-basierten Index der abgelehnten Operation.

| Code | Wann | Templatical Cloud antwortet |
|---|---|---|
| `block_exists` | Die ID eines neuen Blocks oder eine ID darin gibt es im Template schon | 409 |
| `block_missing` | Der Block oder die Eltern-Sektion fehlt | 409 |
| `anchor_missing` | Der `after`-Block ist nicht im Zielcontainer | 409 |
| `precondition_failed` | `expect` gilt nicht | 409 |
| `lock_held` | Eine andere Sitzung hält den Block | 409 |
| `locked_by_others` | Eine andere Sitzung hält eine Sperre, bei `setContent` oder einem Wechsel der Standardsprache | 409 |
| `invalid_operation` | Die Operation ist fehlerhaft oder kann nie gelten | 422 |

### Welcher Fehler gewinnt

Verletzt eine Operation mehrere Regeln, entscheidet die erste fehlschlagende Prüfung:

1. Ihre Form (`invalid_operation`): eine fehlerhafte Operation, ein fehlender Pflichtschlüssel, eine neue Sektion mit Eltern, `id`/`type`/`children` in `set` oder `unset`, `columns` einer Sektion in `unset`.
2. Die Blöcke, die sie nennt (`block_missing`, `block_exists`).
3. Sperren (`lock_held`, `locked_by_others`).
4. Vorbedingungen (`precondition_failed`).
5. Platzierung (`invalid_operation`): eine Sektion in eine Spalte, ein Block hinter sich selbst.
6. Der Zielcontainer: `block_missing` für eine fehlende Eltern-Sektion; `invalid_operation` für Eltern, die keine Sektion sind, oder eine Spalte außerhalb des Bereichs.
7. Der Anker (`anchor_missing`).
8. Die Änderung selbst (`invalid_operation`): ein Layout, das Spalten mit Blöcken wegfallen ließe.

Element-IDs mit `.` oder `/` werden nur abgelehnt, wo eine Operation eine hinzufügt, und die Prüfung steht je Operation an einer anderen Stelle: `addBlock` prüft sie mit seiner Form (Schritt 1) und prüft gar keine Sperren; `updateBlock` prüft sie als die Änderung selbst (Schritt 8), nach Sperren und Vorbedingungen; `setContent` prüft sie nach seiner Form und vor den Sperren. Die gemeinsamen Fixtures halten jeden Fall fest.

## Operationen anwenden

```ts
import {
  applyOperations,
  OperationError,
  type Operation,
} from '@templatical/types';

const ops: Operation[] = [
  { op: 'updateBlock', blockId: 'b1', set: { content: '<p>Hello</p>' } },
];

try {
  const next = applyOperations(template.content, ops, {
    locks: [{ blockId: 'b4', sessionId: 'f7c1…' }],
    session: requestSessionId,
  });
  // `next` speichern
} catch (error) {
  if (error instanceof OperationError) {
    // error.code, error.opIndex
  }
  throw error;
}
```

`applyOperations` liefert ein neues Dokument und verändert das übergebene nie. `applyOperationsInPlace` ändert es direkt, für eine Kopie, die Sie verwerfen können. `getColumnCount(layout)` ist die Layout-Regel von oben.

## In der CLI

```bash
npx -y @templatical/template-tools edit emails/welcome.json \
  --op '{"op":"updateBlock","blockId":"button_1","set":{"backgroundColor":"#1d4ed8"}}'
```

`--ops <file>` nimmt ein JSON-Array für einen Batch. Siehe [Template Tools](/de/api/template-tools).
