---
name: block-update
description: Update the data and pictures shown in copies (instances) of AI-ready Block components (cards, list rows, whole screens with lists) from a scenario such as "3 running shoes under $100". Use when the user asks to fill, change, populate or test components with new content. To make a component AI ready in the first place, use block-create instead.
---

# Block: update data

A Block is a small app, not a picture. You only give it the **source data**, such as the items in a cart, a product's name and price, or the selected category. The Block **works out everything that follows** by itself: totals and subtotals, counts ("3 Results"), badges, which button is disabled, which empty state or row shows, which chip is selected, and titles that follow a choice. That's its reactivity. Your job is to translate the user's scenario into source data, then show them what the Block did with it.

## Hard rules: follow exactly

- Use **at most two tool calls**: READ, then APPLY. If you already know the input keys from earlier in this chat, skip READ. The only extra calls allowed are for new pictures the user agreed to (see Pictures).
- **Selection checks.** Data changes only go on copies (instances) of AI-ready components. A frame, group or section can't take data itself; READ uses the AI-ready copies inside it at any depth (its `note` says which), or returns `stop` when there are none. Say the note in one line, then work on those copies as usual. READ returns `stop` when nothing attached can take data, and a `notReady` note on an instance whose component isn't AI ready. Tell the user that message in one or two lines and don't APPLY to it. When it comes with a `mainId`, ask "Want me to make it AI ready first?" On **yes**, switch to the block-create skill with that `mainId`.
- Copy each script **character for character**. Change only the lines marked `// EDIT`. Never write your own Figma code for this task.
- Take instance ids from the nodes the user attached in this chat, and put them in `IDS` (READ) and `UPDATES` (APPLY). Don't rely on the canvas selection; your code may not see it.
- Do **not** take screenshots, inspect layers, rename anything, or edit text layers directly. The scripts do all the work.
- Use **only** the input keys READ returns. Never invent keys, and never touch fields the user didn't mention.
- **Never try to set a value the Block works out.** Totals, counts, badges and states are not inputs, so APPLY rejects them as unknown. If the user says "make the total $50", change the source data instead (prices, quantities, items) so it adds up.
- Inputs that look like settings (`shippingFee`, `taxRate`, `currency`) are rules for the calculation. Change them only when the scenario calls for it, e.g. "free shipping" or "prices in euros".
- When the user doesn't give the content (names, brands, prices), make up realistic content that fits their scenario, and say it's sample content.
- **Pictures.** An `image` input's value is a Figma image hash; `""` keeps the design's picture.
  - **When do pictures change?** When the content becomes a different kind of thing (shoes → groceries, a new product in a row), the old pictures are now wrong. When only prices, quantities, settings or wording change, keep the pictures as they are.
  - **Where pictures come from, cheapest first:**
    1. **Pictures the user attached**: READ lists them in `attachedPictures`. Use `{ fromNode: "<id>" }`.
    2. **Images already in the file that show the new item**: READ lists them in `fileImages` as hash → layer name and size. Pictures made in earlier requests are there too. Use the hash only when the layer name says it's the same item (a "Banana" picture for a banana row). Never reuse a picture of something else just to fill a gap.
    3. **New pictures from your image tool** (a generate image tool). These cost credits and take a few seconds each, so they're opt-in:
       - Apply the data first, with `""` for the pictures you'd replace. Then ask once: "Want new photos for the 4 grocery items? It uses image credits." Count only pictures that change; a repeated item needs one.
       - If the user's request already asked for pictures ("with photos"), skip the question.
       - On **yes**, make at most **6** per request. If more are needed, make the first 6 and say how many are left. A small square picture (about 512 px) is enough.
       - Create each picture as a new layer, then run APPLY again with the same values plus `{ fromNode: "<new layer id>", remove: true }` for each picture. APPLY takes the picture and deletes the helper layer. Send whole lists again, unchanged apart from the pictures. Title it like "Photos added for 4 items".
    4. **No image tool, or the user said no:** leave `""`, and say plainly which pictures still show the old content, e.g. "The 4 photos still show shoes. Attach pictures, or swap them in the Block plugin."
  - Never write image hashes you made up. They don't exist in the file and are ignored.
- Edge cases are worth showing: an empty list (0 rows) or a sold-out item. The Block reacts to these too, for example by disabling checkout or showing an empty state.
- A `collection` input is a list: its value is an array of rows, and each row uses the `row` keys READ shows. The array length sets how many copies appear, so copies are added or removed to match.
- **Changing child items (cards, rows, list items) always goes through the parent's list input in APPLY.** Send the whole list: copy the rows you aren't changing unchanged, and change only what the user asked for.
- READ's `values` are what each copy **shows now**: inputs are read back from the canvas, so changes a designer typed by hand are included, and APPLY keeps them. Calculated parts (totals, counts) are never read back; the Block works them out again.
  - `editedOnCanvas` lists inputs someone changed by hand since the last update. Keep them as they are unless the user asks to change them, and mention them in one line: "Kept your edits to the title and price."
  - `readErrors` means the Block's script can't read some parts back (it reads text the input can't hold). Values still work; suggest re-running block-create on the main component to fix the reading.
- If READ shows `values: null`, or `notRead` with a `canvas`, some inputs can't be read from the design. Use its `canvas` to see what it shows now: `children` lists each child Block in reading order (top to bottom, then left to right), which is the same order as the list's rows, with its values or its visible text. Build every row from it, mapping each text to the matching `row` key, so nothing is invented or lost.
- If a script returns `error`, report it in one sentence and stop. Don't retry with your own code.
- Reply with **two short lines** per instance:
  - **Changed:** the data you set, from `changed`, e.g. `Cart: items → 4 rows, currency → EUR`.
  - **Block worked out:** the most telling entries from `reacted`, which lists what changed on the canvas because of the data (lists show how many copies are now shown), e.g. `Subtotal 14.25 → 27.75 · Checkout Button State Disable → Default · Bag Item Added false → true`. Skip entries that only repeat what you set. If `reacted` is empty, say nothing else changed.
  - Then, only when pictures should change, one more line: the photo question or the note from Pictures.

## READ: the selected Block instances and their inputs

```js
figma.skipInvisibleInstanceChildren = false; // hidden layers and hidden list copies must be visible to the scripts
const src = figma.root.getSharedPluginData("block", "engine");
if (!src) return { error: "This file has no Block engine yet. Open the Block plugin once in this file (Plugins > Block), then try again." };
new Function(src)();
const engine = globalThis.__blockEngine;
const IDS = ["<instance id>"]; // EDIT: ids of the instances the user attached or selected
const picked = [];
for (const id of IDS) { if (/^[\dI][\d:;I]*$/.test(id)) { const n = await figma.getNodeByIdAsync(id); if (n) picked.push(n); } }
const attachedPictures = (picked.length ? picked : figma.currentPage.selection).filter(n => n.type !== "INSTANCE" && "fills" in n && n.fills !== figma.mixed && n.fills.some(f => f.type === "IMAGE")).map(n => ({ fromNode: n.id, name: n.name }));
// Data goes on copies. A main component's data would change every copy, so it's stopped here.
const notes = [];
for (const n of (picked.length ? picked : figma.currentPage.selection)) {
  if (n.type !== "COMPONENT" && n.type !== "COMPONENT_SET") continue;
  const o = n.parent && n.parent.type === "COMPONENT_SET" ? n.parent : n;
  const ready = !!(o.getSharedPluginData("block", "script") || n.getSharedPluginData("block", "script"));
  notes.push(ready
    ? { stop: "“" + o.name + "” is the main component. Changing its data would change every copy, so attach a copy of it to fill it with data." }
    : { stop: "“" + o.name + "” isn't AI ready yet, so it has no data to change.", mainId: o.id, mainName: o.name });
}
let sel = (picked.length ? picked : figma.currentPage.selection).filter(n => n.type === "INSTANCE");
// A frame, group or section can't take data itself. The AI-ready copies inside it (at any depth) can:
// the outermost ones, since copies inside those are filled through their lists.
const isBlock = async ch => { const m = await ch.getMainComponentAsync(); const o = m && m.parent && m.parent.type === "COMPONENT_SET" ? m.parent : m; return !!(m && (m.getSharedPluginData("block", "script") || o.getSharedPluginData("block", "script"))); };
for (const n of (picked.length ? picked : figma.currentPage.selection)) {
  if (n.type === "INSTANCE" || n.type === "COMPONENT" || n.type === "COMPONENT_SET" || !("findAllWithCriteria" in n)) continue;
  const found = [], ids = {};
  let more = 0;
  for (const ch of n.findAllWithCriteria({ types: ["INSTANCE"] })) {
    let up = ch.parent, skip = false;
    for (; up && up !== n; up = up.parent) if (ids[up.id] || up.type === "COMPONENT" || up.type === "COMPONENT_SET") { skip = true; break; }
    if (skip || !(await isBlock(ch))) continue;
    if (found.length >= 30) { more++; continue; }
    found.push(ch); ids[ch.id] = true;
  }
  const kind = { FRAME: "frame", GROUP: "group", SECTION: "section" }[n.type] || "layer";
  if (!found.length) {
    if (!attachedPictures.some(p => p.fromNode === n.id)) notes.push({ stop: "“" + n.name + "” is a " + kind + ", not a copy of an AI-ready component, and has no AI-ready copies inside, so there's no data to change." });
    continue;
  }
  for (const ch of found) if (!sel.some(x => x.id === ch.id)) sel.push(ch);
  notes.push({ note: "“" + n.name + "” is a " + kind + ", so its " + found.length + " AI-ready " + (found.length === 1 ? "copy" : "copies") + " inside take the data: " + found.map(c => c.name).join(", ") + (more ? " (and " + more + " more; attach those separately)" : "") + "." });
}
if (!sel.length) return notes.length === 1 ? notes[0] : notes.length ? { stop: notes.map(x => x.stop).join(" "), notes } : { stop: "Nothing attached can take data. Attach one or more copies (instances) of an AI-ready component." };
{ let pg = sel[0]; while (pg.parent && pg.type !== "PAGE") pg = pg.parent; if (pg.type === "PAGE" && figma.currentPage !== pg) await figma.setCurrentPageAsync(pg); }
sel.sort((a, b) => a.absoluteTransform[1][2] - b.absoluteTransform[1][2] || a.absoluteTransform[0][2] - b.absoluteTransform[0][2]);
const registry = {};
// A duplicated component carries the original's key; the original says it owns it ("block" / "owner").
const owns = n => !!n && n.getSharedPluginData("block", "owner") === n.id;
function addBlock(k, node, s) { const p = registry[k]; if (!p || (!owns(p.node) && owns(node))) registry[k] = { node, script: s }; }
// Layers are found by their name in the main component, so a layer renamed in a copy still counts.
const nameFns = [];
const layerName = n => { for (const f of nameFns) { const v = f(n); if (v !== null) return v; } return null; };
for (const c of figma.currentPage.findAllWithCriteria({ types: ["COMPONENT", "COMPONENT_SET"] })) {
  const s = c.getSharedPluginData("block", "script"); if (!s) continue;
  if (c.type === "COMPONENT" && c.parent && c.parent.type === "COMPONENT_SET" && c.parent.getSharedPluginData("block", "script")) continue;
  const m = s.match(/key:\s*["']([^"']+)["']/); if (m) addBlock(m[1], c, s);
}
// Blocks inside the selected copies, from this file or a library, so a list finds its row Block.
await addCopies(sel);
if (engine.layerNames) for (const inst of sel) nameFns.push(await engine.layerNames(inst));
const evalCtx = { resolveBlock: k => registry[k] ? { script: registry[k].script } : null };
const readCtx = {
  layerName,
  getBlockValue(t) { const raw = t.getSharedPluginData("block", "values"); return raw ? JSON.parse(raw) : null; },
  getBlockValues(t) { const raw = t.getSharedPluginData("block", "values"); return raw ? JSON.parse(raw) : null; },
};
function describe(inputs, hooks) {
  const o = {};
  for (const k in inputs) {
    const d = inputs[k], h = hooks && hooks[k];
    const options = h && h.options ? "from the rows" : d.options ? d.options.map(x => x.value) : undefined; // "from the rows": a pick among a list's rows
    o[k] = { type: d.type, label: d.label, min: d.min, max: d.max, options, row: d.inputs ? describe(d.inputs, h && h.childHooks) : undefined };
  }
  return o;
}
const schemas = {};
const instances = [];
for (const inst of sel) {
  const main = await inst.getMainComponentAsync();
  const owner = main && main.parent && main.parent.type === "COMPONENT_SET" ? main.parent : main;
  const script = (main && main.getSharedPluginData("block", "script")) || (owner && owner.getSharedPluginData("block", "script"));
  if (!script) {
    if (!owner) { instances.push({ id: inst.id, notReady: "“" + inst.name + "” has lost its main component, so it can't take data." }); continue; }
    instances.push(main.remote || owner.remote
      ? { id: inst.id, notReady: "“" + inst.name + "” isn't AI ready. Its main component “" + owner.name + "” is in another library, so ask your design system manager to make it AI ready." }
      : { id: inst.id, notReady: "“" + inst.name + "” isn't AI ready. Its main component “" + owner.name + "” needs to be set up first.", mainId: owner.id, mainName: owner.name });
    continue;
  }
  if (!schemas[main.name]) {
    const r = engine.evaluateScript(script, evalCtx);
    if (!r.ok) return { error: main.name + ": " + r.error };
    schemas[main.name] = describe(r.definition.inputs, r.inputHooks);
  }
  const raw = inst.getSharedPluginData("block", "values");
  const saved = raw ? JSON.parse(raw) : null;
  if (engine.readValues) {
    // What the copy shows now. Inputs are read back from the canvas, so edits made by hand count.
    const now = engine.readValues(inst, engine.evaluateScript(script, evalCtx), readCtx);
    const entry = { id: inst.id, component: main.name, values: now.values };
    if (saved) {
      const edited = now.fromCanvas.filter(k => k in saved && JSON.stringify(saved[k]) !== JSON.stringify(now.values[k]));
      if (edited.length) entry.editedOnCanvas = edited;
    } else if (now.notRead.length) { entry.notRead = now.notRead; entry.canvas = await canvasOf(inst); }
    if (now.errors.length) entry.readErrors = now.errors;
    instances.push(entry);
  } else if (saved) {
    const keys = Object.keys(describe(engine.evaluateScript(script, evalCtx).definition.inputs)), values = {};
    for (const k of keys) if (k in saved) values[k] = saved[k];
    instances.push({ id: inst.id, component: main.name, values });
  } else instances.push({ id: inst.id, component: main.name, values: null, canvas: await canvasOf(inst) });
}
const fileImages = {};
for (const n of figma.currentPage.findAll(x => "fills" in x && x.fills !== figma.mixed && x.fills.some(f => f.type === "IMAGE"))) {
  for (const f of n.fills) if (f.type === "IMAGE" && f.imageHash && !fileImages[f.imageHash] && Object.keys(fileImages).length < 20) fileImages[f.imageHash] = n.name + " (" + Math.round(n.width) + " × " + Math.round(n.height) + ")";
}
return notes.length ? { schemas, instances, fileImages, attachedPictures, notes } : { schemas, instances, fileImages, attachedPictures };

async function addCopies(roots) {
  const seen = {};
  for (const root of roots) for (const ch of [root].concat(root.findAllWithCriteria({ types: ["INSTANCE"] }))) {
    const m = await ch.getMainComponentAsync(); if (!m || seen[m.id]) continue; seen[m.id] = true;
    const o = m.parent && m.parent.type === "COMPONENT_SET" ? m.parent : m;
    const s = o.getSharedPluginData("block", "script") || m.getSharedPluginData("block", "script");
    const km = s && s.match(/key:\s*["']([^"']+)["']/); if (km) addBlock(km[1], o, s);
  }
}

async function canvasOf(inst) {
  const texts = [], children = [];
  async function walk(n) {
    for (const ch of n.children) {
      if (!ch.visible) continue;
      if (ch.type === "TEXT") { if (texts.length < 20) texts.push(ch.name + ": " + ch.characters.slice(0, 40)); continue; }
      if (ch.type === "INSTANCE") {
        const m = await ch.getMainComponentAsync();
        const s = m && (m.getSharedPluginData("block", "script") || (m.parent && m.parent.type === "COMPONENT_SET" ? m.parent.getSharedPluginData("block", "script") : ""));
        const key = s && (s.match(/key:\s*["']([^"']+)["']/) || [])[1];
        if (key) {
          const v = ch.getSharedPluginData("block", "values");
          const at = { y: Math.round(ch.absoluteTransform[1][2]), x: Math.round(ch.absoluteTransform[0][2]) };
          children.push(v ? { blockKey: key, in: n.name, values: JSON.parse(v), at } : { blockKey: key, in: n.name, texts: ch.findAll(t => t.type === "TEXT" && t.visible).map(t => t.characters.slice(0, 40)), at });
          continue;
        }
      }
      if ("children" in ch) await walk(ch);
    }
  }
  await walk(inst);
  children.sort((a, b) => a.at.y - b.at.y || a.at.x - b.at.x);
  children.forEach(c => delete c.at);
  return { texts, children };
}
```

## APPLY: write the values

Fill `UPDATES` with one entry per instance in the order READ returned them, and `REQUEST` with the user's request in their own words. Give each update a short plain `title` saying what changed, like "Shipping fee set to €4.99" or "Bag filled with 3 running items"; the Block plugin shows it in the component's recent changes. Include only the inputs the user asked to change. Numbers must be numbers, toggles must be `true`/`false`, selects must be one of their options (a select with options `"from the rows"` picks one row of a list: use that row's naming value, such as its `flightNo`, from the rows you send, and if you change the list, pick a row that is still in it), dates are `"2026-04-05"` and times `"09:30"`. Any input can be set, including ones the Edit panel hides until they're needed; an optional part (a checked bag) is switched on with its boolean. A list input replaces the whole list, so include every row you want to keep.

```js
figma.skipInvisibleInstanceChildren = false; // hidden layers and hidden list copies must be visible to the scripts
const REQUEST = "<what the user asked, in their words>"; // EDIT
const UPDATES = [{ id: "<id>", title: "<what changed, in a few plain words>", values: { /* key: value */ } }]; // EDIT

{ let pg = await figma.getNodeByIdAsync(UPDATES[0].id); while (pg && pg.parent && pg.type !== "PAGE") pg = pg.parent; if (pg && pg.type === "PAGE" && figma.currentPage !== pg) await figma.setCurrentPageAsync(pg); }
new Function(figma.root.getSharedPluginData("block", "engine"))();
const engine = globalThis.__blockEngine;
const registry = {};
// A duplicated component carries the original's key; the original says it owns it ("block" / "owner").
const owns = n => !!n && n.getSharedPluginData("block", "owner") === n.id;
function addBlock(k, node, s) { const p = registry[k]; if (!p || (!owns(p.node) && owns(node))) registry[k] = { node, script: s }; }
// Layers are found by their name in the main component, so a layer renamed in a copy still counts.
const nameFns = [];
const layerName = n => { for (const f of nameFns) { const v = f(n); if (v !== null) return v; } return null; };
for (const c of figma.currentPage.findAllWithCriteria({ types: ["COMPONENT", "COMPONENT_SET"] })) {
  const s = c.getSharedPluginData("block", "script"); if (!s) continue;
  if (c.type === "COMPONENT" && c.parent && c.parent.type === "COMPONENT_SET" && c.parent.getSharedPluginData("block", "script")) continue;
  const m = s.match(/key:\s*["']([^"']+)["']/); if (m) addBlock(m[1], c, s);
}
// Blocks inside the copies being updated, from this file or a library, so a list finds its row Block.
{
  const seen = {};
  for (const u of UPDATES) {
    const root = await figma.getNodeByIdAsync(u.id); if (!root || root.type !== "INSTANCE") continue;
    if (engine.layerNames) nameFns.push(await engine.layerNames(root));
    for (const ch of [root].concat(root.findAllWithCriteria({ types: ["INSTANCE"] }))) {
      const m = await ch.getMainComponentAsync(); if (!m || seen[m.id]) continue; seen[m.id] = true;
      const o = m.parent && m.parent.type === "COMPONENT_SET" ? m.parent : m;
      const s = o.getSharedPluginData("block", "script") || m.getSharedPluginData("block", "script");
      const km = s && s.match(/key:\s*["']([^"']+)["']/); if (km) addBlock(km[1], o, s);
    }
  }
}
const evalCtx = { resolveBlock: k => registry[k] ? { script: registry[k].script } : null };
function evalFor(k) { const e = registry[k]; if (!e.r) e.r = engine.evaluateScript(e.script, evalCtx); return e.r; }
let logs = [];
const log = (l, a) => { if (l === "error") logs.push(JSON.stringify(a)); };
const ctx = {
  layerName,
  getBlockValue(t) { const raw = t.getSharedPluginData("block", "values"); return raw ? JSON.parse(raw) : null; },
  getBlockValues(t) { const raw = t.getSharedPluginData("block", "values"); return raw ? JSON.parse(raw) : null; },
  async setBlockValue(t, value, def) {
    const k = def && def.blockKey; if (!k || !registry[k]) { logs.push("unknown block " + k); return; }
    await engine.applyKnobs(t, evalFor(k), value, log, ctx);
    t.setSharedPluginData("block", "values", JSON.stringify(value));
  },
  async setBlockValues(host, rows, def) {
    let k = (def && def.blockKey) || host.getSharedPluginData("block", "key");
    if (!k) for (const ch of host.children) {
      if (ch.type !== "INSTANCE") continue;
      const m = await ch.getMainComponentAsync(); if (!m) continue;
      const o = m.parent && m.parent.type === "COMPONENT_SET" ? m.parent : m;
      const km = (o.getSharedPluginData("block", "script") || m.getSharedPluginData("block", "script")).match(/key:\s*["']([^"']+)["']/);
      if (km) { k = km[1]; break; }
    }
    const e = registry[k]; if (!e) { logs.push("no Block found for the list in " + host.name); return; }
    host.setSharedPluginData("block", "key", k);
    const comp = e.node.type === "COMPONENT_SET" ? e.node.defaultVariant : e.node;
    const existing = [];
    for (const ch of host.children) {
      if (ch.type !== "INSTANCE") continue;
      const m = await ch.getMainComponentAsync();
      if (m && (m.id === comp.id || (m.parent && m.parent.id === e.node.id))) existing.push(ch);
    }
    for (let i = 0; i < rows.length; i++) {
      let inst = existing[i];
      if (!inst) {
        inst = comp.createInstance();
        try { host.appendChild(inst); } catch (err) { inst.remove(); logs.push("only " + existing.length + " copies fit in " + host.name + " (not a slot), so " + (rows.length - existing.length) + " rows were not shown"); break; }
      }
      inst.visible = true;
      try { if (host.children[i].id !== inst.id) host.insertChild(i, inst); } catch (err) {}
      await engine.applyKnobs(inst, evalFor(k), rows[i], log, ctx);
      inst.setSharedPluginData("block", "values", JSON.stringify(rows[i]));
    }
    for (let i = rows.length; i < existing.length; i++) { try { existing[i].remove(); } catch (err) { existing[i].visible = false; } }
    host.setSharedPluginData("block", "values", JSON.stringify(rows));
  },
};

// What the canvas shows outside list rows: texts, hidden parts and variant/on-off states.
function snapshot(root) {
  const out = {};
  (function walk(n, inside) {
    for (const ch of n.children) {
      const rows = ch.getSharedPluginData("block", "values");
      if (rows && rows.charAt(0) === "[") { out[ch.id] = ch.name + ": " + ch.children.filter(c => c.type === "INSTANCE" && c.visible).length + " shown"; continue; }
      if (ch.type === "TEXT") out[ch.id] = (inside || ch.name) + ": " + (ch.visible ? ch.characters : "(hidden)");
      else if (!ch.visible) out[ch.id] = ch.name + ": hidden";
      if (ch.type === "INSTANCE") {
        try {
          const cp = ch.componentProperties;
          for (const k in cp) if (cp[k].type === "VARIANT" || cp[k].type === "BOOLEAN") out[ch.id + "|" + k] = ch.name + " " + k.split("#")[0] + ": " + cp[k].value;
        } catch (err) {}
      }
      if (ch.visible && "children" in ch) walk(ch, ch.type === "INSTANCE" ? ch.name : inside);
    }
  })(root, "");
  return out;
}

// An image value can be { fromNode: "<layer id>" }: the picture in that layer (one you generated, uploaded
// or the user attached). It becomes that picture's hash; with remove: true the helper layer is deleted after.
const helpers = [], pictureIssues = [];
async function resolvePictures(v) {
  if (Array.isArray(v)) { for (let i = 0; i < v.length; i++) v[i] = await resolvePictures(v[i]); return v; }
  if (!v || typeof v !== "object") return v;
  if (typeof v.fromNode === "string") {
    const n = await figma.getNodeByIdAsync(v.fromNode);
    const f = n && "fills" in n && n.fills !== figma.mixed ? n.fills.filter(p => p.type === "IMAGE" && p.imageHash).pop() : null;
    if (!f) { pictureIssues.push("no picture in layer " + v.fromNode); return ""; }
    if (v.remove) helpers.push(n);
    return f.imageHash;
  }
  for (const k in v) v[k] = await resolvePictures(v[k]);
  return v;
}

// Each change is logged on the Block's component, so the Block plugin can show what changed and when.
function addHistory(node, entry) {
  let list = [];
  try { list = JSON.parse(node.getSharedPluginData("block", "history") || "[]"); } catch (err) {}
  if (!Array.isArray(list)) list = [];
  list.push(entry);
  node.setSharedPluginData("block", "history", JSON.stringify(list.slice(-50)));
}
function shown(v) {
  if (v === undefined || v === null || v === "") return "—";
  if (Array.isArray(v)) return v.length + (v.length === 1 ? " row" : " rows");
  if (typeof v === "object") return JSON.stringify(v).slice(0, 80);
  return String(v).slice(0, 80);
}
function splitSnap(s) { const i = s.indexOf(": "); return i < 0 ? { label: s, value: "" } : { label: s.slice(0, i), value: s.slice(i + 2) }; }

const out = [];
for (const u of UPDATES) {
  u.values = await resolvePictures(u.values);
  const inst = await figma.getNodeByIdAsync(u.id);
  if (!inst || inst.type !== "INSTANCE") { out.push({ id: u.id, error: "not an instance" }); continue; }
  const main = await inst.getMainComponentAsync();
  const owner = main && main.parent && main.parent.type === "COMPONENT_SET" ? main.parent : main;
  const script = (main.getSharedPluginData("block", "script")) || (owner && owner.getSharedPluginData("block", "script")) || "";
  const r = engine.evaluateScript(script, evalCtx);
  if (!r.ok) { out.push({ id: u.id, error: r.error }); continue; }
  const unknown = Object.keys(u.values).filter(k => !(k in r.definition.inputs));
  if (unknown.length) { out.push({ id: u.id, error: "unknown inputs: " + unknown.join(", ") }); continue; }
  // Start from what the copy shows now, so edits made by hand on the canvas are kept.
  const raw = inst.getSharedPluginData("block", "values");
  const prev = engine.readValues ? engine.readValues(inst, r, ctx).values : raw ? JSON.parse(raw) : {};
  const values = {};
  for (const k in r.definition.inputs) if (k in u.values) values[k] = u.values[k]; else if (k in prev) values[k] = prev[k];
  logs = [];
  const before = snapshot(inst);
  await engine.applyKnobs(inst, r, values, log, ctx);
  inst.setSharedPluginData("block", "values", JSON.stringify(values));
  const after = snapshot(inst);
  const changed = {};
  for (const k in u.values) if (JSON.stringify(prev[k]) !== JSON.stringify(values[k])) changed[k] = Array.isArray(values[k]) ? values[k].length + " rows" : values[k];
  const reacted = [];
  for (const k in after) if (before[k] !== undefined && before[k] !== after[k] && reacted.length < 12) reacted.push(before[k] + "  →  " + after[k].split(": ").slice(1).join(": "));
  for (const k in before) if (!(k in after) && reacted.length < 12) reacted.push(before[k] + "  →  (hidden)");
  const res = { id: u.id, changed, reacted };
  if (logs.length) res.errors = logs;
  out.push(res);
  const keys = Object.keys(changed);
  if (keys.length) {
    const changes = keys.map(k => ({ key: k, label: r.definition.inputs[k].label || k, before: shown(prev[k]), after: shown(values[k]) }));
    const reactedChanges = [];
    for (const k in after) if (before[k] !== undefined && before[k] !== after[k] && reactedChanges.length < 12) {
      const b = splitSnap(before[k]), a = splitSnap(after[k]);
      reactedChanges.push({ label: b.label, before: b.value, after: a.value });
    }
    const logOn = main && main.getSharedPluginData("block", "script") ? main : owner;
    if (logOn) addHistory(logOn, {
      at: new Date().toISOString(), by: "agent", kind: "data",
      title: u.title || "Changed " + changes.map(c => c.label).join(", "),
      request: REQUEST || undefined, instance: { id: inst.id, name: inst.name },
      changes, reacted: reactedChanges.length ? reactedChanges : undefined,
    });
  }
}
for (const n of helpers) { try { n.remove(); } catch (err) {} }
return pictureIssues.length ? { results: out, pictureIssues, mutatedNodeIds: UPDATES.map(u => u.id) } : { results: out, mutatedNodeIds: UPDATES.map(u => u.id) };
```

Blocks are found on the current page and through the copies inside the selected instances, so copies of library Blocks work in any file.
