feat: durable background execution + HITL suspend/resume schema - #13633
Conversation
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
|
Important Review skippedToo many files! This PR contains 228 files, which is 78 over the limit of 150. To get a review, narrow the scope: Upgrade to a paid plan to raise the limit. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (228)
You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
✅ Migration Validation Passed All migrations follow the Expand-Contract pattern correctly. |
There was a problem hiding this comment.
@Cristhianzl can you take a look at these comments? Thanks!
-
[P1] Resume bypasses current flow permissions. The [resume route](
) checks job ownership but never reauthorizeslangflow/src/backend/base/langflow/api/v2/workflow.py
Lines 892 to 918 in 0b1e540
FlowAction.EXECUTE. A user whose shared access is revoked after suspension can still resume and execute the remaining flow. -
[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. -
[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 returnsfalse, allowing the replayed pause to surface another actionable card. Match the nestedhuman_input.request_idinstead. -
[P2] The configured timeout never autonomously takes the fallback path. The [component promises fallback 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.langflow/src/lfx/src/lfx/components/flow_controls/human_input.py
Lines 52 to 63 in 0b1e540
-
[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), whilemark_card_answeredignoresrequest_idand uses a mutablecard_message_id. If the continuation reaches another pause first, the first decision stamps the second card.
|
Thanks for the careful review @erichare — these were great catches. Quick status: 1, 2, 3 and 5 are being fixed (pushing shortly):
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: 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. |
|
Build successful! ✅ |
1 similar comment
|
Build successful! ✅ |
# 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
|
Build successful! ✅ |
|
Build successful! ✅ |
|
Build successful! ✅ |
1 similar comment
|
Build successful! ✅ |
|
Build successful! ✅ |
1 similar comment
|
Build successful! ✅ |
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.