Workflow Diagnostics
workflowDiagnostics reads one workflow graph and lists things that will probably not do what you meant when the workflow runs: a router with nowhere to go when nothing matches, a branch that can never be taken, an extraction option the node will ignore. Each finding names the node and says what to do.
Diagnostics never block saving or publishing. Publishing has its own checks (What publishing checks); a few diagnostics repeat those as INFO, so you see the problem in a draft before you press Publish.
Running it
Section titled “Running it”query WorkflowDiagnostics($id: Int!, $published: Boolean) { workflowDiagnostics(id: $id, published: $published) { severity code nodeId edgeId message }}- By default it checks the draft graph, the one
updateWorkflowwrites. - With
published: trueit checks the published snapshot that real sessions run. A workflow that was never published answersNOT_FOUNDfor this. - An empty list means nothing was found.
Full reference: workflowDiagnostics.
Severity
Section titled “Severity”WARNING: the graph will probably not do what you meant at run time (a run that fails, a branch that silently stops, a value that is never what you expect).INFO: a hint, or a repeat of a publish rule.
code is a stable identifier: switch on it in your own tooling, not on message, whose wording may change. New codes may be added later, so treat an unknown code as a generic finding.
| Code | Severity | What it means | What to do |
|---|---|---|---|
FAN_IN_WITHOUT_JOIN | WARNING | A node has two or more incoming edges from paths that can both run in the same turn (they split at a node that follows every outgoing edge). The engine has no join: the node runs once, when the first path reaches it, and a later arrival is skipped (VISIT_LIMIT_REACHED in the trace). | Put the steps in one sequence, or make sure the node does not depend on values the other path writes. |
UNREACHABLE_NODE | WARNING | No path from the start node reaches this node (edges and router defaultTarget are followed). Publish refuses such a graph. | Connect the node, or delete it. |
ROUTER_NO_DEFAULT | WARNING | A router has no defaultTarget (and no circuit breaker that trips on the first miss). A value that matches no edge label fails the run, and the user gets the generic error reply. | Set defaultTarget to a fallback node. |
ROUTER_DEFAULT_TARGET_MISSING_NODE | WARNING | A router’s defaultTarget names a node id that does not exist. A miss routes there and the branch silently stops. | Point defaultTarget at an existing node id. |
ROUTER_CIRCUIT_BREAKER_TARGET_MISSING_NODE | WARNING | A router’s circuitBreaker.target names a node id that does not exist. When the breaker trips the branch silently stops. | Point circuitBreaker.target at an existing node id. |
ROUTER_DOTTED_VARIABLE_OF_TEXT | WARNING | A router’s variable is a dotted path whose first part is the output of an LLM node without outputSchema. That output is text, and a path cannot be read from text, so the router never matches. | Write the value to its own variable with a separate step (for example an LLM node with matchAnyOf) and route on that variable. |
ROUTER_DUPLICATE_OR_UNLABELLED_EDGE | WARNING | A router edge has no label, or repeats a label another of its edges already has. A router follows only the first edge whose label equals the value, so this edge is never taken. | Give each router edge its own label, or delete the extra edge. |
CONDITION_MISSING_BRANCH | WARNING | A condition lacks an edge labelled ‘true’ or ‘false’. When the expression gives the missing result, the run fails. | Add edges labelled exactly ‘true’ and ‘false’. |
LLM_HELPER_IGNORED_WHEN_STREAMING | WARNING | An LLM node sets extractFirstInteger, matchAnyOf or extractAlphanumericId but not streamOutput: false. Streaming is the default, and the streaming executor ignores these helpers, so the raw reply is stored. | Set streamOutput: false on the node. |
FAILURE_WITHOUT_SUCCESS | INFO | A node that supports failure routing (tool, action, webhook, llm, sub_agent, set_variable, pdf_export) has a ‘failure’ edge but no ‘success’ edge. Publish refuses this. | Add an edge labelled ‘success’. |
SUCCESS_WITHOUT_FAILURE | INFO | A node that supports failure routing (tool, action, webhook, llm, sub_agent, set_variable, pdf_export) has a ‘success’ edge but no ‘failure’ edge. If the node fails, the whole run fails. | Add an edge labelled ‘failure’ to handle a failed step, if the conversation should continue. |
FAILURE_EDGE_ON_UNROUTABLE_NODE | INFO | A response, end_session, condition or switch node has an edge labelled ‘success’ or ‘failure’. Only tool, action, webhook, llm, sub_agent, set_variable and pdf_export nodes route on failure; on this node the label has no failure meaning (a response or end_session follows every edge, a condition or switch follows only its own labels). | Move the failure handling to the step that can fail, or remove the label. |
DEAD_END_NON_TERMINAL | INFO | A node other than response or end_session has no outgoing edge. The turn ends there; if nothing spoke earlier on that path, the user gets no reply. | Connect the node to the next step, usually a response node. |
EXPLICIT_MAXVISITS_NO_ESCAPE | INFO | A node sets maxVisits but has no ‘loop_exhausted’ edge. If the graph reaches it once more than maxVisits allows, the run fails. | Add an edge labelled ‘loop_exhausted’ to say where to go when the loop runs out. |
RETRY_NOT_IDEMPOTENT | WARNING | A tool, action or webhook node lists TIMEOUT or UPSTREAM_ERROR in retry.on, but its request is not known to be idempotent (no retry.idempotent: true; for a webhook, the method is not GET and there is no Idempotency-Key header). After a timeout or a 5xx the service may already have acted, so those classes are never retried on this node. A connection that was never made, and a 429, are still retried. In production since 2026-10-24. | If sending the request twice is harmless, set retry.idempotent to true or send an Idempotency-Key header the service honours. Otherwise remove TIMEOUT and UPSTREAM_ERROR from retry.on. See Retrying a failed call. |
LLM_STRUCTURED_WITHOUT_FAILURE_EDGE | INFO | An LLM node has an outputSchema (structured output) and no ‘failure’ edge. When the reply still does not match the schema after the one repair call, or is cut off at the token limit, the node fails, and without a ‘failure’ edge the whole run fails and the user gets the generic error reply. In production since 2026-10-27. | |
MOCK_RESPONSE_SET | INFO | A node has a mockResponse. Only a mock-mode test (a draft test or regression replay in mock mode) uses it, returning it instead of calling out; a live run ignores it. A draftMocks entry for the same node wins over it. Available in production since 2026-10-29. | Nothing to fix. Keep it close to what the real call returns, or remove it when it no longer matches. See Testing a Workflow Safely. |
PLAINTEXT_SECRET_IN_CONFIG | WARNING | A node’s configuration looks like it holds a literal credential. Available in production since 2026-10-29. |
Notes on the trickier codes
Section titled “Notes on the trickier codes”FAN_IN_WITHOUT_JOIN. The workflow engine has no “wait for both branches” step. When two paths that both run in the same turn lead into one node, that node runs once, as soon as the first path reaches it; the later arrival is skipped and shows in the run trace asVISIT_LIMIT_REACHED(see Loop re-entry). Paths that split at acondition,router,approvalorawait_replynode are exclusive (only one runs), so a shared step after them is not flagged.ROUTER_NO_DEFAULT. Since 2026-10-10 a router whose value matches no edge label and that has nowhere else to go fails the run. A circuit breaker only takes over after its configured number of misses in a row, so it covers the first miss only whenmaxConsecutiveDefaultRoutesis1.ROUTER_DOTTED_VARIABLE_OF_TEXT. Anllmnode withoutoutputSchemastores its answer as text. A routervariablesuch asanalysis.severitycannot read inside text even when it looks like JSON. Give thellmnode anoutputSchema(it then stores an object, and the warning is not raised; in production since 2026-10-27), or write the value to its own variable with a separate step, for example anllmnode withmatchAnyOf, and route on that.LLM_STRUCTURED_WITHOUT_FAILURE_EDGE. A structuredllmnode makes at most one repair call; a reply that is still invalid fails the node. Add asuccessand afailureedge, and on the failure branch ask the user again with{{_lastNodeErrorPublic}}(the code isINVALID_DATA). See One repair call, then the failure edge.FAILURE_WITHOUT_SUCCESS,SUCCESS_WITHOUT_FAILUREandFAILURE_EDGE_ON_UNROUTABLE_NODE. Since 2026-10-15 failure routing works on six node types, not onlytoolandaction, and the first two codes cover all six. For asub_agent,SUCCESS_WITHOUT_FAILUREadds that the node reports a failed target in itsresultVarand only takesfailurefor a configuration error.FAILURE_EDGE_ON_UNROUTABLE_NODEis new: it flags asuccessorfailurelabel on a node type that cannot route its own failure. Arouteris not flagged, becausesuccessandfailureare ordinary route values there. See Failure routing.LLM_HELPER_IGNORED_WHEN_STREAMING.extractFirstInteger,matchAnyOfandextractAlphanumericIdonly work on a node withstreamOutput: false; anllmnode streams by default (details).PLAINTEXT_SECRET_IN_CONFIG. A heuristic, never blocking, that looks only at configuration that is sent to a third party: awebhooknode’surl,headersandbodyTemplate, and atooloractionnode’sargsTemplate. It flags a secret-named header (such asAuthorization,X-Api-Key,Cookie) with a literal value, a URL query parameter such astoken,key,secret,passwordorapi_keywith a literal value,user:password@inside a URL, a secret-named JSON key with a literal value,BearerorBasicliterals, and a few well-known key shapes. It skips values that contain{{(a template), values under 8 characters and names that are not secret-like. The message names the node and the field and never repeats the value. Where to move the secret: for anactionnode, the custom action’sbearerToken(withcredentialSecretHeaderNameif the header is notAuthorization), notextraHeaders; for atoolnode, the credential on the MCP connector; awebhooknode has no secret store, so move the call to anactionnode.