Skip to content

feat(graph): improve flow control for merge nodes downstream of conditional branches - #13372

Open
chalitbkb wants to merge 50 commits into
langflow-ai:release-1.10.0from
chalitbkb:fix/conditional-merge-flow-control
Open

feat(graph): improve flow control for merge nodes downstream of conditional branches#13372
chalitbkb wants to merge 50 commits into
langflow-ai:release-1.10.0from
chalitbkb:fix/conditional-merge-flow-control

Conversation

@chalitbkb

@chalitbkb chalitbkb commented May 27, 2026

Copy link
Copy Markdown

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:

  1. Conditionally excluded vertices remained in the run manager predecessor lists
  2. mark_branch() marked shared downstream vertices as INACTIVE even with active inputs
  3. _get_result() raised ValueError for excluded/inactive predecessors
  4. None fallback values were assigned to params, failing Pydantic validation

Changes

graph/base.py

  • mark_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 via remove_from_predecessors(). This ensures downstream nodes no longer wait for data that will never arrive from the excluded branch.

vertex/vertex_types.py

  • ComponentVertex._get_result(): Before raising ValueError for an unbuilt vertex, check if the vertex is conditionally excluded or inactive. If so, return None instead of crashing.

vertex/base.py

  • Vertex._build_vertex_and_update_params(): Catch ValueError from get_result() as a safety net for parallel builds and edge cases. Skip assigning None to params so components use their natural input defaults instead of receiving NoneType which fails Pydantic validation.

Fixes #12994

Summary by CodeRabbit

Release Notes

  • Bug Fixes
    • Improved graph execution stability when deactivating branches that feed into merge nodes, allowing downstream processing to continue with available inputs.
    • Enhanced error handling during vertex result retrieval to gracefully manage edge cases.
    • Fixed handling of conditionally excluded vertices in complex graph patterns.

Review Change Stack

erichare and others added 16 commits May 14, 2026 21:01
# 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.
@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0ee73837-d153-4f46-b7b2-3334686cbcc6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Graph 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.

Changes

Merge/Combine Node Availability

Layer / File(s) Summary
Graph branch deactivation and exclusion tracking
src/lfx/src/lfx/graph/graph/base.py
When a branch is marked INACTIVE, downstream merge vertices are reactivated if they have at least one active predecessor from outside the stopped branch. Conditionally excluded vertices are removed from run_manager predecessor lists so merge nodes no longer wait indefinitely for blocked upstream data.
Vertex result retrieval with state awareness
src/lfx/src/lfx/graph/vertex/base.py, src/lfx/src/lfx/graph/vertex/vertex_types.py
Vertex result retrieval wraps get_result() calls in try/except to treat ValueError as None and continue param updates. Cycle-edge target resolution checks if the vertex is conditionally excluded or inactive, allowing merge nodes to use available inputs instead of failing on missing upstream data.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 2 warnings)

Check name Status Explanation Resolution
Test Coverage For New Implementations ❌ Error PR modifies graph flow control but includes no new test files. Review comments cite 2 unaddressed issues: mark_branch lacks iterative reactivation and list/dict functions lack ValueError handling. Add regression tests for conditional merge flow (mark_branch, exclude_branch, ValueError handling) and implement missing iterative reactivation and error handling per review comments.
Test Quality And Coverage ⚠️ Warning No tests verify the core conditional merge functionality: ConditionalRouter + Combine, mark_branch reactivation, exclude_branch_conditionally, or ValueError handling. Add tests for: ConditionalRouter + Combine merge, mark_branch reactivation, exclude_branch_conditionally, ValueError handling in list/dict param building, and async patterns.
Test File Naming And Structure ⚠️ Warning PR adds critical flow control changes but no new tests cover the mark_branch reactivation, exclude_branch, or ValueError handling logic for conditional merge patterns. Add tests for ConditionalRouter + Combine merges, mark_branch reactivation for multi-input vertices, and list/dict edge inputs from excluded branches.
✅ Passed checks (6 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: improving flow control for merge nodes downstream of conditional branches, which directly addresses the core problem of blocked merge/combine nodes.
Linked Issues check ✅ Passed The PR addresses all primary objectives from issue #12994: enabling merge nodes to proceed with available inputs when upstream branches are conditionally excluded, supporting multiple inputs, and preventing indefinite blocking.
Out of Scope Changes check ✅ Passed All changes are tightly scoped to fixing flow control for merge nodes in conditional branches. No unrelated modifications or scope creep detected.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
Excessive Mock Usage Warning ✅ Passed Mock usage is appropriate: test_base.py uses real objects; test_vertex_base.py properly isolates ParameterHandler tests with 4 mocks for external dependencies, standard unit test design.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels May 27, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 lift

Restore scheduler predecessors when a previous exclusion is cleared.

This method removes previous_exclusions from conditionally_excluded_vertices, but it never rebuilds the run_manager.run_predecessors entries that were pruned by remove_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 by ConditionalRouter.iterate_and_stop_once() in src/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

📥 Commits

Reviewing files that changed from the base of the PR and between fb3d6ec and c5dcfac.

📒 Files selected for processing (3)
  • src/lfx/src/lfx/graph/graph/base.py
  • src/lfx/src/lfx/graph/vertex/base.py
  • src/lfx/src/lfx/graph/vertex/vertex_types.py

Comment thread src/lfx/src/lfx/graph/graph/base.py Outdated
Comment on lines +1014 to +1027
# 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
# 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.

Comment thread src/lfx/src/lfx/graph/vertex/base.py Outdated
Comment on lines +649 to +657
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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 ogabrielluiz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/lfx/src/lfx/graph/vertex/base.py Outdated
self._extend_params_list_with_result(key, result)
self.params[key] = result
if result is not None:
self.params[key] = result

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/lfx/src/lfx/graph/vertex/base.py Outdated
result = await vertex.get_result(self, target_handle_name=key)
try:
result = await vertex.get_result(self, target_handle_name=key)
except ValueError:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/lfx/src/lfx/graph/graph/base.py Outdated
]
if active_predecessors:
self.mark_vertex(v_id, VertexStates.ACTIVE)
visited.discard(v_id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels May 27, 2026
@chalitbkb

Copy link
Copy Markdown
Author

@ogabrielluiz thanks for the thorough review. I have addressed all 7 items:1. Params contract (vertex/base.py:657): Now uses self.params.pop(key, None) before get_result when the vertex is excluded/inactive, so the key is truly absent and the component falls through to its input default.2. Blanket except ValueError (vertex/base.py:651): Removed. The excluded/inactive check now happens before calling get_result, so legitimate ValueErrors propagate normally.3. remove_from_predecessors monotonic shrinkage (graph/base.py:1076): When clearing previous exclusions, predecessor entries are rebuilt from the full graph adjacency map and restored via update_run_state.4. Reactivated vertices dropped from visited (graph/base.py:1027): The reactivation loop is now iterative (while changed) to handle descendant chains, and a comment explains the coupling invariant with exclude_branch_conditionally.5. is_active() too broad (vertex_types.py:113): Narrowed to just conditionally_excluded_vertices. The cycle INACTIVE check is handled upstream in the callers in vertex/base.py.6. No symmetric ACTIVE path (graph/base.py:1027): Added successor predecessor rebuild for mark_branch(ACTIVE) to restore any entries pruned by a previous INACTIVE+exclude pass.7. Tests: Added 3 tests in test_conditional_merge.py — two E2E tests (TextInput/ChatInput variants) proving the merge node produces output, and one unit test confirming ComponentVertex._get_result returns None for an excluded vertex. All existing graph tests still pass (63 passed, no regressions).Let me know if there is anything else to adjust!

@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels May 27, 2026
erichare and others added 23 commits May 29, 2026 13:43
…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}.
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Jun 12, 2026
erichare and others added 3 commits June 12, 2026 19:49
…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>
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Jun 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants