wp_patch_page
Surgically edit an Elementor page by element id — send ONLY the elements you're changing. Far cheaper than wp_update_page, which rewrites the WHOLE page. Preserves layout/ids; round-trip validated; write-gated. dry_run=true previews a real before/after diff without writing.
Two kinds of op: CONTENT: {node_id, field, new_value} — field comes from wp_get_page nodes. new_value may be a string OR an object, e.g. {"unit":"px","size":18}. STYLE: {node_id, style:{color, bg, radius, padding, margin, border, shadow, align}} — Xyle translates these into the widget's real Elementor keys, so you never guess. Call wp_style_keys(widget_type) first to see what a widget supports. Works on SECTION/CONTAINER nodes too (node_id of a container from wp_get_page outline): style:{bg, padding, radius, shadow} colors/pads the band with native container keys.
EFFECT CHECK: after applying, the response re-reads the stored page and marks each change effective:true/false (with effective_count / ineffective_count). Read those, not just applied:true — effective:false means the write persisted but the widget ignored that key; the stored value shows what actually landed. Fix by calling wp_widget_controls for the element's real key, then re-patch. (Verifies stored data, not final render — confirm visuals with render_screenshot.)
GUTENBERG / BLOCK pages (wp_get_page builder != elementor, ids like b0, b1.2): ops are {node_id, field:'attrs.<dotted.key>', new_value} to set one block attribute (e.g. attrs.data.headline on an ACF block, attrs.level on a heading; null deletes it) or {node_id, field:'html', new_value} to replace a block's inner markup. style ops are Elementor-only and are refused there. A parent's attrs and a child's html can change in one call; an html op on a parent plus any edit to its child is refused as a conflict.
Use wp_update_page only for full redesigns / adding new sections.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| ops | Yes | JSON array: [{node_id, field, new_value}] and/or [{node_id, style:{...}}]. Editor V4 atomic elements: use the prop name (title, paragraph, text, tag, link, image) with a bare value — XeoRank envelopes it ({$$type, value}); text/content aliases resolve. TABS: {node_id: <e-tabs id>, tabs: {labels: [...], active: 0, order: [...]}} relabels, adds, removes and reorders tabs keeping each panel's content. Every patch also types V4 text the Elementor editor showed blank; ops=[] does only that. | |
| dry_run | No | Preview the diff without writing (propose). Default false. | |
| post_id | Yes | The page/post ID | |
| site_id | Yes | The WordPress site ID |