# How Workflow Layout Reconciliation Works After LLM-Authored Edits in Dograh

> Learn how Dograh reconciles workflow layouts after LLM edits. Discover the three-phase algorithm used to restore node positions before saving changes.

- Repository: [Dograh/dograh](https://github.com/dograh-hq/dograh)
- Tags: internals
- Published: 2026-05-18

---

**When an LLM edits a workflow, the parser drops all node coordinates, and the backend runs a three-phase layout reconciliation algorithm in [`api/services/workflow/layout.py`](https://github.com/dograh-hq/dograh/blob/main/api/services/workflow/layout.py) to restore positions from the previous version before persisting.**

The Dograh platform uses TypeScript-based workflow definitions that can be authored or modified by LLMs. Because generated diagrams often contain cramped or unreliable positioning data, the system deliberately strips coordinates during parsing and relies on **workflow layout reconciliation** to reconstruct sensible visual layouts. This process ensures visual continuity for users while preventing the "stacked at origin" problem common in AI-generated graphs.

## Why Node Coordinates Are Discarded

When an LLM generates or edits workflow code, the resulting JSON typically places nodes at arbitrary or overlapping coordinates. Rather than attempting to sanitize these potentially broken layouts, Dograh's parser removes positioning entirely. The responsibility for layout restoration falls to the backend's reconciliation service, which runs after parsing but before persistence in [`save_workflow.py`](https://github.com/dograh-hq/dograh/blob/main/save_workflow.py) or [`create_workflow.py`](https://github.com/dograh-hq/dograh/blob/main/create_workflow.py).

## The Three-Phase Reconciliation Algorithm

The reconciliation engine is implemented in **[`api/services/workflow/layout.py`](https://github.com/dograh-hq/dograh/blob/main/api/services/workflow/layout.py)** within the `reconcile_positions` function (lines 31-77). This function accepts the newly parsed workflow and the previous workflow JSON (draft or last released version), then executes a matching algorithm in three distinct phases.

### Phase 1: Build Lookup Tables from Previous State

First, the algorithm indexes all nodes from the previous workflow to create lookup tables for position restoration. Named nodes are indexed by a composite key of their **type** and **`data.name`**, while anonymous nodes are collected into **type-ordered lists** of positions.

```python

# From api/services/workflow/layout.py (lines 44-56)

for n in prev_nodes:
    t = n.get("type") or ""
    name = ((n.get("data") or {}).get("name") or "").strip()
    pos = n.get("position") or dict(_DEFAULT_POSITION)
    if name:
        named_positions[(t, name)] = pos
    else:
        unnamed_positions.setdefault(t, []).append(pos)

```

This dual-indexing strategy handles both explicitly named user nodes and system-generated nodes (such as automatic "start" or "end" nodes) that lack display names.

### Phase 2: Match New Nodes to Old Positions

The algorithm iterates through each node in the new workflow, attempting to restore its position using a prioritized matching strategy. It first attempts **named matching** using the `(type, name)` key; if the node lacks a name, it falls back to **nth-occurrence matching** using a per-type cursor.

```python

# From api/services/workflow/layout.py (lines 59-73)

for node in new_wf.get("nodes") or []:
    t = node.get("type") or ""
    name = ((node.get("data") or {}).get("name") or "").strip()
    pos = named_positions.get((t, name)) if name else None
    if pos is None:
        idx = unnamed_cursor.get(t, 0)
        positions = unnamed_positions.get(t, [])
        if idx < len(positions):
            pos = positions[idx]
            unnamed_cursor[t] = idx + 1
    if pos is not None:
        node["position"] = dict(pos)

```

This ensures that unchanged nodes retain their exact previous coordinates, preserving the user's carefully arranged layout even when the LLM modifies other parts of the workflow.

### Phase 3: Place Truly New Nodes

After the matching pass, any node still positioned at the origin `(0,0)` is considered *new*. The `_place_new_nodes` helper function (lines 78-104) positions these nodes relative to their first incoming edge using constants **`_NEW_NODE_DX = 400`** and **`_NEW_NODE_DY = 200`**, chosen to mimic the UI's dagre layout algorithm.

```python

# From api/services/workflow/layout.py (lines 78-104)

def _place_new_nodes(wf: dict[str, Any]) -> None:
    id_to_node = {n["id"]: n for n in wf.get("nodes") or []}
    edges = wf.get("edges") or []

    for node in wf.get("nodes") or []:
        pos = node.get("position") or {}
        if pos.get("x") or pos.get("y"):
            continue                # already positioned

        src_id = next((e["source"] for e in edges if e.get("target") == node["id"]), None)
        if src_id and src_id in id_to_node:
            src_pos = id_to_node[src_id].get("position") or dict(_DEFAULT_POSITION)
            node["position"] = {
                "x": float(src_pos.get("x", 0.0)) + _NEW_NODE_DX,
                "y": float(src_pos.get("y", 0.0)) + _NEW_NODE_DY,
            }

```

Orphan nodes with no incoming edges remain at `(0,0)`, where the UI will auto-layout them later using the dagre algorithm defined in `ui/src/app/workflow/[workflowId]/utils/layoutNodes.ts`.

## Integration with Persistence Flows

The reconciliation step is invoked in both draft saving and workflow creation paths. In [`api/mcp_server/tools/save_workflow.py`](https://github.com/dograh-hq/dograh/blob/main/api/mcp_server/tools/save_workflow.py) (lines 42-56), the `_previous_workflow_json` helper loads the existing draft or released version, then passes it along with the parsed payload to `reconcile_positions`.

```python

# From api/mcp_server/tools/save_workflow.py

payload = parsed["workflow"]
payload = reconcile_positions(
    payload,
    await _previous_workflow_json(workflow)   # previous version or draft

)

```

The same pattern appears in [`create_workflow.py`](https://github.com/dograh-hq/dograh/blob/main/create_workflow.py), ensuring that even newly created workflows benefit from position reconciliation if they are derived from templates or existing drafts.

## Validation and Persistence

After reconciliation, the modified payload undergoes Pydantic validation and graph rule checking. The resulting JSON contains a hybrid layout: **restored positions** for unchanged nodes and **sensible defaults** for added nodes. This reconciled structure is then persisted either as a **draft** via `save_workflow` or as a **published version** via `create_workflow`.

## Summary

- **workflow layout reconciliation** runs automatically after LLM-authored edits to restore visual continuity
- The algorithm uses **named keys** `(type, data.name)` and **type-ordered lists** to match nodes between versions
- New nodes receive offsets of **400px horizontally** and **200px vertically** from their first incoming neighbor
- The implementation resides in **[`api/services/workflow/layout.py`](https://github.com/dograh-hq/dograh/blob/main/api/services/workflow/layout.py)** and is invoked by **[`save_workflow.py`](https://github.com/dograh-hq/dograh/blob/main/save_workflow.py)** and **[`create_workflow.py`](https://github.com/dograh-hq/dograh/blob/main/create_workflow.py)**
- Orphan nodes remain at `(0,0)` for UI-side auto-layout, while matched nodes preserve their exact previous coordinates

## Frequently Asked Questions

### What happens if a node is renamed during an LLM edit?

If a node's name changes, the **named matching** phase fails to find the old position. The algorithm then attempts **nth-occurrence matching** based on node type. If the node was the third node of its type in the previous version, it receives the third stored position for that type, preserving approximate location even when identifiers change.

### Why strip coordinates instead of sanitizing the LLM output?

LLM-generated coordinates often overlap or place nodes outside the visible canvas. Sanitizing these arbitrary values requires complex heuristics that may still produce poor layouts. By stripping coordinates and running deterministic reconciliation, Dograh guarantees **predictable, user-friendly positioning** that respects the existing visual structure.

### How does the system handle entirely new workflows with no previous version?

When no previous workflow exists, `_previous_workflow_json` returns `None`, and `reconcile_positions` skips the lookup table construction. All nodes remain at `(0,0)` until the **placement phase** positions them relative to their edges, or the UI applies auto-layout when the user views the workflow.

### What are the `_NEW_NODE_DX` and `_NEW_NODE_DY` constants used for?

These constants define the **offset distances** (400px on the X-axis, 200px on the Y-axis) used when positioning brand-new nodes adjacent to their source nodes. These values approximate the spacing used by Dograh's dagre-based UI layout engine, ensuring that backend-placed nodes align visually with UI-generated layouts.