Skip to content

feat: durable background execution + HITL suspend/resume schema - #13633

Merged
Cristhianzl merged 147 commits into
release-1.11.0from
cz/hitl-v2
Jul 14, 2026
Merged

feat: durable background execution + HITL suspend/resume schema#13633
Cristhianzl merged 147 commits into
release-1.11.0from
cz/hitl-v2

Conversation

@Cristhianzl

Copy link
Copy Markdown
Member

Objective

Open the human-in-the-loop groundwork (LE-1437) on release-1.11.0: a durable background-execution substrate whose jobs can later suspend for human input and resume without losing state.

ogabrielluiz and others added 26 commits June 1, 2026 15:39
Rebased onto release-1.10.0. The base independently rebuilt the v2
workflows backend (RBAC, body globals, share-aware fetch); keep our
forward design and conform its auth to that work:

1. Auth: keep get_current_user_for_workflow (session-or-API-key authN
   that does not hold a DB connection during the inline run, avoiding
   the SQLite lock contention api_key_security would cause) and enforce
   the base's RBAC on top: ensure_flow_permission(EXECUTE) before run,
   (READ) before status reconstruct, with widen_for_shares fetch.
2. Port the base's request-body globals onto the v2 WorkflowRunRequest.
   The X-LANGFLOW-GLOBAL-VAR-* headers stay supported (the Responses API
   passes globals that way); body globals win on conflict. Converters
   echo the effective globals via effective_globals.
3. Public endpoint keeps the v1 build_public_tmp posture
   (access_type==PUBLIC, run-as-owner); RBAC applies to the
   authenticated endpoint only.
4. Preserve the base's post-build KB-cache invalidation in the AG-UI
   build path.

The endpoint, AG-UI bridge, pluggable stream adapters, public endpoint,
and re-attach are unchanged.
The synchronous /api/v2/workflows response keyed every result under its
component id, so reading the answer meant knowing an id you can't predict.
Surface two additive fields:

- output_text: the flow's single text answer (ChatOutput/TextOutput). None
  when the flow has zero or multiple text outputs, so callers read outputs
  rather than the shortcut guessing which channel is the answer.
- session_id: echoes the resolved session so chat/memory callers can
  continue the same thread (v1 /run returned this; v2 had dropped it).

outputs is unchanged, so this is non-breaking.
…ponse

Pin the sync-response shortcuts on the v2 workflows endpoint:
- output_text surfaces the lone ChatOutput/TextOutput text and stays None for
  non-output message nodes, data-only flows, and multi-text flows
- session_id echoes the resolved session; the error response exposes neither
- each outputs entry exposes only {type, status, content, metadata}, with the
  component id carried by the dict key

Also drop the component_id kwarg the converter passed to ComponentOutput, which
has no such field and silently dropped it.
Replace the flat output_text shortcut with an `output` object carrying the
resolved text answer plus a `reason` that explains why it resolved that way
(single/multiple/none/non_string/failed), so a null answer is always
diagnosable instead of silently None. `reason` follows the LLM-domain
finish_reason/stop_reason convention, distinct from the lifecycle status.

Also add `display_name` to each ComponentOutput (the stable component id
stays the dict key) and a computed `has_errors` flag derived from errors.
Let a sync caller name the output(s) they want via output_ids so
output.text resolves deterministically (reason=single) on multi-output
flows instead of going null. Selection is steer-only: it picks the
answer among the named outputs without filtering the outputs map.

Invalid ids are rejected with 422 before the flow runs (and before any
job row is created), so a typo costs no compute. Resolution considers
selected outputs that actually fired, so branching flows resolve to
whichever candidate ran.
Give v2-workflows sync and the langflow stream protocol one parser. The
stream now emits a normalized "output" event per terminal output carrying
an OutputEvent (the ComponentOutput shape sync returns in outputs[id], plus
component_id). A shared build_component_output() backs both the sync
converter and the adapter, and the build loop ships authoritative vertex
metadata as an additive output_meta key on end_vertex (existing consumers
read build_data and ignore it).

This is access-pattern parity (one parser, same fields, same terminal set),
not byte-identical content: the stream reuses the v1 build path whose
display serialization differs from sync's run_graph output.
…ackend)

Turns v2 mode:background into a durable, in-API background execution service behind a BackgroundExecutionService facade. Adds the store layer (result/error columns, job_events durable milestone log, execution_signals control, heartbeat/lease, 3 migrations), the default backend (bounded executor, runner, in-memory live bus, liveness-aware single-flight orphan sweep), the v2 endpoint rewiring, and the real-instance test harness. Needs no new infra; works on the SQLite single-process install. The redis-scaled worker backend is stacked on top in a follow-up PR.
The hard_proof marker name was a vibe word that said nothing about what the
tests need. Rename it to real_services everywhere: the pytest marker
registration, the *_hard_proof.py test files, the Makefile target
(real_services_tests), the -m selector in migration-validation.yml, and the
CI job. real_services says what these tests require: real Postgres + Redis +
worker subprocesses. (integration was already taken for the external-API
suite under tests/integration.)
…kflows-agui

# Conflicts:
#	src/frontend/src/stores/flowStore.ts
The v2 public endpoint only ran validate_flow_for_current_settings and
skipped validate_public_flow_no_code_execution, which the v1
build_public_tmp path applies. A public flow containing a Python
interpreter/REPL (or the legacy Python Code Structured tool, Smart
Transform lambda) was therefore an unauthenticated server-side
code-execution primitive (report H1-3754930).

Mirror v1: import the validator and call it right after the
public-access gate. PublicFlowValidationError subclasses
CustomComponentValidationError, so the existing handler already
sanitizes it to a 400 'This flow cannot be executed.' without leaking
the blocked component class names.

Add a non-mocking test that builds a public flow with a real
PythonREPLComponent and asserts the sanitized 400 (verified RED: returns
200 without the gate).

LE-1389
…rtex builds

A completed background job's GET status 500'd with 'No vertex builds found
for job_id'. The background build path differed from the sync path twice:

1. generate_flow_events minted a fresh run_id instead of using job_id, so
   vertex builds were keyed by an id the status query never uses. Thread
   run_id through _stream_event_frames -> generate_flow_events and pass
   job_id from the background buffer so graph.run_id == job_id (the sync
   path already does graph.set_run_id(job_id)).

2. The SSE build loop (build_vertices) only persisted builds when log_builds
   was set and never passed job_id. Tie log_builds to job-tracked runs
   (run_id present) and pass job_id=graph.run_id on the persist call.
   Job-tracked runs also persist streaming terminal vertices so
   reconstruction is complete; the live build path (run_id is None) keeps
   its original behavior, so the v1 build path is unchanged.

Test: a real background run polled to completion, then GET status asserts a
reconstructed 200 (verified RED: 500 'No vertex builds found' before the
fix). Covers the non-streaming flow. v1 build path unchanged (35 build
tests pass); AG-UI suite 46 pass.

LE-1389
Adopt 1.11.0's composed group-mixin Settings for lfx settings/base.py (all v2 settings already present in the new groups); port the Python 3.14 cors_origins ['*']->'*' fix into groups/security.py.
…ows-bg-default

Port the 6 background-execution settings and background_backend_is_scaled into 1.11.0's RuntimeSettings group mixin; union the BACKGROUND_EXECUTION_SERVICE / TELEMETRY_WRITER_SERVICE additions in schema.py and deps.py.
… of dropping them

Parallel components stream tokens for different message ids interleaved.
The translator tracked a single open message: the first foreign token
closed the open message and tombstoned its id, so every later event for
it was dropped and its remaining text never reached the client.

Tokens for a message that cannot take the wire now buffer until the open
message genuinely ends (its add_message finalizer), then flush in arrival
order; complete messages landing mid-stream buffer the same way instead
of interleaving a second START. end/error drain all buffers before the
terminal event. The wire still carries at most one open text message, so
the stream stays AG-UI-conformant.
… purge removed buffers

A partial add_message re-fire (the agent path emits these at tool
start/end for a message it is still streaming) was treated as the
finalizer: it closed and tombstoned the id, so the post-tool answer
was dropped. Only a non-partial add_message finalizes now; state
defaults to complete, so payloads without properties are unchanged.

remove_message now purges a buffered message and tombstones its id,
so text the backend retracted is not flushed to the client later.
…ai/langflow into codex/v2-workflows-agui-merge

# Conflicts:
#	src/backend/base/langflow/api/v2/agui_translator.py
#	src/backend/tests/unit/api/v2/test_agui_translator.py
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Too many files!

This PR contains 228 files, which is 78 over the limit of 150.

To get a review, narrow the scope:
• coderabbit review --type committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

Upgrade to a paid plan to raise the limit.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 92938519-946b-4415-84f2-0f416d59de8c

📥 Commits

Reviewing files that changed from the base of the PR and between 1f8f98d and 602223f.

⛔ Files ignored due to path filters (1)
  • src/frontend/src/assets/hitl_icon.svg is excluded by !**/*.svg
📒 Files selected for processing (228)
  • .agents/skills/frontend-testing/SKILL.md
  • .agents/skills/frontend-testing/references/mocking.md
  • .secrets.baseline
  • docs/docs/Components/run-flow.mdx
  • docs/features/durable-execution-hitl.md
  • src/backend/base/langflow/agentic/flows/SystemMessageGen.json
  • src/backend/base/langflow/agentic/flows/TemplateAssistant.json
  • src/backend/base/langflow/alembic/versions/5e6d61582763_merge_message_user_id_and_hitl_heads.py
  • src/backend/base/langflow/alembic/versions/9cbe2f682e12_merge_hitl_checkpoints_and_release_heads.py
  • src/backend/base/langflow/alembic/versions/a1f4c9d27b30_add_job_checkpoints_table.py
  • src/backend/base/langflow/alembic/versions/c7412b389256_hitl_suspended_status_and_pause_resume_.py
  • src/backend/base/langflow/api/build.py
  • src/backend/base/langflow/api/utils/flow_utils.py
  • src/backend/base/langflow/api/v1/endpoints.py
  • src/backend/base/langflow/api/v1/mcp_utils.py
  • src/backend/base/langflow/api/v1/run_validation.py
  • src/backend/base/langflow/api/v2/hitl.py
  • src/backend/base/langflow/api/v2/workflow.py
  • src/backend/base/langflow/api/v2/workflow_background.py
  • src/backend/base/langflow/api/v2/workflow_execution.py
  • src/backend/base/langflow/api/v2/workflow_reconstruction.py
  • src/backend/base/langflow/helpers/flow.py
  • src/backend/base/langflow/initial_setup/starter_projects/Blog Writer.json
  • src/backend/base/langflow/initial_setup/starter_projects/Content Aggregator.json
  • src/backend/base/langflow/initial_setup/starter_projects/Custom Component Generator.json
  • src/backend/base/langflow/initial_setup/starter_projects/Deep Research Agent.json
  • src/backend/base/langflow/initial_setup/starter_projects/Document Q&A.json
  • src/backend/base/langflow/initial_setup/starter_projects/Financial Report Parser.json
  • src/backend/base/langflow/initial_setup/starter_projects/Hybrid Search RAG.json
  • src/backend/base/langflow/initial_setup/starter_projects/Instagram Copywriter.json
  • src/backend/base/langflow/initial_setup/starter_projects/Market Research.json
  • src/backend/base/langflow/initial_setup/starter_projects/Meeting Summary.json
  • src/backend/base/langflow/initial_setup/starter_projects/Memory Chatbot.json
  • src/backend/base/langflow/initial_setup/starter_projects/Multi Agent Flow.json
  • src/backend/base/langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json
  • src/backend/base/langflow/initial_setup/starter_projects/Price Deal Finder.json
  • src/backend/base/langflow/initial_setup/starter_projects/SEO Keyword Generator.json
  • src/backend/base/langflow/initial_setup/starter_projects/SaaS Pricing.json
  • src/backend/base/langflow/initial_setup/starter_projects/Sequential Tasks Agents.json
  • src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json
  • src/backend/base/langflow/initial_setup/starter_projects/Social Media Agent.json
  • src/backend/base/langflow/initial_setup/starter_projects/Text Sentiment Analysis.json
  • src/backend/base/langflow/initial_setup/starter_projects/Travel Planning Agents.json
  • src/backend/base/langflow/initial_setup/starter_projects/Twitter Thread Generator.json
  • src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json
  • src/backend/base/langflow/initial_setup/starter_projects/Youtube Analysis.json
  • src/backend/base/langflow/locales/en.json
  • src/backend/base/langflow/schema/content_types.py
  • src/backend/base/langflow/services/background_execution/live_bus.py
  • src/backend/base/langflow/services/background_execution/runner.py
  • src/backend/base/langflow/services/background_execution/service.py
  • src/backend/base/langflow/services/checkpoint/__init__.py
  • src/backend/base/langflow/services/checkpoint/factory.py
  • src/backend/base/langflow/services/checkpoint/store.py
  • src/backend/base/langflow/services/database/models/__init__.py
  • src/backend/base/langflow/services/database/models/jobs/__init__.py
  • src/backend/base/langflow/services/database/models/jobs/model.py
  • src/backend/base/langflow/services/jobs/exceptions.py
  • src/backend/base/langflow/services/jobs/service.py
  • src/backend/base/langflow/services/tracing/native.py
  • src/backend/base/langflow/services/tracing/native_callback.py
  • src/backend/base/langflow/services/tracing/service.py
  • src/backend/base/langflow/services/utils.py
  • src/backend/tests/unit/api/test_build_pause_seam.py
  • src/backend/tests/unit/api/test_resume_rerun_predecessors.py
  • src/backend/tests/unit/api/v1/test_mcp_utils.py
  • src/backend/tests/unit/api/v1/test_run_hitl_block.py
  • src/backend/tests/unit/api/v2/test_resume_route.py
  • src/backend/tests/unit/api/v2/test_workflow_background.py
  • src/backend/tests/unit/api/v2/test_workflow_reconstruction.py
  • src/backend/tests/unit/background_execution/test_durable_checkpoint_store.py
  • src/backend/tests/unit/background_execution/test_hitl_suspended_signals.py
  • src/backend/tests/unit/background_execution/test_input_deadline.py
  • src/backend/tests/unit/background_execution/test_job_status_enum_consistency.py
  • src/backend/tests/unit/background_execution/test_resume.py
  • src/backend/tests/unit/background_execution/test_runner_suspend.py
  • src/backend/tests/unit/base/mcp/test_mcp_util.py
  • src/backend/tests/unit/base/mcp/test_tool_iserror_propagation.py
  • src/backend/tests/unit/base/tools/test_run_flow_error_surface.py
  • src/backend/tests/unit/components/flow_controls/test_human_input.py
  • src/backend/tests/unit/components/models_and_agents/test_agent_events.py
  • src/backend/tests/unit/components/models_and_agents/test_mcp_component_cache.py
  • src/backend/tests/unit/components/models_and_agents/test_mcp_component_output.py
  • src/backend/tests/unit/schema/test_human_input_content_block.py
  • src/backend/tests/unit/services/background_execution/test_runner.py
  • src/backend/tests/unit/services/background_execution/test_service.py
  • src/backend/tests/unit/services/background_execution/test_standalone_slice.py
  • src/backend/tests/unit/services/tracing/test_native_callback.py
  • src/backend/tests/unit/services/tracing/test_native_tracer.py
  • src/backend/tests/unit/services/tracing/test_tracing_service.py
  • src/backend/tests/unit/test_redis_job_queue_service.py
  • src/frontend/jest.setup.js
  • src/frontend/src/CustomNodes/GenericNode/components/HumanInputNodeBadge/__tests__/card-anchoring.test.tsx
  • src/frontend/src/CustomNodes/GenericNode/components/HumanInputNodeBadge/__tests__/index.test.tsx
  • src/frontend/src/CustomNodes/GenericNode/components/HumanInputNodeBadge/index.tsx
  • src/frontend/src/CustomNodes/GenericNode/components/NodeInputField/index.tsx
  • src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/__tests__/computeDisplayHandle.test.ts
  • src/frontend/src/CustomNodes/GenericNode/index.tsx
  • src/frontend/src/components/core/chatComponents/ContentDisplay.tsx
  • src/frontend/src/components/core/chatComponents/HumanInputCard.tsx
  • src/frontend/src/components/core/chatComponents/__tests__/HumanInputCard.test.tsx
  • src/frontend/src/components/core/parameterRenderComponent/components/actionPickerComponent/AddButton.tsx
  • src/frontend/src/components/core/parameterRenderComponent/components/actionPickerComponent/__tests__/addButton.test.tsx
  • src/frontend/src/components/core/parameterRenderComponent/components/actionPickerComponent/__tests__/addFlow.test.tsx
  • src/frontend/src/components/core/parameterRenderComponent/components/actionPickerComponent/__tests__/inlineEdit.test.tsx
  • src/frontend/src/components/core/parameterRenderComponent/components/actionPickerComponent/addingContext.ts
  • src/frontend/src/components/core/parameterRenderComponent/components/actionPickerComponent/index.tsx
  • src/frontend/src/components/core/parameterRenderComponent/components/durationComponent/__tests__/durationComponent.test.tsx
  • src/frontend/src/components/core/parameterRenderComponent/components/durationComponent/index.tsx
  • src/frontend/src/components/core/parameterRenderComponent/index.tsx
  • src/frontend/src/components/core/playgroundComponent/chat-view/chat-messages/components/__tests__/chat-message.test.tsx
  • src/frontend/src/components/core/playgroundComponent/chat-view/chat-messages/components/bot-message.tsx
  • src/frontend/src/components/core/playgroundComponent/chat-view/chat-messages/utils/content-blocks.ts
  • src/frontend/src/components/core/playgroundComponent/chat-view/utils/__tests__/should-force-scroll.test.ts
  • src/frontend/src/components/core/playgroundComponent/chat-view/utils/should-force-scroll.ts
  • src/frontend/src/components/core/playgroundComponent/sliding-container/components/flow-page-sliding-container.tsx
  • src/frontend/src/constants/constants.ts
  • src/frontend/src/controllers/API/agui/__tests__/consume-background-events.test.ts
  • src/frontend/src/controllers/API/agui/__tests__/human-input-card.test.ts
  • src/frontend/src/controllers/API/agui/__tests__/run-flow-bridge.test.ts
  • src/frontend/src/controllers/API/agui/__tests__/use-restore-canvas-hitl.test.ts
  • src/frontend/src/controllers/API/agui/human-input-card.ts
  • src/frontend/src/controllers/API/agui/run-agent.ts
  • src/frontend/src/controllers/API/agui/run-flow-bridge.ts
  • src/frontend/src/controllers/API/agui/use-restore-canvas-hitl.ts
  • src/frontend/src/controllers/API/queries/traces/types.ts
  • src/frontend/src/controllers/API/queries/workflows/use-get-pending-workflows.ts
  • src/frontend/src/controllers/API/queries/workflows/use-resume-workflow.ts
  • src/frontend/src/icons/HumanInput/HumanInput.jsx
  • src/frontend/src/icons/HumanInput/index.tsx
  • src/frontend/src/icons/lazyIconImports.ts
  • src/frontend/src/locales/de.json
  • src/frontend/src/locales/en.json
  • src/frontend/src/locales/es.json
  • src/frontend/src/locales/fr.json
  • src/frontend/src/locales/ja.json
  • src/frontend/src/locales/pt.json
  • src/frontend/src/locales/zh-Hans.json
  • src/frontend/src/modals/toolsModal/components/toolsTable/RequiresApprovalToggle.tsx
  • src/frontend/src/modals/toolsModal/components/toolsTable/__tests__/requiresApprovalToggle.test.tsx
  • src/frontend/src/modals/toolsModal/components/toolsTable/index.tsx
  • src/frontend/src/pages/FlowPage/components/TraceComponent/FlowInsightsContent.tsx
  • src/frontend/src/pages/FlowPage/components/TraceComponent/TraceDetailView.tsx
  • src/frontend/src/pages/FlowPage/components/TraceComponent/TraceHitlBar.tsx
  • src/frontend/src/pages/FlowPage/components/TraceComponent/__tests__/TraceDetailView.test.tsx
  • src/frontend/src/pages/FlowPage/components/TraceComponent/__tests__/traceViewHelpers.test.ts
  • src/frontend/src/pages/FlowPage/components/TraceComponent/config/flowTraceColumns.tsx
  • src/frontend/src/pages/FlowPage/components/TraceComponent/traceViewHelpers.ts
  • src/frontend/src/pages/FlowPage/components/TraceComponent/types.ts
  • src/frontend/src/pages/FlowPage/index.tsx
  • src/frontend/src/stores/__tests__/flowStore.test.ts
  • src/frontend/src/stores/flowStore.ts
  • src/frontend/src/stores/hitlStore.ts
  • src/frontend/src/style/classes.css
  • src/frontend/src/types/chat/index.ts
  • src/frontend/src/types/zustand/flow/index.ts
  • src/frontend/tests/extended/features/edit-tools.spec.ts
  • src/lfx/README.md
  • src/lfx/src/lfx/_assets/component_index.json
  • src/lfx/src/lfx/base/agents/events.py
  • src/lfx/src/lfx/base/mcp/util.py
  • src/lfx/src/lfx/base/tools/component_tool.py
  • src/lfx/src/lfx/base/tools/run_flow.py
  • src/lfx/src/lfx/cli/_running_commands.py
  • src/lfx/src/lfx/cli/run.py
  • src/lfx/src/lfx/cli/serve_app.py
  • src/lfx/src/lfx/cli/serve_durable.py
  • src/lfx/src/lfx/components/flow_controls/__init__.py
  • src/lfx/src/lfx/components/flow_controls/human_input.py
  • src/lfx/src/lfx/components/logic/__init__.py
  • src/lfx/src/lfx/components/models_and_agents/agent.py
  • src/lfx/src/lfx/components/models_and_agents/agent_helpers/job_checkpoint_saver.py
  • src/lfx/src/lfx/components/models_and_agents/agent_helpers/tool_approval.py
  • src/lfx/src/lfx/components/models_and_agents/mcp_component.py
  • src/lfx/src/lfx/custom/custom_component/component.py
  • src/lfx/src/lfx/graph/checkpoint/__init__.py
  • src/lfx/src/lfx/graph/checkpoint/builder.py
  • src/lfx/src/lfx/graph/checkpoint/probe.py
  • src/lfx/src/lfx/graph/checkpoint/resume.py
  • src/lfx/src/lfx/graph/checkpoint/schema.py
  • src/lfx/src/lfx/graph/checkpoint/store.py
  • src/lfx/src/lfx/graph/exceptions.py
  • src/lfx/src/lfx/graph/graph/base.py
  • src/lfx/src/lfx/graph/vertex/base.py
  • src/lfx/src/lfx/helpers/flow.py
  • src/lfx/src/lfx/inputs/input_mixin.py
  • src/lfx/src/lfx/inputs/inputs.py
  • src/lfx/src/lfx/run/base.py
  • src/lfx/src/lfx/run/hitl.py
  • src/lfx/src/lfx/schema/content_types.py
  • src/lfx/src/lfx/schema/workflow.py
  • src/lfx/src/lfx/services/deps.py
  • src/lfx/src/lfx/services/durable/__init__.py
  • src/lfx/src/lfx/services/durable/models.py
  • src/lfx/src/lfx/services/durable/sqlite_checkpoints.py
  • src/lfx/src/lfx/services/durable/sqlite_store.py
  • src/lfx/src/lfx/services/schema.py
  • src/lfx/src/lfx/services/settings/groups/runtime.py
  • src/lfx/src/lfx/utils/constants.py
  • src/lfx/src/lfx/workflow/adapters/langflow.py
  • src/lfx/src/lfx/workflow/agui_translator.py
  • src/lfx/tests/unit/base/tools/__init__.py
  • src/lfx/tests/unit/cli/test_serve_durable.py
  • src/lfx/tests/unit/cli/test_serve_identity.py
  • src/lfx/tests/unit/components/models_and_agents/agent_helpers/test_tool_approval_request_id.py
  • src/lfx/tests/unit/components/models_and_agents/test_agent_create_agent.py
  • src/lfx/tests/unit/components/models_and_agents/test_job_checkpoint_saver.py
  • src/lfx/tests/unit/custom/component/test_concurrent_tool_invocation.py
  • src/lfx/tests/unit/graph/checkpoint/__init__.py
  • src/lfx/tests/unit/graph/checkpoint/_static_pauser.py
  • src/lfx/tests/unit/graph/checkpoint/test_graph_pause.py
  • src/lfx/tests/unit/graph/checkpoint/test_human_input_branch_exclusion.py
  • src/lfx/tests/unit/graph/checkpoint/test_resume.py
  • src/lfx/tests/unit/graph/checkpoint/test_resume_round_trip.py
  • src/lfx/tests/unit/graph/checkpoint/test_schema.py
  • src/lfx/tests/unit/graph/checkpoint/test_service_plumbing.py
  • src/lfx/tests/unit/graph/checkpoint/test_store.py
  • src/lfx/tests/unit/graph/test_orphaned_tool_mode.py
  • src/lfx/tests/unit/helpers/test_run_flow_hitl_guard.py
  • src/lfx/tests/unit/run/test_hitl_driver.py
  • src/lfx/tests/unit/run/test_hitl_noninteractive_warning.py
  • src/lfx/tests/unit/services/durable/__init__.py
  • src/lfx/tests/unit/services/durable/test_resume_after_restart.py
  • src/lfx/tests/unit/services/durable/test_sqlite_checkpoints.py
  • src/lfx/tests/unit/services/durable/test_sqlite_store.py
  • src/lfx/tests/unit/services/settings/test_settings_composition.py
  • src/lfx/tests/unit/workflow/adapters/test_human_input_event.py
  • src/lfx/tests/unit/workflow/test_agui_translator.py

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cz/hitl-v2

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.

@github-actions github-actions Bot added the enhancement New feature or request label Jun 12, 2026
@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Migration Validation Passed

All migrations follow the Expand-Contract pattern correctly.

@Cristhianzl
Cristhianzl changed the base branch from release-1.11.0 to feat/v2-workflows-agui June 12, 2026 17:00

@erichare erichare left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@Cristhianzl can you take a look at these comments? Thanks!

  1. [P1] Resume bypasses current flow permissions. The [resume route](

    job = await get_job_service().get_job_by_job_id(parsed_job_id)
    is_owner = job is not None and job.user_id is not None and job.user_id == current_user.id
    if job is None or job.type != JobType.WORKFLOW or not (is_owner or current_user.is_superuser):
    raise _not_found()
    from langflow.api.v2.hitl import is_decision_allowed, mark_card_answered
    if not await is_decision_allowed(parsed_job_id, request.decision or {}):
    raise HTTPException(
    status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
    detail={
    "error": "Invalid decision",
    "code": "INVALID_DECISION",
    "message": "decision.action_id is not one of the pending request's allowed_decisions.",
    "job_id": job_id,
    },
    )
    service = get_background_execution_service()
    if service._frame_source_factory is None: # noqa: SLF001
    service._frame_source_factory = _default_frame_source_factory # noqa: SLF001
    accepted = await service.resume_job(
    parsed_job_id,
    current_user,
    request_id=request.request_id,
    decision=request.decision or {},
    )
    ) checks job ownership but never reauthorizes FlowAction.EXECUTE. A user whose shared access is revoked after suspension can still resume and execute the remaining flow.

  2. [P1] Tool-approval request IDs are reused. Every approval from one Agent run gets the same [component_id:run_id](https://github.com/langflow-ai/langflow/blob/0b1e54003535b25e3067da2a34be3f0385d0db51/src/lfx/src/lfx/components/models_and_agents/agent_helpers/tool_approval.py#L85). A delayed retry for approval N can therefore be accepted during approval N+1, applying a decision to a different tool call. Include the LangGraph interrupt ID or another per-pause nonce.

  3. [P2] Answered cards are not recognized after reload. [cardAlreadyAnswered](https://github.com/langflow-ai/langflow/blob/0b1e54003535b25e3067da2a34be3f0385d0db51/src/frontend/src/controllers/API/agui/human-input-card.ts#L100-L121) searches for a synthetic outer message ID, but persisted messages reload with a database UUID. A regression test using the persisted shape returns false, allowing the replayed pause to surface another actionable card. Match the nested human_input.request_id instead.

  4. [P2] The configured timeout never autonomously takes the fallback path. The [component promises fallback after the timeout](

    DurationInput(
    name="timeout",
    display_name="Timeout",
    info="How long to wait for a human response before taking the fallback path (when enabled). "
    "Set to 0 to wait indefinitely.",
    options=["Minutes", "Hours", "Days"],
    value={"value": 3, "unit": "Days"},
    ),
    BoolInput(
    name="enable_fallback",
    display_name="Enable Fallback",
    info="Add a 'fallback' output taken when no user action is answered (e.g. after the timeout).",
    ), but timeout handling only reroutes a response received after expiry. With no response, the job remains suspended indefinitely. This needs a durable wakeup or different user-facing semantics.

  5. [P2] Card completion can target the next pause. The route enqueues the continuation before calling [mark_card_answered](https://github.com/langflow-ai/langflow/blob/0b1e54003535b25e3067da2a34be3f0385d0db51/src/backend/base/langflow/api/v2/hitl.py#L140-L159), while mark_card_answered ignores request_id and uses a mutable card_message_id. If the continuation reaches another pause first, the first decision stamps the second card.

@Cristhianzl

Copy link
Copy Markdown
Member Author

Thanks for the careful review @erichare — these were great catches. Quick status:

1, 2, 3 and 5 are being fixed (pushing shortly):

  • 1: the resume route now re-enforces flow:execute before applying a decision — job ownership alone no longer lets a revoked share resume the run.
  • 2: tool-approval request_ids now carry the LangGraph interrupt id as a per-pause nonce (component:run:interrupt_id), so a delayed retry for approval N gets a 409 during approval N+1 instead of being applied to the wrong tool call. Legacy 2-part ids from pre-upgrade checkpoints still resume.
  • 3: the frontend now matches answered cards by the nested human_input.request_id instead of the synthetic message id, so a card reloaded with its database UUID is still recognized (and stamped) correctly.
  • 5: mark_card_answered now receives the card id snapshotted before the continuation is enqueued, and refuses to stamp any card whose request_id doesn't match the decision — so a first decision can no longer land on a second pause's card.

On 4, I'd like to push back a little — the behavior is intentional, though I agree the component copy oversold it. The node timeout is deliberately lazy: reroute_decision_on_timeout compares the response time against paused_at + timeout when a decision arrives, so a late answer is rerouted to the fallback branch (or the expired sentinel) rather than taking the path the human picked. For the "nobody ever answers" case, the autonomous backstop is the global input-deadline sweep (sweep_input_deadlines via background_input_deadline_s), which fails overdue suspended runs so they don't linger forever.

Making the node timeout fire the fallback branch on its own would need a durable per-node timer that survives restarts and doesn't race a human decision landing at the same instant — real infrastructure I'd rather not bolt onto a PR this size. What I've done here instead is fix the component's info text so it no longer promises autonomous fallback, and I'll file a follow-up ticket for a durable wakeup that actually takes the fallback path on expiry. If you feel strongly it should land together, happy to discuss.

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Build successful! ✅
Deploying docs draft.
Deploy successful! View draft

1 similar comment
@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Build successful! ✅
Deploying docs draft.
Deploy successful! View draft

# Conflicts:
#	src/backend/base/langflow/initial_setup/starter_projects/Blog Writer.json
#	src/backend/base/langflow/initial_setup/starter_projects/Content Aggregator.json
#	src/backend/base/langflow/initial_setup/starter_projects/Custom Component Generator.json
#	src/backend/base/langflow/initial_setup/starter_projects/Deep Research Agent.json
#	src/backend/base/langflow/initial_setup/starter_projects/Document Q&A.json
#	src/backend/base/langflow/initial_setup/starter_projects/Financial Report Parser.json
#	src/backend/base/langflow/initial_setup/starter_projects/Hybrid Search RAG.json
#	src/backend/base/langflow/initial_setup/starter_projects/Instagram Copywriter.json
#	src/backend/base/langflow/initial_setup/starter_projects/Market Research.json
#	src/backend/base/langflow/initial_setup/starter_projects/Meeting Summary.json
#	src/backend/base/langflow/initial_setup/starter_projects/Memory Chatbot.json
#	src/backend/base/langflow/initial_setup/starter_projects/Multi Agent Flow.json
#	src/backend/base/langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json
#	src/backend/base/langflow/initial_setup/starter_projects/Price Deal Finder.json
#	src/backend/base/langflow/initial_setup/starter_projects/SEO Keyword Generator.json
#	src/backend/base/langflow/initial_setup/starter_projects/SaaS Pricing.json
#	src/backend/base/langflow/initial_setup/starter_projects/Sequential Tasks Agents.json
#	src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json
#	src/backend/base/langflow/initial_setup/starter_projects/Social Media Agent.json
#	src/backend/base/langflow/initial_setup/starter_projects/Text Sentiment Analysis.json
#	src/backend/base/langflow/initial_setup/starter_projects/Travel Planning Agents.json
#	src/backend/base/langflow/initial_setup/starter_projects/Twitter Thread Generator.json
#	src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json
#	src/backend/base/langflow/initial_setup/starter_projects/Youtube Analysis.json
#	src/lfx/src/lfx/_assets/component_index.json

@erichare erichare left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Build successful! ✅
Deploying docs draft.
Deploy successful! View draft

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Build successful! ✅
Deploying docs draft.
Deploy successful! View draft

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Build successful! ✅
Deploying docs draft.
Deploy successful! View draft

1 similar comment
@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Build successful! ✅
Deploying docs draft.
Deploy successful! View draft

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Build successful! ✅
Deploying docs draft.
Deploy successful! View draft

1 similar comment
@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Build successful! ✅
Deploying docs draft.
Deploy successful! View draft

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request lgtm This PR has been approved by a maintainer Release Label to be set only on release PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants