The planner names a branch's outgoing source_handle values in one LLM
call; the node builder chooses the case / class / action ids in another.
ESQ1-303's edges left its if-else on "score_equals_60" and "else"
against a case named "true", so the engine skipped both arms and the
run succeeded with nothing.
declared_branch_handles lists what a node routes on; repair_branch_edge_
handles re-homes an edge only when it is forced (exact / alias / name /
stem match, one arm fanning out under a single invented name, default-
source edges onto unused handles in order) and returns everything
ambiguous untouched; undeclared_branch_handles reports what is left so a
validator can reject it. A wrong guess would silently swap IF and ELSE.
Aliases are trusted as names -- the documented limit -- which is why the
node-builder handle contract, not this repair, is the primary fix.
A fail-branch node's success path stays on "source" (what graphon's
NodeRunResult.edge_source_handle actually emits by default), not an
invented "success" handle nothing ever routes on; a fail-branch
branch-type node (if-else / question-classifier / human-input) just
gains the extra "fail-branch" arm. human-input also always declares its
implicit "__timeout" arm, since every human-input node has a timeout
config and the FE always renders that handle; that arm is excluded from
the "unused declared handles" pool the fan-out/default-order repair
rules count against, since it is routinely left unwired by design and
must not block or absorb repair of the arms the author did configure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
graphon turns a falsy body.data into an empty list before the executor
counts items, so a json / raw-text / binary body whose data is "" fails
at run time. http_request_body_errors counted any string as one item;
only a non-empty string is one now.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
graphon's BodyData.type (Literal["file","text"]) has no default and the
shared builder prompt only ever shows an empty "none" body, so the LLM
writes {key, value} items and the graph dies at Graph.init (ESQ1-302).
normalize_http_request_bodies fills the type (file when the item carries
a file selector, else text) and empties a "none" body.
The executor sends json / raw-text / binary bodies as ONE rendered
template. Two {key, value} items under a json body have no correct
automatic collapse -- quoting breaks a JSON-valued variable (ESQ1-302's
node3.text is JSON), not quoting breaks plain text -- so
http_request_body_errors reports the node instead of guessing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
varType is a frontend-only hint the canvas uses to pick the operator
list; the runtime ignores it. Builder-made if-else nodes never carried it
(ESQ1-303), which steers the panel to string operators on a number and
reproduces ESQ1-285's runtime error. Derive it only when a start
variable's declared type makes it certain; never overwrite, never guess.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
list-operator's filter conditions are FilterCondition (value: str |
Sequence[str] | bool, default ""), and filtering an array of strings or
numbers requires a plain string. The normalizer treated them like
if-else's Condition: it wrapped an in/not-in scalar into a list and left
None alone, both of which graphon rejects. They now get their own rule:
None becomes "", numbers become text, nothing is wrapped. List items use
the same integral-float rule as scalars.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
graphon's Condition.value is str | Sequence[str] | bool | None. The node
builder wrote "value": 60 for the ESQ1-303 draft; Graph.init builds every
node up front, so that one number stopped the whole workflow. pydantic
also coerces int 1 / 0 to True / False, so bool must be checked before
int.
core/workflow/graph_normalizers.py is the one place both the shared
generator's postprocess and the Builder's write chokepoint will call, so
the two paths cannot disagree. This commit adds the module with the
condition-value normalizer only (if-else cases, sub-variable conditions,
loop break conditions, list-operator filters); nothing calls it yet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
validate_node_config promises to raise exactly when Graph.init would.
That only holds while it and DifyNodeFactory.create_node run the same
validation steps, so both now call one helper instead of two copies.
No behavior or error-text change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A graph-init failure surfaced as "2 validation errors for
HttpRequestNodeData ..." -- pydantic names the model class, never the
node. graphon logs the node id and re-raises the raw error, so the error
frame the Builder received (ESQ1-302) could not say which node was
wrong, and its repair agent had to guess.
DifyNodeFactory.create_node now re-raises every ValueError from the
validation stage (shared-shape validation, class resolution, concrete
NodeData re-validation) prefixed "node '<id>' (<type>): ". This runs for
EVERY workflow start, so the error text of every graph-init failure
changes, not only the Builder's. The wrapped error is a plain ValueError,
so app_generator's handling takes its ValueError branch (log only under
DEBUG) instead of the ValidationError branch; the published error is the
same.
validate_node_config runs the same three validation steps with no
factory instance and no runtime state -- the dry Graph.init a draft can
be put through before it is written or run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round-1 review defect: the TOOL branch added for ESQ1-296/ESQ1-289 assumed
output_schema was always a dict once truthy. A non-dict output_schema (or a
non-dict `properties` inside it) raised AttributeError instead of failing
open. That crash is new -- the old branch never read output_schema at all --
and it happens outside the _run_stage try/except and outside the
controller's narrow exception allowlist, so it would take down the SSE
generator mid-stream for both cmd+K and the Builder.
Reachable via refine mode: _hydrate_tool_nodes only stamps a schema when it
finds a matching catalogue entry, so a hand-edited or legacy graph can carry
forward an output_schema of any shape.
Fix: shape-check before every .get() -- output_schema must be a dict and its
properties must be a dict, or the branch returns True.
SHARED WITH CMD+K: fails open whenever the schema is absent, empty, or an
unreadable shape, so a tool we cannot describe behaves exactly as before.
Only provably-wrong references, from a schema we can actually read, are
rejected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_declares_variable returned True unconditionally for TOOL nodes, so any
invented downstream reference survived generation. ESQ1-296 and ESQ1-289
are the same three lines: code/simple_code is a TOOL node, so the real
code-node validation directly above never saw it. They differ only in
consumer -- End tolerates the missing variable and yields a silent null,
if-else fails at run time and feeds the repair loop.
Tool outputs are not opaque: tool_node.py fixes text/files/json and the
generator already copies the provider's output_schema onto the node.
Validate against both, and alias the invented `result` to `text` so the
common mistake self-heals.
SHARED WITH CMD+K: fails open when no output_schema is present, so a tool
we cannot describe behaves exactly as before. Only provably-wrong
references are rejected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three minor whole-branch-review fixups:
- ResourceOption.readiness ("ready"/"missing_config") was computed but never
shown to the model: discover_resources's recommender prompt listed ids and
labels only, so it could recommend an unauthorized tool; _GAP_SYSTEM showed
readiness in the listing (assess_capability_gap) but never defined what
missing_config means. Both prompts now show readiness and state that
missing_config is installed-but-not-authorized, i.e. not usable -- pinned
with tests asserting the literal "missing_config" in the constructed
prompts.
- strings.py: dropped three orphaned catalogue entries ("Here's the initial
plan.", "Ask each time", "Prefer audited") -- the conflict-policy concept
they backed was removed end to end; nothing in the handlers emits them.
- core/workflow/generator/tool_catalogue.py: provider.need_credentials was
read OUTSIDE the per-provider try/except that already guards
get_tools()/tool parsing. This module is shared with the cmd+K /create and
/refine flows, so a provider whose property raised would break those too,
not just the Builder. Moved the read inside the try/except so a bad
provider is skipped like any other.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Builder recommended every installed tool as ready to use without
ever checking whether its provider actually has credentials configured.
A credentialed tool with no tool_builtin_providers row got bound into a
plan and then failed at test time with an authorization error the user
can't act on.
tool_catalogue.py now carries provider_controller.need_credentials onto
each ToolCatalogueEntry as needs_credentials. resources.py resolves
which providers are actually authorized for the tenant (mirroring
ToolTransformService.builtin_provider_to_user_provider's need_credentials
/ db_provider rule exactly, so the Builder and the console's tools page
can never disagree) and sets ResourceRef.readiness to "missing_config"
for a credentialed tool with no authorized provider row, "ready"
otherwise. Datasets and models are unaffected. If the authorization
query itself fails, readiness fails open to "ready" rather than
mass-labeling every credentialed tool unauthorized on a transient error.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Dify has no native boolean start-variable type, so a bool requirement the
Builder proposes gets emitted as a `select` — and the LLM often leaves its
`options` empty. An options-less select renders as an empty, unfillable dropdown
in the Builder's test-input form, so the user can never submit test data
(affects every workflow whose requirements include a bool field).
Add _normalize_start_select_variables to the generator postprocess (beside
_normalize_start_file_variables): a `select` start variable with no usable
(non-empty string) options is converted to a `text-input` the user can type
into; a select that declares real options is normalized to those strings and
kept. Idempotent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Builds failed with an empty graph when the generator LLM wired downstream
references to an HTTP-request node's output as `{#node.response#}`. HTTP-request
nodes expose only {body, status_code, headers, files} — never `response` — and
the reference-repair pass only rewrites to a node's SOLE output, so with four
outputs the ref stayed unresolved: structural validation failed
(runner.py:646 UNRESOLVED_REFERENCE), generate_workflow_graph returned no graph,
and build_nodes produced an empty build ("Couldn't build the workflow").
Add a targeted, unambiguous output-name alias (http-request `response` -> `body`,
the response body) applied in _reconcile_variable_references before the
sole-output fallback, guarded so it only rewrites when `body` is actually
declared. Extensible via _OUTPUT_ALIASES for future known-wrong names.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>