---
title: "Save edits as JSON and replay them"
url: https://recursiveai.org/onehtml/cookbook/save-and-replay-edits/
description: "Store edits in the versioned onehtml-edits JSON format and apply them to another copy of the page later. Replay finds each target again or reports why it…"
version: "1.0.0"
---

# Save edits as JSON and replay them

Store edits in the versioned onehtml-edits JSON format and apply them to another copy of the page later. Replay finds each target again or reports why it can’t, and never edits the wrong element.

An `Edit` in memory points at a node of one snapshot. To keep edits across processes or page versions, `EditJson.Create` turns them into an envelope whose targets are durable. Each target has a locator (a CSS selector unique in the page), a fingerprint (tag, key attributes and hashes of the text and path), and an anchor (the byte range and a hash of its bytes).

```csharp
using RecursiveAI.OneHtml;
using RecursiveAI.OneHtml.Editing;

string html = File.ReadAllText("events.html");
Document original = Document.Parse(html);
Node img = original.Root.QuerySelector("main img:not([alt])");
Node partner = original.Root.QuerySelector("a[target=_blank]");

// Save edits as versioned JSON, with durable targets instead of in-memory handles
EditEnvelope envelope = EditJson.Create(original, new[]
{
    Edit.SetAttribute(img, "alt", "Children listening to a story")
        .WithMeta(new Dictionary<string, object> { ["ticket"] = "WEB-212" }),
    Edit.SetAttribute(partner, "rel", "noopener"),
});
File.WriteAllText("events.edits.json", EditJson.ToJson(envelope, indented: true));
Console.WriteLine($"First target: {envelope.Edits[0].Target!.Locator}");

void Replay(string label, string page)
{
    EditEnvelope saved = EditJson.Parse(File.ReadAllText("events.edits.json"));
    ReplayResult replay = EditReplay.Replay(saved, Document.Parse(page));
    Console.WriteLine($"{label}: succeeded {replay.Succeeded}");
    foreach (ReplayOutcome outcome in replay.Outcomes)
    {
        Console.WriteLine($"  edit {outcome.Index}: {outcome.Status} {outcome.Reason} {outcome.MetaJson}");
    }
}

// 1. Someone changed the text above the image: every byte offset after it moved
Replay("Text changed", html.Replace("Saturdays at 10:30", "Saturdays at 11:00, starting in May"));

// 2. A new card was inserted first: article:nth-of-type(1) is now a different element
Replay("Card inserted", html.Replace(
    "<!-- events are edited by hand",
    "<article class=\"card\"><h2>Book sale</h2><img src=\"/img/sale.jpg\" alt=\"\"></article>\n    <!-- events are edited by hand"));

// 3. The image is gone
Replay("Image removed", html.Replace("<img src=\"/img/story-time.jpg\" width=640 height=360>", ""));
```

```text
First target: #content > article:nth-of-type(1) > img
Text changed: succeeded True
  edit 0: Applied  {"ticket":"WEB-212"}
  edit 1: Applied  
Card inserted: succeeded False
  edit 0: Stale the element the locator finds has a different tag or key attributes than the target had {"ticket":"WEB-212"}
  edit 1: Stale the element the locator finds has a different tag or key attributes than the target had 
Image removed: succeeded False
  edit 0: Stale the locator matches nothing: #content > article:nth-of-type(1) > img {"ticket":"WEB-212"}
  edit 1: NotAttempted  
```

The saved first edit looks like this:

```json
{
  "op": "setAttribute",
  "target": {
    "locator": "#content > article:nth-of-type(1) > img",
    "fingerprint": {
      "tag": "img",
      "attrs": {
        "src": "/img/story-time.jpg"
      },
      "text": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
      "path": "sha256:e65c3a7e9beab0adae31be014068e912aae457e050b4d3adea04e09597f884de"
    },
    "anchor": {
      "start": 1035,
      "end": 1087,
      "bytes": "sha256:1a78698d2b635aade3ba3ba8c10bcae8d4452e3c395ca1826e08b66ebf3a34ec"
    }
  },
  "name": "alt",
  "value": "Children listening to a story",
  "meta": {"ticket":"WEB-212"}
}
```

How replay decided:

1. **Text changed.** Every byte offset after the edit moved, so the anchor no longer held. The locator still matched exactly one element, and its fingerprint matched, so both edits applied.
2. **Card inserted.** `article:nth-of-type(1)` now selects the new card. Its image has a different `src`, so the fingerprint didn’t match and the edit is `Stale`. A positional locator selects whatever sits at that position now, so a mismatch means stop, not guess. The second edit’s target moved the same way.
3. **Image removed.** The locator matches nothing, so the edit is `Stale`. The second edit resolved but is `NotAttempted`, because a transaction applies all of its edits or none.

Other outcomes are `Ambiguous` (several candidates), `Invalid` and `Failed`. `meta` is yours: the library stores it and hands it back as `MetaJson`, and never reads it. In memory it must be JSON-shaped: strings, numbers, booleans, `JsonElement`, string-keyed dictionaries or sequences of those. An anonymous object throws `ArgumentException`.

Writing is deterministic: the same edits give the same bytes on every platform. The full format and replay rules are in [Edit format](https://recursiveai.org/onehtml/docs/edit-format/), and how locators are chosen is in [Locators](https://recursiveai.org/onehtml/docs/locators/).
