Bundle API
Stable surface that Langflow Extension Bundles consume. Every public symbol
Loading actions...
Skill content
Main instructions and any bundled files for this skill.
Bundle API
Stable surface that Langflow Extension Bundles consume. Every public symbol
listed below is part of the contract: changes to its name, signature, semantics,
or visibility require a coordinated version bump and a ## Changelog entry.
This document is paired with the integer BUNDLE_API_VERSION declared in
lfx.extension.manifest. Manifests
declare the contract versions they support via lfx.compat: ["1"]; a bundle
that does not list str(BUNDLE_API_VERSION) is rejected at install time with
version-constraint-unsatisfied.
CI gate: any PR that modifies a file containing an in-scope surface MUST add a
## Changelogentry describing the change. The CI guardscripts/migrate/check_bundle_api_changelog.pyenforces this. Pure-internal refactors that preserve every public symbol's name and signature do not require a changelog entry, but reviewers should be skeptical.
Surface (v0)
Component base class
| Symbol | Source |
|---|---|
Component | lfx.custom.custom_component.component.Component |
Component.build() (declared on subclasses) | call site of every loaded bundle module |
Component.inputs | declarative input list |
Component.outputs | declarative output list |
Component.display_name / Component.description / Component.icon / Component.documentation | metadata read by the palette |
Component.name | optional override of the registry class name |
Inputs
| Symbol | Source |
|---|---|
Input (base) | lfx.io |
MessageTextInput / MultilineInput / SecretStrInput | lfx.io |
IntInput / FloatInput / BoolInput | lfx.io |
DropdownInput / TabInput | lfx.io |
DictInput / NestedDictInput | lfx.io |
FileInput / LinkInput | lfx.io |
HandleInput | lfx.io |
Outputs
| Symbol | Source |
|---|---|
Output | lfx.io |
Schema types
| Symbol | Source |
|---|---|
Data | lfx.schema.data |
DataFrame | lfx.schema.dataframe |
Message | lfx.schema.message |
Manifest contract (consumed by the loader)
| Symbol | Source |
|---|---|
Manifest schema (extension.json / [tool.langflow.extension]) | lfx.extension.manifest.ExtensionManifest |
BundleRef (one entry in bundles[]) | lfx.extension.manifest.BundleRef |
LfxCompat (declared as manifest.lfx) | lfx.extension.manifest.LfxCompat |
BUNDLE_API_VERSION (the integer this lfx ships) | lfx.extension.manifest |
EXTENSION_SCHEMA_URL / SCHEMA_VERSION | lfx.extension.manifest |
Slot vocabulary: official (installed pip distributions and seed
directories) and extra (paths declared in LANGFLOW_COMPONENTS_PATH).
Component IDs at runtime are ext:<bundle>:<Class>@<slot>.
Discovery + loading entry points
| Symbol | Source |
|---|---|
load_extension(root) | lfx.extension.loader |
load_installed_extensions() | lfx.extension.loader |
discover_inline_bundles() | lfx.extension.loader |
discover_installed_extensions() / discover_seed_extensions() / discover_all_extensions() | lfx.extension.discovery |
LoadedComponent | lfx.extension.loader (frozen dataclass; what the registry stores) |
LoadResult | lfx.extension.loader |
SLOT_OFFICIAL / SLOT_EXTRA | lfx.extension.loader |
Reload pipeline
| Symbol | Source |
|---|---|
reload_bundle(registry, bundle_name) | lfx.extension.reload |
BundleRegistry | lfx.extension.bundle_registry |
BundleRecord | lfx.extension.bundle_registry |
ReloadInProgressError | lfx.extension.bundle_registry |
POST /api/v1/extensions/{id}/bundles/{name}/reload | langflow.api.v1.extensions |
Errors
| Symbol | Source |
|---|---|
ExtensionError | lfx.extension.errors |
ExtensionErrorCollection | lfx.extension.errors |
format_extension_error(error) | lfx.extension.errors |
ERROR_CODES (frozenset of every typed code) | lfx.extension.errors |
The full kebab-case discriminant set is the contract — adding a code is
backward-compatible; removing or renaming a code is a breaking change and
requires a BUNDLE_API_VERSION bump.
Validate / authoring CLI
| Symbol | Source |
|---|---|
validate_extension(root, *, execute_imports=False) | lfx.extension.validate |
ValidateReport | lfx.extension.validate |
lfx extension validate (CLI) | lfx.cli._extension_commands |
lfx extension schema (CLI) | lfx.cli._extension_commands |
lfx extension init (CLI) | lfx.cli._extension_commands |
lfx extension dev (CLI -- registers a local path and execs langflow run) | lfx.cli._extension_commands |
lfx extension list (CLI) | lfx.cli._extension_commands |
lfx extension reload (CLI) | lfx.cli._extension_commands |
register_dev_extension / unregister_dev_extension (Python API) | lfx.extension.dev_registry |
Migration
| Symbol | Source |
|---|---|
| Migration-table file | src/lfx/src/lfx/extension/migration/migration_table.json |
MigrationEntry | lfx.extension.migration.schema |
MigrationTable | lfx.extension.migration.schema |
migrate_flow_payload(payload, table) | lfx.extension.migration.rewrite |
MIGRATION_SCHEMA_VERSION | lfx.extension.migration.schema |
Out of scope (v0)
These are reserved in the manifest schema and produce a typed
field-deferred-in-this-milestone error if set; they are NOT part of the
v0 contract:
services— bundle-declared service factoriesroutes— bundle-mounted HTTP routeshooks— bundle-declared lifecycle hooksstarter_projects— bundle-shipped starter flowsuserConfig— bundle-declared user-config schema- Multi-bundle manifests (
bundleslist with length > 1)
Pilot bundle: lfx-duckduckgo
The shipped LE-1023 pilot is duckduckgo, extracted into the
standalone distribution
lfx-duckduckgo under src/bundles/duckduckgo/
with its own pyproject.toml. langflow's own pyproject.toml
declares lfx-duckduckgo>=0.1.0 as a regular dependency so a flat
pip install langflow continues to ship the bundle as before.
Why this bundle:
- Single component (
DuckDuckGoSearchComponent) in a single file (duck_duck_go_search_run.py). - Zero git churn over the last six months.
- Modern
Componentbase class (noLCToolComponentlegacy). - No authentication required — failure mode is a single failed request, not a paid-API outage.
- Class name is globally unique across
src/lfx/src/lfx/components/**, so the bare-name migration entry is allowed bycheck_bare_names.py.
The runtime half of the M1 proof-of-delivery gate (save a flow on
pre-migration Langflow, upgrade, confirm it loads AND runs identically)
lives in the dogfood checklist at
src/bundles/duckduckgo/M1_DOGFOOD_CHECKLIST.md;
the deserialize half is covered by
src/lfx/tests/integration/extension/test_pilot_duckduckgo_upgrade.py.
Changelog
v0 (this release)
- Initial surface enumerated above. Frozen as
BUNDLE_API_VERSION = 1. BundleRegistry.write_locked()exposed as a public context manager so the reload pipeline can hold the registry write lock across both thesys.modulesswap and theBundleRecordinstall. Concurrent readers can no longer observe new modules paired with the old record. No change to the addressable component contract.- HTTP reload endpoint (
POST /api/v1/extensions/{id}/bundles/{name}/reload) returns422 Unprocessable Entityfor structural failures (broken bundle, missing source path, name mismatch) instead of200 OKwithok=false. Body is{...primaryError, result: ReloadResult}so the full typed result is preserved under the FastAPIdetailenvelope.409 Conflictforreload-in-progressis unchanged. - CLI table updated to remove the obsolete
dev register/dev unregister/dev listsubcommands; the actual surface isextension dev <path>plus the Python helpersregister_dev_extension/unregister_dev_extension. MigrationTable.ambiguous_bare_namesadded. Each entry is{name, candidates: [list of canonical IDs]}and registers a bare class name that exists in 2+ bundles. The deserializer now surfacescomponent-name-ambiguous(with the candidate targets) for any bare name listed here, instead of falling through to the genericcomponent-not-found-with-hint. Seeded with the canonical regression cases (MergeDataComponent,SplitTextComponent,SubFlowComponent).check_bare_names.pynow verifies every Component class found in 2+ bundle folders has a matching marker, so a future bundle move that introduces a new ambiguity is caught at PR time.- Router-trust CI guard broadened to scan every
.pyundersrc/backend/base/langflow/api/**andsrc/lfx/src/lfx/**; a new file that mounts anAPIRouter(prefix=".../extensions...")is auto-detected and checked for forbidden install/uninstall/registry-mutation handlers. Authors of files with non-literal prefixes can opt in via a# router-trust: in-scopemarker. - Router-trust guard rewritten to use AST-based cross-file resolution.
A forbidden handler in module A is now caught when module B mounts A's
router via
parent.include_router(child, prefix=".../extensions..."), and the same applies transitively across multi-hop include_router chains. An imported router that cannot be statically resolved is ignored (the guard never flags routes it cannot prove reachable from/extensions); routes co-located with an in-scope router ARE flagged. check_migration_append_only.pynow comparesambiguous_bare_namesalongsideentries. A marker may not be removed once published, and itscandidateslist may only grow -- shrinking it would silently regress flows fromcomponent-name-ambiguoustocomponent-not-found-with-hint.- Router-trust guard now resolves dotted attribute references in
include_routerand decorators.include_router(child.api.router, prefix="/extensions")afterimport child.api(and theimport child.api as alias; alias.routershape) are caught -- not justfrom child.api import router as child_router. The parser flattens anyName/Attributechain, and the resolver walks imports of either kind (from M import Nandimport M, with or without an asname) back to the source file. - Router-trust guard's relative-import resolver is now
__init__.py-aware. Inside a package,from .child import Yanchors at the package itself (level=1 ->pkg); inside a regular modulepkg.fooit anchors at the parent package (level=1 ->pkg). The arithmetic differs because__init__.py's file module IS the package, whilepkg/foo.py's file module ispkg.foo. The resolver tracksis_packageand decrementslevelby one for__init__.pyfiles so both shapes resolve correctly. - Code-review hardening pass across the extension subsystem. No public
symbol's name or signature changed; this entry covers behavioural
tightening that bundle authors and operators should be aware of:
- Path-safety contract honored on every discovery path.
DiscoveredExtensionrecords emitted fromdiscover_installed_extensions/discover_seed_extensionsnow run the same resolve-and-relative_tocontainment check thatvalidate_extensionperforms. A symlinkedbundles[0].pathor a symlinked seed subdirectory that escapes the extension root is now rejected withpath-escapebefore reaching the loader, instead of slipping through toexec_module(). The shared primitive lives atlfx.extension._paths.is_within; every walker (loader, validator, seed discovery, inline-bundle discovery) uses the same function and the sameSKIP_DIR_NAMES. --execute-importsenv allowlist. The validator's--execute-importssubprocess now inherits an explicit allowlist (PATH,LANG,LC_*,SYSTEMROOT,TMPDIR,TZ, Python locale + encoding vars) instead of denylisting onlyLANGFLOW_*/LFX_*. Cloud / CI credentials (AWS_*,OPENAI_API_KEY,GITHUB_TOKEN, ...) no longer propagate into untrusted bundle import. The CLI / module docs re-frame this pass as best-effort hygiene lint, not a sandbox.- AST hygiene lint widened.
_find_top_level_ionow flagsexec,eval,__import__,compileas top-level primitives andimportlib.import_module/importlib.__import__as dotted-name primitives. Still best-effort literal-name matching; trivially bypassable by obfuscation, and documented as such. - Reload swap is non-destructive.
_swap_sys_modulesnow builds the staging->prod rename map before anysys.modulesmutation, snapshots popped old modules into a recovery map, and restores them on any mid-swap exception. The length-mismatch tripwire onzip(strict=True)no longer leaves the prod namespace shredded. A new typed code,reload-class-retag-failed, is appended toReloadResult.warningswhencls.__module__cannot be retagged so the empty-palette-after-reload regression leaves a trail instead of silently failing. - Cross-source bundle-name collision.
load_installed_extensionsnow detects two distributions with different canonical names but identicalbundle.name(which would silently clobber each other at_lfx_ext.official.<name>.*) and emits a typedduplicate-bundle-nameerror on the loser, dropping its components.BundleRegistry.install_bundleadditionally logs a WARNING when an existing record is replaced by a record from a differentsource_path(catches collisions the upstream precedence resolver missed). - Reload endpoint off event loop.
POST /api/v1/extensions/{id}/bundles/{name}/reloadnow invokesreload_bundleviaasyncio.to_threadso slow or large bundle imports do not freeze the worker for other in-flight requests. The wire contract (status codes, body shape) is unchanged. - Stable typed-error code rename.
multi-bundle-deferred-in-this-milestoneis renamed to the stablemulti-bundle-unsupported. The old code is retained inERROR_CODESas a deprecated alias for one milestone for log scrapers. Three new codes are added toERROR_CODES:duplicate-bundle-name(see above),reload-class-retag-failed(see above), andreload-transport-error(CLI-side connectivity failure, previously misreported asreload-source-missing). - Discovery preserves "unreadable" vs "absent" distinction.
_pyproject_declares_extensionnow propagatesOSErrorso a permission failure on a pyproject that might declare an extension surfaces asmanifest-unreadableinstead of being silently dropped as "no extension here". - Dev registry corruption is logged.
_read_statenow distinguishes file absent (silent, legitimate empty registry), file present but unreadable (WARNING), and file present but corrupt JSON / wrong shape (WARNING with detail). The state file is written with mode 0600 so a hostile third-party process cannot inject an extension path into the developer's nextlangflow run. - Entry-point predicate avoids module-level side effects.
_entry_point_loads_to_componentnow consultsimportlib.util.find_specfirst and only falls through toep.load()when the spec lookup is insufficient. Theexcept BaseExceptionwas narrowed toexcept ExceptionsoSystemExit/KeyboardInterruptare no longer swallowed at filter time. - Frontend reload-success warnings surfaced. The reload route's
ReloadResult.warnings(non-empty on success) now reach the user via a notice toast in addition to the green success toast. Wire shape unchanged; this is a UI fix that consumes existing payload fields. - Internal-only file split.
sys.modulessurgery primitives moved tolfx.extension.reload_swap;load_installed_extensions/load_seed_extensionsmoved tolfx.extension.loader._startup. Both are re-exported from their previous import paths so external imports are unchanged. - Editable installs are discovered via the entry-point fallback.
_distribution_manifest_pathnow falls back to thelangflow.extensionsentry-point group whendist.filesonly surfacesdist-info/entries (thepip install -e/uv pip install -ecase). The entry-point value is resolved viaimportlib.util.find_spec-- which runs import-system finders but never executes the module body -- and the resulting package directory is scanned forextension.jsonor a[tool.langflow.extension]pyproject. Wheel installs are unaffected: the fallback only fires when the primarydist.filesscan finds no manifest. Previously, editable-installed bundles were silently dropped bylfx extension listand the registry, even though the bundle pyproject already declared the entry-point. - Reload CLI:
--bundleis optional;--allis implemented.lfx extension reload <ext_id>now resolves the bundle name from localdiscover_all_extensionswhen--bundleis omitted; explicit--bundlestill wins for cases where the local install is not visible to the running server.lfx extension reload --alliterates every locally-discovered bundle, POSTs reload to each, and exits non-zero if any reload fails (previously hard-errored as "not yet wired").--allis mutually exclusive with a positional id /--bundle(exit 2). The HTTP wire contract (POST /api/v1/extensions/{id}/bundles/ {name}/reloadper-bundle) is unchanged; this is a CLI-only surface change.
- Path-safety contract honored on every discovery path.
- User-scoped extension events. Bundle lifecycle events
(
bundle_reloaded,bundle_reload_failed,flow_migrated,extension_error) now publish to a per-user keyspace (user:<user_id>) instead of the shared"global"bucket so flow-migration and reload payloads cannot leak across users via the poll endpoint.reload_bundlegains an optional keyword-onlyuser_id: str | None = Noneargument. When supplied,bundle_reloaded/bundle_reload_failedevents are emitted to keyspaceuser:<user_id>;None(CLI / authless dev) keeps the legacy"global"emission. Existing positional callers are unaffected.POST /api/v1/extensions/{id}/bundles/{name}/reloadnow resolves the authenticated user and threads its id intoreload_bundle, so every reload triggered via HTTP is published to that user's keyspace. Wire contract (status codes, body shape) unchanged.GET /api/v1/extensions/eventsdrops its client-suppliedkeyspacequery parameter. The endpoint derives the keyspace from the authenticated user server-side, so an authenticated client can no longer poll another user's keyspace. Frontends that polled withoutkeyspace(the in-tree consumer) are unaffected; third-party callers that explicitly passedkeyspace=...will now receive422from FastAPI's strict parameter validation.
- Reload event payload aligned with
ReloadResult. Bothbundle_reloadedandbundle_reload_failedevents now carry the fullReloadResult.to_dict()envelope (ok,bundle,reload_id,components_added,components_removed,components_changed,warnings,errors) instead of a hand-rolled subset. Polling clients can now (a) detect body-only edits viacomponents_changedinstead of mis-reporting them as "no source changes detected", and (b) surface a failed reload'serrors[0].messageinstead of degrading to a generic "check server logs" fallback. HTTP response shape unchanged. GET /api/v1/extensions/eventsrejectskeyspaceexplicitly. Previously the endpoint accepted but silently ignored any client-suppliedkeyspacequery parameter (server-derived from the authenticated user since the prior entry). Silent drop masked client bugs that assumed the value had effect. The route now returns422 Unprocessable Entitywith a typedextension-events-keyspace-forbiddenerror envelope when the parameter is present.extension-events-keyspace-forbiddenis added toERROR_CODES(additive; codes-as-contract semantics preserved). In-tree polling clients that never sent the parameter are unaffected.
Related Skills
Frontend Typescript Linting.mdc
TypeScript and ESLint rules that MUST be followed when creating, modifying, or reviewing any file under apps/frontend/, including .ts, .tsx, .js, and .jsx files. Also apply when discussing frontend li...
2. Apply Deepthink Protocol (reason about dependencies
risks