NEVER Do in Signal Architecture
- NEVER use the legacy string-based
Object.connect() — Typos result in silent failures. Always use signal.connect(_callback) for compile-time validation.
- NEVER use signals to dictate behavior top-down — Signals are past-tense events (e.g., "died"). Use direct method calls for commands (e.g., "kill").
- NEVER connect a signal twice to the same Callable — This throws an
ERR_INVALID_PARAMETER at runtime unless using the Object.CONNECT_REFERENCE_COUNTED flag to stack connections.
- NEVER use a Global Signal Bus for local data — Pollutes global state and makes debugging harder. Use local connections for scene-specific logic.
- NEVER assume callbacks must accept all signal arguments — Use
unbind() to drop unwanted parameters and keep your API clean.
- NEVER create circular signal dependencies — A signals B, B signals back to A? Use a mediator (parent or AutoLoad) to break the loop.
- NEVER skip signal typing —
signal moved without types lacks editor support. Always use signal moved(dir: Vector2).
- NEVER forget to disconnect dynamic signals — Ghost connections cause "call on null instance" errors. Disconnect in
_exit_tree() or when retargeting (disconnect_ghost_signals.gd).
- NEVER emit signals with immediate side effects on the emitter — If
died.emit() calls queue_free(), listeners might fail to respond. Emit first.
- NEVER use signals for high-frequency data streams — Sending 1000+ signals/second (like per-particle updates) is inefficient. Use shared arrays or direct buffers.
Signal Up / Call Down
- Children → parents: past-tense signals (
health_changed, died).
- Parents → children: direct calls / properties (
apply_damage, play_anim).
- Siblings: parent mediator or carefully scoped Autoload bus — never sibling hard refs.
Use signals for: UI presses, death → game over, loot → inventory, cross-scene bus events.
Use direct calls for: parent commanding child, local property access.
Decision Tree: Where to Connect
| Scope |
Pattern |
MANDATORY script |
| Child notifies parent / UI |
Local signal.connect in parent _ready |
signal_up_call_down_pattern.gd |
| Parent orchestrates children |
Method calls down (not signals) |
same |
| Cross-scene / systems (achievements, save) |
Autoload bus |
global_signal_bus_router.gd / global_event_bus.gd |
| Linear async steps (load → fade → spawn) |
await signal sequence |
await_signal_sequencing.gd / complex_signal_sequencer.gd |
| Retarget tracking (new enemy) |
Disconnect old first |
disconnect_ghost_signals.gd |
| One-shot / physics-safe |
CONNECT_ONE_SHOT / CONNECT_DEFERRED |
one_shot_deferred_connections.gd |
| Extra context / drop args |
Callable.bind / unbind |
callable_bind_context.gd / unbind_unwanted_args.gd |
Available Scripts
- signal_up_call_down_pattern.gd — MANDATORY before hierarchy wiring.
- global_signal_bus_router.gd / global_event_bus.gd — MANDATORY before Autoload buses.
- disconnect_ghost_signals.gd — MANDATORY when switching tracked emitters.
- await_signal_sequencing.gd / complex_signal_sequencer.gd — MANDATORY for multi-step awaits.
- safe_dynamic_connections.gd —
is_connected guards.
- one_shot_deferred_connections.gd — one-shot / deferred flags.
- callable_bind_context.gd / unbind_unwanted_args.gd — bind/unbind.
- track_signal_emitter_source.gd —
CONNECT_APPEND_SOURCE_OBJECT.
- signal_debugger.gd / signal_spy.gd — debug / test spies.
Lambda Capture Cleanup (complete)
Godot auto-disconnects most connections when a node frees. Exception: lambdas that capture locals — you must disconnect manually.
var my_lambda: Callable
func _ready() -> void:
var x := 10
my_lambda = func(): print(x)
player.died.connect(my_lambda)
func _exit_tree() -> void:
if player and player.died.is_connected(my_lambda):
player.died.disconnect(my_lambda)
Prefer named methods or disconnect_ghost_signals.gd when retargeting.
CONNECT_REFERENCE_COUNTED — Correct Semantics
CONNECT_REFERENCE_COUNTED means multiple identical connects share one connection with a refcount (connect N times / disconnect N times). It is not "auto-cleanup when the emitter frees" and does not fix capturing-lambda leaks.
- Auto-cleanup on free: normal connections to Object methods (non-capturing) are cleared when either side is freed.
- Capturing lambdas: always manual
disconnect (see above).
- One-shot auto-remove after fire:
CONNECT_ONE_SHOT.
Deep recipes (on demand)
LLM-ignorance rule: if a general agent would not know it before reading, it lives here or in scripts/ — never delete, only move.
| Topic |
Reference |
| Patterns 1–7 + gotchas |
implementation-patterns.md |
Reference
Progressive disclosure: open Official Documentation links only when researching a specific API; load Related Skills when routing to a peer domain — do not preload the whole lattice.
Official Documentation
- Using signals — Core emit/connect model and why signals decouple nodes without hard references.
- Scene organization — Canonical “signal up, call down” ownership rules that keep parent→child command flows explicit.
- Instancing with signals — Emit from spawned scenes so parents/managers receive bullets, loot, and other products without fixed node paths.
- Autoloads versus regular nodes — When a global EventBus is justified vs when scene-local signal wiring is safer.
- Singletons (Autoload) — How to register a typed signal bus that survives scene changes.
- Signal — Typed
Signal API: emit, connect, is_connected, and disconnect helpers used throughout this skill.
- Callable —
bind() / unbind() for injecting or discarding callback context without wrapper lambdas.
- Object —
CONNECT_ONE_SHOT, CONNECT_DEFERRED, CONNECT_REFERENCE_COUNTED, and CONNECT_APPEND_SOURCE_OBJECT flags.
- GDScript basics — Typed
signal declarations and await on signals for linear async sequences.
- Using SceneTree — Connection lifetime across enter/exit tree and why dynamic listeners must disconnect when retargeting.
- Godot notifications — Safe connection timing relative to
_ready, parent caches, and user signals.
- Idle and Physics Processing — Why deferred signal handlers matter when callbacks mutate physics bodies mid-step.
Related Skills
Prerequisites
Complements
- godot-composition — Component nodes emit past-tense events; parents compose by connecting those signals and calling down.
- godot-scene-management — Scene swaps and loaders must reconnect or re-emit through buses without ghost listeners.
- godot-state-machine-advanced — State enter/exit often drives signal fan-out; keeps FSM transitions from becoming circular signal graphs.
- godot-resource-data-patterns — Prefer Resources for shared config; signals carry change events, not duplicated mutable state blobs.
- godot-testing-patterns —
watch_signals / spies pair with this skill’s emit contracts for unit and integration tests.
- godot-ui-containers — Buttons and menus should signal intent upward; controllers call down to update Control trees.
Downstream / consumers
- godot-dialogue-system — Line/choice completion events should follow signal-up orchestration into UI and quest listeners.
- godot-ability-system — Cooldown, cast, and hit payloads need typed signals so HUD/VFX stay decoupled from ability nodes.
- godot-combat-system — Damage/death/score chains are the classic signal-up fan-out into UI, audio, and progression.
- godot-performance-optimization — Escalate when high-frequency emit storms show up; replace per-tick signals with buffers or direct reads.
Master
- godot-master — Library router and mirrored module entry; open when discovering which Domain Skill owns a cross-cutting architecture concern.
1---2name: godot-signal-architecture3description: Expert blueprint for signal-driven architecture using "Signal Up, Call Down" pattern for loose coupling. Covers typed signals, signal chains, one-shot connections, and AutoLoad event buses. Use when implementing event systems OR decoupling nodes. Keywords signal, emit, connect, CONNECT_ONE_SHOT, CONNECT_REFERENCE_COUNTED, event bus, AutoLoad, decoupling.4---5
6## NEVER Do in Signal Architecture
7
8- **NEVER use the legacy string-based `Object.connect()`** — Typos result in silent failures. Always use `signal.connect(_callback)` for compile-time validation.
9- **NEVER use signals to dictate behavior top-down** — Signals are past-tense events (e.g., "died"). Use direct method calls for commands (e.g., "kill").
10- **NEVER connect a signal twice to the same Callable** — This throws an `ERR_INVALID_PARAMETER` at runtime unless using the `Object.CONNECT_REFERENCE_COUNTED` flag to stack connections.
11- **NEVER use a Global Signal Bus for local data** — Pollutes global state and makes debugging harder. Use local connections for scene-specific logic.
12- **NEVER assume callbacks must accept all signal arguments** — Use `unbind()` to drop unwanted parameters and keep your API clean.
13- **NEVER create circular signal dependencies** — A signals B, B signals back to A? Use a mediator (parent or AutoLoad) to break the loop.
14- **NEVER skip signal typing** — `signal moved` without types lacks editor support. Always use `signal moved(dir: Vector2)`.
15- **NEVER forget to disconnect dynamic signals** — Ghost connections cause "call on null instance" errors. Disconnect in `_exit_tree()` or when retargeting ([disconnect_ghost_signals.gd](scripts/disconnect_ghost_signals.gd)).
16- **NEVER emit signals with immediate side effects on the emitter** — If `died.emit()` calls `queue_free()`, listeners might fail to respond. Emit first.
17- **NEVER use signals for high-frequency data streams** — Sending 1000+ signals/second (like per-particle updates) is inefficient. Use shared arrays or direct buffers.
18
19---
20
21## Signal Up / Call Down
22
23- **Children → parents:** past-tense signals (`health_changed`, `died`).
24- **Parents → children:** direct calls / properties (`apply_damage`, `play_anim`).
25- **Siblings:** parent mediator or carefully scoped Autoload bus — never sibling hard refs.
26
27**Use signals for:** UI presses, death → game over, loot → inventory, cross-scene bus events.
28**Use direct calls for:** parent commanding child, local property access.
29
30## Decision Tree: Where to Connect
31
32| Scope | Pattern | MANDATORY script |
33|-------|---------|------------------|
34| Child notifies parent / UI | Local `signal.connect` in parent `_ready` | [signal_up_call_down_pattern.gd](scripts/signal_up_call_down_pattern.gd) |
35| Parent orchestrates children | Method calls down (not signals) | same |
36| Cross-scene / systems (achievements, save) | Autoload bus | [global_signal_bus_router.gd](scripts/global_signal_bus_router.gd) / [global_event_bus.gd](scripts/global_event_bus.gd) |
37| Linear async steps (load → fade → spawn) | `await` signal sequence | [await_signal_sequencing.gd](scripts/await_signal_sequencing.gd) / [complex_signal_sequencer.gd](scripts/complex_signal_sequencer.gd) |
38| Retarget tracking (new enemy) | Disconnect old first | [disconnect_ghost_signals.gd](scripts/disconnect_ghost_signals.gd) |
39| One-shot / physics-safe | `CONNECT_ONE_SHOT` / `CONNECT_DEFERRED` | [one_shot_deferred_connections.gd](scripts/one_shot_deferred_connections.gd) |
40| Extra context / drop args | `Callable.bind` / `unbind` | [callable_bind_context.gd](scripts/callable_bind_context.gd) / [unbind_unwanted_args.gd](scripts/unbind_unwanted_args.gd) |
41
42## Available Scripts
43
44- [signal_up_call_down_pattern.gd](scripts/signal_up_call_down_pattern.gd) — **MANDATORY** before hierarchy wiring.
45- [global_signal_bus_router.gd](scripts/global_signal_bus_router.gd) / [global_event_bus.gd](scripts/global_event_bus.gd) — **MANDATORY** before Autoload buses.
46- [disconnect_ghost_signals.gd](scripts/disconnect_ghost_signals.gd) — **MANDATORY** when switching tracked emitters.
47- [await_signal_sequencing.gd](scripts/await_signal_sequencing.gd) / [complex_signal_sequencer.gd](scripts/complex_signal_sequencer.gd) — **MANDATORY** for multi-step awaits.
48- [safe_dynamic_connections.gd](scripts/safe_dynamic_connections.gd) — `is_connected` guards.
49- [one_shot_deferred_connections.gd](scripts/one_shot_deferred_connections.gd) — one-shot / deferred flags.
50- [callable_bind_context.gd](scripts/callable_bind_context.gd) / [unbind_unwanted_args.gd](scripts/unbind_unwanted_args.gd) — bind/unbind.
51- [track_signal_emitter_source.gd](scripts/track_signal_emitter_source.gd) — `CONNECT_APPEND_SOURCE_OBJECT`.
52- [signal_debugger.gd](scripts/signal_debugger.gd) / [signal_spy.gd](scripts/signal_spy.gd) — debug / test spies.
53
54## Lambda Capture Cleanup (complete)
55
56Godot auto-disconnects most connections when a node frees. **Exception:** lambdas that capture locals — you must disconnect manually.
57
58```gdscript
59var my_lambda: Callable
60
61func _ready() -> void:
62 var x := 10
63 my_lambda = func(): print(x)
64 player.died.connect(my_lambda)
65
66func _exit_tree() -> void:
67 if player and player.died.is_connected(my_lambda):
68 player.died.disconnect(my_lambda)
69```
70
71Prefer named methods or [disconnect_ghost_signals.gd](scripts/disconnect_ghost_signals.gd) when retargeting.
72
73## CONNECT_REFERENCE_COUNTED — Correct Semantics
74
75`CONNECT_REFERENCE_COUNTED` means **multiple identical connects share one connection with a refcount** (connect N times / disconnect N times). It is **not** "auto-cleanup when the emitter frees" and does **not** fix capturing-lambda leaks.
76
77- Auto-cleanup on free: normal connections to Object methods (non-capturing) are cleared when either side is freed.
78- Capturing lambdas: always manual `disconnect` (see above).
79- One-shot auto-remove after fire: `CONNECT_ONE_SHOT`.
80
81## Deep recipes (on demand)
82
83> LLM-ignorance rule: if a general agent would not know it before reading, it lives here or in `scripts/` — never delete, only move.
84
85| Topic | Reference |
86|-------|-----------|
87| Patterns 1–7 + gotchas | [implementation-patterns.md](references/implementation-patterns.md) |
88
89## Reference
90
91> Progressive disclosure: open Official Documentation links only when researching a specific API; load Related Skills when routing to a peer domain — do not preload the whole lattice.
92
93### Official Documentation
94- [Using signals](https://docs.godotengine.org/en/stable/getting_started/step_by_step/signals.html) — Core emit/connect model and why signals decouple nodes without hard references.
95- [Scene organization](https://docs.godotengine.org/en/stable/tutorials/best_practices/scene_organization.html) — Canonical “signal up, call down” ownership rules that keep parent→child command flows explicit.
96- [Instancing with signals](https://docs.godotengine.org/en/stable/tutorials/scripting/instancing_with_signals.html) — Emit from spawned scenes so parents/managers receive bullets, loot, and other products without fixed node paths.
97- [Autoloads versus regular nodes](https://docs.godotengine.org/en/stable/tutorials/best_practices/autoloads_versus_regular_nodes.html) — When a global EventBus is justified vs when scene-local signal wiring is safer.
98- [Singletons (Autoload)](https://docs.godotengine.org/en/stable/tutorials/scripting/singletons_autoload.html) — How to register a typed signal bus that survives scene changes.
99- [Signal](https://docs.godotengine.org/en/stable/classes/class_signal.html) — Typed `Signal` API: `emit`, `connect`, `is_connected`, and disconnect helpers used throughout this skill.
100- [Callable](https://docs.godotengine.org/en/stable/classes/class_callable.html) — `bind()` / `unbind()` for injecting or discarding callback context without wrapper lambdas.
101- [Object](https://docs.godotengine.org/en/stable/classes/class_object.html) — `CONNECT_ONE_SHOT`, `CONNECT_DEFERRED`, `CONNECT_REFERENCE_COUNTED`, and `CONNECT_APPEND_SOURCE_OBJECT` flags.
102- [GDScript basics](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_basics.html) — Typed `signal` declarations and `await` on signals for linear async sequences.
103- [Using SceneTree](https://docs.godotengine.org/en/stable/tutorials/scripting/scene_tree.html) — Connection lifetime across enter/exit tree and why dynamic listeners must disconnect when retargeting.
104- [Godot notifications](https://docs.godotengine.org/en/stable/tutorials/best_practices/godot_notifications.html) — Safe connection timing relative to `_ready`, parent caches, and user signals.
105- [Idle and Physics Processing](https://docs.godotengine.org/en/stable/tutorials/scripting/idle_and_physics_processing.html) — Why deferred signal handlers matter when callbacks mutate physics bodies mid-step.
106
107### Related Skills
108
109#### Prerequisites
110- [godot-project-foundations](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-project-foundations/SKILL.md) — Project layout, Autoload registration, and scene ownership conventions signals plug into.
111- [godot-gdscript-mastery](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-gdscript-mastery/SKILL.md) — Typed Callables, `await`, and signal syntax required before advanced connect flags and sequencers.
112- [godot-autoload-architecture](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-autoload-architecture/SKILL.md) — Singleton boot order and ownership rules for global EventBus routers (not for local scene events).
113
114#### Complements
115- [godot-composition](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-composition/SKILL.md) — Component nodes emit past-tense events; parents compose by connecting those signals and calling down.
116- [godot-scene-management](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-scene-management/SKILL.md) — Scene swaps and loaders must reconnect or re-emit through buses without ghost listeners.
117- [godot-state-machine-advanced](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-state-machine-advanced/SKILL.md) — State enter/exit often drives signal fan-out; keeps FSM transitions from becoming circular signal graphs.
118- [godot-resource-data-patterns](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-resource-data-patterns/SKILL.md) — Prefer Resources for shared config; signals carry change events, not duplicated mutable state blobs.
119- [godot-testing-patterns](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-testing-patterns/SKILL.md) — `watch_signals` / spies pair with this skill’s emit contracts for unit and integration tests.
120- [godot-ui-containers](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-ui-containers/SKILL.md) — Buttons and menus should signal intent upward; controllers call down to update Control trees.
121
122#### Downstream / consumers
123- [godot-dialogue-system](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-dialogue-system/SKILL.md) — Line/choice completion events should follow signal-up orchestration into UI and quest listeners.
124- [godot-ability-system](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-ability-system/SKILL.md) — Cooldown, cast, and hit payloads need typed signals so HUD/VFX stay decoupled from ability nodes.
125- [godot-combat-system](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-combat-system/SKILL.md) — Damage/death/score chains are the classic signal-up fan-out into UI, audio, and progression.
126- [godot-performance-optimization](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-performance-optimization/SKILL.md) — Escalate when high-frequency emit storms show up; replace per-tick signals with buffers or direct reads.
127
128#### Master
129- [godot-master](https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-master/SKILL.md) — Library router and mirrored module entry; open when discovering which Domain Skill owns a cross-cutting architecture concern.