* 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 commit3b92500349) * 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 commita3e8b40811) * 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 commit6f6639374f) * 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) * 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. --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Langflow is a powerful platform for building and deploying AI-powered agents and workflows. It provides developers with both a visual authoring experience and built-in API and MCP servers that turn every workflow into a tool that can be integrated into applications built on any framework or stack. Langflow comes with batteries included and supports all major LLMs, vector databases and a growing library of AI tools.
✨ Highlight features
- Visual builder interface to quickly get started and iterate.
- Source code access lets you customize any component using Python.
- Interactive playground to immediately test and refine your flows with step-by-step control.
- Multi-agent orchestration with conversation management and retrieval.
- Deploy as an API or export as JSON for Python apps.
- Deploy as an MCP server and turn your flows into tools for MCP clients.
- Observability with LangSmith, LangFuse and other integrations.
- Enterprise-ready security and scalability.
🖥️ Langflow Desktop
Langflow Desktop is the easiest way to get started with Langflow. All dependencies are included, so you don't need to manage Python environments or install packages manually. Available for Windows and macOS.
⚡️ Quickstart
Install locally (recommended)
Requires Python 3.10–3.14 and uv (recommended package manager).
Install
From a fresh directory, run:
uv pip install langflow -U
The latest Langflow package is installed. For more information, see Install and run the Langflow OSS Python package.
Run
To start Langflow, run:
uv run langflow run
Langflow starts at http://127.0.0.1:7860.
That's it! You're ready to build with Langflow! 🎉
📦 Other install options
Run from source
If you've cloned this repository and want to contribute, run this command from the repository root:
make run_cli
For more information, see DEVELOPMENT.md.
Docker
Start a Langflow container with default settings:
docker run -p 7860:7860 langflowai/langflow:latest
Langflow is available at http://localhost:7860/. For configuration options, see the Docker deployment guide.
🛡️ Security
For security information, see our Security Policy.
🚀 Deployment
Langflow is completely open source and you can deploy it to all major deployment clouds. To learn how to deploy Langflow, see our Langflow deployment guides.
⭐ Stay up-to-date
Star Langflow on GitHub to be instantly notified of new releases.
👋 Contribute
We welcome contributions from developers of all levels. If you'd like to contribute, please check our contributing guidelines and help make Langflow more accessible.