feat(graph): improve flow control for merge nodes downstream of conditional branches - #13372
feat(graph): improve flow control for merge nodes downstream of conditional branches#13372chalitbkb wants to merge 50 commits into
Conversation
# Conflicts: # docker/build_and_push.Dockerfile # docker/build_and_push_backend.Dockerfile # docker/build_and_push_base.Dockerfile # docker/build_and_push_ep.Dockerfile # docker/build_and_push_with_extras.Dockerfile
…angflow-ai#13191) * Model handling for tool calling in agents and update IBM models * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
…angflow-ai#13038) Splits monolithic agent instructions into a slim AGENTS.md hub plus docs/agents/ topic files (philosophy, architecture, components, contracts, testing, anti-patterns) so AI coding agents have a single source of truth for how the project thinks. Removes the .cursor/rules/ directory; AGENTS.md is now the only agent-facing doc. Fixes the long-standing pointer to the empty src/backend/base/langflow/components/ stubs (real components live in src/lfx/src/lfx/components/) and corrects the v2 API description from 'future' to its actual mounted state.
…gflow-ai#13195) * ci: call update_bundle_versions in nightly workflow * ci: guard bundle steps against missing script or folder
…ndles/* wheels (langflow-ai#13199) * chore: bump backend unit test timeout from 30 to 40 minutes * ci: anchor langflow version grep so new workspace packages can't break it The pattern `uv tree | grep 'langflow' | grep -v 'langflow-base' | grep -v 'langflow-sdk'` was used in 8 places to pluck the root `langflow` (or `langflow-nightly`) line out of `uv tree`. It relied on enumerating every other workspace package that starts with `langflow-`. As soon as a new one was added (`langflow-stepflow`, via langflow-ai#12015) the filter let two lines through and the downstream `awk` / `cut` produced a multi-line string, e.g.: Name langflow-stepflow langflow-nightly does not match langflow-nightly. Exiting the workflow. Replace the enumeration with an anchored regex `grep -E '^langflow(-nightly)?[[:space:]]'` so only the exact `langflow` or `langflow-nightly` root line matches, regardless of how many other `langflow-*` workspace packages exist now or in the future. * ci: build, test, and publish src/bundles/* wheels Langflow main pins exact versions of every bundle (e.g. `lfx-arxiv-nightly==0.1.0.dev38`, `lfx-arxiv==0.1.0`), but neither the nightly workflow nor the release workflow built or published those bundle wheels. So when cross-platform tests (or downstream consumers) ran `pip install langflow-nightly`, the resolver couldn't find `lfx-arxiv-nightly` / `lfx-duckduckgo-nightly` on PyPI and bailed: Because lfx-arxiv-nightly was not found in the package registry and langflow-nightly==1.10.0.dev38 depends on lfx-arxiv-nightly==0.1.0.dev38, we can conclude that langflow-nightly==1.10.0.dev38 cannot be used. Add a build → test → publish lane for every package under `src/bundles/*/` in both workflows, plus a matching bundle install step in cross-platform-test.yml. `release_nightly.yml` - `build-nightly-bundles`: iterates `src/bundles/*/pyproject.toml`, builds each wheel, uploads as `dist-nightly-bundles`. - `test-cross-platform` now passes `bundles-artifact-name` so the cross-platform test installs bundles before installing main. - `publish-nightly-bundles`: publishes each bundle wheel to PyPI after cross-platform passes and lfx is already published. - `publish-nightly-main` now waits on `publish-nightly-bundles` so main reaches PyPI only after its bundle deps do. `release.yml` - New `release_bundles` input (false by default). - `validate-dependencies` rejects `release_package_main=true` without `release_bundles=true`, mirroring the existing lfx/sdk gate. - `build-bundles`, `publish-bundles` mirror the nightly jobs. - `publish-main` now needs `publish-bundles`. `cross-platform-test.yml` - New optional `bundles-artifact-name` input; download step is gated on it being set. - `build-if-needed` (workflow_dispatch path) also builds bundles and publishes the artifact, so adhoc runs stay self-contained. - Each of the four "Install main package from wheel" steps gets a preceding "Install bundle packages from wheel" step that runs only when the artifact is present, leaving non-bundle callers (e.g. pre-bundle release branches) unaffected. * ci: drop mapfile from bundle install (macOS Bash 3.2 compat) `mapfile` is a Bash 4+ builtin and the macOS runners use the system Bash (3.2). The "Install bundle packages from wheel (Unix)" step failed on `Install & Run - macos amd64 3.12` with: mapfile: command not found find: stdout: Broken pipe Replace with the `shopt -s nullglob` + glob-array pattern already used in the bundle build/publish steps, which works on Bash 3.2. All artifact wheels live at the top level of ./bundles-dist, so the simple glob is equivalent to the previous `find` (which also wasn't recursing anywhere useful). --------- Co-authored-by: Claude <noreply@anthropic.com>
…nd (langflow-ai#13206) Bundles (lfx-arxiv, lfx-duckduckgo) change infrequently enough that a nightly cadence is overkill. Drop the rename-to-`-nightly` and bundle publish lanes from the nightly pipeline and add a dedicated workflow_dispatch-only release_bundles.yml for purposeful releases. - nightly_build.yml: drop update_bundle_versions.py invocation and the src/bundles/*/pyproject.toml git-add guard. - release_nightly.yml: remove build-nightly-bundles and publish-nightly-bundles jobs; untether test-cross-platform and publish-nightly-main from them. - release_bundles.yml (new): build all src/bundles/* wheels, run a cross-OS install smoke test, then publish to PyPI under their stable names. Tolerates already-published versions so re-runs without a version bump are no-ops. release.yml still owns bundle publishing as part of main releases. Manual follow-up (PyPI admin): delete the lfx-arxiv-nightly and lfx-duckduckgo-nightly projects from PyPI.
…-ai#13207) The release_bundles.yml smoke test installs bundle wheels into a fresh venv, which makes uv resolve their `lfx>=X.Y,<Z` pin against PyPI. When bundles are released ahead of a matching lfx (typical case — bundles ship more often than lfx bumps land on PyPI), the resolve fails: Because only lfx<=0.4.3 is available and lfx-arxiv==0.1.0 depends on lfx>=0.5.0,<0.6.0, we can conclude that lfx-arxiv==0.1.0 cannot be used. Build the lfx wheel from the current ref in the build-bundles job and install it alongside the bundle wheels in the smoke test. The lfx wheel ships as a separate `dist-lfx-smoketest` artifact and is NOT included in the publish-bundles step — only the `dist-bundles` artifact gets pushed to PyPI.
…ld (langflow-ai#13208) The nightly pipeline renames the workspace `lfx` package to `lfx-nightly` in `src/lfx/pyproject.toml` and the root workspace dep, but leaves each `src/bundles/*/pyproject.toml`'s `"lfx>=X.Y,<Z"` pin unchanged. With the workspace `lfx` gone and PyPI still on lfx 0.4.3, the create-nightly-tag job's `uv lock` fails: × No solution found when resolving dependencies for split [...] ╰─▶ Because only lfx<=0.4.3 is available and lfx-arxiv depends on lfx>=0.5.0,<0.6.0, we can conclude that lfx-arxiv's requirements are unsatisfiable. `update_lfx_version.py` now also rewrites the `lfx` (or already-rewritten `lfx-nightly`) specifier in every `src/bundles/*/pyproject.toml` to `lfx-nightly==<dev version>`, mirroring how `update_lf_base_dependency` re-pins the lfx dep inside `langflow-base`. The lookahead in the regex prevents matching sibling packages like `lfx-arxiv` / `lfx-duckduckgo`. No-op when no bundle pyprojects exist (e.g. on main today), so this is safe to merge ahead of the bundle extraction landing on main. Restored the guarded `git add src/bundles/*/pyproject.toml` step so the modified bundle pyprojects ride the same commit/tag as the rest of the nightly version bumps. Cherry-pick to release-1.10.0 to unblock nightlies cut from that branch.
langflow-ai#13303) fix(ci): narrow --prerelease=allow to wheels in cross-platform install tests Nightly cross-platform install tests were failing on Python 3.12/3.13 with a pydantic ValidationError at langchain_core import time: File ".../langchain_core/runnables/passthrough.py", line 349, in <module> _graph_passthrough: RunnablePassthrough = RunnablePassthrough() pydantic_core._pydantic_core.ValidationError: 1 validation error for RunnablePassthrough name Field required ... Cause: --prerelease=allow was pulling pydantic 2.14.0a1 as a transitive dep, which is incompatible with langchain-core 1.4.0's top-level RunnablePassthrough() initialization. We need pre-releases enabled for the nightly langflow* .dev wheels we pass on the command line, but not for transitive deps. Switch to --prerelease=if-necessary-or-explicit, which keeps the explicit .dev wheel installs working while resolving stable pydantic for everything else.
…ixins (langflow-ai#13141) * refactor(settings): split monolithic Settings into per-domain group mixins Move the ~70-field Settings class from one 755-line base.py into 13 cohesive BaseModel mixins under lfx/services/settings/groups/ (paths, server, database, cache, storage, mcp, telemetry, observability, security, components, ui, runtime, variables). Settings now composes them via multiple inheritance. Inheritance order is chosen so cross-group validators see their dependencies in info.data: PathSettings rightmost (config_dir before database_url), ServerSettings just left of it (workers before event_delivery). No env var or call-site changes. BASE_COMPONENTS_PATH re-export preserved for tests that import it transitively. * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * docs(settings): fix wrong mcp_server_timeout docstring and idle-timeout comment mcp_server_timeout's docstring was copy-pasted from a database setting and mentioned 'lock to released' / 'database connection'. Replace with text that describes the actual field. mcp_session_idle_timeout's comment said 'Defaults to 5 minutes' but 400s is ~6.7 minutes. Drop the misleading 'minutes' claim and keep the value. * test(settings): add structural safety tests for the group composition Adds 26 tests to guard the refactor: - All 105 fields that lived on the monolithic Settings still exist on the composed class. A missing group in the inheritance list trips this loudly. - A sampling of critical scalar and dict defaults (host, port, workers, cache_type, sqlite_pragmas, db_connection_settings, etc.) are byte-for-byte unchanged. - Cross-group validator dependencies still resolve via info.data: workers > 1 forces event_delivery=direct (ServerSettings -> RuntimeSettings) and database_url falls back to a sqlite path under config_dir without raising 'config_dir not set' (PathSettings -> DatabaseSettings). - A parametrized sweep verifies a representative set of LANGFLOW_* env vars still populate their fields. - Back-compat exports (CustomSource, is_list_of_any, yaml helpers, BASE_COMPONENTS_PATH) are still importable from settings.base. - update_settings handles scalars and list-with-no-duplicates correctly. - save_settings_to_yaml round-trips without error. * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
skipped was never set correctly in build-nightly-base so we always skipped the push
# Conflicts: # .secrets.baseline # src/lfx/src/lfx/_assets/component_index.json # src/lfx/src/lfx/services/settings/base.py
…tional branches When a ConditionalRouter (If-Else) excludes one branch, merge/combine nodes at the intersection of active and excluded branches were blocked indefinitely. This fixes three points in the graph engine that prevented downstream nodes from proceeding with available data from the active branch. - mark_branch(): reactivate common downstream vertices that have at least one active predecessor outside the stopped branch, so merge nodes shared between active and inactive paths are not blocked forever. - exclude_branch_conditionally(): remove conditionally excluded vertices from the run manager's predecessor lists, so downstream nodes no longer wait for data that will never arrive from the excluded branch. - ComponentVertex._get_result(): return None instead of raising ValueError when a vertex is conditionally excluded or inactive, allowing the caller to proceed without crashing. - Vertex._build_vertex_and_update_params(): catch ValueError from get_result() as a safety net for edge cases (parallel builds, complex graph structures), and skip assigning None to params so components use their natural input defaults instead of receiving NoneType values that fail Pydantic validation.
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughGraph execution now allows merge and combine nodes to proceed when input branches are deactivated or conditionally excluded. Downstream merge vertices are reactivated if they have active external predecessors; conditionally excluded vertices are removed from blocker tracking; vertex result retrieval handles missing data gracefully and skips unavailable inputs from excluded branches. ChangesMerge/Combine Node Availability
Sequence DiagramsequenceDiagram
participant Graph as Graph.mark_branch()
participant DownstreamV as Downstream Merge Vertex
participant RunMgr as run_manager
participant VertexBuild as Vertex._build_vertex_and_update_params()
participant VertexRes as ComponentVertex._get_result()
Graph->>DownstreamV: scan visited vertices for reactivation
alt Merge has active external predecessor
DownstreamV->>Graph: reactivate and clear from visited
else All predecessors from stopped branch
DownstreamV->>Graph: remains inactive
end
Graph->>RunMgr: remove excluded vertices from predecessor lists
RunMgr-->>Graph: predecessors updated
VertexBuild->>VertexRes: get_result() from upstream
alt result available
VertexRes-->>VertexBuild: return result
else ValueError or excluded
VertexBuild->>VertexRes: catch exception
VertexRes-->>VertexBuild: treat as None
end
VertexBuild->>VertexBuild: continue with available inputs
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (1 error, 2 warnings)
✅ Passed checks (6 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/lfx/src/lfx/graph/graph/base.py (1)
1057-1076:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftRestore scheduler predecessors when a previous exclusion is cleared.
This method removes
previous_exclusionsfromconditionally_excluded_vertices, but it never rebuilds therun_manager.run_predecessorsentries that were pruned byremove_from_predecessors(). On a later router iteration, a branch that becomes active again can be treated as already-fulfilled, so downstream merges may run before that re-enabled predecessor actually finishes. That breaks the re-evaluation path used byConditionalRouter.iterate_and_stop_once()insrc/lfx/src/lfx/components/flow_controls/conditional_router.py.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lfx/src/lfx/graph/graph/base.py` around lines 1057 - 1076, The code clears previous_exclusions from conditionally_excluded_vertices but never restores the scheduler predecessor entries that were removed via run_manager.remove_from_predecessors(), causing re-enabled branches to be treated as already-fulfilled; update the block that handles previous_exclusions (using conditional_exclusion_sources, previous_exclusions, and run_manager) to reinstate each previously-excluded vertex into run_manager.run_predecessors (or call a run_manager method to add predecessors back) for the current vertex_id so downstream merge/combine nodes see the restored predecessor relationships when a branch becomes active again; ensure symmetry with the removal logic used in run_manager.remove_from_predecessors and adjust any bookkeeping (_exclude_branch_conditionally, conditionally_excluded_vertices) accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/lfx/src/lfx/graph/graph/base.py`:
- Around line 1014-1027: The downstream reactivation loop over `visited` in the
block checking `if state == VertexStates.INACTIVE` is single-pass and
order-dependent, which can leave shared descendants inactive; make this pass
iterative by repeating the scan until no vertices were reactivated in an
iteration (e.g., loop while a `changed` flag is true) so previously-skipped
descendants get revisited; use `self.predecessor_map`,
`self.get_vertex(p_id).is_active()`, and `self.mark_vertex(v_id,
VertexStates.ACTIVE)` as-is inside the loop and remove the unused `vertex` local
variable to fix the Ruff warning.
In `@src/lfx/src/lfx/graph/vertex/base.py`:
- Around line 649-657: The current skip logic only avoids setting scalar params
when result is None but still appends/stores None for list/dict inputs; update
_build_list_of_vertices_and_update_params and _build_dict_and_update_params to
mirror the scalar skip behavior: after obtaining result from vertex.get_result
(and after calling _handle_func), if result is None do not extend the params
list or store the dict entry; for lists, filter out None entries before calling
_extend_params_list_with_result (or skip calling it entirely when result is None
or empty after filtering); for dicts, only assign self.params[key] when result
is not None and contains valid entries. Ensure you reference and change
_build_list_of_vertices_and_update_params, _build_dict_and_update_params, and
keep the existing _handle_func(key, result) call sequence.
---
Outside diff comments:
In `@src/lfx/src/lfx/graph/graph/base.py`:
- Around line 1057-1076: The code clears previous_exclusions from
conditionally_excluded_vertices but never restores the scheduler predecessor
entries that were removed via run_manager.remove_from_predecessors(), causing
re-enabled branches to be treated as already-fulfilled; update the block that
handles previous_exclusions (using conditional_exclusion_sources,
previous_exclusions, and run_manager) to reinstate each previously-excluded
vertex into run_manager.run_predecessors (or call a run_manager method to add
predecessors back) for the current vertex_id so downstream merge/combine nodes
see the restored predecessor relationships when a branch becomes active again;
ensure symmetry with the removal logic used in
run_manager.remove_from_predecessors and adjust any bookkeeping
(_exclude_branch_conditionally, conditionally_excluded_vertices) accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 1a3caae2-221f-4c81-a202-f8912c3688e2
📒 Files selected for processing (3)
src/lfx/src/lfx/graph/graph/base.pysrc/lfx/src/lfx/graph/vertex/base.pysrc/lfx/src/lfx/graph/vertex/vertex_types.py
| # Reactivate common downstream vertices that have at least one active predecessor | ||
| # outside the stopped branch. Without this, merge/combine nodes that receive input | ||
| # from both active and inactive branches would be blocked forever. | ||
| if state == VertexStates.INACTIVE: | ||
| for v_id in list(visited): | ||
| vertex = self.get_vertex(v_id) | ||
| active_predecessors = [ | ||
| p_id | ||
| for p_id in self.predecessor_map.get(v_id, []) | ||
| if p_id not in visited and self.get_vertex(p_id).is_active() | ||
| ] | ||
| if active_predecessors: | ||
| self.mark_vertex(v_id, VertexStates.ACTIVE) | ||
| visited.discard(v_id) |
There was a problem hiding this comment.
Make the downstream reactivation pass iterative.
visited is a set, so this single pass is order-dependent. If a shared descendant is checked before its upstream merge vertex gets reactivated, it stays INACTIVE because the loop never revisits it. A stopped branch with ... -> merge_a -> merge_b can still deadlock depending on set iteration order. You can also drop the unused vertex local while fixing this, which is the Ruff failure on this hunk.
🔁 Suggested fix
if state == VertexStates.INACTIVE:
- for v_id in list(visited):
- vertex = self.get_vertex(v_id)
- active_predecessors = [
- p_id
- for p_id in self.predecessor_map.get(v_id, [])
- if p_id not in visited and self.get_vertex(p_id).is_active()
- ]
- if active_predecessors:
- self.mark_vertex(v_id, VertexStates.ACTIVE)
- visited.discard(v_id)
+ changed = True
+ while changed:
+ changed = False
+ for v_id in list(visited):
+ active_predecessors = [
+ p_id
+ for p_id in self.predecessor_map.get(v_id, [])
+ if p_id not in visited and self.get_vertex(p_id).is_active()
+ ]
+ if active_predecessors:
+ self.mark_vertex(v_id, VertexStates.ACTIVE)
+ visited.discard(v_id)
+ changed = True📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Reactivate common downstream vertices that have at least one active predecessor | |
| # outside the stopped branch. Without this, merge/combine nodes that receive input | |
| # from both active and inactive branches would be blocked forever. | |
| if state == VertexStates.INACTIVE: | |
| for v_id in list(visited): | |
| vertex = self.get_vertex(v_id) | |
| active_predecessors = [ | |
| p_id | |
| for p_id in self.predecessor_map.get(v_id, []) | |
| if p_id not in visited and self.get_vertex(p_id).is_active() | |
| ] | |
| if active_predecessors: | |
| self.mark_vertex(v_id, VertexStates.ACTIVE) | |
| visited.discard(v_id) | |
| # Reactivate common downstream vertices that have at least one active predecessor | |
| # outside the stopped branch. Without this, merge/combine nodes that receive input | |
| # from both active and inactive branches would be blocked forever. | |
| if state == VertexStates.INACTIVE: | |
| changed = True | |
| while changed: | |
| changed = False | |
| for v_id in list(visited): | |
| active_predecessors = [ | |
| p_id | |
| for p_id in self.predecessor_map.get(v_id, []) | |
| if p_id not in visited and self.get_vertex(p_id).is_active() | |
| ] | |
| if active_predecessors: | |
| self.mark_vertex(v_id, VertexStates.ACTIVE) | |
| visited.discard(v_id) | |
| changed = True |
🧰 Tools
🪛 GitHub Actions: Ruff Style Check / 0_Ruff Style Check (3.13).txt
[error] 1019-1019: Ruff check failed (F841). Local variable vertex is assigned to but never used.
🪛 GitHub Actions: Ruff Style Check / Ruff Style Check (3.13)
[error] 1019-1019: ruff check failed (F841): Local variable vertex is assigned to but never used
🪛 GitHub Check: Ruff Style Check (3.13)
[failure] 1019-1019: Ruff (F841)
src/lfx/src/lfx/graph/graph/base.py:1019:17: F841 Local variable vertex is assigned to but never used
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lfx/src/lfx/graph/graph/base.py` around lines 1014 - 1027, The downstream
reactivation loop over `visited` in the block checking `if state ==
VertexStates.INACTIVE` is single-pass and order-dependent, which can leave
shared descendants inactive; make this pass iterative by repeating the scan
until no vertices were reactivated in an iteration (e.g., loop while a `changed`
flag is true) so previously-skipped descendants get revisited; use
`self.predecessor_map`, `self.get_vertex(p_id).is_active()`, and
`self.mark_vertex(v_id, VertexStates.ACTIVE)` as-is inside the loop and remove
the unused `vertex` local variable to fix the Ruff warning.
| try: | ||
| result = await vertex.get_result(self, target_handle_name=key) | ||
| except ValueError: | ||
| result = None | ||
| self._handle_func(key, result) | ||
| if isinstance(result, list): | ||
| self._extend_params_list_with_result(key, result) | ||
| self.params[key] = result | ||
| if result is not None: | ||
| self.params[key] = result |
There was a problem hiding this comment.
Apply the same skip logic to list/dict inputs.
This only fixes scalar params. _build_list_of_vertices_and_update_params() still appends None, and _build_dict_and_update_params() still stores it, so merge/combine inputs backed by multi-source edges do not actually drop excluded branches.
🧩 Suggested fix
async def _build_dict_and_update_params(
self,
key,
vertices_dict: dict[str, Vertex],
) -> None:
"""Iterates over a dictionary of vertices, builds each and updates the params dictionary."""
for sub_key, value in vertices_dict.items():
if not self._is_vertex(value):
self.params[key][sub_key] = value
else:
- result = await value.get_result(self, target_handle_name=key)
- self.params[key][sub_key] = result
+ try:
+ result = await value.get_result(self, target_handle_name=key)
+ except ValueError:
+ result = None
+ if result is not None:
+ self.params[key][sub_key] = result
@@
async def _build_list_of_vertices_and_update_params(
self,
key,
vertices: list[Vertex],
) -> None:
"""Iterates over a list of vertices, builds each and updates the params dictionary."""
self.params[key] = []
for vertex in vertices:
- result = await vertex.get_result(self, target_handle_name=key)
+ try:
+ result = await vertex.get_result(self, target_handle_name=key)
+ except ValueError:
+ result = None
+ if result is None:
+ continue
# Weird check to see if the params[key] is a list
# because sometimes it is a Data and breaks the code
if not isinstance(self.params[key], list):
self.params[key] = [self.params[key]]🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lfx/src/lfx/graph/vertex/base.py` around lines 649 - 657, The current
skip logic only avoids setting scalar params when result is None but still
appends/stores None for list/dict inputs; update
_build_list_of_vertices_and_update_params and _build_dict_and_update_params to
mirror the scalar skip behavior: after obtaining result from vertex.get_result
(and after calling _handle_func), if result is None do not extend the params
list or store the dict entry; for lists, filter out None entries before calling
_extend_params_list_with_result (or skip calling it entirely when result is None
or empty after filtering); for dicts, only assign self.params[key] when result
is not None and contains valid entries. Ensure you reference and change
_build_list_of_vertices_and_update_params, _build_dict_and_update_params, and
keep the existing _handle_func(key, result) call sequence.
ogabrielluiz
left a comment
There was a problem hiding this comment.
Notes from a review pass on this change. The most concerning one is the params contract change in _build_vertex_and_update_params. I'm not convinced it does what the commit message claims; details inline at vertex/base.py:657.
No tests for any of the four behavior changes. The headline scenario (ConditionalRouter feeding a merge node) is currently uncovered in lfx. The only existing test that imports ConditionalRouterComponent is tests/unit/graph/graph/test_cycles.py, which is module-skipped because lfx doesn't ship langchain_openai. Could you add an end-to-end test that builds the failing graph from the original repro, runs it, and asserts the merge node produces output? Plus a unit test that ComponentVertex._get_result returns None for an excluded vertex would pin the new contract.
Other notes inline.
| self._extend_params_list_with_result(key, result) | ||
| self.params[key] = result | ||
| if result is not None: | ||
| self.params[key] = result |
There was a problem hiding this comment.
I think this is the load-bearing concern with the PR. The commit message says skipping the assignment lets components "use their natural input defaults instead of receiving NoneType", but self.params[key] was already pre-seeded with the upstream Vertex instance during build_params (via param_handler._set_params_from_normal_edge at param_handler.py:100: params[param_key] = self.vertex.graph.get_vertex(edge.source_id)). When result is None, the gate here just leaves that stale Vertex object in place. It then flows through loading.get_params(self.params) into set_attributes -> _validate_inputs (component.py:1102), which assigns input_.value = <Vertex> and re-triggers Pydantic validation against a Vertex object. The default fallback at component.py:1124 only fires when the key is absent, not when it holds a stale value.
Have you been able to reproduce the original Pydantic failure end-to-end after this change? I think the merge node still fails, just with a different message. If the intent is "absent", the fix would need to be self.params.pop(key, None) here instead of the conditional set.
There was a problem hiding this comment.
Fixed. Now the excluded/inactive check happens before calling get_result. When the vertex is excluded or inactive, self.params.pop(key, None) removes the pre-seeded Vertex object and returns early, so the key is truly absent and the component falls through to its natural input default at component.py:1124.
| result = await vertex.get_result(self, target_handle_name=key) | ||
| try: | ||
| result = await vertex.get_result(self, target_handle_name=key) | ||
| except ValueError: |
There was a problem hiding this comment.
I think this except ValueError is too broad. ComponentVertex._get_result raises ValueError for at least four distinct conditions in vertex_types.py: "has not been built yet", "Edge not found between X and Y", "Result not found for X", and "Result not found for X in edge". The new code silently turns all of them into None with no logging, so a misconfigured graph (missing edge, missing result) is indistinguishable from a legitimately excluded branch.
Could you narrow this? Either check vertex.id in self.graph.conditionally_excluded_vertices or not vertex.is_active() before calling get_result, or define a dedicated exception type for the excluded case. The blanket catch loses diagnosability for unrelated bugs.
There was a problem hiding this comment.
Fixed. The try/except ValueError block has been removed. The check for conditionally_excluded_vertices or not is_active() now happens before calling vertex.get_result(), so legitimate ValueErrors (edge not found, result not found, etc.) propagate normally. The silent catch is gone.
| # lists so that downstream merge/combine nodes can proceed with available | ||
| # inputs instead of waiting forever for data from the excluded branch. | ||
| for excluded_id in excluded: | ||
| self.run_manager.remove_from_predecessors(excluded_id) |
There was a problem hiding this comment.
remove_from_predecessors is destructive and there's no inverse. ConditionalRouter.iterate_and_stop_once (conditional_router.py:166) calls exclude_branch_conditionally on every cycle iteration. The top of this function (around line 1041) clears conditional_exclusion_sources for the source but does not restore run_predecessors. The only writers to run_predecessors are remove_from_predecessors (removal) and update_run_state/build_run_map (overwrite). So across cycle iterations where the router toggles branches, the run manager's predecessor accounting monotonically shrinks.
First iteration probably works. Have you tested this inside a loop (e.g. a router being re-evaluated each cycle)? I think we'd want a regression test for the multi-iteration case.
There was a problem hiding this comment.
Fixed. When clearing previous exclusions from conditional_exclusion_sources, the code now rebuilds predecessor entries for successors of previously excluded vertices from the full graph adjacency map, then restores them via run_manager.update_run_state(). This prevents the monotonic shrinkage of run_predecessors across cycle iterations.
| ] | ||
| if active_predecessors: | ||
| self.mark_vertex(v_id, VertexStates.ACTIVE) | ||
| visited.discard(v_id) |
There was a problem hiding this comment.
Reactivated vertices get dropped from visited here, and on the next line new_predecessor_map = {k: v ... if k in visited} filters by visited. So the reactivated merge vertex isn't a key in new_predecessor_map, and the subsequent run_manager.update_run_state(new_predecessor_map, ...) never resets that vertex's run_predecessors to the graph-level list. The reactivation flips vertex.state to ACTIVE but the run manager's accounting is left in whatever shape mark_vertex(INACTIVE) left it.
It happens to work in the simple case (the merge proceeds with only active predecessors, which is what you want), but the coupling is coincidental. I think this needs either an explicit comment explaining the invariant, or the reactivated vertex should be kept in visited and the cycle/predecessor filtering handled differently.
There was a problem hiding this comment.
Fixed two ways: (1) The reactivation loop is now iterative (while changed) so descendants of reactivated vertices are revisited in the next pass. (2) An explicit comment explains the coupling invariant: reactivated vertices are discarded from visited and therefore omitted from new_predecessor_map, but this is correct when coupled with exclude_branch_conditionally (called right after) which removes excluded branch vertices from successor predecessor lists via remove_from_predecessors.
|
|
||
| if default_value is UNDEFINED and self.graph: | ||
| if self.id in self.graph.conditionally_excluded_vertices or not self.is_active(): | ||
| default_value = None |
There was a problem hiding this comment.
Heads up that is_active() returns False for any non-ACTIVE state, not just conditionally-excluded ones. INACTIVE is set by cycle management at graph/base.py:581 independently of conditional routing. After this change, any cycle-inactivated vertex silently returns None from _get_result where it previously raised.
Is that intentional? The commit message frames the change as "conditionally excluded or inactive", but "inactive" covers a much wider set of cases. If you want to scope this strictly to the conditional-routing case, the check could be just self.id in self.graph.conditionally_excluded_vertices.
There was a problem hiding this comment.
Fixed. The check in vertex_types.py is now narrowed to just self.id in self.graph.conditionally_excluded_vertices. The cycle INACTIVE state is handled upstream in the callers (_build_vertex_and_update_params, _build_list_of_vertices_and_update_params, _build_dict_and_update_params in vertex/base.py), which check not vertex.is_active() before calling get_result.
| # Reactivate common downstream vertices that have at least one active predecessor | ||
| # outside the stopped branch. Without this, merge/combine nodes that receive input | ||
| # from both active and inactive branches would be blocked forever. | ||
| if state == VertexStates.INACTIVE: |
There was a problem hiding this comment.
Small thing: this whole block is gated on state == VertexStates.INACTIVE, with no symmetric path for state == VertexStates.ACTIVE. mark_branch is called with ACTIVE elsewhere in the codebase (e.g. branch resets). Worth a thought on whether a reset-to-active also needs to restore the run manager's predecessor accounting, since remove_from_predecessors from the earlier INACTIVE pass is still in effect.
There was a problem hiding this comment.
Fixed. Added a symmetric path for state == VertexStates.ACTIVE: when activating a branch, the code now also rebuilds predecessor entries for successors of the activated vertices from the full graph adjacency map. This restores any entries that were pruned by a previous INACTIVE+exclude pass via remove_from_predecessors.
- Use params.pop(key, None) instead of conditional set to truly absent excluded inputs, letting components fall through to their defaults - Check conditionally_excluded_vertices or is_active() BEFORE calling get_result, removing the blanket except ValueError - Make mark_branch reactivation loop iterative to handle descendant chain - Restore run_predecessors when clearing previous conditional exclusions to prevent monotonic shrinkage across cycle iterations - Narrow _get_result check to conditionally_excluded_vertices only; cycle INACTIVE state is handled in the callers - Add symmetric successor predecessor rebuild for mark_branch(ACTIVE) - Apply skip/pop logic to _build_list_of_vertices and _build_dict methods - Add E2E and unit tests for the fix Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
@ogabrielluiz thanks for the thorough review. I have addressed all 7 items:1. Params contract ( |
…esolves (forward-port langflow-ai#13418) (langflow-ai#13419) Forward-port of langflow-ai#13418 (merged to release-1.10.0) to main, so a nightly dispatched off main uses workflow definitions that publish the bundles. The nightly builds the resolved release branch's code, but the workflow files (release_nightly.yml) are read from the dispatch ref, so main needs the publish-nightly-bundles job too. Background: the nightly tagger renames lfx -> lfx-nightly, but the extension bundles were published as stable lfx-<name> wheels pinning lfx>=X.Y, so langflow-nightly dragged in a stable lfx that only exists as lfx-nightly -> unsatisfiable. Give the bundles their own nightly track: - update_lfx_version.py renames each src/bundles/* package to lfx-<name>-nightly, versions it <base>.dev<N>, and repoints the root langflow deps + [tool.uv.sources] entries. (No-op on main until bundles are declared there; kept in lockstep with release-1.10.0.) - release_nightly.yml publishes the nightly bundle wheels and gates publish-nightly-main on them.
…e (forward-port langflow-ai#13421) (langflow-ai#13422) fix(ci): re-enable migration-pip-venv now that nightly bundles resolve The PyPI langflow-nightly is pip-installable again, so the temporary `if: false` guard added on 2026-05-28 is no longer needed. langflow-ai#13418 (release-1.10.0) and its forward-port langflow-ai#13419 (main) gave the extension bundles their own nightly track: langflow-nightly now depends on lfx-arxiv-nightly / lfx-duckduckgo-nightly / lfx-ibm-nightly, which pin lfx-nightly==0.5.0.dev* instead of an unsatisfiable stable lfx>=0.5.0,<0.6.0. Verified against the published dev57 wheels: a dry-run resolve of langflow-nightly[postgresql] succeeds (549 packages, exit 0), pulling lfx-nightly==0.5.0.dev57 via the *-nightly bundle variants.
…low-ai#13326 to main) (langflow-ai#13427) fix: remove duplicate uv.lock from workspace member Forward-port of langflow-ai#13326 to main. The monorepo should keep only one uv.lock at the workspace root; src/backend/base/uv.lock was a stale duplicate. The scheduled nightly build runs from main's workflow definition, so create-nightly-tag still ran `git add ... src/backend/base/uv.lock` against a tree where the file no longer exists, failing with exit 128. - Delete src/backend/base/uv.lock - nightly_build.yml: drop base-dir `uv lock` regeneration and the base lockfile from `git add` - Remove COPY/bind-mount of the base lockfile from all Dockerfiles - Makefile: lock_base/lock now lock only the root workspace - changes-filter.yaml: drop the base lockfile path - .secrets.baseline: shift nightly_build.yml line number 305 -> 304 Fixes the failing nightly: https://github.com/langflow-ai/langflow/actions/runs/26699469476
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Non-squash union back-merge, ahead of cutting release-1.11.0 / release-1.10.1. - Preserves main's settings-mixin refactor (langflow-ai#13141): release-1.10.0's 40 new settings ported into the per-group mixins, verified field-for-field and behaviorally against release's monolith. - Keeps main's model-handling (langflow-ai#13191, auto-merged). - CI workflows, docling deps, component index, starter projects, AGENTS.md, .secrets.baseline resolved to release-1.10.0. main: 1.9.6 -> 1.10.0.
…ion record [gated] (langflow-ai#13528) * docs: record nightly→stable bundle cutover plan (gated on lfx 1.10.0) Add src/bundles/NIGHTLY.md documenting why langflow-nightly currently renames the bundles (lfx and lfx-nightly ship the same lfx/ import, so a stable bundle would co-install both and collide) and the deferred cutover (Approach A: canonical pre-releases; B: lfx as a bundle extra), gated on stable lfx 1.10.0 being published to PyPI. Also expand two docstrings in scripts/ci/update_lfx_version.py to state the deeper install-conflict reason, not just the resolve failure. No behavior change. * feat(ci): nightly Approach A — canonical pre-releases, drop nightly bundles [DRAFT/gated] (langflow-ai#13529) feat(ci): nightly Approach A — canonical pre-releases, drop nightly bundles DRAFT reference implementation of the nightly→stable-bundle cutover documented in src/bundles/NIGHTLY.md. Publishes the nightly under CANONICAL package names as .devN pre-releases instead of separate *-nightly distributions, so the stable lfx-* bundles resolve against a single canonical lfx (no dual-lfx install collision) and no nightly bundle packages are produced. - tag scripts (pypi/lfx/sdk_nightly_tag.py): count .devN against the canonical PyPI histories instead of the *-nightly projects - update scripts: stop renaming to *-nightly; set .devN versions; re-pin inter-package deps to exact canonical dev versions; delete the bundle rename/repin (update_lfx_dep_in_bundles, rename_bundles_for_nightly) - release_nightly.yml: publish canonical pre-releases; remove bundle build, dist-nightly-bundles artifact, publish-nightly-bundles job + its gate; verify canonical names; main wheel glob dist/langflow-*.whl - nightly_build.yml: drop the bundle git-add in the tag commit - NIGHTLY.md: Approach A marked implemented + activation gate + A1/A2 follow-ups Held as DRAFT: do not activate until stable lfx 1.10.0 is published AND the nightly base is the next minor (release-1.11.0). A 1.10.0.devN core sorts below 1.10.0 and would fail the bundles' >=1.10.0 floor. Stacked on langflow-ai#13528. (.secrets.baseline: incidental line-number shifts for the two workflows + prune of pre-existing stale Pokédex Agent.json entries.) * docs(ci): drop internal 'Approach A' label from nightly cutover comments Comment- and docstring-only change across scripts/ci/* and the two nightly workflows; no logic change. The two workflow edits stay single-line so .secrets.baseline line numbers are unaffected. src/bundles/NIGHTLY.md keeps its A/B decision-record framing intentionally. * feat(ci): make nightly consumers work with canonical pre-releases Follow-ups from the nightly cutover that are part of its blast radius (the nightly now publishes canonical `.devN` pre-releases, not `*-nightly` distributions): - version.py: derive the "Nightly" label from the `.dev` version marker, since the canonical `langflow`/`langflow-base` distribution matches first in the lookup. Keeps the startup banner and telemetry `package` field identifying nightlies. Adds a canonical-dev test; updates the base-dev assertion. - ci.yml check-nightly-status: query the canonical `langflow` project and pick the latest `.devN` release date instead of `langflow-nightly`'s `.info.version`. - db-migration-validation.yml: install the nightly as `langflow[postgresql]==<dev>` (pre-release) instead of `langflow-nightly[...]`; verify via version("langflow"). - src/lfx/README.md: nightly install is `uv pip install --pre lfx`. - NIGHTLY.md: rewrite the follow-ups section (these are addressed; Docker image, A2 meta-package, and website docs remain deferred by design). The `langflowai/langflow-nightly` Docker image name is intentionally unchanged. * fix(ci): correct nightly verify uv tree parsing + stale base-dep regex Addresses review of langflow-ai#13528: - release_nightly.yml LFX verify: `uv tree | grep lfx | head -n1` matches the bundle `lfx-ibm` first → 'Name lfx-ibm does not match lfx'. Root the tree with `uv tree --package lfx` so the first line is the lfx package itself. - release_nightly.yml base verify: under canonical naming `langflow-base` prints as a top-level `langflow-base v<ver>` line, so the old $2/$3 field parse read name="v0.10.0" and version="". Use `uv tree --package langflow-base` and $1/$2. - update_lf_base_dependency.py update_base_dep: regex only accepted ~=/==, so its CLI entry point couldn't match the current root dep `langflow-base[complete]>=0.10.0`. Add >= (parity with update_uv_dependency.py). The active nightly path uses update_uv_dependency.py and was unaffected. * docs(nightly): point NIGHTLY.md status at the activation gate, not draft state Per review of langflow-ai#13528: this file ships inside langflow-ai#13528 (langflow-ai#13529 was folded in), so the 'stacked on prep langflow-ai#13528' + 'held as a draft' framing is stale and misleading. Reword the status block to state the real guard is the activation gate (stable lfx 1.10.0 published AND next-minor base), not merge/draft state. Also reword the follow-ups heading 'decide before un-drafting' -> 'decide before activating'.
…solve (forward-port) The first 1.11.0.dev0 nightly failed its "Test Langflow Main CLI" step: the fork bump's sync_bundle_lfx_pin.py re-synced every bundle floor to lfx>=1.11.0, and PEP 440 sorts the nightly's 1.11.0.dev0 BELOW 1.11.0, so the workspace-built bundles (whose metadata shadows the satisfiable PyPI lfx-arxiv 0.1.1 during `uv pip install dist/*.whl`) reject the branch's own lfx while langflow-base pins it exactly — unresolvable. Floor at lfx>=X.Y.0.dev0,<(X+1).0.0 instead: X.Y.0.dev0 is the lowest version PEP 440 admits in the minor line, so every devN / rcN / final satisfies the floor while older lines and the next major stay excluded. This closes the NIGHTLY.md activation-gate hole structurally — future minor forks re-sync to a floor their own nightlies already satisfy. - sync_bundle_lfx_pin.py: lfx_floor_spec emits the .dev0 floor - port_bundle.py: mirrored _current_lfx_floor kept in step - bundle pyprojects restamped via the script (arxiv/docling/duckduckgo/ibm) - test_bundle_lfx_pin.py expectations updated (20/20 passing) - NIGHTLY.md gate section annotated with the post-activation fix uv.lock is unaffected (workspace lfx is recorded as an editable source with no specifier — which is also why uv sync/lock passed in the same job). The release.yml RC floor-relax sed still matches the new form; now redundant but harmless. No bundle version bump needed: published 0.1.1 floors >=1.10.0.rc0, satisfiable by the whole 1.11 line. Forward-port of ee659ca from release-1.11.0. Main is on the 1.10.0 line, so the bundle floors here restamp to lfx>=1.10.0.dev0,<2.0.0 via the same sync script; the next minor fork's make patch now produces a floor its own X.Y.0.devN nightlies satisfy.
…e unit-test step timeout (main) (langflow-ai#13584) fix(ci): rebalance backend test splits with measured durations + raise unit-test step timeout Unit Tests - Python 3.12 - Group 3 has been failing at 98-99% on the nightly: the job hits the nick-fields/retry 40-minute per-attempt timeout and pytest is SIGTERM'd mid-test, so it looks like one flaky last test when it is actually a deterministic timeout (and the internal retry plus run-level retries can never succeed). Root cause is twofold: 1. .test_durations was last regenerated ~May 2025 and covered only 2,219 of ~9,500 current unit tests, so pytest-split weighted 77% of the suite at the 0.73s average. The expensive client-fixture tests (test_webhook.py, test_login.py - each pays a full create_app + lifespan boot per test, 60-120s late in a CI run) clustered into group 3's tail, making it ~8-10 minutes slower than its siblings (33:15 on 3.13, >40min on 3.12 which is additionally slowed by astrapy disabling SSL connection reuse on Python 3.12.0-11). The weekly store_pytest_durations workflow that should refresh the file has been dying at the 6-hour GitHub job limit every week (serial full-suite run no longer fits), so the file silently froze. This regenerates the file from real per-test wall-clock measured in the passing Python 3.13 nightly jobs (run 27246228762 attempt 1, all 5 groups, parsed from the -vv xdist logs: per-worker start-to-start deltas). 9,508 tests now have measured durations; old entries are kept where no new measurement exists. Simulated least_duration split goes from one outlier group to 5 even groups (~24.6 min each) with the >50s tests spread 1-2 per group instead of 7 in one. 2. timeout_minutes 40 -> 50 gives headroom for matrix-cell variance (3.12 runs ~20% slower than 3.13) so a slightly slow cell degrades gracefully instead of burning 2x40min and failing the whole run. Follow-ups (not in this PR): fix store_pytest_durations to run with xdist or split groups so it fits the 6h limit; investigate the in-worker degradation that makes client-fixture boots cost 8-12s early in a run but 60-120s after ~30 minutes.
…ility (main) (langflow-ai#13589) fix(test): raise spawn-child join timeout in test_multi_process_visibility The test spawns a child via multiprocessing spawn context, which cold-imports the full langflow package (plus coverage's multiprocessing hooks in CI) before appending a single event. The 10s join timeout is routinely exceeded on a loaded CI runner sharing 4 vCPUs with a second xdist worker: in nightly run 27253229568 (Unit Tests - Python 3.12 - Group 5) the test failed all 12 executions (5 reruns x 2 step attempts), each rerun exactly 10s apart - the join deadline, not a product bug. Raise the liveness bound to 60s (join returns immediately when the child exits, so the passing case is unaffected) and kill the child on timeout so a hung spawn can't leak into later tests.
…ild (langflow-ai#13591) The first nightly on the 1.11 workspace (tag v1.11.0.dev0, run 27253229568) failed in Build Nightly Base Package within seconds: 'Base version format is incorrect'. The extraction (uv tree, grep langflow-base unanchored, awk field 3, first line) broke because uv tree now prints the langflow-base workspace-root line ('langflow-base v1.11.0.dev0', two fields) before any dependency line ('langflow-base[complete] v1.11.0.dev0' under the langflow root, three fields), so the first match yields an empty field 3 and the format check exits 1. uv tree's stderr is discarded, which made the real cause invisible in CI. Anchor to the root line and take field 2 instead - the exact pattern the langflow (main package) extraction in this same file already uses, which is why the main-package builds passed on the same tag. Verified both extractions print 1.11.0.dev0 against a local checkout of v1.11.0.dev0.
…ngflow-ai#13597) fix(test): gate models.dev background refresh out of tests Integration tests failed twice in nightly run 27260425158 with pyleak EventLoopBlockError - first Integration Tests 3.14, then 3.12 on the rerun, each time in a different test. The blocking stack points at refresh_models_dev_periodically: every app boot unconditionally starts a lifespan task that immediately fetches https://models.dev/api.json, so the request lands mid-test in whatever test happens to be running. Under pyleak's asyncio debug instrumentation the fetch blocked the loop 0.797s against a 0.2s threshold. Whichever test draws the short straw flakes - which is why it looked transient and moved between versions. Add a LANGFLOW_MODELS_DEV_REFRESH env gate (default unchanged: enabled) and disable it session-wide in the backend test conftest. Tests fall back to the bundled static model lists, which is also deterministic. Verified: with the gate set, app boot makes zero models.dev requests; the previously failing integration test passes.
…idation (langflow-ai#13599) Migration Test: pip/venv (stable -> nightly) failed deterministically on nightly run 27260425158 (twice, including a rerun): hint: langflow-base was requested with a pre-release marker (e.g., langflow-base==1.11.0.dev1), but pre-releases weren't enabled (try: --prerelease=allow) This is the first nightly publishing as a canonical .devN pre-release of the langflow distribution (nightly -> stable bundle cutover). The 'latest' branch of the upgrade step already passes --prerelease=allow, but the pinned-version branches do not. uv implicitly allows the pre-release for the directly requested ==X.Y.Z.devN pin, yet langflow's metadata pins langflow-base==X.Y.Z.devN transitively, and uv rejects transitive pre-releases unless they are enabled - so the install fails after the stable uninstall, sinking the migration test. Add --prerelease=allow to both pinned-version install lines.
…ack (langflow-ai#13603) The previous fix (langflow-ai#13599) added a global --prerelease=allow, which let UNRELATED dependencies resolve to alphas: on nightly run 27274206250 the stable->nightly upgrade pulled pydantic 2.14.0a1 + pydantic-yaml 1.6.1a1, and the pydantic alpha breaks langchain-core at import time (RunnablePassthrough pydantic ValidationError), failing the nightly boot right after a successful install. Clean installs were fine - only this upgrade path resolved the alpha combo. Scope pre-release eligibility to the langflow lockstep stack instead: uv accepts a pre-release when the package's own requirement carries a pre-release marker, but not via transitive pins, and the nightly chain is langflow -> langflow-base -> lfx -> langflow-sdk with exact ==devN pins. Request each directly; langflow-sdk versions independently (0.2.0.devN), so an explicit .dev0 floor marks it eligible while lfx's exact pin selects the version. The 'latest' branch gets the same treatment via .dev0 floors on all four. Verified by dry-run against PyPI: langflow/langflow-base/lfx at 1.11.0.dev2 + langflow-sdk 0.2.0.dev2 resolve with pydantic staying at stable 2.13.4.
…gflow-ai#13608) The create_release job passed the bare version (v stripped) as the release tag with no commit target, so when that tag did not exist GitHub minted a new lightweight tag at the default-branch HEAD -- the wrong commit, still carrying the previous version (main only adopts a release's version via the post-release back-merge). Every release since 1.8.2 shipped a stray bare tag (1.8.2, 1.8.3, 1.9.0-1.9.6, 1.10.0) pointing at a previous-version commit, and the GitHub release had to be manually re-pointed to the real vX.Y.Z tag after each release. - create_release now attaches the release to inputs.release_tag for stable releases; pre-releases keep their computed tag (e.g. 1.10.0rc1) but it is minted at the release commit via 'commit:'. - release-lfx.yml pins the minted lfx-v* tag to github.sha instead of the default branch. The validate-tag-format guard (langflow-ai#12847) only blocks at dispatch time; create_release was re-creating the very duplicates it guards against.
langflow-ai#13610) The create-release job references needs.validate-version.outputs.current_version in its generated release notes (the Full Changelog compare link), but validate-version was not in the job's needs array, so the expression evaluated empty and the link rendered as compare/v...lfx-vX.Y.Z (broken base). Add validate-version to the create-release needs. This adds no real serialization: create-release already waits on release-lfx, which transitively requires validate-version via run-tests, and the job's always() if-condition is unaffected. Flagged by actionlint: property "validate-version" is not defined in object type {build-docker, release-lfx}.
…n failure (langflow-ai#13586) * ci: fit weekly test-durations refresh inside the 6h limit and alert on failure The serial full-suite run no longer fits the 6h job limit; every weekly run since mid-2025 was cancelled at exactly 6h, so .test_durations silently froze and the 5 CI test groups drifted out of balance (the recurring "Group 3 times out at 99%" nightly failures). - Measure durations as a 5-group matrix (same pytest-split groups, same xdist and test selection as make unit_tests), ~45-60 min per group instead of >6h serial. Each group stores only its own tests' durations (clean-durations); a merge job unions the disjoint group files and sanity-checks coverage before committing anything. - Add job-level timeouts everywhere and a Slack alert job (LANGFLOW_ENG_SLACK_WEBHOOK_URL, same channel as nightly_build) so a silent freeze cannot recur unnoticed. - Label the auto-PR skip-nightly-check: the required CI Success check previously failed on these PRs whenever the nightly was red, which is why none of them (langflow-ai#6225..langflow-ai#8669) ever merged. - Dispatch ci.yml on the PR branch after creation: PRs created with the default GITHUB_TOKEN never trigger pull_request workflows, so the required checks otherwise never run. workflow_dispatch is exempt from that restriction and from the nightly gate. Optionally supports a DURATIONS_PR_TOKEN PAT for fully native triggering. - Request review/assign so the PR gets attention instead of rotting. - Commit only .test_durations (add-paths) and drop the unused ASTRA/OPENAI api_key_required test env (CI never runs those tests). * ci: paginate the stale-durations-PR close step pulls.list returns one page (30) and the repo has hundreds of open PRs, so months-old stale durations PRs never appeared in the results — that is how langflow-ai#6225..langflow-ai#8669 accumulated unclosed even while the workflow still ran. Paginate and additionally match on the update-test-durations head branch prefix. * chore: update test durations (langflow-ai#13587) Co-authored-by: erichare <700235+erichare@users.noreply.github.com> * ci: fail the Slack alert step on webhook HTTP errors Without --fail-with-body, curl exits 0 on a Slack 4xx/5xx (e.g. a rotated webhook), leaving the alert job green while no alert was sent. * ci: address durations workflow review questions --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: erichare <700235+erichare@users.noreply.github.com>
Co-authored-by: erichare <700235+erichare@users.noreply.github.com>
Summary
When a ConditionalRouter (If-Else) excludes one branch, merge/combine nodes at the intersection of active and excluded branches were blocked indefinitely. Downstream nodes could not proceed with available data from the active branch.
This fixes four points in the graph engine that caused the bottleneck:
mark_branch()marked shared downstream vertices as INACTIVE even with active inputs_get_result()raisedValueErrorfor excluded/inactive predecessorsNonefallback values were assigned to params, failing Pydantic validationChanges
graph/base.pymark_branch(): After marking vertices as INACTIVE, reactivate any vertex that has at least one active predecessor outside the stopped branch. This keeps merge/combine nodes runnable when they still receive data from the active path.exclude_branch_conditionally(): Remove conditionally excluded vertices from run_manager predecessor lists viaremove_from_predecessors(). This ensures downstream nodes no longer wait for data that will never arrive from the excluded branch.vertex/vertex_types.pyComponentVertex._get_result(): Before raisingValueErrorfor an unbuilt vertex, check if the vertex is conditionally excluded or inactive. If so, returnNoneinstead of crashing.vertex/base.pyVertex._build_vertex_and_update_params(): CatchValueErrorfromget_result()as a safety net for parallel builds and edge cases. Skip assigningNoneto params so components use their natural input defaults instead of receivingNoneTypewhich fails Pydantic validation.Fixes #12994
Summary by CodeRabbit
Release Notes