d2lang/d2 · error

board %v cannot be modified through this file

Error message

board %v cannot be modified through this file

What it means

d2oracle.Create edits a board by locating its graph via GetBoardGraph(g, boardPath) and using the board's BaseAST (the original AST slice in the parent file) as the write target. When BaseAST is nil the board exists in the compiled graph but has no original AST node in the file (e.g. it was generated programmatically or is a root/imported board), so Create refuses to write edits 'through this file' and returns this error.

Source

Thrown at d2oracle/edit.go:46

	return "operation would modify AST outside of given scope"
}

func Create(g *d2graph.Graph, boardPath []string, key string) (_ *d2graph.Graph, newKey string, err error) {
	defer xdefer.Errorf(&err, "failed to create %#v", key)

	boardG := g
	baseAST := g.AST

	if len(boardPath) > 0 {
		// When compiling a nested board, we can read from boardG but only write to baseBoardG
		boardG = GetBoardGraph(g, boardPath)
		if boardG == nil {
			return nil, "", fmt.Errorf("board %v not found", boardPath)
		}
		// TODO beter name
		baseAST = boardG.BaseAST
		if baseAST == nil {
			return nil, "", fmt.Errorf("board %v cannot be modified through this file", boardPath)
		}
	}

	newKey, edge, err := generateUniqueKey(boardG, key, nil, nil)
	if err != nil {
		return nil, "", err
	}

	if edge {
		err = _set(boardG, baseAST, key, nil, nil)
	} else {
		err = _set(boardG, baseAST, newKey, nil, nil)
	}

	if len(boardPath) > 0 {
		replaced := ReplaceBoardNode(g.AST, baseAST, boardPath)
		if !replaced {
			return nil, "", fmt.Errorf("board %v AST not found", boardPath)

View on GitHub (pinned to 0d69dca6f5)

Solutions

  1. Ensure the target board is declared in the file whose AST g was compiled from (e.g. under layers:, scenarios:, or steps:) so its BaseAST is populated.
  2. Apply the edit to the graph that owns the board's file instead of routing it through g with a boardPath.
  3. Pass an empty boardPath and edit the root board if that is what you actually intend.
  4. Recompile the whole diagram from source after mutating the in-memory board, rather than using d2oracle for that board.

Example fix

// before
g2, _, err := d2oracle.Create(g, []string{"layers", "generated"}, "x") // BaseAST nil -> error
// after
boardG := d2oracle.GetBoardGraph(g, []string{"layers", "generated"})
if boardG.BaseAST == nil {
    // edit the owning graph's AST or recompile from serialized source instead
    g2, _, err = d2oracle.Create(g, nil, "x")
}
Defensive patterns

Strategy: validation

Validate before calling

func canEditBoard(g *d2graph.Graph, boardPath []string) bool {
    if len(boardPath) == 0 { return true }
    bg := d2oracle.GetBoardGraph(g, boardPath)
    return bg != nil && bg.BaseAST != nil
}

Type guard

func editableBoard(bg *d2graph.Graph) bool { return bg != nil && bg.BaseAST != nil }

Try / catch

g2, _, err := d2oracle.Create(g, boardPath, key)
if err != nil && strings.Contains(err.Error(), "cannot be modified through this file") {
    // fall back to editing the owning graph or recompiling from source
}

Prevention

When it happens

Trigger: Calling Create(g, boardPath, key) where boardPath resolves to a board whose graph has a nil BaseAST — typically a board created only in-memory, a board defined in a file other than the one backing g.AST, or a synthetic/imported board rather than one declared under layers/scenarios/steps in the current file.

Common situations: Editors or automations that build graphs programmatically then try to apply oracle edits to a nested board that was never parsed from the file; passing a boardPath pointing at a board whose source lives in a different .d2 file than the graph's root AST.

Related errors


AI-assisted analysis of d2lang/d2@0d69dca6f5 (2026-08-31). Data as JSON: /api/errors/cf49eb5f4ba6bf85. Report an issue: GitHub.