Commit f85a32a
feat: native v2 workflows endpoint with pluggable stream protocols (langflow-ai#13307)
* 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.
* 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): 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 langflow-ai#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.
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>1 parent 2b7b113 commit f85a32a
52 files changed
Lines changed: 11391 additions & 2686 deletions
File tree
- src
- backend
- base
- langflow
- api
- utils
- v1
- v2
- adapters
- services/auth
- tests/unit/api/v2
- adapters
- frontend
- src
- controllers/API/agui
- __tests__
- stores
- __tests__
- tests/utils
- lfx
- src/lfx
- custom/custom_component
- schema
- services/settings/groups
- tests/unit
- custom/custom_component
- schema
- services/settings
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
55 | 55 | | |
56 | 56 | | |
57 | 57 | | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
58 | 84 | | |
59 | 85 | | |
60 | 86 | | |
| |||
337 | 363 | | |
338 | 364 | | |
339 | 365 | | |
| 366 | + | |
| 367 | + | |
340 | 368 | | |
341 | 369 | | |
342 | 370 | | |
343 | 371 | | |
344 | 372 | | |
345 | 373 | | |
346 | 374 | | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
347 | 379 | | |
348 | 380 | | |
349 | 381 | | |
| |||
354 | 386 | | |
355 | 387 | | |
356 | 388 | | |
357 | | - | |
| 389 | + | |
358 | 390 | | |
359 | 391 | | |
360 | 392 | | |
361 | 393 | | |
362 | 394 | | |
363 | 395 | | |
364 | | - | |
| 396 | + | |
365 | 397 | | |
366 | 398 | | |
367 | 399 | | |
| |||
374 | 406 | | |
375 | 407 | | |
376 | 408 | | |
377 | | - | |
| 409 | + | |
378 | 410 | | |
379 | 411 | | |
380 | 412 | | |
381 | 413 | | |
382 | 414 | | |
383 | | - | |
| 415 | + | |
384 | 416 | | |
385 | 417 | | |
386 | 418 | | |
| |||
497 | 529 | | |
498 | 530 | | |
499 | 531 | | |
500 | | - | |
501 | | - | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
502 | 538 | | |
503 | 539 | | |
504 | 540 | | |
| |||
507 | 543 | | |
508 | 544 | | |
509 | 545 | | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
510 | 549 | | |
511 | 550 | | |
512 | 551 | | |
| |||
615 | 654 | | |
616 | 655 | | |
617 | 656 | | |
618 | | - | |
| 657 | + | |
| 658 | + | |
| 659 | + | |
619 | 660 | | |
620 | 661 | | |
621 | 662 | | |
| |||
648 | 689 | | |
649 | 690 | | |
650 | 691 | | |
651 | | - | |
| 692 | + | |
652 | 693 | | |
653 | 694 | | |
654 | 695 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
39 | 39 | | |
40 | 40 | | |
41 | 41 | | |
| 42 | + | |
42 | 43 | | |
43 | 44 | | |
44 | 45 | | |
| |||
124 | 125 | | |
125 | 126 | | |
126 | 127 | | |
| 128 | + | |
127 | 129 | | |
128 | 130 | | |
129 | 131 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
45 | 45 | | |
46 | 46 | | |
47 | 47 | | |
| 48 | + | |
48 | 49 | | |
49 | 50 | | |
50 | 51 | | |
| |||
90 | 91 | | |
91 | 92 | | |
92 | 93 | | |
| 94 | + | |
93 | 95 | | |
94 | 96 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| 5 | + | |
5 | 6 | | |
6 | 7 | | |
7 | 8 | | |
| |||
131 | 132 | | |
132 | 133 | | |
133 | 134 | | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
134 | 171 | | |
135 | 172 | | |
136 | 173 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
5 | 4 | | |
6 | 5 | | |
7 | 6 | | |
| |||
34 | 33 | | |
35 | 34 | | |
36 | 35 | | |
| 36 | + | |
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
| |||
747 | 747 | | |
748 | 748 | | |
749 | 749 | | |
750 | | - | |
751 | | - | |
752 | | - | |
753 | | - | |
754 | | - | |
755 | | - | |
756 | | - | |
757 | | - | |
758 | | - | |
759 | | - | |
760 | | - | |
761 | | - | |
762 | | - | |
763 | | - | |
764 | | - | |
765 | | - | |
766 | | - | |
767 | | - | |
768 | | - | |
769 | | - | |
770 | | - | |
771 | | - | |
772 | | - | |
773 | | - | |
774 | | - | |
775 | | - | |
776 | | - | |
777 | | - | |
| 750 | + | |
| 751 | + | |
| 752 | + | |
778 | 753 | | |
779 | 754 | | |
780 | 755 | | |
| |||
837 | 812 | | |
838 | 813 | | |
839 | 814 | | |
840 | | - | |
| 815 | + | |
841 | 816 | | |
842 | 817 | | |
843 | 818 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| 7 | + | |
7 | 8 | | |
8 | | - | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
0 commit comments