Skip to content

feat: Remove use of vertex builds table in workflow API background mode - #14353

Merged
erichare merged 2 commits into
release-1.12.0from
workflow-api-prod
Aug 5, 2026
Merged

feat: Remove use of vertex builds table in workflow API background mode#14353
erichare merged 2 commits into
release-1.12.0from
workflow-api-prod

Conversation

@Jkavia

@Jkavia Jkavia commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Make the v2 workflow GET-status endpoint use the durable Job.result blob as its primary output source, with vertex_build reconstruction as the recovery path for legacy, missing, malformed, or incomplete captures. The completed response preserves the submitted session_id (falling back to the flow ID) across both paths.

Populate Job.result for both Langflow and AG-UI runs through protocol-neutral off-wire output capture. The capture is kept in memory only and never appended or published, so it cannot leak onto /events. The runner also accepts the standard durable output frame as a fallback for custom frame sources, with final deduplication preventing double capture.

Keep HITL suspension coherent by committing the SUSPENDED status and resume-critical metadata (pending_request_id and pre-pause outputs) atomically. This prevents a racing resume from observing a suspended job before the metadata needed to continue it is visible.

Store workflow API sync results in the Job table as well.

Verified with focused background-workflow, HITL suspend/resume, status-recovery, and converter tests, including the vertex-build-storage-off integration path and malformed stored-output recovery.

Screenshot 2026-08-04 at 3 45 16 PM Screenshot 2026-08-04 at 3 39 11 PM

@coderabbitai

coderabbitai Bot commented Jul 31, 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 Plus

Run ID: 35d38fe6-176e-4c44-a9c0-506313f1d69f

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

Background workflows now capture terminal outputs in Job.result. Completed-job status recovery preserves the effective session ID, uses captured outputs first, and falls back to vertex-build reconstruction. Job-event persistence can be disabled without affecting live streaming or completed results.

Changes

Background output recovery

Layer / File(s) Summary
Terminal output contract
src/lfx/src/lfx/workflow/adapters/langflow.py, src/lfx/src/lfx/workflow/converters.py
Terminal vertex data is normalized into protocol-neutral output events. Reconstructed responses can include a session ID.
Capture and storage handling
src/backend/base/langflow/api/v2/workflow_execution.py, src/backend/base/langflow/services/background_execution/runner.py, src/lfx/src/lfx/services/settings/groups/telemetry.py
Streaming execution emits internal capture frames. The runner collects terminal outputs in memory and persists durable events only when enabled.
Completed status recovery and validation
src/backend/base/langflow/api/v2/workflow.py, src/backend/tests/unit/api/v2/test_workflow_background.py
Completed-job status uses captured results before vertex-build reconstruction. Tests cover disabled vertex-build storage, AGUI capture, and disabled job-event storage.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • langflow-ai/langflow#14332: Both PRs modify _stream_event_frames and background workflow execution, but address distinct concerns.

Suggested labels: enhancement

Suggested reviewers: ogabrielluiz

Sequence Diagram(s)

sequenceDiagram
  participant WorkflowExecution
  participant BackgroundRunner
  participant JobResult
  participant WorkflowStatusAPI
  WorkflowExecution->>BackgroundRunner: internal terminal output frame
  BackgroundRunner->>JobResult: store captured output
  WorkflowStatusAPI->>JobResult: read completed result
  JobResult-->>WorkflowStatusAPI: output events and session ID
Loading
🚥 Pre-merge checks | ✅ 9
✅ Passed checks (9 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 92.31% which is sufficient. The required threshold is 80.00%.
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 for all new implementations. New test file test_workflow_background.py contains 10 test functions with 47 assertions covering GET-status reconstruction, off-...
Test Quality And Coverage ✅ Passed Tests comprehensively cover main functionality with 10 async integration tests, 43 assertions, and proper pytest patterns. Tests validate: Job.result population, off-wire capture for agui, fallback...
Test File Naming And Structure ✅ Passed The changed backend tests use the test_*.py pattern and async pytest structure, with fixture cleanup and flag restoration; names and cases cover normal, AG-UI, disabled-storage, replay, and termina...
Excessive Mock Usage Warning ✅ Passed The PR adds 3 real HTTP/DB background tests with no mocks; the file's 2 existing mock-based unit tests isolate the job-service boundary and verify call assertions.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: removing the workflow API background mode's dependency on the vertex builds table.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch workflow-api-prod

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions github-actions Bot added the enhancement New feature or request label Jul 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Migration Validation Passed

All migrations follow the Expand-Contract pattern correctly.

@github-actions

Copy link
Copy Markdown
Contributor

✅ Test Coverage Advisor

No source changes detected without accompanying tests. Thanks for keeping coverage up! 🎉

Advisory check only — never blocks merge.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/backend/base/langflow/services/background_execution/runner.py (2)

235-241: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the stale comment: agui now populates captured outputs.

This comment states that the agui adapter does not emit terminal outputs and that agui-protocol runs leave output_events empty. The code that follows (lines 251-261) captures WORKFLOW_OUTPUT_CAPTURE_EVENT frames for both protocols, which is exactly the mechanism this PR adds to close that agui gap. The comment now contradicts the code it precedes and will mislead future readers into thinking agui results are still empty.

Update the comment to describe the off-wire capture mechanism instead of the old langflow-only behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/base/langflow/services/background_execution/runner.py` around
lines 235 - 241, Update the comment preceding the terminal-output capture logic
in the background runner to reflect that both langflow and agui adapters capture
WORKFLOW_OUTPUT_CAPTURE_EVENT frames into output_events, and remove the outdated
claim that agui-protocol runs leave results empty. Describe that captured
outputs are persisted for status responses while remaining available on the
/events log.

228-261: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Capture resumed terminal outputs before setting result.outputs.

graph.resume_from_checkpoint() only restores vertices and queues the resume layer; vertices built before the pause are skipped unless they are dropped downstream. Since output_events resets in _drive() and Job.result["outputs"] is sent as-is, a resumed run can leave terminal outputs incomplete. Persist the prior output_events with the job/checkpoint and merge them on resume, or replay them from the durable output frames before set_result.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/base/langflow/services/background_execution/runner.py` around
lines 228 - 261, Update _drive and the resume flow so previously captured
terminal output_events are restored before processing new frames and before
Job.result is set. Persist output_events with the job/checkpoint and merge prior
outputs on resume, or replay the durable output frames, while preserving newly
captured outputs and avoiding duplicates.
🧹 Nitpick comments (1)
src/backend/tests/unit/api/v2/test_workflow_background.py (1)

152-155: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use monkeypatch instead of manual save/restore for settings overrides. Both new tests temporarily mutate the shared settings singleton with a hand-rolled original = ...; settings.x = False; try/finally: settings.x = original pattern. pytest's monkeypatch.setattr fixture is the idiomatic mechanism for this and guarantees restoration automatically, including on test failure inside the body.

  • src/backend/tests/unit/api/v2/test_workflow_background.py#L152-L155: replace the manual original/assignment for vertex_builds_storage_enabled with monkeypatch.setattr(settings, "vertex_builds_storage_enabled", False).
  • src/backend/tests/unit/api/v2/test_workflow_background.py#L194-L195: drop the manual finally: settings.vertex_builds_storage_enabled = original restoration once monkeypatch is used.
  • src/backend/tests/unit/api/v2/test_workflow_background.py#L256-L258: replace the manual original/assignment for job_events_storage_enabled with monkeypatch.setattr(settings, "job_events_storage_enabled", False).
  • src/backend/tests/unit/api/v2/test_workflow_background.py#L291-L292: drop the manual finally: settings.job_events_storage_enabled = original restoration once monkeypatch is used.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/tests/unit/api/v2/test_workflow_background.py` around lines 152 -
155, In src/backend/tests/unit/api/v2/test_workflow_background.py at lines
152-155 and 256-258, replace the manual settings save-and-override logic with
monkeypatch.setattr for vertex_builds_storage_enabled and
job_events_storage_enabled respectively; at lines 194-195 and 291-292, remove
the corresponding manual finally restorations, relying on monkeypatch cleanup
for both tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/backend/tests/unit/api/v2/test_workflow_background.py`:
- Around line 190-193: Update the comment above the session_id assertion to
state that Job.result obtains session_id from the persisted submit request in
job.job_metadata["request"], not from terminal events, while preserving the
explanation that background GET continues the same chat thread with vertex-build
storage disabled.

---

Outside diff comments:
In `@src/backend/base/langflow/services/background_execution/runner.py`:
- Around line 235-241: Update the comment preceding the terminal-output capture
logic in the background runner to reflect that both langflow and agui adapters
capture WORKFLOW_OUTPUT_CAPTURE_EVENT frames into output_events, and remove the
outdated claim that agui-protocol runs leave results empty. Describe that
captured outputs are persisted for status responses while remaining available on
the /events log.
- Around line 228-261: Update _drive and the resume flow so previously captured
terminal output_events are restored before processing new frames and before
Job.result is set. Persist output_events with the job/checkpoint and merge prior
outputs on resume, or replay the durable output frames, while preserving newly
captured outputs and avoiding duplicates.

---

Nitpick comments:
In `@src/backend/tests/unit/api/v2/test_workflow_background.py`:
- Around line 152-155: In
src/backend/tests/unit/api/v2/test_workflow_background.py at lines 152-155 and
256-258, replace the manual settings save-and-override logic with
monkeypatch.setattr for vertex_builds_storage_enabled and
job_events_storage_enabled respectively; at lines 194-195 and 291-292, remove
the corresponding manual finally restorations, relying on monkeypatch cleanup
for both tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3383102b-a487-43e5-851b-79a3236faf38

📥 Commits

Reviewing files that changed from the base of the PR and between 64cfb92 and f3ae922.

📒 Files selected for processing (7)
  • src/backend/base/langflow/api/v2/workflow.py
  • src/backend/base/langflow/api/v2/workflow_execution.py
  • src/backend/base/langflow/services/background_execution/runner.py
  • src/backend/tests/unit/api/v2/test_workflow_background.py
  • src/lfx/src/lfx/services/settings/groups/telemetry.py
  • src/lfx/src/lfx/workflow/adapters/langflow.py
  • src/lfx/src/lfx/workflow/converters.py

Comment thread src/backend/tests/unit/api/v2/test_workflow_background.py Outdated
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Frontend Unit Test Coverage Report

Coverage Summary

Lines Statements Branches Functions
Coverage: 49%
49.94% (71956/144060) 70.31% (10066/14316) 46.88% (1653/3526)

Unit Test Results

Tests Skipped Failures Errors Time
5539 0 💤 0 ❌ 0 🔥 18m 59s ⏱️

@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.00000% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 63.26%. Comparing base (83b26b2) to head (646b0f2).
⚠️ Report is 3 commits behind head on release-1.12.0.

Files with missing lines Patch % Lines
src/lfx/src/lfx/workflow/adapters/langflow.py 38.46% 6 Missing and 2 partials ⚠️
src/backend/base/langflow/api/v2/workflow.py 87.50% 2 Missing ⚠️
src/backend/base/langflow/services/jobs/service.py 90.00% 1 Missing ⚠️
src/lfx/src/lfx/workflow/converters.py 87.50% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##           release-1.12.0   #14353      +/-   ##
==================================================
+ Coverage           61.95%   63.26%   +1.31%     
==================================================
  Files                2417     2390      -27     
  Lines              242524   243285     +761     
  Branches            36184    37295    +1111     
==================================================
+ Hits               150251   153924    +3673     
+ Misses              90354    87436    -2918     
- Partials             1919     1925       +6     
Flag Coverage Δ
backend 70.79% <96.20%> (+0.68%) ⬆️
frontend 61.69% <ø> (+1.83%) ⬆️
lfx 61.11% <57.14%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...backend/base/langflow/api/v2/workflow_execution.py 90.95% <100.00%> (+0.65%) ⬆️
...e/langflow/services/background_execution/runner.py 98.66% <100.00%> (+0.17%) ⬆️
.../backend/base/langflow/services/jobs/exceptions.py 100.00% <100.00%> (ø)
src/backend/base/langflow/services/jobs/service.py 88.20% <90.00%> (-0.48%) ⬇️
src/lfx/src/lfx/workflow/converters.py 89.23% <87.50%> (+1.99%) ⬆️
src/backend/base/langflow/api/v2/workflow.py 79.55% <87.50%> (+0.33%) ⬆️
src/lfx/src/lfx/workflow/adapters/langflow.py 78.57% <38.46%> (+0.14%) ⬆️

... and 563 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Jkavia
Jkavia force-pushed the workflow-api-prod branch from f3ae922 to 2a07971 Compare July 31, 2026 14:04
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Jul 31, 2026
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Aug 3, 2026
@erichare
erichare self-requested a review August 3, 2026 22:16

@erichare erichare left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hi @Jkavia , could you take a look?

Code Review Summary

Found 2 critical issues that need to be fixed:

🔴 Critical (Must Fix)

1. [P1] Preserve outputs produced before a HITL pause

FilePath: [src/backend/base/langflow/api/v2/workflow.py](

) line 667

if output_events:
    return workflow_response_from_output_events(...)

Explanation

Any non-empty Job.result.outputs is now treated as the complete result. However, each runner pass starts a fresh output accumulator, suspension exits before saving it, and the resumed pass overwrites Job.result with only its own outputs.

For parallel branches, an output completed before a HITL pause is therefore omitted when another output completes after resume. The non-empty final result also prevents vertex-build fallback; with vertex-build storage disabled, recovery is impossible.

A two-pass real-JobService reproduction confirmed that only the post-resume output survives.

Suggested Fix

  1. Persist partial captures when suspending.
  2. Merge captures by component_id across resume passes.
  3. Add a parallel-branch HITL regression test with vertex-build storage disabled.

2. [P1] Event-storage-off mode drops live HITL requests

FilePath: [src/backend/base/langflow/services/background_execution/runner.py](

) line 283

seq = last_durable_seq + 1

Explanation

With event persistence disabled, normal frames use a local sequence. The human-input frame is intercepted instead of published, then _suspend() writes it to the otherwise-empty database log with sequence 1.

If the connected stream has already advanced to local cursor N, replay requests only rows after N, so the lower-numbered HITL prompt is discarded before the stream closes as suspended. Resume starts another local sequence at 1, creating the same cursor collision.

A connected-tail reproduction received live sequences [1, 2] while the database contained only the unseen human-input event at sequence 1. This contradicts the setting’s “live streaming … unaffected” contract and can strand the job awaiting an invisible decision.

Suggested Fix

  1. Use one job-scoped monotonic cursor across normal, control, and resumed frames.
  2. Publish the HITL request with that cursor, or store pending-request state outside job_events.
  3. Add storage-disabled live pause/resume coverage.

✅ What's Good

  • The off-wire capture cleanly preserves AG-UI wire behavior.
  • The PR merges cleanly onto current release-1.12.0 (e73fd4bf) and passes git diff --check.
  • Focused validation passed: 163 LFX tests, 31 runner/resume tests, and all 4 newly added API tests.
  • Required CI is currently red from Playwright shard 53’s file-upload failure/SQLite lock and shard 58’s Microsoft apt repository 403; neither overlaps the changed files, but a rerun is still required.

@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Aug 4, 2026
@Jkavia

Jkavia commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the first "Preserve outputs produced before a HITL pause" issue and removed the events flag for now as it was only optional addition, will skip that for now as its needed so that should resolve second review comment as well.

@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Aug 4, 2026
@erichare
erichare self-requested a review August 4, 2026 21:02

@erichare erichare left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@Jkavia looking good, can you take a look at these comments?

Code Review Summary

Found 1 critical issue that needs to be fixed:

🔴 Critical (Must Fix)

1. [P1] Make suspended state and resume metadata atomic

FilePath: [src/backend/base/langflow/services/background_execution/runner.py](

await self._jobs.append_event(job_id, HUMAN_INPUT_REQUIRED_EVENT, exc.payload)
await self._jobs.update_job_status(job_id, JobStatus.SUSPENDED)
metadata: dict[str, Any] = {}
if exc.request_id is not None:
metadata["pending_request_id"] = exc.request_id
if self._input_deadline_s is not None:
deadline = datetime.now(timezone.utc) + timedelta(seconds=self._input_deadline_s)
metadata["input_deadline_at"] = deadline.isoformat()
if exc.output_events:
# Durable stash of the pre-pause terminal outputs so the resumed pass can
# pre-seed its capture list. Deduped so a resume-pause-resume cycle does
# not accumulate duplicates (the pre-seed already folds the prior stash in).
metadata["pre_pause_outputs"] = self._dedup_outputs(exc.output_events)
if metadata:
await self._jobs.update_job_metadata(job_id, metadata)
) line 150

await self._jobs.update_job_status(job_id, JobStatus.SUSPENDED)
# pending_request_id and pre_pause_outputs are persisted afterward

Explanation

SUSPENDED is committed before pending_request_id and pre_pause_outputs. A client can observe that status and [claim the resume](

job = await job_service.get_job_by_job_id(job_id)
if job is None or job.type != JobType.WORKFLOW or job.status != JobStatus.SUSPENDED:
return False
pending = (job.job_metadata or {}).get("pending_request_id")
if pending is not None and request_id != pending:
return False
# Win the single-flight flip BEFORE writing the RESUME signal, so exactly one
# RESUME row exists per suspend and a loser never strands a stray decision.
if not await job_service.claim_suspended_for_resume(job_id, owner=self._owner):
return False
await job_service.write_signal(job_id, SignalType.RESUME, {"decision": decision, "request_id": request_id})
) before the metadata transaction completes.

I reproduced this with a barrier around update_job_metadata(): the continuation completed while the stash was unavailable, and Job.result.outputs contained only the post-resume output. The same window can expose a missing or previous pending_request_id.

Suggested Fix

  1. Add a job-service operation that persists the suspended status and metadata in one transaction.
  2. Add a barrier-based regression test that resumes while suspension is being persisted.

Found 3 suggestions for improvement:

🟡 Suggestions (Should Consider)

1. [P2] Continue capturing standard output frames

FilePath: [src/backend/base/langflow/services/background_execution/runner.py](

if event_type == WORKFLOW_OUTPUT_CAPTURE_EVENT:
# Off-wire terminal-output capture (both protocols): record it into
# the in-memory result ONLY. Deliberately no ``append_event`` (never
# in ``job_events``) and no ``publish`` (never on the live bus), so the
# wire is unchanged and ``Job.result`` fills from these captures alone.
payload = self._decode_payload(frame_bytes)
output_data = payload.get("data")
if isinstance(output_data, dict):
output_events.append(output_data)
continue
if event_type == HUMAN_INPUT_REQUIRED_EVENT:
from langflow.services.jobs.service import _unwrap_pause_payload
payload = self._decode_payload(frame_bytes)
request = _unwrap_pause_payload(payload) or {}
# Carry the pre-pause captures out with the exception so _suspend can
# stash them; without this the resumed pass starts empty and the final
# result drops every terminal output produced before the pause.
raise PauseRequested(
payload=payload,
request_id=request.get("request_id"),
output_events=output_events,
)
if self._adapter.is_durable(event_type):
# Vertex/milestone-boundary cooperative cancel: a STOP written to
# the durable signal table flips the job at the next durable
# frame. Poll only here (not on every ephemeral token): a stop is
# honored at boundaries anyway, so a per-token DB read is wasted
# work that scales with the token stream.
if await self._stop_requested(job_id):
raise self._user_cancelled()
payload = self._decode_payload(frame_bytes)
seq = await self._jobs.append_event(job_id, event_type, payload)
last_durable_seq = seq
await self._bus.publish(str(job_id), LiveFrame(seq=seq, data=self._restamp_id(frame_bytes, seq)))
if event_type == self._adapter.terminal_error_type:
errored_payload = payload
# NOTE: terminal outputs are captured off-wire via the
# WORKFLOW_OUTPUT_CAPTURE_EVENT frame above (protocol-neutral), not
# from the langflow adapter's durable wire ``output`` event — that
# left agui-protocol ``Job.result`` empty. The wire ``output`` still
# flows to streaming clients via ``append_event``/``publish`` here.
) line 255

Explanation

The runner now populates results exclusively from the private WORKFLOW_OUTPUT_CAPTURE_EVENT, removing the previous standard "output" handling. An injected/exported FrameSource can emit a valid durable output followed by end; that output is persisted and published, but the job completes with outputs: []. I reproduced this against the current head.

Suggested Fix

  1. Retain "output" as a fallback capture path.
  2. Deduplicate the private and standard captures by component_id.
  3. Add a runner test using only normal adapter events.

2. [P2] Preserve the effective session on vertex-build fallback

FilePath: [src/backend/base/langflow/api/v2/workflow.py](

# The session the run executed under, resolved exactly as the runner's
# frame source did (``parsed.session_id or str(flow.id)``): the submit
# request is persisted on ``job_metadata["request"]``, so a completed
# background GET echoes the same chat/memory thread sync returns. The
# terminal output's ``content`` is a rendered string, so it can't be
# searched structurally — the persisted request is the source of truth.
persisted_request = (job.job_metadata or {}).get("request") or {}
effective_session_id = persisted_request.get("session_id") or flow_id_str
# Default GET-status path: rebuild from the durable ``output`` events
# the runner captured into ``Job.result`` (langflow-protocol). This
# needs no ``vertex_build`` rows, so it works with vertex-build storage
# off (headless), and skips the graph reconstruction the vertex-build
# path does.
result = job.result if isinstance(job.result, dict) else {}
output_events = result.get("outputs") or []
if output_events:
return workflow_response_from_output_events(
output_events,
flow_id=flow_id_str,
job_id=job_id_str,
session_id=effective_session_id,
)
# Fallback: ``Job.result`` carried no outputs (an agui-protocol run
# leaves it empty; the terminal output lives only on /events) or a
# legacy job predates output capture. Reconstruct from the
# ``vertex_build`` rows keyed by job_id when they exist; if none do,
# ValueError degrades to a bare COMPLETED with an empty outputs map.
try:
return await reconstruct_workflow_response_from_job_id(
session=session,
flow=flow,
job_id=job_id_str,
user_id=str(current_user.id),
)
) line 681

Explanation

The endpoint derives effective_session_id from the persisted request, but returns the reconstructed vertex-build response unchanged. When Job.result.outputs is empty and the terminal builds are data-only, reconstruction cannot infer a session and returns null despite an explicit request session. That violates the documented response contract.

Suggested Fix

Apply effective_session_id when the reconstructed response does not already contain a session, and add a data-only fallback test.


3. [P2] Fall back when stored outputs cannot be deserialized

FilePath: [src/backend/base/langflow/api/v2/workflow.py](

result = job.result if isinstance(job.result, dict) else {}
output_events = result.get("outputs") or []
if output_events:
return workflow_response_from_output_events(
output_events,
flow_id=flow_id_str,
job_id=job_id_str,
session_id=effective_session_id,
)
) line 667
FilePath: [src/lfx/src/lfx/workflow/converters.py](
outputs: dict[str, ComponentOutput] = {}
for item in output_events:
if not isinstance(item, dict):
continue
component_id = item.get("component_id")
if not component_id:
continue
fields = {key: value for key, value in item.items() if key != "component_id"}
try:
outputs[component_id] = ComponentOutput(**fields)
except ValueError:
# A malformed stored payload should not 500 the status read; skip it
# and report whatever outputs did rebuild cleanly.
continue
) line 509

Explanation

Any truthy stored output list suppresses vertex-build reconstruction, while the converter silently skips malformed or version-skewed entries. A completed job can consequently return empty or partial outputs despite having valid vertex-build recovery data. I reproduced an invalid stored capture returning {} without invoking reconstruction.

Suggested Fix

Make conversion report rejected entries and fall back—or merge with vertex reconstruction—when the stored result cannot be reconstructed completely.


Found 1 optional nit:

🟢 Nits (Optional)

1. Refresh stale AG-UI comments and the PR description

FilePath: [runner.py](

resume = await self._maybe_resume(job_id)
if resume is not None:
source_kwargs = {**source_kwargs, "resume": resume}
last_durable_seq = 0
errored_payload: dict[str, Any] | None = None
# Terminal outputs the run produced, captured so GET status can return
# the result without forcing a /events re-attach. The langflow adapter
# normalizes each terminal output into a durable ``output`` event whose
# ``data`` is the same ``OutputEvent`` (a ``ComponentOutput`` plus its
# component id) that sync returns in ``outputs[id]``. The agui adapter
# does not emit these, so agui-protocol runs leave this empty and their
# status stays result-less (the result is still on the /events log).
) line 235

Explanation

Several comments still say AG-UI leaves results empty, while the new off-wire capture populates them. The PR description also still advertises LANGFLOW_JOB_EVENTS_STORAGE_ENABLED, which this head removes.

Suggested Fix

Update the comments and PR description to match the final design.


✅ What's Good

  • The event-storage-off failure is resolved by removing that mode.
  • Pre-pause output preservation works in ordinary, non-racing resume sequences.
  • The off-wire capture preserves both Langflow and AG-UI wire protocols.
  • Focused validation passed: 154 LFX tests, 23 SQLite runner tests, 3 new SQLite HITL tests, and 2 API integration tests.
  • The head 76d02486a0 is mergeable onto current release-1.12.0, and git diff --check passes.

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

@erichare erichare left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Re-reviewed at 46fee33. The suspend transition and resume-critical metadata now commit atomically; standard output frames retain a Job.result fallback with deduplication; vertex-build recovery preserves the effective session; malformed or incomplete stored outputs recover without returning a partial authoritative result or a 500. The stale protocol comments and PR description are corrected. Focused and broader backend tests, the full LFX converter suite, Ruff, formatting, and merge-tree validation are green. No remaining findings.

@github-actions github-actions Bot added lgtm This PR has been approved by a maintainer and removed enhancement New feature or request labels Aug 5, 2026
@erichare

erichare commented Aug 5, 2026

Copy link
Copy Markdown
Member

@Jkavia I addressed the review findings in 46fee33 and re-reviewed the updated head. The focused/backend regression suites, full LFX converter suite, lint/format, and merge-tree validation are green, and I have approved it from my side. Could you take a look and confirm the fixes?

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

@dkaushik94 dkaushik94 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One point I'f fix:
src/backend/base/langflow/api/v2/workflow_execution.py:464 catches only (RuntimeError, ValueError, OSError). But set_result commits through session_scope(), and a DB-layer failure raises a sqlalchemy.exc.SQLAlchemyError (e.g. OperationalError — SQLite "database is locked", which is a documented pain point in this very repo). That's not in the caught tuple, so it escapes the helper, propagates to the broad except Exception at src/backend/base/langflow/api/v2/workflow_execution.py:637, and gets returned as create_error_response(...).

So under DB contention, a run that fully succeeded (response already built inline) reports an error to the client — the exact opposite of the docstring's promise that "a persistence failure must never fail the run." It's a separate session, so there's no transaction-state risk; the fix is just to broaden the except to except Exception (with # noqa: BLE001). None of the new tests cover this path.

Approving since everything else looks good. Pushing this fix to the branch myself.
Should be good to go @Jkavia

…h session_id

Flip the v2 workflow GET-status construction so the durable Job.result blob
is the primary output source and vertex_build reconstruction is the fallback.
Echo session_id (from job_metadata request, falling back to flow id) on the
response so completed background runs return the same shape as sync.

Populate Job.result for AG-UI runs via protocol-neutral off-wire capture: a
synthesized WORKFLOW_OUTPUT_CAPTURE_EVENT frame built from the raw end_vertex
before adapter.translate, captured in-memory only (never appended or published,
so it cannot leak to /events). This makes AG-UI runs carry full outputs even
though the wire protocol emits no output event.

Add LANGFLOW_JOB_EVENTS_STORAGE_ENABLED to gate job_events persistence. When
off, the runner uses a local seq fallback so live subscribers still work while
per-milestone DB writes and reattach/replay are skipped; Job.result and
GET-status output are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

fix(workflow-api): preserve pre-pause HITL outputs; drop job-events storage flag

A background run that produced a terminal output on one branch, then paused
for human input on another, lost that output: the resumed pass starts a fresh
capture list and the output loop skips re-emitting checkpoint-restored
vertices, so finalize only had the resumed pass's captures.

Fix: PauseRequested carries the pre-pause output_events out of _drive; _suspend
stashes them (deduped) in job_metadata.pre_pause_outputs; a resumed _drive
pre-seeds its capture list from that stash; set_result dedups by component_id
(first position, latest value) so a re-emitted vertex overwrites its stale
entry and branch order is preserved.

Also remove the job_events_storage_enabled setting — the durable event log is
now always persisted (removes the event-storage-off path that dropped live
HITL requests). Drops the field from EXPECTED_FIELDS and the now-impossible
off-mode test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

fix: harden background workflow result handling

feat(workflow-api): persist sync run outputs to Job.result

A sync run already creates a Job row (to support HITL suspend + run_id-keyed
vertex builds) and reaches COMPLETED, but never wrote Job.result — so a later
GET status on its job_id could not return the outputs the request returned
inline. Persist the terminal outputs in the same list-of-OutputEvent shape the
background runner writes, so the status read is protocol-uniform across sync and
background. Best-effort: the caller already holds the response inline, so a
result-cache write failure is logged and never fails the run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Fixed exception handling upon persistence failure but execution success. now persistence issue won't block users from getting the output if the workflow ran successfully.
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Aug 5, 2026
@Jkavia
Jkavia added this pull request to the merge queue Aug 5, 2026
@erichare
erichare removed this pull request from the merge queue due to a manual request Aug 5, 2026
@erichare
erichare merged commit 18f9875 into release-1.12.0 Aug 5, 2026
75 checks passed
@erichare
erichare deleted the workflow-api-prod branch August 5, 2026 23:09
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Aug 5, 2026
thesaadmirza pushed a commit to thesaadmirza/langflow that referenced this pull request Aug 6, 2026
Both conflicts were additive: langflow-ai#14353 added emit_output_capture to
_stream_event_frames and its background call site at the same spot this branch
adds execution_timeout. Kept both parameters.
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants