scene
Manipulate Godot scene trees and nodes via the AgentBridge execute tool — use when adding, removing, or inspecting nodes programmatically.
Files1
View on GitHubSKILL.mdskills/scene/SKILL.md
GitHubGodot Scene Manipulation
You are an expert Godot editor automation engineer who manipulates live scene trees through AgentBridge GDScript execution.
GDEXEC="bash ${CLAUDE_SKILL_DIR}/../runner/scripts/godot-exec.sh"
Overview
Use $GDEXEC --execute to run GDScript inside the live Godot editor process via AgentBridge. All scene manipulations happen in the currently open scene. Changes are not persisted until you explicitly save. Always assign node.owner = root for any node that must be serialized to disk.
Common Patterns
Get the edited scene root:
$GDEXEC --execute 'print(EditorInterface.get_edited_scene_root().name)'
Add a node to the scene:
$GDEXEC --execute - <<'GD'
var root = EditorInterface.get_edited_scene_root()
var node = MeshInstance3D.new()
node.name = "MyMesh"
root.add_child(node, true)
node.owner = root # required for serialization
GD
Find a node by name:
$GDEXEC --execute - <<'GD'
var root = EditorInterface.get_edited_scene_root()
var player = root.get_node_or_null("Player")
if player:
print("Found: ", player.get_class())
GD
Remove a node:
$GDEXEC --execute - <<'GD'
var root = EditorInterface.get_edited_scene_root()
var node = root.get_node_or_null("OldNode")
if node:
node.queue_free()
GD
Set a node property:
$GDEXEC --execute - <<'GD'
var root = EditorInterface.get_edited_scene_root()
var mesh: MeshInstance3D = root.get_node("MyMesh")
mesh.position = Vector3(0, 1, 0)
GD
Query node metadata:
$GDEXEC --execute - <<'GD'
var root = EditorInterface.get_edited_scene_root()
var node = root.get_node("Player")
print("Meta keys: ", node.get_meta_list())
if node.has_meta("team"):
print("team = ", node.get_meta("team"))
GD
Check if a node is queued for deletion before accessing it:
$GDEXEC --execute - <<'GD'
var root = EditorInterface.get_edited_scene_root()
var node = root.get_node_or_null("Temporary")
if node and not node.is_queued_for_deletion():
print("Safe to access: ", node.name)
GD
Select a node in the Scene panel:
$GDEXEC --execute - <<'GD'
var root = EditorInterface.get_edited_scene_root()
var node = root.get_node("Player")
EditorInterface.select_node(node)
GD
Bulk Operations
Set a property on all children of a given type:
$GDEXEC --execute - <<'GD'
var root = EditorInterface.get_edited_scene_root()
for child in root.get_children():
if child is Light3D:
child.light_energy = 2.0
print("Updated: ", child.name)
GD
Rename all nodes matching a prefix:
$GDEXEC --execute - <<'GD'
var root = EditorInterface.get_edited_scene_root()
var index = 0
for child in root.get_children():
if child.name.begins_with("Enemy"):
child.name = "Enemy_%02d" % index
index += 1
GD
Recursively collect all nodes of a type:
$GDEXEC --execute - <<'GD'
func collect(node: Node, type: String, result: Array) -> void:
if node.get_class() == type:
result.append(node)
for child in node.get_children():
collect(child, type, result)
var root = EditorInterface.get_edited_scene_root()
var meshes = []
collect(root, "MeshInstance3D", meshes)
print("Found %d MeshInstance3D nodes" % meshes.size())
GD
Resource Operations
Load a PackedScene and instantiate it into the scene:
$GDEXEC --execute - <<'GD'
var root = EditorInterface.get_edited_scene_root()
var packed: PackedScene = load("res://scenes/enemy.tscn")
var instance = packed.instantiate()
instance.name = "Enemy"
root.add_child(instance, true)
instance.owner = root
GD
Set an exported Resource property on a node:
$GDEXEC --execute - <<'GD'
var root = EditorInterface.get_edited_scene_root()
var node = root.get_node("Terrain")
var mat: StandardMaterial3D = load("res://materials/ground.tres")
node.set("material_override", mat)
GD
Scan the filesystem after creating files programmatically:
$GDEXEC --execute 'EditorInterface.get_resource_filesystem().scan()'
Call this any time you create or modify files on disk outside of the editor (e.g., via a shell script) so the editor's FileSystem dock picks them up immediately.
Editor Operations
Save the current scene (active scene only):
$GDEXEC --execute 'EditorInterface.save_scene()'
Use EditorInterface.save_scene() to save only the currently edited scene. Use --save-all to flush every open and dirty resource to disk.
Save all open resources:
$GDEXEC --save-all
Perform an undoable operation with EditorUndoRedoManager:
$GDEXEC --execute - <<'GD'
var root = EditorInterface.get_edited_scene_root()
var node = root.get_node("Player")
var undo_redo = EditorInterface.get_editor_undo_redo()
undo_redo.create_action("Move Player")
undo_redo.add_do_method(node, "set", "position", Vector3(5, 0, 0))
undo_redo.add_undo_method(node, "set", "position", node.position)
undo_redo.commit_action()
GD
create_action names the entry in Edit > Undo history. Always pair every add_do_method with a corresponding add_undo_method so Ctrl+Z works correctly.
Common Pitfalls
- Missing
node.owner = root— nodes added viaadd_child()without settingownerare invisible to the scene serializer and will silently disappear on save/reload. Setownerrecursively for instantiated subscenes. queue_free()is deferred — the node is not freed until the end of the current frame. Accessing it immediately after callingqueue_free()is safe, but guard withis_queued_for_deletion()if another code path may have already freed it.load()returnsnullon a bad path — always check the return value. Typos inres://paths fail silently if you skip the null check, causing a null dereference crash on the next property access.EditorInterface.save_scene()only saves the root scene — it does not save modified sub-resources or inherited scenes that live in separate files. UseResourceSaver.save()explicitly for those, or call--save-allto catch everything.- Filesystem not refreshed after external file writes — if you create
.tscn/.tres/.gdfiles from a shell script, callEditorInterface.get_resource_filesystem().scan()afterwards or the editor will not see them andload()will returnnull. get_node()throws on missing paths — preferget_node_or_null()and check the result;get_node()crashes the script on an invalid NodePath, which silently aborts the entire--executeinvocation.