description: Symptom-first fixes for the common ways Logic Driver Assist fails, grouped by where the failure happens: setup and connection, calling over a bridge, authoring the graph, running the machine, and capturing screenshots, plus known gaps on the Monolith surface.¶
Troubleshooting
¶
When Assist misbehaves, the failure almost always lands in one of five places: the plugin did not load, the transport did not carry the call, the graph was authored in a way that does not run, the machine was never started, or a capture ran without a live editor. This page lists the symptoms you actually see and the fix for each, plus a short list of gaps on the Monolith surface.
Start here
Run LDAssist.List in the editor console first. If it lists operations, the plugin is loaded and any problem is in the transport or the call, not the plugin itself, so skip to Calling over a bridge. If it lists nothing, fix that first; the sections below start there.
Setup and connection¶
The console lists no operations¶
LDAssist.List prints nothing, or the console reports an unknown command. The SMAssist module did not load. Assist is a C++ editor plugin, so it must be compiled, not enabled from a binary install. Enable SMAssist in the host .uproject, build the Development Editor target, relaunch, and re-run LDAssist.List. If the build itself failed, the plugin cannot register anything, so a clean build is the real fix. See Setup.
An MCP client sees no ld.* tools¶
The console lists operations but your MCP client does not. The registry is up; the transport is the problem. Two usual causes: the transport is inert (each adapter module always compiles, but the Monolith bridge is a no-op unless Plugins/Monolith/ is present at build time, and the ToolsetRegistry adapter is a no-op unless the engine ships ToolsetRegistry, which means UE 5.8+ with its ModelContextProtocol plugin enabled to be reachable), or the client points at the wrong URL. Confirm the port matches the transport: http://localhost:9316/mcp for Monolith, http://localhost:8000/mcp for the engine server. See Transports & Bridges.
The plugin fails to load after an engine upgrade¶
After moving the project to a different engine version, the editor reports that SMAssist is incompatible or was built with a different engine. The compiled binaries and Intermediate/ are stamped for the old engine. Delete the stale build products and rebuild the editor target against the new engine, then relaunch.
Two editors collide on one bridge port¶
You run more than one editor at once and only one bridge is reachable, or the second editor fails to bind. Both are trying to serve the same port. Give each editor its own: for Monolith, set ServerPort= under [/Script/MonolithCore.MonolithSettings] in Config/DefaultMonolith.ini; for the engine server, launch with -ModelContextProtocolPort=N.
Calling over a bridge¶
Monolith: calls fail as ld.ld.<action>¶
An operation errors with a doubled namespace like ld.ld.add_state. On the Monolith bridge, the action field takes the bare action name; the ld namespace is already the tool. Pass "action":"add_state", not "action":"ld.add_state". See Transports & Bridges.
Monolith: array or object arguments are ignored¶
A call that takes an array or a nested object silently drops it, or reports something like "array is required and must not be empty," while scalar arguments go through. The arguments were scattered as top-level siblings. Monolith's <namespace>_query tool takes exactly action plus a single params object; nest every argument under params so nested arrays and objects survive the round trip. Scalars survive either way, which is why the failure looks selective.
Monolith: the result is a JSON string, not an object¶
The reply parses to a string where you expected the operation's payload. Monolith double-wraps the result: the payload arrives as a JSON string inside result.content[0].text. Parse that inner string again to get the operation's JSON. See Transports & Bridges.
The agent keeps reaching for logicdriver.* instead of ld.*¶
On the Monolith bridge the client sees both the ld.* surface (Assist) and Monolith's native logicdriver.* surface, and the agent picks the wrong one. Prefer ld.*: it is maintained alongside the plugin. Either silence the native surface with bEnableLogicDriver=false under [/Script/MonolithCore.MonolithSettings], or, if you keep both live, add a name-level rule to the agent's instructions. This overlap is specific to Monolith. See Authoring with an AI Agent.
ToolsetRegistry: a call is rejected for a field you meant to leave default¶
On the UE 5.8+ engine transport, a call fails validation for a missing field even though the C++ operation has a default for it. Every parameter is required at the schema layer on this transport. Pass a sentinel to mean "use the default": an empty string, -1, or -1.0, depending on the field's type. The marshaling layer strips sentinels before the operation runs. See Transports & Bridges.
ToolsetRegistry: an asset path is rejected¶
A call fails because the asset path is not accepted. The engine transport requires the full object path (/Game/Path/SM_Foo.SM_Foo), not the bare package path (/Game/Path/SM_Foo). Use the asset_path that create_blueprint and get_asset return, and thread it forward rather than rebuilding it. This is the right habit on every transport; the engine transport is the one that enforces it.
Ultimate Engine Co-Pilot: the ld_* tools are missing¶
The console lists operations, but an agent driving UECP sees no ld_* tools. Have the agent call get_logic_driver_status; its message says whether Assist was detected and what is missing. The usual causes are the Logic Driver extension still disabled (it ships disabled; enable it under Settings > Extensions), LogicDriver-Assist not in the project's Plugins/ folder, or the editor not restarted after enabling the extension, since the bridge discovers operations at startup. See Transports & Bridges.
The editor stops responding after a raw HTTP call¶
You drive an HTTP transport with hand-written one-shot requests (curl, urllib) and the editor hangs or the next call never returns. Drive the HTTP transports with a real MCP client, or the MCP SDK, which holds one connection open for the session rather than reopening a socket per call. Most MCP clients (Claude Code, Cursor, Cline, and others) already do this; the failure mode is specific to naive per-call HTTP scripting.
Authoring the graph¶
A transition never fires¶
The machine compiles but sits in the source state forever. A transition with no authored condition and no transition class evaluates to false. Make it fire one of three ways: set it default-true with ld.set_transition_condition for an unconditional edge, assign a transition class, or author the transition graph inline (a TimeInState read into a comparison into the result pin). Note that a wired result pin and ld.set_transition_condition are mutually exclusive: once the pin is wired, the constant default is ignored. See Authoring with an AI Agent.
The machine compiles but has no active state¶
ld.compile reports clean, but at runtime the machine has no active state. There is no initial state. Exactly one state must be marked the entry state and connected from Entry: pass is_entry=true on ld.add_state, or call ld.set_initial_state. Without one, the class builds but has nowhere to start.
A node was added but does nothing¶
A state, conduit, reference, or link state exists in the graph but never participates. It is an orphan; nothing is wired to or from it. Wire every node into the flow in the same step you add it. An always-true entry gate is better modeled as an empty state than as a conduit.
configure_sm_component_on_actor reports no component¶
The call fails with "No SCS component named ..." The operation configures a USMStateMachineComponent that already exists on the actor blueprint; it does not add one. Add the component to the actor blueprint in the editor first (by hand or with generic engine tools, since no ld.* operation adds a component), then pass its component variable name as component_name. If the named component exists but is a different class, the error instead names the template class it found. See Authoring with an AI Agent.
The layout overlaps, or a new state hides under another¶
ld.layout_states ran but states overlap, or a state added by hand sits underneath an existing one. Check the layout result before anything else. A non-empty measurement_warnings names a part of the graph that could not be measured, whose nodes were spaced against sizes that read too small. The per-graph warnings list pinned nodes the layout had to flow around. For a state placed by hand, the usual cause is size: a state that displays property widgets (dialogue lines, choice labels) is several times taller and wider than a bare one, so spacing that fits bare states buries it. Find the colliding pairs with ld.get_graph_view, whose overlaps array lists them, then either re-run the layout or fix positions with ld.set_node_property (NodePosX/NodePosY).
The layout ran but nothing moved¶
ld.layout_states with apply=true reports success, but the graph is unchanged and the result shows declined: true for it. By default the layout leaves a graph alone when the computed arrangement would not beat the current one on overlapping nodes, transitions drawn through a state, markers drawn on a state, and reroutes needed, in that order. The warning on that graph gives both sets of numbers. Pass only_if_improved=false to apply the computed layout regardless. See Layout.
A transition's marker sits on top of a state, or two markers overlap¶
A transition is drawn through a state it does not connect, its marker lands on a state box, or two transition markers stack on each other so one cannot be clicked. The first two are what the layout's rails exist to prevent, so confirm route_edges was not passed as false and read edges_through_states and markers_over_states in the layout result; both should be 0. Stacked markers are reported in transition_overlaps from ld.get_graph_view, and state spacing never separates them; add a reroute with ld.add_transition_reroute to move one aside, then re-read the array.
spawn_local_graph_event_node refuses OnStateUpdate or OnStateEnd¶
The call fails with "cannot be spawned: every state graph is created with On State Begin, On State Update, and On State End already in it, and they cannot be deleted." Nothing needs creating. Call ld.get_local_graph on the state, find the node whose title matches, and wire from its exec pin; the node draws dimmed until something is wired into it. The spawner is only for entries that are absent until spawned, such as OnInitialized or OnTransitionEntered. See Where entry and start logic live.
get_asset does not list the states inside a nested state machine¶
The asset has a nested state machine, but ld.get_asset shows it as one state_machine_state entry and none of the states inside it. That is the default root scope. Pass scope="all" to walk every nesting level; each state and transition then carries graph_path and parent_state_guid so the levels can be told apart. ld.get_local_graph on the container's guid enumerates its contents too. See Nested state machines.
A new node landed in the root graph instead of the nested one¶
ld.add_state (or another node-adding operation) succeeded, but the node appeared at the top level rather than inside the nested state machine you meant. Pass the container's guid as parent_state_guid. A node with a reference already assigned is rejected as a parent, because its states live in the referenced blueprint; open that asset and target it directly.
set_node_property refuses a property that exists¶
The write fails with "is deprecated and writing it changes nothing." Reflection still resolves a deprecated property under its name without the _DEPRECATED suffix, but nothing reads it back, so ld.set_node_property and ld.reset_node_property refuse it. Call ld.get_node_properties to list the properties the node supports and write the current one. Two related refusals are worth knowing. A property_path is relative to property_name and must not repeat it: pass property_name="Tuning" and property_path="Close", never "Tuning.Close". And ld.split_pin on a property that is already split fails rather than resetting every sub-pin to the class default; write sub-pin values with property_path instead.
add_transition rejects a guid as a reroute node¶
The call fails with "is a transition reroute node and cannot be a transition endpoint." ld.get_graph_view returns reroute guids beside state guids, so a guid picked from its node list can be a waypoint rather than a state. A reroute belongs to a transition that already exists; add one with ld.add_transition_reroute, and pass state, conduit, reference, link state, or Any State guids as transition endpoints.
A cross-blueprint node bound to the wrong class¶
A call node shows the wrong pin names, a compile reports "function not found" on a class that has the function, or a connect fails with an object-compatibility error between two types with the same name. A bare class name in a target_class, cast_class, or variable-type token resolves by global search, and it bound a same-named class from another folder. Pass the full object path form (/Game/Path/BP_Foo.BP_Foo_C) wherever an operation accepts a class token, exactly as asset arguments take full object paths. Nothing fails at bind time, so when in doubt, read a node's title back after adding it.
A variable node for another blueprint's variable comes up dead¶
A variable get or set pointed at another blueprint's variable is created without pins (or with only exec pins) and does nothing. Monolith's blueprint.add_node with VariableGet or VariableSet binds only the blueprint being edited; an external member does not resolve, and the node is created broken rather than rejected. Monolith 0.21.3 has blueprint.add_property_access, which binds the external class and returns the value and target pins to wire. On an older build, expose a small getter or setter function on the owning blueprint and call that instead.
Known gaps on the Monolith surface¶
The generic blueprint.*, ui.*, animation.*, and editor.* actions belong to Monolith, not to Assist, and each Monolith release closes some of these gaps. This list was checked against Monolith 0.21.3. If your build is newer, try the direct action before the workaround. Each entry says what an agent does instead.
- No input event node.
blueprint.add_nodehas no node type for a key or action event. PollWasInputKeyJustPressedon tick instead; it is edge-triggered, so a held key does not repeat. - A Return node added to a function has no return pins.
blueprint.add_nodewithReturncreates a bare result node without the function's signature pins. Converge branch paths on the function's original result node instead of adding another. blueprint.save_dirty_assetsskips maps. It iterates Blueprint packages only. Save a level with a one-line editor python call.- A montage created over
animation.*has zero length. Nothing recomputes the composed length after a segment is added, so the montage plays as an instant no-op. Author it from a source animation withAnimMontageFactoryin editor python and confirmget_play_length()before running. The runtime symptom is under A montage-gated beat releases instantly. - No action writes a property on a placed component instance.
ld.configure_sm_component_on_actorwrites the template, and no Monolith action reaches the instance. SetStateMachineClasson the placed component instance withset_editor_propertyin editor python. See Different placed actors all run the same machine.
Two behaviors are engine facts, not gaps. A new actor blueprint already contains its BeginPlay and Tick nodes, so adding them again fails with "already exists"; see Where entry and start logic live. A blueprint must be compiled before another blueprint's graph can call its new functions; see Compile and verify.
Running and verifying¶
A placed machine does nothing in PIE¶
The actor is in the level and the component is configured, but the machine never runs. bStartOnBeginPlay defaults to false. Pass b_start_on_begin_play=true to ld.configure_sm_component_on_actor so the placed machine starts on begin play.
runtime_get_state says no PIE session is running¶
The call returns "No Play-In-Editor session is running. Start PIE first." ld.runtime_get_state reads a live instance, so PIE has to be running when you call it. Start PIE, then read the state.
runtime_get_state says the component has no live instance¶
PIE is running and the component is found, but the call returns "has no live state machine instance (not initialized)." The component exists but its machine has not been created and started. Confirm the component is set to initialize and start (or is started explicitly), and that it has had a chance to start before you read it. This is the runtime-side symptom of the same start flags covered above.
Different placed actors all run the same machine¶
You configured the component and every placed actor now runs the same state machine, but you wanted them to differ. ld.configure_sm_component_on_actor edits the component template, so StateMachineClass persists to every placed instance. No ld.* operation writes a per-instance override; set StateMachineClass per placed actor in the editor (or via editor python set_editor_property on the placed component instance) when instances need to differ.
The first beat of a machine never appears¶
The machine runs (states advance) but the opening beat never reached whatever displays it. The machine started before the display existed: a component set to start on begin play starts during component initialization, which runs before the actor's begin-play chain has created a widget. Create and wire the display first, then start the machine explicitly at the end of that chain. See the dialogue system for this exact ordering.
A nested machine cuts off its last state¶
A sub-machine's final beat flashes and the parent machine moves on immediately. The edge leaving the reference fires on "the nested machine reached its end state", and the last meaningful state IS its end state, so arrival and departure are the same update. Give the sub-machine an empty terminal state after its last meaningful one; the parent then leaves only after the sub-machine has moved past it.
A montage-gated beat releases instantly, or the animation never plays¶
A beat gated on "montage finished" falls through the frame it begins, or the character stands still. Three causes, in the order to check: a montage authored over the animation surface can report a play length of zero (nothing recomputes the composed length after segments are added; author it from a source animation via the montage factory in editor python, and confirm get_play_length() is a real duration before running); a bare skeletal actor has no anim instance, so playing a montage on it silently does nothing (give the character an animation blueprint with a montage slot, on the same skeleton as the clip); and a transition can be evaluated before the state's begin logic has armed the beat, which a naive fail-open check reads as finished (hold until the beat has genuinely begun; fail open only when it began and could not play).
A short beat looks skipped when verifying over MCP¶
You sample a running machine between calls and a beat that should hold for a couple of seconds seems to have released instantly. Each MCP round trip costs real play time, so a short beat can begin and end between two samples. Slow the beat down for the probe (a low montage play rate sustains the hold), verify, then restore the shipped rate and verify once more.
Screenshots and capture¶
A capture op fails with "GEditor unavailable"¶
ld.capture_graph_view or ld.capture_local_graph returns "GEditor unavailable; this op requires the editor to be running." Capture renders the graph through Slate, so it needs a live, non-headless editor. It opens the asset tab itself, so you do not need to open it first, but a headless or -NullRHI process cannot capture a screenshot.
A captured graph looks empty or stale¶
The screenshot is blank, or it does not reflect a change you just made. Capture renders what exists at the moment of the call, so compile and lay the graph out (ld.layout_states) before capturing, and re-capture after any edit. A state's appearance also changes once it carries its own graph content (an empty state is gray; a state with entry logic is tinted), so re-run the capture after adding local-graph logic. See What the Calls Look Like.
Related¶
- Setup & Connecting an Agent: install, build, and confirm the operations are live.
- Transports & Bridges: per-transport call mechanics behind the bridge failures above.
- Authoring with an AI Agent: the conventions that prevent most of the authoring failures above.
- A Dialogue System & the Lighthouse: the prompts most of these symptoms come from, and the prompt habits that avoid them.
- What the Calls Look Like: the authoring flow, call by call.