Skip to content

feat(api): durable background execution service (store + default backend) - #13507

Merged
ogabrielluiz merged 47 commits into
release-1.11.0from
feat/v2-workflows-bg-default
Jul 10, 2026
Merged

feat(api): durable background execution service (store + default backend)#13507
ogabrielluiz merged 47 commits into
release-1.11.0from
feat/v2-workflows-bg-default

Conversation

@ogabrielluiz

@ogabrielluiz ogabrielluiz commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Builds on #13307. First of a two-PR stack. This one turns the v2 mode: background stub into an actual durable background execution service that needs no new infrastructure (it works on the default SQLite single-process install). The redis-scaled worker backend is stacked on top in a follow-up PR.

What it does

The old mode: background ran the flow as a fire-and-forget asyncio.create_task in the API worker, buffered SSE frames in a process-local dict, and persisted only job metadata. A restart killed the run, reattach/stop only worked on the worker that served the POST, and a completed job's output vanished. This replaces that with one BackgroundExecutionService facade over small primitives (store / queue / runner / live bus / control), wired here to the in-process default backend:

  • A bounded in-process executor (so background runs no longer swamp the event loop), the runner driving the existing build-vertex loop through a stream adapter, an in-memory live bus, control via an execution_signals DB row, and a liveness-aware single-flight startup sweep.
  • State, result, error, and a seq-ordered durable milestone log are all persisted, so a reattach replays the run and a completed job's result is fetchable forever, across a restart.

Same WorkflowJobResponse and SSE / Last-Event-ID contract. The change is additive: a client that works now keeps working, it just stops losing jobs.

Data model (three migrations)

result / error columns on job, a new job_events table (the durable milestone log, UNIQUE(job_id, seq), seq is the Last-Event-ID cursor), and a new execution_signals table (cross-worker stop without redis). No new JobStatus values. A heartbeat/lease lives in job_metadata so the sweep only ever reconciles genuinely orphaned runs.

Semantics

At-least-once for not-yet-started (QUEUED) work, at-most-once for in-flight work by default (a crashed run becomes FAILED, not silently re-run, since flows are not generally idempotent), and opt-in bounded retry per flow. Durable result/error plus a terminal job_events row on every terminal path including TIMED_OUT and CANCELLED. Cooperative stop at vertex boundaries. Cross-tenant safe: the idempotency key is scoped per user. Inline request globals are redacted from the persisted re-enqueue request (use named stored variables for secrets in background runs).

What's proven

Built TDD against real Postgres (and SQLite), no fakes on the durability paths. The default-backend crown jewels are covered: restart-survival (a fresh service instance against the same DB replays the durable milestones and returns, a hang fails the test), the orphan sweep only fails genuinely-orphaned runs (a heartbeating job on another replica is spared), bounded concurrency, and the Last-Event-ID reattach contract (live and durable-replay frames share one seq namespace, no gap or duplicate). The test harness also runs the durability tier against real Postgres in CI.

There were several rounds of review while building this; the findings (a restart reattach hang, a Last-Event-ID seq namespace mismatch, a multi-worker orphan-sweep race, a per-user idempotency leak, an attempt-cap race) are all fixed with regression tests in this PR.

Notes

HITL is deliberately deferred, but the lifecycle (extensible JobStatus) and the seq-based event log are shaped so a later resume-from-checkpoint can drop in.

Summary by CodeRabbit

Release Notes

  • New Features

    • Background execution for workflows with durable job persistence, async queuing, and status polling.
    • Public workflow endpoint with access controls, session isolation, and rate limiting.
    • Multiple streaming protocols (AG-UI, Langflow) for workflow event delivery.
    • Job event replay for reconnection to in-progress runs via Last-Event-ID.
    • Execution signal handling (stop/cancel) with heartbeat-based liveness detection and orphan recovery.
  • Tests

    • Comprehensive real-service integration tests for background execution.
    • Database migration validation and schema verification tests.

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.
@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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: abce8d0d-9db2-403d-97ca-719518366139

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

Introduces a durable background job execution system: three Alembic migrations add job_events, execution_signals, and result/error columns; a new BackgroundExecutionService coordinates an in-process executor, in-memory live bus, and JobRunner with SSE replay. The v2 workflow API is overhauled to support sync/stream/background modes and a new public endpoint. A pluggable stream adapter registry with langflow and agui implementations translates internal events to SSE frames. The frontend buildFlow pipeline migrates from a callback-based builder to runFlowAGUI using the AG-UI bridge.

Changes

Durable Background Execution + AG-UI Streaming v2

Layer / File(s) Summary
DB schema: migrations and job models
src/backend/base/langflow/alembic/versions/185482a2d715_...py, 8ce44e4858c6_...py, b026885b89c8_...py, src/backend/base/langflow/services/database/models/jobs/model.py, jobs/__init__.py, src/backend/base/langflow/services/database/models/__init__.py, src/backend/tests/unit/alembic/test_background_execution_migrations.py
Three Alembic migrations conditionally add result/error JSON columns to job, create job_events (ordered per-job event log with (job_id, seq) uniqueness), and create execution_signals (cooperative control signals with consumed_at). DB models are extended with JobEvent, SignalType, ExecutionSignal, and two new nullable JSONB fields on JobBase.
Workflow schemas and settings contracts
src/lfx/src/lfx/schema/workflow.py, src/lfx/src/lfx/services/settings/groups/runtime.py, src/lfx/src/lfx/services/settings/groups/security.py, src/backend/base/pyproject.toml, src/frontend/package.json, src/lfx/tests/unit/schema/*, src/lfx/tests/unit/services/settings/*
Adds WorkflowRunRequest (full v2 body with strict extra forbiddance), PublicWorkflowRunRequest (restricted, forced mode=stream), WorkflowMode, WorkflowOutput, OutputEvent, OutputReason; extends WorkflowExecutionResponse with session_id, output, and computed has_errors; adds links.events to WorkflowJobResponse; adds background execution runtime settings; adds public_flow_rate_limit_per_minute; adds ag-ui-protocol and @ag-ui/client dependencies.
Stream adapter registry, langflow and AG-UI adapters, AGUITranslator
src/lfx/src/lfx/workflow/__init__.py, src/lfx/src/lfx/workflow/adapters/__init__.py, src/lfx/src/lfx/workflow/adapters/agui.py, src/lfx/src/lfx/workflow/adapters/langflow.py, src/lfx/src/lfx/workflow/agui_translator.py, src/lfx/src/lfx/custom/custom_component/component.py, src/lfx/tests/unit/workflow/...
Defines StreamAdapter protocol, StreamEvent, global registry; LangflowAdapter passes events through and emits an extra normalized output event for terminal vertices; AGUIAdapter wraps a stateful AGUITranslator that converts Langflow events to AG-UI RunStarted/Finished/Error, StateDelta, StepStarted/Finished, and text/tool-call lifecycle events with buffering and deduplication. Both adapters classify events as durable or ephemeral for replay.
Workflow converters and output resolution
src/lfx/src/lfx/workflow/converters.py, src/backend/base/langflow/api/build.py, src/lfx/tests/unit/workflow/test_converters.py
Replaces parse_flat_inputs with ParsedWorkflowRun dataclass and parse_workflow_run_request; adds resolve_output_type, build_component_output, and _resolve_output for normalized component output; updates run_response_to_workflow_response and create_error_response to accept raw inputs dict and selected_ids; adds _output_meta_for_vertex for authoritative per-vertex metadata; gates track_job_status and on_flow_output hooks.
Background execution: executor, live bus, runner, service, JobService extensions
src/backend/base/langflow/services/background_execution/..., src/backend/base/langflow/services/jobs/service.py, src/backend/base/langflow/services/schema.py, src/backend/base/langflow/services/deps.py, src/backend/base/langflow/main.py, src/backend/base/langflow/services/auth/utils.py
InProcessExecutor (bounded async worker pool), InMemoryLiveBus (fan-out pub/sub with replay+tail dedup), JobRunner (processes frames, polls STOP signals at durable boundaries, writes heartbeats), BackgroundExecutionService (facade for submit/events/stop/orphan sweep); JobService gains append_event (optimistic seq with retry), write_signal/unconsumed_signals/consume_signals, heartbeat/lease/claim primitives, and sweep_orphans; startup reconciliation calls sweep_orphans_on_startup.
V2 workflow API overhaul: workflow.py, public endpoint, validation, reconstruction
src/backend/base/langflow/api/v2/workflow.py, src/backend/base/langflow/api/v2/workflow_public.py, src/backend/base/langflow/api/v2/workflow_validation.py, src/backend/base/langflow/api/v2/workflow_reconstruction.py, src/backend/base/langflow/api/v2/__init__.py, src/backend/base/langflow/api/router.py, src/backend/base/langflow/api/utils/flow_utils.py, src/backend/base/langflow/api/v1/chat.py, src/backend/base/langflow/api/utils/__init__.py
POST /workflows accepts WorkflowRunRequest with session-cookie/API-key auth; dispatches sync/stream/background; GET /workflows/{job_id}/events re-attaches via BackgroundExecutionService.events; POST /workflows/stop persists durable STOP signal before status flip; new POST /workflows/public enforces public-gating, virtual-flow scoping (CVE-2026-33017), per-IP rate limiting, and owner impersonation; pre-execution guards validate mode fields, owner override, and component policy; validate_public_files centralized from chat.py.
Frontend AG-UI bridge: run-agent, run-flow-bridge, flowStore migration
src/frontend/src/controllers/API/agui/run-agent.ts, src/frontend/src/controllers/API/agui/run-flow-bridge.ts, src/frontend/src/stores/flowStore.ts, src/frontend/tsconfig.json, src/frontend/tests/utils/withEventDeliveryModes.ts, src/frontend/src/stores/__tests__/flowStore.test.ts, src/frontend/src/controllers/API/agui/__tests__/*
buildWorkflowRunRequest/createWorkflowAgent build a native v2 workflow POST with AG-UI SSE; runFlowAGUI subscribes to the agent observable, dispatches handleAGUIEvent per frame, applies applyStateDelta JSON-patch ops to update per-node build status and flow pool, routes message/log/end events into stores; flowStore.buildFlow replaces buildFlowVerticesWithFallback with runFlowAGUI using per-run AbortController and uuidv5-derived virtual flow id for playground sessions.
Test infrastructure, real-service tests, and CI wiring
.github/workflows/migration-validation.yml, Makefile, pyproject.toml, src/backend/tests/conftest.py, src/backend/tests/unit/background_execution/*, src/backend/tests/unit/services/background_execution/*, src/backend/tests/unit/services/jobs/*, src/backend/tests/unit/api/v2/test_workflow*.py
Adds real_services pytest marker and CI background-real-service job provisioning Postgres and Redis; real-service fixtures in background_execution/conftest.py run actual Alembic migrations; comprehensive test suites for executor resilience, live bus dedup, runner durable semantics, service facade, orphan sweep, liveness/lease claiming, dedupe scope, SSE replay, facade contracts, AGUI API end-to-end, public endpoint security, and CI wiring verification.

Sequence Diagram(s)

sequenceDiagram
    rect rgba(70, 130, 180, 0.5)
        note over FlowStore,Server: Browser Build Flow (AG-UI path)
    end
    participant FlowStore as flowStore.buildFlow
    participant Bridge as runFlowAGUI
    participant Server as POST /api/v2/workflows
    participant BES as BackgroundExecutionService
    participant Runner as JobRunner
    participant JobSvc as JobService

    FlowStore->>Bridge: runFlowAGUI({flowId, mode="stream", signal})
    Bridge->>Server: POST (stream_protocol="agui", SSE)
    Server->>Server: parse WorkflowRunRequest → ParsedWorkflowRun
    Server->>Server: _stream_event_frames(adapter=AGUIAdapter)
    loop SSE frames
        Runner->>JobSvc: append_event(durable_seq)
        Runner-->>Server: (frame_bytes, event_type)
        Server-->>Bridge: SSE: RUN_STARTED / STATE_DELTA / RUN_FINISHED
        Bridge->>Bridge: handleAGUIEvent → applyStateDelta
        Bridge->>Bridge: update flowStore / messagesStore
    end
    Bridge-->>FlowStore: resolve (success/error/abort)
    FlowStore->>FlowStore: trackFlowBuild(buildInfo)
Loading
sequenceDiagram
    rect rgba(60, 179, 113, 0.5)
        note over Client,JobSvc: Background job submission and re-attach
    end
    participant Client as HTTP Client
    participant API as workflow.py
    participant BES as BackgroundExecutionService
    participant JobSvc as JobService
    participant Executor as InProcessExecutor
    participant Bus as InMemoryLiveBus

    Client->>API: POST /workflows (mode=background)
    API->>BES: submit(flow_id, request, user)
    BES->>JobSvc: create_job(dedupe_key, user_id)
    BES->>Executor: submit(job_id, coro_factory)
    API-->>Client: WorkflowJobResponse {job_id, links.events}

    note over Executor,Bus: Job runs asynchronously
    Executor->>Bus: publish(job_id, LiveFrame)
    Executor->>JobSvc: append_event / set_result

    Client->>API: GET /workflows/{job_id}/events (Last-Event-ID)
    API->>BES: events(job_id, last_event_id, user)
    BES->>JobSvc: read_events(after_seq=last_seq)
    BES->>Bus: reattach(job_id, last_seq, read_durable)
    Bus-->>Client: replayed durable frames + live tail SSE
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • langflow-ai/langflow#13307: Overlaps directly in src/backend/base/langflow/api/v2/workflow.py streaming/background execution and the stream adapter registry code paths introduced in both PRs.
  • langflow-ai/langflow#13351: Both PRs modify the v2 workflow pipeline to read and resolve request globals from the JSON body, overriding legacy X-LANGFLOW-GLOBAL-VAR-* headers.
  • langflow-ai/langflow#13371: Both PRs change share-aware flow authorization in src/backend/base/langflow/api/v1/chat.py and the deny_to_404 / _read_flow permission gating pattern used by the public endpoints.
🚥 Pre-merge checks | ✅ 8 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.72% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (8 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Test Coverage For New Implementations ✅ Passed PR includes comprehensive test coverage: 39+ new test files across backend, frontend, and LFX with 150+ test functions covering all new components, integration tests with real Postgres/SQLite, regr...
Test Quality And Coverage ✅ Passed Tests comprehensively cover new background execution implementation: 3,575+ lines of tests across 21+ new test files; 154+ async tests with proper pytest patterns; real services integration tests a...
Test File Naming And Structure ✅ Passed All test files follow correct naming patterns (test_*.py, *.test.ts), have descriptive function names explaining what is tested, are organized logically with proper fixtures and cleanup, and includ...
Excessive Mock Usage Warning ✅ Passed Test suite demonstrates appropriate mock usage. Core durability paths tested with real implementations (BackgroundExecutionService, JobService, InProcessExecutor, InMemoryLiveBus never mocked). Moc...
Title check ✅ Passed The title clearly summarizes the main change: adding a durable background execution service with storage and a default backend.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/v2-workflows-bg-default

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.

@ogabrielluiz

Copy link
Copy Markdown
Contributor Author

Stacked PR 1 of 2. The redis-scaled worker backend is in #13508 (based on this branch). Combined view: #13505.

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Migration Validation Passed

All migrations follow the Expand-Contract pattern correctly.

Comment thread src/backend/base/langflow/services/database/models/jobs/model.py
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.)
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Jun 6, 2026
Comment thread src/backend/base/langflow/api/v2/workflow.py
…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
Comment on lines +960 to +977
# Reconstruct response from vertex_build table (sync path persists
# those keyed by job_id). Background runs do not write vertex_builds
# keyed by job_id, so reconstruction finds nothing and raises
# ValueError — fall back to the durable Job.result the runner wrote
# so a completed background run reports completed instead of 500ing.
try:
return await reconstruct_workflow_response_from_job_id(
session=session,
flow=flow,
job_id=job_id_str,
user_id=str(current_user.id),
)
except ValueError:
return WorkflowExecutionResponse(
flow_id=flow_id_str,
job_id=job_id_str,
status=JobStatus.COMPLETED,
)

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.

@ogabrielluiz
What are we planning with the persisting final workflow outputs? Looks like we have the durable events that can be replayed to construct the final response and/or end_vertex events that can be marked as output nodes to get their outputs as the final results.

Should we store the terminal vertex outputs in a workflow response table or under jobs.result that the sync path and the async path both write to, to ensure a single place of results? Sync/Async is the method of execution, but the expectation of outcomes would always remain the same, right? Thoughts?

I suggest we write to to job.result the final outputs so that it proves to be the source of truth for any runs that have been completed. Do you think a dedicated WorkflowResults table is a better way considering jobs can be more than just workflows (Ingestion, Memory LLM Preprocessing etc which will lead to different shaped outputs save to the job.results JSONB format)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hey @dkaushik94, good question. Right now the two paths don't share one results home, and I think they should. The sync path reconstructs the response from vertex_build rows keyed by job_id, and the background path falls back to the durable Job.result the runner persisted (workflow.py around the reconstruct_workflow_response_from_job_id -> ValueError fallback). So the result is already in Job.result for background runs, just not for sync.

I lean toward Job.result as the single source of truth rather than a dedicated WorkflowResults table, for the reason you gave: jobs are more than workflows (ingestion, memory preprocessing), so a workflow-specific table would force a second results home the moment we add a non-workflow job. The JSONB result column already absorbs differently-shaped outputs.

The piece I'd want to nail down is the shape we write, so the reconstruction code can stop being path-specific and just read Job.result. I think that's worth a follow-up rather than this PR, since making sync write Job.result too touches the sync orchestration. What do you think, keep Job.result as the contract and converge both paths onto it next?

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.

Yes, I think we should converge to Job.results as the canonical result field for all jobs. Let's do a follow-up on this to have sync and async both converge. Thanks @ogabrielluiz 👍🏼

Do you want to track this somehow? I can take up the follow up as well if you need me to. Lmk.

@@ -85,11 +85,21 @@
from langflow.services.database.models.flow.model import FlowRead

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.

This is probably not worth doing in this PR, but I want to surface the pattern of instrumentation rather than the scope this PR is addressing, which is the infra for handling what is generated.

We currently use generate_flow_events() and have multiple call sites to emit dedicated events. A more maintainable and enforced way would be the ABC design.

Follow-up PR suggestion: Observable base & @observable decorator

(Suggesting as a separate PR on top of this one — not asking for changes here.)

Right now, every place we want to emit a workflow event has to thread event_manager through and call event_manager.on_something(...) by hand. That's fine when there are a few call sites, but it scales badly: each new event lives at the call site, custom component authors can silently emit nothing, and there's no contract that says "an observable thing produces these events."

I'd like us to explore moving to a pattern where any class that participates in execution — Graph, Vertex, Component, the LangChain callback bridge inherits from a small Observable base that requires three callbacks: one for when work starts, one for when work finishes, one for when it errors (and this is felxible we can have more methods for special cases). A decorator on each runnable method then handles the actual publication, reading the current run's event manager from context. Authors stop calling event_manager directly; they describe what their event looks like, and the decorator decides when to fire it.

The payoff is the kind that doesn't show up in a single diff but compounds across the codebase. New components get instrumented the moment they inherit. Forgetting to wire events fails at import instead of at runtime. The shape of every event stays consistent because the base defines it. And if we ever want to add a new layer of instrumentation, for example, span timing, or a structured trace ID — it's one change in the decorator, not a sweep across call sites. (I am still thinking about metrics that span across the flow; we might need to think about how to track data throughout the execution.)

The work is non-trivial because it also wants us to clean up how events are emitted today (some of the explicit calls in build.py would move into the callback methods of extracted helpers). That's why I think it belongs in its own PR.

On another note:

The dual engine path under /api/v2/workflows.

Tracing back the v2 endpoint, I noticed something worth flagging for a future conversation. Sync mode runs the graph through run_graph_internal -> graph.arun. Stream and background modes run it through _stream_event_frames -> generate_flow_events. Two different orchestrations of the same graph, behind one API.

They mostly do the same thing, but they don't share code. Vertex iteration, error construction, output collection, and event emission live in two places.

The shape I'd love us to reach is a single path through the engine where events fall out naturally as the call goes in and comes back. The graph is invoked, and a workflow_started event fires. Inside, each vertex and component announces itself as it begins and finishes. Inside those, LangChain callbacks emit their own milestones. As the call returns, events bubble back up: vertex done, component done, and finally workflow_ended (or whatever the langflow-dialect terminal event is, like end_message or something). Sync mode just doesn't ship them over the wire; stream and background do. The engine doesn't care.

That removes a whole class of "did we remember to update both paths?" bugs, and it's the natural home for the Observable pattern above. The decorator at each layer is what produces the rise-and-fall of events as the call descends and returns. Worth keeping in mind as v2 becomes the canonical execution path, the longer the two engines coexist, the more they'll drift.

I don't ask that we do this here, but should we follow up on this PR (x4) set with what I mentioned?
@ogabrielluiz @jordanrfrazier

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is a great writeup, @dkaushik94, and I'm on board with the direction. Two things stand out to me.

The dual-engine path under /api/v2/workflows is the one I'd prioritize. Sync going through run_graph_internal -> graph.arun while stream/background go through _stream_event_frames -> generate_flow_events is exactly the "did we update both paths?" trap, and it's the same drift that's behind the Job.result question on line 977 (two paths, two result homes). Converging them so events fall out of a single engine pass, with sync just not shipping them over the wire, is the right shape. I'd like to do that before v2 hardens into the canonical path, since it only gets more expensive the longer they coexist.

The Observable base + decorator is the natural payoff once there's one engine path, and I agree it's its own PR. The thing I'd want to be careful about is custom component authors: "fails at import instead of at runtime" is a real win, but we have to make sure the contract doesn't make a third-party component harder to write. As long as inheriting is the default and emitting is automatic, that holds.

So: not in this PR, but yes, let's follow up on the stack with the engine convergence first and the Observable pattern on top of it. @jordanrfrazier does that ordering match how you're thinking about v2?

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.

@ogabrielluiz
I think the ordering makes sense, and yes, we don't want to make DX very stringent by adding hard rules arbitrarily. Probably a warning saying, "Your component is not instrumented; to do so, please use the observable decorator and related method definitions," or something like that.

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.
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Jun 10, 2026

Copilot AI 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.

Pull request overview

Implements a durable background execution service for v2 workflows (default in-process backend) by persisting job results/errors and a seq-ordered event log, adding DB-backed stop signals, and wiring restart-safe orphan reconciliation on startup.

Changes:

  • Added background execution primitives (bounded executor, runner, live bus) and integrated them into /api/v2/workflows background run, stop, and events reattach flows.
  • Extended job persistence with result/error, a durable job_events log (UNIQUE(job_id, seq)), and execution_signals for cooperative stop; added liveness-aware orphan sweeping and per-user idempotency scoping.
  • Added a large test suite (including a real-services tier with Postgres + Redis) and CI/Makefile wiring to validate durability and concurrency contracts.

Reviewed changes

Copilot reviewed 63 out of 66 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/lfx/tests/unit/workflow/adapters/test_event_durability.py Tests durable vs ephemeral adapter classification
src/lfx/tests/unit/services/settings/test_settings_composition.py Adds background settings keys to composition test
src/lfx/tests/unit/services/settings/test_background_settings.py Tests background settings defaults + env overrides
src/lfx/tests/unit/services/settings/test_background_scaled_settings.py Tests scaled-backend selection + redis test URL
src/lfx/tests/unit/schema/test_workflow_run_request.py Covers new idempotency_key field round-trip
src/lfx/src/lfx/workflow/converters.py Parses idempotency_key; rebuilds response from stored outputs
src/lfx/src/lfx/workflow/adapters/langflow.py Defines durable milestone set + durability check
src/lfx/src/lfx/workflow/adapters/agui.py Defines durable milestone set + durability check
src/lfx/src/lfx/workflow/adapters/init.py Adds is_durable to adapter interface
src/lfx/src/lfx/services/settings/groups/runtime.py Adds background execution runtime settings + backend selection helper
src/lfx/src/lfx/schema/workflow.py Adds idempotency_key to request schema
src/backend/tests/unit/services/jobs/test_sweep_liveness.py Verifies liveness-aware orphan sweep behavior
src/backend/tests/unit/services/jobs/test_jobs_service.py JobService store-layer tests (result/error/events/signals/sweep)
src/backend/tests/unit/services/jobs/test_jobs_liveness.py Tests heartbeat, lease staleness, atomic attempt/claim primitives
src/backend/tests/unit/services/jobs/test_jobs_dedupe_scope.py Tests per-user dedupe scoping for idempotency keys
src/backend/tests/unit/services/jobs/init.py Package init for jobs service tests
src/backend/tests/unit/services/background_execution/test_service.py Facade-level default-backend integration tests
src/backend/tests/unit/services/background_execution/test_runner.py Runner durability + terminal-state tests
src/backend/tests/unit/services/background_execution/test_orphan_sweep.py Startup sweep behavior tests for queued/in-progress jobs
src/backend/tests/unit/services/background_execution/test_live_bus.py Live bus subscription + replay/reattach tests
src/backend/tests/unit/services/background_execution/test_executor.py Bounded executor behavior tests
src/backend/tests/unit/services/background_execution/conftest.py Real-redis fixture for scaled-backend timing tests
src/backend/tests/unit/services/background_execution/init.py Package init for background_execution service tests
src/backend/tests/unit/background_execution/test_store_real_services.py Real-services store tests across sqlite + postgres
src/backend/tests/unit/background_execution/test_real_services_make_target.py Verifies Makefile target for real-services tests
src/backend/tests/unit/background_execution/test_real_services_harness.py Validates real-services harness fixtures + marker registration
src/backend/tests/unit/background_execution/test_real_services_ci_wiring.py Verifies CI job wiring for real-services tier
src/backend/tests/unit/background_execution/test_multi_worker_boot_real_services.py Proves multi-worker boot sweep spares live heartbeated jobs
src/backend/tests/unit/background_execution/test_last_event_id_contract.py Pins Last-Event-ID namespace correctness (live vs durable)
src/backend/tests/unit/background_execution/test_head_to_head_deltas.py Head-to-head “safeguard off vs on” regression proofs
src/backend/tests/unit/background_execution/test_executor_real_services.py Real-services executor resilience tests
src/backend/tests/unit/background_execution/test_bounded_concurrency.py Real-services bounded concurrency proof
src/backend/tests/unit/background_execution/test_agui_replay_byte_identical.py Verifies replay bytes match live bytes (agui/langflow)
src/backend/tests/unit/background_execution/conftest.py Real-services DB/Redis fixtures + migrations binding
src/backend/tests/unit/background_execution/_side_effect_component.py Side-effect component used for exactly-once proofs
src/backend/tests/unit/background_execution/init.py Package init for background_execution real-services tests
src/backend/tests/unit/api/v2/test_workflow.py Updates v2 workflow tests for durable stop + error_detail
src/backend/tests/unit/api/v2/test_workflow_facade.py Endpoint contract tests for facade-backed background mode
src/backend/tests/unit/api/v2/test_workflow_background.py HTTP E2E tests for background submit/status/events/stop
src/backend/tests/unit/api/v2/test_workflow_agui.py Adds tweaks regression test; updates background/stop semantics tests
src/backend/tests/unit/alembic/test_background_execution_migrations.py Structure tests for new background-execution migrations
src/backend/tests/conftest.py Registers real_services marker
src/backend/base/langflow/services/schema.py Registers BACKGROUND_EXECUTION_SERVICE in ServiceType
src/backend/base/langflow/services/jobs/service.py Adds durable result/error/events/signals + liveness/dedupe primitives
src/backend/base/langflow/services/deps.py Adds dependency getter for BackgroundExecutionService
src/backend/base/langflow/services/database/models/jobs/model.py Adds result/error, JobEvent, ExecutionSignal, SignalType
src/backend/base/langflow/services/database/models/jobs/init.py Exposes new job-related models in package exports
src/backend/base/langflow/services/database/models/init.py Adds new job-related models to global model exports
src/backend/base/langflow/services/background_execution/runner.py Implements runner: persist durable frames + STOP reconciliation
src/backend/base/langflow/services/background_execution/live_bus.py Implements in-process live bus with durable replay + tail
src/backend/base/langflow/services/background_execution/factory.py Adds service factory + backend selection helper
src/backend/base/langflow/services/background_execution/executor.py Adds bounded in-process executor
src/backend/base/langflow/services/background_execution/init.py Background execution package init
src/backend/base/langflow/main.py Triggers background orphan sweep during app lifespan startup
src/backend/base/langflow/api/v2/workflow.py Wires background mode to facade; durable stop/events/status behaviors
src/backend/base/langflow/api/v2/workflow_execution.py Ensures tweaks reach streaming/background build loop
src/backend/base/langflow/api/build.py Applies request tweaks in streaming/background graph builds; gates memory hook
src/backend/base/langflow/alembic/versions/b026885b89c8_add_job_events_table.py Migration: add job_events table
src/backend/base/langflow/alembic/versions/8ce44e4858c6_add_execution_signals_table.py Migration: add execution_signals + enum
src/backend/base/langflow/alembic/versions/185482a2d715_add_result_error_to_job.py Migration: add job.result and job.error
pyproject.toml Registers real_services marker
Makefile Adds real_services_tests target
.secrets.baseline Updates baseline after workflow allowlist pragmas
.github/workflows/migration-validation.yml Adds background real-services CI job (Postgres + Redis)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +165 to +170
if item is _CLOSED:
return
if item.seq <= highest:
continue
highest = item.seq
yield item
Comment on lines +804 to 808
# in-flight runner racing to a terminal state reliably observes the stop
# and finalizes CANCELLED rather than overwriting it with COMPLETED/FAILED.
with contextlib.suppress(Exception):
await get_background_execution_service().stop_job(job_id, current_user)
await job_service.update_job_status(job_id, JobStatus.CANCELLED)
Comment thread src/backend/base/langflow/services/background_execution/executor.py Outdated
ogabrielluiz added a commit that referenced this pull request Jul 8, 2026
The base got its own release-1.11.0 merge (#13507), resolved independently of
this branch's, so the two collided on the files both touched. Resolutions:
workflow.py keeps this branch's HITL routes and job_id/resume threading; the
build.py and workflow_execution.py conflicts were comment wording only, taking
the base's. .secrets.baseline regenerated.
@ogabrielluiz
ogabrielluiz enabled auto-merge July 8, 2026 18:55
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Jul 8, 2026
…copg in real-service tests

The real-service CI job broke on `ModuleNotFoundError: No module named
'asyncpg'`. asyncpg was never declared anywhere in this repo; it arrived
transitively through `cuga`, which release-1.11.0 dropped when it moved the
root dep to `lfx-bundles[all-no-torch]` (#13886). Normalize the harness URL to
`postgresql+psycopg` instead: it is what `--extra postgresql` installs and what
DatabaseService already selects for async Postgres, so the test now exercises
the production driver rather than a stowaway.

Also fix a real reattach bug. The runner publishes ephemeral token frames
tagged with the last durable seq (they have no job_events row), while
reattach's tail skipped anything with `seq <= highest`. After replay left
`highest` at that same seq, every token delta was dropped until the next
durable milestone advanced it, so a reconnect mid-stream saw no tokens. Mark
frames durable/ephemeral and dedupe only the durable ones, which are the only
frames a replay can return.

Log instead of silently suppressing a failed stop_job signal, and drop a
comment block duplicated verbatim in InProcessExecutor.stop().
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Jul 8, 2026
@ogabrielluiz
ogabrielluiz requested a review from dkaushik94 July 10, 2026 16:54
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Jul 10, 2026
@github-actions github-actions Bot added the lgtm This PR has been approved by a maintainer label Jul 10, 2026
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Jul 10, 2026
@ogabrielluiz
ogabrielluiz added this pull request to the merge queue Jul 10, 2026
Merged via the queue into release-1.11.0 with commit 645b838 Jul 10, 2026
166 checks passed
@ogabrielluiz
ogabrielluiz deleted the feat/v2-workflows-bg-default branch July 10, 2026 20:33
jordanrfrazier pushed a commit that referenced this pull request Jul 13, 2026
…end) (#13507)

* feat: native v2 workflows endpoint with pluggable stream protocols

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.

* feat(api): add output_text and session_id to v2 workflow response

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.

* test(api/v2): cover output_text and session_id on the v2 workflow response

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.

* feat(api/v2): structured output with resolution reason on v2 response

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.

* feat(api/v2): add request-side output selection (output_ids)

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.

* feat(api/v2): emit per-output events on the langflow stream

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.

* feat(api/v2): durable background execution service (store + default backend)

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.

* test(background-execution): rename hard_proof marker to real_services

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

* fix(api/v2): enforce no-code-execution gate on public workflow endpoint

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

* fix(api/v2): reconstruct background workflow status from job-keyed vertex 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

* fix(api/v2): merge workflow AG-UI cancellation hardening LE-1389

* fix(api/v2): signal cross-worker workflow stops LE-1389

* fix(api/v2): report unconfirmed workflow stops LE-1389

* fix(api/v2): keep background workflows out of polling watchdog LE-1389

* fix(api/v2): buffer parallel messages in the AG-UI translator instead 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.

* fix(api/v2): gate AG-UI message finalization on non-partial state and 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.

* Fix AG-UI workflow lifecycle edges

* [autofix.ci] apply automated fixes

* fix(frontend): enable downlevelIteration for jest Set/Map iteration

ts-jest compiles with target es5; without downlevelIteration, [...set] and
for...of over a Set/Map emit ES5 that yields nothing. That silently broke the
AG-UI bridge tests: runningNodeIds spread, markRunningNodesFailed, and
restoreOriginalBuildStatuses all iterated empty. Production (Vite/SWC, modern
target) was never affected; only the ts-jest harness was. Fixes the 3 failing
jest tests on this branch with no other suite changes (4994/4994 pass).

* fix(api/v2): surface inactivated branch vertices over AG-UI

A branch component (If-Else, Conditional Router) reports its not-taken
vertices in build_data.inactivated_vertices, but the AG-UI translator only
emitted the branch node's own success/error status and dropped that list. The
canvas seeds every planned node as pending from vertices_sorted; skipped
vertices then get no build_start/end_vertex, so they stayed stuck on pending
instead of rendering as inactive (the v1 build path marked them INACTIVE).

The translator now appends an inactive STATE_DELTA op per inactivated vertex,
and the frontend bridge maps the new inactive status to BuildStatus.INACTIVE
and tears its edges down like a completed node. Fixes the If-Else regression
in general-bugs-reset-flow-run.spec.ts.

* fix(api/v2): dedupe repeated inactive node deltas in AG-UI stream

build.py keeps reporting a conditionally-excluded vertex in
inactivated_vertices on every subsequent end_vertex (the excluded set
persists until the ConditionalRouter clears it), so the translator was
putting the same inactive STATE_DELTA on the wire once per remaining
vertex. Track emitted inactive nodes and skip re-emitting; drop a node
from the set when it actually runs again (build_start/end_vertex) so a
loop re-activation can still re-emit inactive later.

* fix(api/v2): no duplicate WORKFLOW job row on durable background runs

A v2 background run created TWO JobType.WORKFLOW rows for one flow execution:
the durable row (submit()'s job_id, owned by JobRunner) plus an orphan keyed by
the run_id generate_flow_events mints, because the build pipeline's
track_job_status defaults True and the durable frame source never passed False.
The flow ran once (double bookkeeping), but every background run left a phantom
WORKFLOW row + job_events and double-fired the memory-base hook, skewing metrics.

Thread track_job_status through _stream_event_frames; pass False only from the
background frame source (the durable runner already owns the row + fires the
hook with the durable job_id). Stream/public paths keep default True. Also gate
build.py's memory-base hook fire behind track_job_status so background doesn't
double-fire. Adds a regression test (RED before fix: found 2 rows).

* test(api/v2): update stale workflow-stop tests for the durable design

These 3 tests targeted the removed queue-service stop helper
(_cancel_workflow_queue_job / get_queue_service), inherited via the
agui->bg-default merge and failing with AttributeError across the stack:

- test_stop_workflow_success: adapted to the durable stop path
  (revoke_task -> stop_job -> update_job_status(CANCELLED)).
- test_stop_workflow_allowed_for_legacy_job_with_no_user_id (IDOR): adapted to
  the durable mechanism; still asserts the ownership check does not block a
  legacy user_id=None row.
- test_stop_workflow_returns_503_when_queue_cancel_cannot_be_confirmed: dropped
  — the durable stop writes a best-effort STOP signal and always finalizes
  CANCELLED; the queue-service 'cannot confirm -> 503' path no longer exists.

test_workflow.py now passes 24/24.

* fix(api/v2): move FrameSourceFactory alias under TYPE_CHECKING

As a module-level runtime value, FrameSourceFactory = Callable[..., Any] is a
GenericAlias that passes isinstance(obj, type) but makes issubclass(obj, Service)
raise on Python 3.10/3.14. The service factory scans this module for Service
subclasses (services/factory.py:90), so the runtime alias crashed service
initialization on those interpreters with 'issubclass() arg 1 must be a class'
-> 'Could not initialize services', erroring out dozens of unrelated tests at
setup (3.13 was unaffected, which is why local runs passed).

The alias is only referenced in a lazy annotation (from __future__ import
annotations), so moving it + the Callable import under TYPE_CHECKING removes it
from the runtime namespace with no behavior change.

Verified: alias absent from runtime module namespace, factory scan finds
BackgroundExecutionService cleanly, durable service tests 26/26 pass.

* test(lfx): register background-execution Settings fields in the field-count gate

The durable background-execution work added six Settings fields
(background_max_concurrency, background_job_timeout, background_lease_ttl_s,
background_heartbeat_interval_s, background_watchdog_interval_s, test_redis_url)
without updating EXPECTED_FIELDS, so test_field_count_unchanged failed 152 != 146.
These are intentional bg-exec config; add them to the gate.

* fix(api/v2): restore "end" side-channel event in AG-UI workflow stream

The durable background-execution rewrite of workflow.py reverted
`side_channel_events` to its pre-"end" form, dropping the "end" event
from the AG-UI side-channel. That event carries `build_duration` to the
playground chat-view, and the message metadata badge only renders when
`hasDuration || hasTokens`. With build_duration gone the badge vanished,
failing the token-usage and shareable-playground "Finished In" regression
tests. Re-add "end" so the streaming playground path delivers it again.

* fix(api/v2): apply request tweaks on the streaming and background paths

The v2 workflows endpoint applied `tweaks` only on mode=sync. The stream
and background paths build the graph via the v1 build-vertex loop
(`generate_flow_events`), which never received the tweaks, so they were
silently dropped. The confusing symptom: a model passed via tweaks
surfaced as "A model selection is required", and any per-component
override was ignored on non-sync runs.

Thread `parsed.tweaks` into `generate_flow_events` and apply them to the
built graph via `vertex.update_raw_params`. We do not use the lfx
`process_tweaks_on_graph` helper because it only sets `vertex.params`,
which does not persist to runtime (the same bug
`lfx.base.tools.run_flow._process_tweaks_on_graph` works around). No-tweaks
runs are unchanged (guarded by `if tweaks`). Adds a streaming regression
test that overrides ChatInput via tweaks and asserts the value drives the run.

* fix(api/v2): return background run output from completed status

A completed background run's GET status returned a bare COMPLETED with an
empty `outputs` and a null `output`. The COMPLETED branch reconstructs
from `vertex_builds` keyed by job_id, which the durable path does not
write, so reconstruction raised ValueError and fell through to an empty
response; the result was only retrievable via a /events re-attach.

The runner now captures the terminal `output` events (the langflow
adapter's normalized ComponentOutput payloads) into `Job.result`, and the
status COMPLETED branch rebuilds the `outputs` map and resolved `output`
from them via `workflow_response_from_output_events`, matching the sync
response. agui-protocol runs emit no `output` events, so their status
stays result-less (the result remains on the /events log).

* fix(api/v2): address review findings on the v2 workflows endpoint

- recover session_id for completed background jobs from the persisted
  terminal message instead of always returning null, so GET status can
  continue the same chat/memory thread
- replay a user-cancel as a CUSTOM cancel marker + RUN_FINISHED (agui) and
  a `cancelled` terminal (langflow) instead of RUN_ERROR, so a re-attaching
  client no longer reads a deliberate stop as a failure
- cancel the evicted still-running buffer writer when the background-run
  registry is full, so it stops appending into a run no reader can find
- derive per-component status from the error artifact / valid flag instead
  of hardcoding COMPLETED, and stop the langflow adapter dropping `valid`
- throttle the unauthenticated public endpoint per IP and bound its
  input_value/session_id length
- document the sync-only scope of request-body globals
- document that live event re-attach is intentionally owner-only

* test(lfx): register public_flow_rate_limit_per_minute in settings composition

* refactor(v2 workflows): split workflow.py and address review blockers

Splits the ~1.5k-line workflow.py into focused modules and folds in the
execution-timeout and error-sanitization fixes from Cristhianzl's review of #13307.

- B1: workflow.py now holds only the four route handlers. Validation guards move
  to workflow_validation, the sync/stream run loop to workflow_execution, and the
  durable background machinery to workflow_background (layered, acyclic).
- I1: add workflow_execution_timeout (default 300) and apply a single wall-clock
  ceiling across sync, stream, background, and public via _stream_event_frames. A
  timeout becomes a sanitized terminal error and marks a background job failed.
- I3: the route error handlers no longer echo raw exception text. They return a
  generic, code-tagged message and log the full exception server-side.
- R1: remove the "commented out / future scope" comments that sat over live
  dataframe-extraction code in converters.py.
- R4: drop the worker-routing internals from the reattach 409 message.

Tests cover the timeout terminal-error path and the error-body sanitization, and
the settings field-count guard is updated for the new setting.

* refactor(lfx): extract v2 workflow contract layer into lfx.workflow

Moves the protocol-agnostic pieces of the v2 workflows API out of the langflow
backend into lfx so both the backend and `lfx serve` can share one contract.
First step toward giving lfx (the production runtime) the v2 workflows API.

- Move api/v2/adapters/, agui_translator.py, and converters.py to lfx/workflow/.
  They depend only on lfx.schema.workflow and ag_ui (already an lfx dep), so lfx
  carries the contract with zero langflow imports.
- Decouple the one langflow reference: converters typed run_response against
  langflow.api.v1.schemas.RunResponse (TYPE_CHECKING only). Replaced with a local
  RunResponseLike Protocol (outputs + session_id), the only attributes used.
- Repoint the six backend v2 workflow modules to import from lfx.workflow.
- Move the five protocol-agnostic contract tests into src/lfx/tests/unit/workflow/
  (run in the lfx-only env). test_output_event_parity and test_workflow_agui stay
  in langflow (they need langflow.api.build) with repointed imports.

Coverage unchanged: 201 contract tests pass in the lfx-only env, 191 backend v2
tests pass; 392 total, same as before the move.

* fix(background-execution): prevent worker deadlock on stop() under Python 3.10

The bounded executor's worker awaited the in-flight job task with a bare
`await task`. On Python 3.10, when stop() cancels a worker while its job
task is finishing, the awaiter's wakeup is lost and the event loop idles
forever in select(), deadlocking stop(). Await via a done-callback Event
(the same mechanism stop()'s own asyncio.gather already uses), which
delivers the wakeup reliably; task.result() preserves the cancellation
and exception semantics of the bare await.

* fix(background-execution): keep ephemeral frames on reattach, pin psycopg in real-service tests

The real-service CI job broke on `ModuleNotFoundError: No module named
'asyncpg'`. asyncpg was never declared anywhere in this repo; it arrived
transitively through `cuga`, which release-1.11.0 dropped when it moved the
root dep to `lfx-bundles[all-no-torch]` (#13886). Normalize the harness URL to
`postgresql+psycopg` instead: it is what `--extra postgresql` installs and what
DatabaseService already selects for async Postgres, so the test now exercises
the production driver rather than a stowaway.

Also fix a real reattach bug. The runner publishes ephemeral token frames
tagged with the last durable seq (they have no job_events row), while
reattach's tail skipped anything with `seq <= highest`. After replay left
`highest` at that same seq, every token delta was dropped until the next
durable milestone advanced it, so a reconnect mid-stream saw no tokens. Mark
frames durable/ephemeral and dedupe only the durable ones, which are the only
frames a replay can return.

Log instead of silently suppressing a failed stop_job signal, and drop a
comment block duplicated verbatim in InProcessExecutor.stop().

* fix(migrations): merge alembic heads (mcp_server + execution_signals)

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
erichare added a commit that referenced this pull request Jul 13, 2026
…rve (#13818)

* feat(lfx): per-request-isolated flow execution for lfx serve

Run `lfx serve --workers N` flows under per-request process isolation so
cross-request os.environ credential leakage is structurally impossible,
while sharing the warm library + flow graphs across workers via COW.

- gunicorn --preload + UvicornWorker + max_requests=1: the master builds and
  warms the FlowRegistry once and forks workers (copy-on-write); each worker is
  recycled after one request.
- serve_preloaded_app.py: import-time master entrypoint (build_registry_from_env
  + gc.freeze) inherited by forks.
- serve_gunicorn.py: LFXGunicornApp process manager.
- _EXECUTE_GUARD asyncio.Semaphore(1) + guarded_execute around
  execute_graph_with_capture (run + stream), closing the async concurrent-overlap
  window that max_requests=1 alone leaves open.
- commands.py: _launch_workers launches gunicorn on Unix; refuses --workers>1 on
  Windows (gunicorn is Unix-only).
- credentials.py: gate the post-DB-miss SECRET_KEY-rotation env fallback behind
  is_env_fallback_disabled() so no_env_fallback is honored (A3).
- FlowRegistry.get logs store reconstruction so per-request rebuild cost of
  uploaded flows is observable.
- gunicorn>=22.0 added as a platform-gated (non-Windows) lfx dependency.

Tests: process-model (preload/warm/guard/fork-safety), synchronous run contract,
no_env_fallback credential regression; existing multi-worker serve tests migrated
to the gunicorn launcher. 173 serve+credentials tests green.

* Add pre-fork hook

* feat(lfx): add opt-in --reset-environ and --sync-workers to lfx serve

Two opt-in flags for per-request isolation in multi-worker lfx serve, both defaulting to the existing committed behavior:

- --reset-environ: snapshot/restore os.environ around each flow run so a flow's env mutations (or request-scoped credentials) cannot leak into the next request served by the same warm worker (gated by LFX_SERVE_RESET_ENVIRON; read per request in guarded_execute).

- --sync-workers: serve via gunicorn's blocking sync worker behind an a2wsgi ASGI->WSGI bridge so the kernel routes each request to an idle worker (one whole request per worker at a time) instead of queueing behind an in-flight request on a busy async worker. The bridge is built lazily post-fork; refused on Windows.

Also fix LFXGunicornApp.load() to honor the app import string (it hardcoded the ASGI app, which fed the WSGI sync worker the wrong callable), and declare a2wsgi as a Unix-only dependency alongside gunicorn.

* feat(lfx): add --timeout flag for lfx serve worker timeout

Expose gunicorn's worker timeout (previously hardcoded at 120s) as --timeout, on both serve_command and the CLI wrapper. A worker that doesn't complete a request within --timeout seconds is killed and restarted.

Matters most under --sync-workers: a blocking sync worker cannot heartbeat mid-request, so long-running flows need a higher timeout. Default stays 120s (unchanged behavior); no effect on the Windows uvicorn fallback.

* fix(lfx): register VariableService in serve so request-scoped global_vars resolve

lfx serve never registered a VariableService, so get_variable_service() returned
None in the worker. Every credential path that resolves through it
(get_api_key_for_provider, get_all_variables_for_provider, model_utils, KB
connectors) therefore never consulted the request scope and fell back to
os.environ -- which --no-env-fallback then blocks. As a result, request-scoped
global_vars could not supply a model/agent provider API key unless the flow's
api_key field was explicitly wired to load the variable.

Register the minimal in-memory VariableService in create_multi_serve_app
(idempotent; a real DB-backed service is left untouched), making all of those
paths request-scope-aware at once. Guard the MCP component's DB-only
get_all_decrypted_variables call with hasattr so the minimal service is skipped
cleanly instead of raising into its broad except.

Verified end-to-end: Basic Prompting and Simple Agent starter flows execute
successfully under --no-env-fallback with the OpenAI key supplied only via
global_vars, while a request without global_vars still fails (no env leakage).

* refactor(lfx): rework lfx serve worker/concurrency flags

- Remove --limit-concurrency; --use-sync-workers covers bounded concurrency
- Rename --sync-workers to --use-sync-workers/--use-async-workers (default async)
- Default --max-requests to periodic recycling (~1000, 10% jitter) for memory
  hygiene; 0 disables, explicit N overrides
- Make Windows/a2wsgi flag refusals loud (typer.echo, not verbose_print)
- Declare uvicorn[standard] so standalone lfx installs get uvloop/httptools
- DRY: shared serve --help strings (_serve_help) + env context manager in
  _launch_workers
- Document --use-sync-workers / --reset-environ as the per-request isolation
  mechanisms (async --max-requests is worker hygiene, not isolation)

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* fix(lfx): warn on --timeout with single worker; drop dead limit-concurrency env

- Make --timeout an int|None sentinel (matching --max-requests) so the
  single-worker path can warn that it is ignored instead of silently
  honoring it; None resolves to DEFAULT_TIMEOUT (120) for gunicorn.
- Remove the unused _SERVE_LIMIT_CONCURRENCY_ENV constant.
- Dedup the FakeGunicornApp test stubs behind _make_fake_gunicorn_app.

* update dep docs

* simplify cmmments

* skip gunicorn threads in ghost finder

* chore: auto-bake note keys and regenerate backend locales/en.json [skip ci]

* fix(lfx): address review comments on serve worker flags

- normalize reset_environ/sync_workers OptionInfo sentinels for direct
  serve_command() callers (truthy sentinels silently enabled the flags)
- _exported_env now restores overwritten env vars instead of deleting them
- clarify MAX_REQUESTS help: strict isolation needs --reset-environ, or
  --use-sync-workers WITH --max-requests 1 (sync worker alone is not isolation)
- correct Windows fallback comment in pyproject
- assert LFX_SERVE_RESET_ENVIRON cleanup; reset preloaded module state in test
- add synchronous error-contract test for /run

* chore: auto-bake note keys and regenerate backend locales/en.json [skip ci]

* fix(lfx): close reset-environ auth race and warn on unisolated multi-worker

- guarded_execute restores os.environ by diff instead of clear()+update(): clear()
  empties key-by-key and could race a threadpool-run verify_api_key into a 401
- snapshot the API key at app startup (app.state) so auth never reads live os.environ
  per request; lazy-cache fallback preserves post-creation config (tests)
- warn when --workers > 1 has no per-request isolation (no --reset-environ and not
  --use-sync-workers with --max-requests 1)
- wrap the gunicorn import with a friendly error, matching the a2wsgi check
- xfail(strict=False) for the timing-dependent async-recycle leak test
- --workers help: gunicorn on Unix, uvicorn on Windows
- tests: diff-restore add/change/delete + no-clear lock, cached-key auth, isolation warning

* chore: auto-bake note keys and regenerate backend locales/en.json [skip ci]

* fix: prevent LangfuseResourceManager deepcopy failure in Agent/Tool Calling Agent (#13966)

* fix: prevent LangfuseResourceManager deepcopy failure in Agent components

Adds __deepcopy__ to _RootRunReparentingCallbackHandler so that
deepcopy(component) calls in component_tool.py no longer trigger
LangfuseResourceManager.__new__() with no credential kwargs.

Root cause: the #13429 fix (c4d27818a) introduced
_RootRunReparentingCallbackHandler as a CallbackHandler subclass.
CallbackHandler internally holds a LangfuseResourceManager whose
__new__ now requires explicit public_key/secret_key/base_url in newer
langfuse builds. Python's deepcopy calls __new__ with no args during
copy construction, raising:
  LangfuseResourceManager.new() missing required keyword arguments

The handler carries no per-invocation mutable state, so returning self
from __deepcopy__ is safe. The same root cause and workaround are
already documented in flow_loader.py (agentic path) for issue #13429.

Adds a focused regression test asserting deepcopy(handler) is handler
and does not raise.

Fixes #13965

* fix(ruff): replace unused lambda args a/kw with _ to fix ARG005

* fix(ci): publish bundle deps before nightly main (#13982)

* fix(ci): publish bundle deps before nightly main

* fix(ci): wait for bundle pypi propagation

---------

Co-authored-by: Eric Hare <ericrhare@gmail.com>

* feat(mcp): persist MCP servers in a database table (#13976)

* feat(mcp): persist MCP servers in a database table

MCP servers were stored in a per-user JSON file guarded by an in-process lock, so concurrent edits lose updates and the list diverges across workers/replicas. Store them in a new mcp_server table (one row per server, unique on (user_id, name)); get_server_list/get_server/update_server become per-row reads/upserts and the in-process lock is removed, so MCP config is safe at any worker/replica count. Secret values (env/headers) are encrypted at rest. Existing _mcp_servers_*.json files are imported by an idempotent startup backfill and a `langflow migrate-mcp` CLI; the file is kept for rollback. REST shape and function signatures are unchanged.

Fixes #13970

* fix(mcp): clear hashed cache-key variants when a server changes

The shared tool cache is keyed {server_name}:{hash of headers+timeout} (MCPComponent._mcp_servers_cache_key), so clearing only the bare server name left servers with custom headers/timeouts serving stale config after an update. Clear the bare name and all {server_name}: variants.

* fix(mcp): re-apply create/merge rules after IntegrityError refetch

The IntegrityError fallback in update_server reused the pre-race config, so a concurrent same-name create (check_existing) could overwrite the winning row and a concurrent PATCH (merge_existing) skipped the merge. Now re-apply the rules against the refetched row: check_existing raises 'already exists', merge_existing merges, otherwise overwrite. Adds a concurrency test.

* fix(mcp): order get_server_list by created_at to preserve insertion order

get_server_list had no ORDER BY, so with several rows the server list order was undefined. The legacy file (a dict) preserved insertion order and the UI relies on it (the starter project must stay first). Order by created_at to restore stable insertion order.

* fix(mcp): re-point migration onto current base head to resolve alembic multi-head

release-1.11.0 gained migration c3e7a1b9d2f4 after this branch opened. Our migration also chained off 4f0d2c9a8b7e, producing two alembic heads so 'alembic upgrade head' failed and the DB never initialized (cascading to all e2e/integration/docker jobs). Re-point down_revision from 4f0d2c9a8b7e to c3e7a1b9d2f4 so the chain is linear with a single head.

---------

Co-authored-by: Saad ur Rehman <saad.urrehman@cleura.com>

* docs: external authentication, RBAC, SSO (#13866)

* docs: add rate limiting and signup env vars

* docs: configure global vars in k8s secrets

* docs: per-process login rate limiting

* docs: initial rbac and sso content

* docs: move jwt page to auth and add redirect

* docs: add keycloak example

* docs: authorization plugin interface

* fix(docker): give runtime user a writable npm cache so stdio MCP servers start (#13992)

* fix(docker): give runtime user a writable npm cache so stdio MCP servers start

The ubi10 runtime base sets HOME=/opt/app-root/src, and the main image runs
`npm install -g` as root, which seeds a root-owned npm cache at
/opt/app-root/src/.npm. The runtime user (uid 1000) spawns stdio MCP servers
via `npx`, which then fails with `EACCES` on ~/.npm/_cacache, so every stdio
MCP server registers but never lists any tools (toolsCount stays null in
GET /api/v2/mcp/servers?action_count=true).

Pin npm's cache to a uid-1000-owned directory via NPM_CONFIG_CACHE=/app/.npm
(immune to the base image's HOME) and hand ownership of the default HOME cache
to the runtime user as a fallback. Applied to all three runtime images
(main, with_extras, base) that ship Node so npx has a writable cache.

* fix(docker): avoid incompatible npm latest

* fix: thin single focus border on global-variable input (#13995)

fix input style on GV input

* ci: add non-blocking flaky test report job with 30-day retention (#13996)

* ci: add non-blocking flaky test report job with 30-day retention

* fix: tolerate partial blob downloads in flaky-report job

One of the ~62 shard blob artifacts failed its download after 5 retries
(transient artifact storage error), which failed the whole download step
and skipped the merge. For a metrics-only job, merging the shards that
did download beats producing nothing: mark the download step
continue-on-error and no-op the merge cleanly when no blobs landed.

* fix(a11y): improve API keys tab order (#13953)

* fix(frontend): repair flow list card a11y

* fix(frontend): clear flows page a11y debt

* test(frontend): update header count a11y names

* test(e2e): open flow cards via action button

* fix(a11y): address flow card review

* chore: update secrets baseline

* fix(a11y): improve settings table scans

* refactor(a11y): isolate table scan fixes

* fix(a11y): improve API keys tab order

* test(frontend): fix shard click targets

* fix(a11y): link API key expiry label

* fix(a11y): resolve AG Grid accessibility bugs in table component

- Fix hidden row control restoration bug by tracking original tabindex values
- Fix pagination button self-latching bug by using only class as input signal
- Add comprehensive unit tests for both fixes (10 tests, all passing)
- Export patchGridAccessibility function and AgGridAccessibilityLabels type for testing
- Improve api-keys.a11y.spec.ts to be less brittle with dynamic checks

Addresses feedback from keval718 on PR #13953

* fix(a11y): rework API keys table keyboard nav and clear IBM violations

Replace the AG Grid DOM-patching hack (MutationObserver + global keydown/
focusin listeners + role rewriting + multi-timeout retries) with a minimal,
event-driven patch applied via the grid's own events.

Keyboard/tab order:
- Tab from the last cell reaches the next control in one press (was three via
  two dead <body> stops) using AG Grid's tabToNextCell hook.
- Disabled pagination buttons are no longer tab stops, and a container-scoped
  focusin redirect keeps AG Grid from programmatically focusing them; inert/
  disabled attributes are avoided because they break the tab guards and trap
  reverse (Shift+Tab) entry into the grid (WCAG 2.1.2).
- API key name cell opens by keyboard (Enter/Space).

IBM Equal Access: 8 -> 0 violations on /settings/api-keys (and the shared
/settings/global-variables table):
- tab guards given role=button + label (element_tabbable_role_valid, 4.1.2)
- empty rowgroups demoted to role=presentation (aria_child_valid, 1.3.1)
- first body row made tabbable via roving tabindex (aria_child_tabbable)

Also: ProviderListItem and the text-cell modal trigger become real buttons,
and Shortcuts settings cells open by keyboard.

Tests: Playwright single-Tab exit + reverse-tab re-entry regression guards,
tab-order test updated to the roving-tabindex model, and Jest unit tests for
the tab-guard / rowgroup / roving-row / disabled-paging patches.

* fix(frontend): center update toast buttons vertically (#14004)

* docs: lfx user docs and bundle extension changes (#13775)

* docs: add LFX user documentation section

Creates a new Lfx/ folder in the docs with three pages covering
the LFX executor from a user perspective:

- lfx-overview.mdx: what LFX is, when to use it, and a full command
  reference table linking to the DevOps SDK docs
- lfx-install.mdx: install from PyPI, with bundles, from source, or via
  uvx — plus bundle install guidance for standalone lfx users
- lfx-run.mdx: lfx serve and lfx run with full option tables,
  stdin/inline JSON, Python script usage, and component category
  allowlist/blocklist controls

Also adds an LFX sidebar category in the "Develop & Deploy" section
and a tip in flow-devops-sdk.mdx noting that standalone lfx installs
do not include bundle package components.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: move Flow DevOps SDK from API Reference to LFX sidebar

* docs: add terminal sidebar icon for LFX section

* docs: move Flow DevOps SDK page into Lfx folder

* docs: move extensions to lfx section

* docs: clean up lfx overview

* docs: extension manifest update and release notes

* docs: revert release notes

* docs: add back nextplaid bundle to release notes

* docs: move mcp and compatibility, add prewarm

* docs: fix relative link

* docs: partials for lfx-bundles

* docs: clarify bundle differences

* docs: remove lfx prewarm

* docs: fix broken lfx user docs link

* docs: lfx bundle installation

* docs: update graduated bundles

* docs: torch opt-in changes

* docs: fix partial link

* docs: bundles some small fixes

* docs: allowlist and blocklist for lfx

* docs: separate run and serve pages

* docs: final extensions check

* docs: lfx MCP page instructions

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(a11y): make /assets/files accessible (#13987)

* fix(a11y): make /assets/files accessible (LE-1744)

Bring the Files page to WCAG 2.1 A/AA, verified with IBM Equal Access
plus keyboard/focus tests.

- Name the icon-only actions column header, hidden visually (4.1.2)
- Open the row actions menu by keyboard from the grid cell (2.1.1)
- Label the upload/bulk-delete buttons so they are named on mobile (4.1.2)
- Restore a keyboard-only :focus-visible ring on borderless grid cells (2.4.7)
- Merge the bulk-delete dialog trigger via asChild to remove the
  duplicate tab stop (2.4.3)
- Move focus into the cell editor when renaming (2.4.3)
- Make the upload "try again" retry a real button (2.1.1)

Add a dedicated Playwright a11y suite (files.a11y.spec.ts) covering the
full state matrix and keyboard/focus behaviour, move the IBM baseline
folder under tests/a11y, and baseline the known Radix menu-portal
landmark item.

* feat(a11y): add frontend accessibility check skill documentation

* fix(a11y): return focus to upload button after file picker cancel (LE-1744)

Keyboard-activating Upload Files then dismissing the native picker with
Esc dropped focus to <body> (WCAG 2.4.3). Only blur on mouse clicks (to
keep the #13178 tooltip fix); restore focus to the trigger on keyboard
activation. Adds two behavioural a11y tests.

* feat(playground): content blocks frontend renderer for v2 workflows (#13391)

* feat: native v2 workflows endpoint with pluggable stream protocols

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.

* feat(api): add output_text and session_id to v2 workflow response

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.

* test(api/v2): cover output_text and session_id on the v2 workflow response

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.

* feat(api/v2): structured output with resolution reason on v2 response

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.

* feat(api/v2): add request-side output selection (output_ids)

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.

* feat(api/v2): emit per-output events on the langflow stream

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.

* feat: make content_blocks the source of truth for Message content

Migrate Message.text from a Pydantic field to a @computed_field over
content_blocks, and unify ContentBlock into the discriminated ContentType
union so a Message's payload is one uniform shape.

Schema changes:
- Add 7 new content types (Image, Audio, Video, File, Reasoning, Usage,
  Citation) with validators for media sources, non-negative tokens, and
  ordered citation indices
- Promote 'contents: list[ContentType]' to BaseContent so any node can
  nest (multimodal tool outputs, multi-step reasoning, grouped errors)
- Fold ContentBlock into BaseContent and into the ContentType union with
  tag 'group'; content_blocks is now 'list[ContentType]' everywhere
- Fix Data.__setattr__ to route through property descriptors via MRO walk

Setter / serialization:
- text setter appends a single TextContent at the end of content_blocks,
  preserving non-text blocks in chronological order (tool calls first,
  final text last)
- model_post_init preserves explicit None in data['text'] when no
  TextContent exists in content_blocks, so callers can still distinguish
  'text was never set' from 'text was set to empty string'

from_lc_message:
- Handle AIMessage tool_calls and usage_metadata regardless of whether
  content is a string or a list (tool-calling agents commonly emit
  content='' alongside tool_calls)
- Tolerate explicit source=None in multimodal image payloads

MessageResponse.from_message / MessageTable.from_message:
- Accept any of (data['text'] set, text_stream pending, content_blocks
  non-empty) as 'content present', so tool-call-only and media-only
  messages persist rather than getting rejected as missing required
  fields

Tests cover all new content types, the unified ContentType union, the
text/content_blocks contract, the setter's chronological append, and the
required-fields gate.

* feat: stable id on content blocks + plumb LangChain tool_call_id

Adds an optional 'id: str | None' field to BaseContent for stable
identity across re-emissions of the same logical block. Producers
that have a natural id (LangChain tool_call_id, external API id, a
UUID stamped before the first emission) set it; consumers use it for
dedup and cross-frame correlation. Without an id, consumers fall back
to position-derived dedup, which assumes content_blocks is append-only
within a message lifetime.

Plumbs LangChain's 'tool_call_id' through 'Message.from_lc_message'
into 'ToolContent.id'. The same logical tool call across start, args
streaming, and result lifecycle now carries the same id, so a
re-fired add_message dedups to one ToolContent instead of producing
duplicates.

Tests cover id default/round-trip/inheritance across every concrete
content type, plus tool_call_id stability across repeated conversion,
multiple tool calls each keeping their own id, and tool_calls
alongside string content.

* fix(schema): MessageResponse parses microsecond timestamps and ContentBlock partial updates preserve unset fields

Two schema regressions surfaced in QA across the content-blocks chain:

1. MessageResponse.timestamp was typed as a bare datetime, but
   Message.timestamp default is a string with microsecond precision and
   a UTC timezone label ('%Y-%m-%d %H:%M:%S.%f %Z') that Pydantic's
   default datetime parser rejects. Any freshly built Message routed
   through MessageResponse.from_message raised ValidationError. Reuse
   the shared str_to_timestamp_validator so MessageResponse accepts
   every format Message itself recognises.

2. ContentBlock.__init__ marked every field as model_fields_set, not
   just the discriminator. The override defeated exclude_unset for the
   group content type: a patch like ContentBlock(title='new') dumped
   every defaulted field and, when merged onto an existing block by
   aupdate_messages, overwrote fields the caller never touched. Mark
   only 'type' (the discriminator) so partial updates carry the variant
   tag without clobbering the rest.

Adds regression tests in test_message_content_blocks.py: from_message
round-trips Message.timestamp without crashing, and ContentBlock
exclude_unset stays narrow to the explicit fields plus the
discriminator.

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* feat(playground): content blocks frontend renderer for v2 workflows

* feat(frontend): add v2 (beta) tab to the API Access modal

Add a v1 / v2 (beta) version toggle above the language tabs. The v2 tab
emits Python, JavaScript, and cURL snippets for POST /api/v2/workflows.

The two examples are framed by outcome, not by API jargon: "Get the full
result" (the default single JSON response) and "Stream the result as it
runs", each with a one-line plain-language description. The streaming
snippets consume the default langflow protocol (switch on the event field;
handle add_message, token, and end) rather than forcing the agui protocol.
The API key is read from an env var, and a short response peek shows the
shape a caller gets back.

* feat(frontend): v2 examples read the answer from output.text

* feat(agent): interleaved text + tool_use rendering and tabbed tool-output visualizer

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes

* fix(api/v2): buffer parallel messages in the AG-UI translator instead 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.

* [autofix.ci] apply automated fixes

* fix(api/v2): gate AG-UI message finalization on non-partial state and 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.

* fix(playground): scheme-guard content block URLs against unsafe schemes

Lift safeUrl into a shared chatComponents/url.ts and route the file
content block's download anchor through it, degrading unsafe-scheme
URLs to a non-clickable label. Gate the image/audio/video src through
the same helper so a data:text/html or attacker-chosen src is dropped
rather than rendered. Matches the existing citation sanitization in
SourcesStrip. Adds file-branch tests for an http URL and a
javascript: URL.

* refactor(playground): extract content-block layout + media renderers, drop favicon egress

- Extract resolveContentBlockLayout from the duplicated shape-detection
  block in bot-message and chat-message into chat-messages/utils, with a
  direct unit test (legacy group, interleaved flat, text-only, divergent
  text, edit mode).
- Split image/audio/video/file renderers out of ContentDisplay into
  MediaContentDisplay so the dispatcher stops growing per content type.
- Replace the Google favicon fetch in SourcesStrip with a local Globe
  icon so cited source domains aren't leaked to a third party.
- Trim WHAT comments to WHY, add typed test factories.

* feat: make content_blocks the source of truth for Message content

Migrate Message.text from a Pydantic field to a @computed_field over
content_blocks, and unify ContentBlock into the discriminated ContentType
union so a Message's payload is one uniform shape.

Schema changes:
- Add 7 new content types (Image, Audio, Video, File, Reasoning, Usage,
  Citation) with validators for media sources, non-negative tokens, and
  ordered citation indices
- Promote 'contents: list[ContentType]' to BaseContent so any node can
  nest (multimodal tool outputs, multi-step reasoning, grouped errors)
- Fold ContentBlock into BaseContent and into the ContentType union with
  tag 'group'; content_blocks is now 'list[ContentType]' everywhere
- Fix Data.__setattr__ to route through property descriptors via MRO walk

Setter / serialization:
- text setter appends a single TextContent at the end of content_blocks,
  preserving non-text blocks in chronological order (tool calls first,
  final text last)
- model_post_init preserves explicit None in data['text'] when no
  TextContent exists in content_blocks, so callers can still distinguish
  'text was never set' from 'text was set to empty string'

from_lc_message:
- Handle AIMessage tool_calls and usage_metadata regardless of whether
  content is a string or a list (tool-calling agents commonly emit
  content='' alongside tool_calls)
- Tolerate explicit source=None in multimodal image payloads

MessageResponse.from_message / MessageTable.from_message:
- Accept any of (data['text'] set, text_stream pending, content_blocks
  non-empty) as 'content present', so tool-call-only and media-only
  messages persist rather than getting rejected as missing required
  fields

Tests cover all new content types, the unified ContentType union, the
text/content_blocks contract, the setter's chronological append, and the
required-fields gate.

(cherry picked from commit 3b92500349420e2651a08587aa4e4292314ef8bf)

* feat: stable id on content blocks + plumb LangChain tool_call_id

Adds an optional 'id: str | None' field to BaseContent for stable
identity across re-emissions of the same logical block. Producers
that have a natural id (LangChain tool_call_id, external API id, a
UUID stamped before the first emission) set it; consumers use it for
dedup and cross-frame correlation. Without an id, consumers fall back
to position-derived dedup, which assumes content_blocks is append-only
within a message lifetime.

Plumbs LangChain's 'tool_call_id' through 'Message.from_lc_message'
into 'ToolContent.id'. The same logical tool call across start, args
streaming, and result lifecycle now carries the same id, so a
re-fired add_message dedups to one ToolContent instead of producing
duplicates.

Tests cover id default/round-trip/inheritance across every concrete
content type, plus tool_call_id stability across repeated conversion,
multiple tool calls each keeping their own id, and tool_calls
alongside string content.

(cherry picked from commit a3e8b40811dd72eb1650b299dccb20be7cd0c8cf)

* fix(schema): MessageResponse parses microsecond timestamps and ContentBlock partial updates preserve unset fields

Two schema regressions surfaced in QA across the content-blocks chain:

1. MessageResponse.timestamp was typed as a bare datetime, but
   Message.timestamp default is a string with microsecond precision and
   a UTC timezone label ('%Y-%m-%d %H:%M:%S.%f %Z') that Pydantic's
   default datetime parser rejects. Any freshly built Message routed
   through MessageResponse.from_message raised ValidationError. Reuse
   the shared str_to_timestamp_validator so MessageResponse accepts
   every format Message itself recognises.

2. ContentBlock.__init__ marked every field as model_fields_set, not
   just the discriminator. The override defeated exclude_unset for the
   group content type: a patch like ContentBlock(title='new') dumped
   every defaulted field and, when merged onto an existing block by
   aupdate_messages, overwrote fields the caller never touched. Mark
   only 'type' (the discriminator) so partial updates carry the variant
   tag without clobbering the rest.

Adds regression tests in test_message_content_blocks.py: from_message
round-trips Message.timestamp without crashing, and ContentBlock
exclude_unset stays narrow to the explicit fields plus the
discriminator.

(cherry picked from commit 6f6639374faf52bfd7d0041a3fa3aec7a1c26a8a)

* fix(schema): address content_blocks review feedback

- sync langflow-base ContentBlock.__init__ with the lfx copy (model_fields_set
  parity) so exclude_unset no longer clobbers type; add cross-module regression test
- route MessageResponse content_blocks discriminator-first so stored flat blocks
  with contents=[] validate instead of raising
- move Message SecretStr coercion into model_post_init and drop the dead
  validate_text before-validator
- drop the no-op _fold_text_into_content_blocks validator
- log a shape-only debug line when from_lc_message drops an undecodable image
- type MessageResponse.content_blocks as list[ContentType] | None

(cherry picked from commit 6c7cec8a529c38d2f9eb7a35f94277611c8696cc)

* fix(agents): stop duplicating the final answer in content_blocks

With content_blocks as the source of truth, Message.text is a computed
field whose setter appends a trailing top-level TextContent. handle_on_chain_end
also appended the same answer into the Agent Steps group, so the final
answer rendered twice (assert 2 == 1 in test_multiple_events). The streaming
path already relies on the setter alone; make the non-streaming path match.

* chore: auto-bake note keys and regenerate backend locales/en.json [skip ci]

* feat(schema): project new content_blocks back to the v1 wire shape

The in-memory Message and the v2 (AG-UI) path use the new content_blocks
union (groups tagged "group", every node carries id/contents, the agent
answer is a trailing top-level TextContent). The v1 API keeps emitting the
pre-1.11.0 shape via a pure legacy_render projection that runs only at the
v1 boundaries: the v1 read/response models, the memories endpoint, the v1
build SSE stream, the webhook events SSE stream, and the /run response and
stream. The build, webhook, and /run projections recurse so the Data mirror
(data.data.content_blocks) is projected alongside the top-level copy. v2
serializes the live Message and keeps the new shape.

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* [autofix.ci] apply automated fixes

* fix(api): keep simple_run_flow returning RunResponse, project v1 at the HTTP boundary

simple_run_flow is a shared helper, so wrapping its return in a JSONResponse to
apply the v1 content_blocks projection broke internal callers that call
.model_dump() on the result (the streaming run_flow_generator and
get_build_results). Return the RunResponse object from the helper and apply the
projection at the non-stream HTTP boundary in _run_flow_internal instead. The
streaming path already projects the end event via _project_run_event.

* [autofix.ci] apply automated fixes

* fix(frontend): route media content URLs through safeUrl

The media content type rendered <img src={url}> with the raw url, unlike
the sibling image/audio/video/file cases that guard via safeUrl. A
javascript:/data: scheme from untrusted tool output reached the img src.
Apply the same safeUrl guard and skip rendering when it returns null.

* fix(frontend): detect untyped legacy groups in content-block layout

resolveContentBlockLayout detected groups with type === "group", missing the
legacy / v1-projected "Agent Steps" group that is persisted without a type
field (just title + contents). Those untyped groups failed hasGroup and were
miscounted as flat non-text items, wrongly enabling ordering mode so the
duplicate top-level text rendered above the tools and the bubble body was
suppressed. Use the shared isGroupedBlock predicate, matching the rest of the
render pipeline. Also document the latent tool-less-group gap in
ContentBlockDisplay (a no-tool answer projects to a text-only group; the drop
is benign today but the gate keys off toolItems, not groupedBlocks).

* fix(frontend): render a group's displayable non-tool content

ContentBlockDisplay gated entirely on a group's tool_use leaves, so a group
whose contents had no tool_use (reasoning / citation / media, …) rendered
nothing. Collect a group's displayable non-tool leaves and render them through
the same loose renderer as top-level flat leaves, while keeping text and usage
out so the legacy v1 Input/Output scaffolding stays hidden (the bubble body
already paints the answer). Adds collectGroupLooseLeaves and unit tests.

* test(frontend): deep probes for ContentBlockDisplay group rendering

Renders the real ContentBlockDisplay (real ContentDisplay / ToolCallCard /
SourcesStrip / accordion; only ESM infra is mocked) and asserts the DOM for
each content_blocks shape: a tool-less group's citation and media now render,
an untyped legacy group renders its non-tool content, a tool-bearing group
renders both the tool and its extra leaf, the legacy Input/Output text stays
hidden, text-only and usage-only groups render nothing, and the flat shape is
unchanged. Confirmed RED on the pre-fix component (4/8 fail) and GREEN after.

* chore: auto-bake note keys and regenerate backend locales/en.json [skip ci]

* chore: re-trigger CI after [skip ci] bake commit

* fix: regenerate component_index.json after agent.py content_blocks change

The merged index still carried AgentComponent's pre-#13390 code, so the
runtime hash allow-list (built from the index) didn't match the live
agent.py. That failed the custom-component admin-only known-template
carve-out (403 instead of 200) and drifted Update Component Index.
Rebuilt via 'make build_component_index'; sha256 now 070077b2.

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* feat(api): durable background execution service (store + default backend) (#13507)

* feat: native v2 workflows endpoint with pluggable stream protocols

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.

* feat(api): add output_text and session_id to v2 workflow response

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.

* test(api/v2): cover output_text and session_id on the v2 workflow response

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.

* feat(api/v2): structured output with resolution reason on v2 response

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.

* feat(api/v2): add request-side output selection (output_ids)

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.

* feat(api/v2): emit per-output events on the langflow stream

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.

* feat(api/v2): durable background execution service (store + default backend)

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.

* test(background-execution): rename hard_proof marker to real_services

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

* fix(api/v2): enforce no-code-execution gate on public workflow endpoint

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

* fix(api/v2): reconstruct background workflow status from job-keyed vertex 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

* fix(api/v2): merge workflow AG-UI cancellation hardening LE-1389

* fix(api/v2): signal cross-worker workflow stops LE-1389

* fix(api/v2): report unconfirmed workflow stops LE-1389

* fix(api/v2): keep background workflows out of polling watchdog LE-1389

* fix(api/v2): buffer parallel messages in the AG-UI translator instead 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.

* fix(api/v2): gate AG-UI message finalization on non-partial state and 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.

* Fix AG-UI workflow lifecycle edges

* [autofix.ci] apply automated fixes

* fix(frontend): enable downlevelIteration for jest Set/Map iteration

ts-jest compiles with target es5; without downlevelIteration, [...set] and
for...of over a Set/Map emit ES5 that yields nothing. That silently broke the
AG-UI bridge tests: runningNodeIds spread, markRunningNodesFailed, and
restoreOriginalBuildStatuses all iterated empty. Production (Vite/SWC, modern
target) was never affected; only the ts-jest harness was. Fixes the 3 failing
jest tests on this branch with no other suite changes (4994/4994 pass).

* fix(api/v2): surface inactivated branch vertices over AG-UI

A branch component (If-Else, Conditional Router) reports its not-taken
vertices in build_data.inactivated_vertices, but the AG-UI translator only
emitted the branch node's own success/error status and dropped that list. The
canvas seeds every planned node as pending from vertices_sorted; skipped
vertices then get no build_start/end_vertex, so they stayed stuck on pending
instead of rendering as inactive (the v1 build path marked them INACTIVE).

The translator now appends an inactive STATE_DELTA op per inactivated vertex,
and the frontend bridge maps the new inactive status to BuildStatus.INACTIVE
and tears its edges down like a completed node. Fixes the If-Else regression
in general-bugs-reset-flow-run.spec.ts.

* fix(api/v2): dedupe repeated inactive node deltas in AG-UI stream

build.py keeps reporting a conditionally-excluded vertex in
inactivated_vertices on every subsequent end_vertex (the excluded set
persists until the ConditionalRouter clears it), so the translator was
putting the same inactive STATE_DELTA on the wire once per remaining
vertex. Track emitted inactive nodes and skip re-emitting; drop a node
from the set when it actually runs again (build_start/end_vertex) so a
loop re-activation can still re-emit inactive later.

* fix(api/v2): no duplicate WORKFLOW job row on durable background runs

A v2 background run created TWO JobType.WORKFLOW rows for one flow execution:
the durable row (submit()'s job_id, owned by JobRunner) plus an orphan keyed by
the run_id generate_flow_events mints, because the build pipeline's
track_job_status defaults True and the durable frame source never passed False.
The flow ran once (double bookkeeping), but every background run left a phantom
WORKFLOW row + job_events and double-fired the memory-base hook, skewing metrics.

Thread track_job_status through _stream_event_frames; pass False only from the
background frame source (the durable runner already owns the row + fires the
hook with the durable job_id). Stream/public paths keep default True. Also gate
build.py's memory-base hook fire behind track_job_status so background doesn't
double-fire. Adds a regression test (RED before fix: found 2 rows).

* test(api/v2): update stale workflow-stop tests for the durable design

These 3 tests targeted the removed queue-service stop helper
(_cancel_workflow_queue_job / get_queue_service), inherited via the
agui->bg-default merge and failing with AttributeError across the stack:

- test_stop_workflow_success: adapted to the durable stop path
  (revoke_task -> stop_job -> update_job_status(CANCELLED)).
- test_stop_workflow_allowed_for_legacy_job_with_no_user_id (IDOR): adapted to
  the durable mechanism; still asserts the ownership check does not block a
  legacy user_id=None row.
- test_stop_workflow_returns_503_when_queue_cancel_cannot_be_confirmed: dropped
  — the durable stop writes a best-effort STOP signal and always finalizes
  CANCELLED; the queue-service 'cannot confirm -> 503' path no longer exists.

test_workflow.py now passes 24/24.

* fix(api/v2): move FrameSourceFactory alias under TYPE_CHECKING

As a module-level runtime value, FrameSourceFactory = Callable[..., Any] is a
GenericAlias that passes isinstance(obj, type) but makes issubclass(obj, Service)
raise on Python 3.10/3.14. The service factory scans this module for Service
subclasses (services/factory.py:90), so the runtime alias crashed service
initialization on those interpreters with 'issubclass() arg 1 must be a class'
-> 'Could not initialize services', erroring out dozens of unrelated tests at
setup (3.13 was unaffected, which is why local runs passed).

The alias is only referenced in a lazy annotation (from __future__ import
annotations), so moving it + the Callable import under TYPE_CHECKING removes it
from the runtime namespace with no behavior change.

Verified: alias absent from runtime module namespace, factory scan finds
BackgroundExecutionService cleanly, durable service tests 26/26 pass.

* test(lfx): register background-execution Settings fields in the field-count gate

The durable background-execution work added six Settings fields
(background_max_concurrency, background_job_timeout, background_lease_ttl_s,
background_heartbeat_interval_s, background_watchdog_interval_s, test_redis_url)
without updating EXPECTED_FIELDS, so test_field_count_unchanged failed 152 != 146.
These are intentional bg-exec config; add them to the gate.

* fix(api/v2): restore "end" side-channel event in AG-UI workflow stream

The durable background-execution rewrite of workflow.py reverted
`side_channel_events` to its pre-"end" form, dropping the "end" event
from the AG-UI side-channel. That event carries `build_duration` to the
playground chat-view, and the message metadata badge only renders when
`hasDuration || hasTokens`. With build_duration gone the badge vanished,
failing the token-usage and shareable-playground "Finished In" regression
tests. Re-add "end" so the streaming playground path delivers it again.

* fix(api/v2): apply request tweaks on the streaming and background paths

The v2 workflows endpoint applied `tweaks` only on mode=sync. The stream
and background paths build the graph via the v1 build-vertex loop
(`generate_flow_events`), which never received the tweaks, so they were
silently dropped. The confusing symptom: a model passed via tweaks
surfaced as "A model selection is required", and any per-component
override was ignored on non-sync runs.

Thread `parsed.tweaks` into `generate_flow_events` and apply them to the
built graph via `vertex.update_raw_params`. We do not use the lfx
`process_tweaks_on_graph` helper because it only sets `vertex.params`,
which does not persist to runtime (the same bug
`lfx.base.tools.run_flow._process_tweaks_on_graph` works around). No-tweaks
runs are unchanged (guarded by `if tweaks`). Adds a streaming regression
test that overrides ChatInput via tweaks and asserts the value drives the run.

* fix(api/v2): return background run output from completed status

A completed background run's GET status returned a bare COMPLETED with an
empty `outputs` and a null `output`. The COMPLETED branch reconstructs
from `vertex_builds` keyed by job_id, which the durable path does not
write, so reconstruction raised ValueError and fell through to an empty
response; the result was only retrievable via a /events re-attach.

The runner now captures the terminal `output` events (the langflow
adapter's normalized ComponentOutput payloads) into `Job.result`, and the
status COMPLETED branch rebuilds the `outputs` map and resolved `output`
from them via `workflow_response_from_output_events`, matching the sync
response. agui-protocol runs emit no `output` events, so their status
stays result-less (the result remains on the /events log).

* fix(api/v2): address review findings on the v2 workflows endpoint

- recover session_id for completed background jobs from the persisted
  terminal message instead of always returning null, so GET status can
  continue the same chat/memory thread
- replay a user-cancel as a CUSTOM cancel marker + RUN_FINISHED (agui) and
  a `cancelled` terminal (langflow) instead of RUN_ERROR, so a re-attaching
  client no longer reads a deliberate stop as a failure
- cancel the evicted still-running buffer writer when the background-run
  registry is full, so it stops appending into a run no reader can find
- derive per-component status from the error artifact / valid flag instead
  of hardcoding COMPLETED, and stop the langflow adapter dropping `valid`
- throttle the unauthenticated public endpoint per IP and bound its
  input_value/session_id length
- document the sync-only scope of request-body globals
- document that live event re-attach is intentionally owner-only

* test(lfx): register public_flow_rate_limit_per_minute in settings composition

* refactor(v2 workflows): split workflow.py and address review blockers

Splits the ~1.5k-line workflow.py into focused modules and folds in the
execution-timeout and error-sanitization fixes from Cristhianzl's review of #13307.

- B1: workflow.py now holds only the four route handlers. Validation guards move
  to workflow_validation, the sync/stream run loop to workflow_execution, and the
  durable background machinery to workflow_background (layered, acyclic).
- I1: add workflow_execution_timeout (default 300) and apply a single wall-clock
  ceiling across sync, stream, background, and public via _stream_event_frames. A
  timeout becomes a sanitized terminal error and marks a background job failed.
- I3: the route error handlers no longer echo raw exception text. They return a
  generic, code-tagged message and log the full exception server-side.
- R1: remove the "commented out / future scope" comments that sat over live
  dataframe-extraction code in converters.py.
- R4: drop the worker-routing internals from the reattach 409 message.

Tests cover the timeout terminal-error path and the error-body sanitization, and
the settings field-count guard is updated for the new setting.

* refactor(lfx): extract v2 workflow contract layer into lfx.workflow

Moves the protocol-agnostic pieces of the v2 workflows API out of the langflow
backend into lfx so both the backend and `lfx serve` can share one contract.
First step toward giving lfx (the production runtime) the v2 workflows API.

- Move api/v2/adapters/, agui_translator.py, and converters.py to lfx/workflow/.
  They depend only on lfx.schema.workflow and ag_ui (already an lfx dep), so lfx
  carries the contract with zero langflow imports.
- Decouple the one langflow reference: converters typed run_response against
  langflow.api.v1.schemas.RunResponse (TYPE_CHECKING only). Replaced with a local
  RunResponseLike Protocol (outputs + session_id), the only attributes used.
- Repoint the six backend v2 workflow modules to import from lfx.workflow.
- Move the five protocol-agnostic contract tests into src/lfx/tests/unit/workflow/
  (run in the lfx-only env). test_output_event_parity and test_workflow_agui stay
  in langflow (they need langflow.api.build) with repointed imports.

Coverage unchanged: 201 contract tests pass in the lfx-only env, 191 backend v2
tests pass; 392 total, same as before the move.

* fix(background-execution): prevent worker deadlock on stop() under Python 3.10

The bounded executor's worker awaited the in-flight job task with a bare
`await task`. On Python 3.10, when stop() cancels a worker while its job
task is finishing, the awaiter's wakeup is lost and the event loop idles
forever in select(), deadlocking stop(). Await via a done-callback Event
(the same mechanism stop()'s own asyncio.gather already uses), which
delivers the wakeup reliably; task.result() preserves the cancellation
and exception semantics of the bare await.

* fix(background-execution): keep ephemeral frames on reattach, pin psycopg in real-service tests

The real-service CI job broke on `ModuleNotFoundError: No module named
'asyncpg'`. asyncpg was never declared anywhere in this repo; it arrived
transitively through `cuga`, which release-1.11.0 dropped when it moved the
root dep to `lfx-bundles[all-no-torch]` (#13886). Normalize the harness URL to
`postgresql+psycopg` instead: it is what `--extra postgresql` installs and what
DatabaseService already selects for async Postgres, so the test now exercises
the production driver rather than a stowaway.

Also fix a real reattach bug. The runner publishes ephemeral token frames
tagged with the last durable seq (they have no job_events row), while
reattach's tail skipped anything with `seq <= highest`. After replay left
`highest` at that same seq, every token delta was dropped until the next
durable milestone advanced it, so a reconnect mid-stream saw no tokens. Mark
frames durable/ephemeral and dedupe only the durable ones, which are the only
frames a replay can return.

Log instead of silently suppressing a failed stop_job signal, and drop a
comment block duplicated verbatim in InProcessExecutor.stop().

* fix(migrations): merge alembic heads (mcp_server + execution_signals)

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

* docs: AGUI protocol, workflows quickstart, update examples (#13911)

* docs: curl code snippet contains placeholder

* docs: update workflows example to match new schema

* docs: include ag-ui in workflow api reference

* docs: workflows api agui streaming

* docs: workflows api quickstart and ag-ui example

* docs: fix broken build links

* docs: agui peer review

* feat(frontend): render the flow share seam in the editor Share dropdown (#14009)

The CustomFlowShareAction customization seam only rendered in the flow
list's card menu, so overlays that implement user/team sharing had no entry
point inside the flow editor. Render the same seam at the top of the
editor's Share dropdown; the OSS stub renders nothing, so the OSS menu is
unchanged. Adds an optional menuContext prop to the seam so overlays can
label the editor placement differently from the card menu.

* fix: fail loud on undecryptable KB embedding key with recovery path (#13806)

* fix: fail loud on undecryptable KB embedding key with recovery path

- Add KBKeyDecryptError exception for SECRET_KEY rotation scenarios
- Implement require_api_key flag in load_kb_metadata for critical vs non-critical paths
- Retrieval: raise when component doesn't supply key, allow component key bypass
- Ingestion: raise when no component key, warn when component key used as fallback
- Add comprehensive test coverage for decrypt failure scenarios
- Consolidate _kb_paths imports in knowledge.py

* chore: auto-bake note keys and regenerate backend locales/en.json [skip ci]

* chore: auto-bake note keys and regenerate backend locales/en.json [skip ci]

* chore: trigger CI

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

---------

Co-authored-by: Janardan S Kavia <janardanskavia@Janardans-MacBook-Pro.local>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Eric Hare <ericrhare@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

* feat(models): add Azure AI Foundry to unified model provider setup (#13912)

* feat(models): add Azure AI Foundry to unified model provider setup

Expose Azure AI…
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 scaling-up

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants