Back to Langflow

Bundle API

BUNDLE_API.md

1.10.0.dev4116.3 KB
Original Source

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 ## Changelog entry describing the change. The CI guard scripts/migrate/check_bundle_api_changelog.py enforces 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

SymbolSource
Componentlfx.custom.custom_component.component.Component
Component.build() (declared on subclasses)call site of every loaded bundle module
Component.inputsdeclarative input list
Component.outputsdeclarative output list
Component.display_name / Component.description / Component.icon / Component.documentationmetadata read by the palette
Component.nameoptional override of the registry class name

Inputs

SymbolSource
Input (base)lfx.io
MessageTextInput / MultilineInput / SecretStrInputlfx.io
IntInput / FloatInput / BoolInputlfx.io
DropdownInput / TabInputlfx.io
DictInput / NestedDictInputlfx.io
FileInput / LinkInputlfx.io
HandleInputlfx.io

Outputs

SymbolSource
Outputlfx.io

Schema types

SymbolSource
Datalfx.schema.data
DataFramelfx.schema.dataframe
Messagelfx.schema.message

Manifest contract (consumed by the loader)

SymbolSource
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_VERSIONlfx.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

SymbolSource
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
LoadedComponentlfx.extension.loader (frozen dataclass; what the registry stores)
LoadResultlfx.extension.loader
SLOT_OFFICIAL / SLOT_EXTRAlfx.extension.loader

Reload pipeline

SymbolSource
reload_bundle(registry, bundle_name)lfx.extension.reload
BundleRegistrylfx.extension.bundle_registry
BundleRecordlfx.extension.bundle_registry
ReloadInProgressErrorlfx.extension.bundle_registry
POST /api/v1/extensions/{id}/bundles/{name}/reloadlangflow.api.v1.extensions

Errors

SymbolSource
ExtensionErrorlfx.extension.errors
ExtensionErrorCollectionlfx.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

SymbolSource
validate_extension(root, *, execute_imports=False)lfx.extension.validate
ValidateReportlfx.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

SymbolSource
Migration-table filesrc/lfx/src/lfx/extension/migration/migration_table.json
MigrationEntrylfx.extension.migration.schema
MigrationTablelfx.extension.migration.schema
migrate_flow_payload(payload, table)lfx.extension.migration.rewrite
MIGRATION_SCHEMA_VERSIONlfx.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 factories
  • routes — bundle-mounted HTTP routes
  • hooks — bundle-declared lifecycle hooks
  • starter_projects — bundle-shipped starter flows
  • userConfig — bundle-declared user-config schema
  • Multi-bundle manifests (bundles list 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 Component base class (no LCToolComponent legacy).
  • 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 by check_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 the sys.modules swap and the BundleRecord install. 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) returns 422 Unprocessable Entity for structural failures (broken bundle, missing source path, name mismatch) instead of 200 OK with ok=false. Body is {...primaryError, result: ReloadResult} so the full typed result is preserved under the FastAPI detail envelope. 409 Conflict for reload-in-progress is unchanged.
  • CLI table updated to remove the obsolete dev register / dev unregister / dev list subcommands; the actual surface is extension dev <path> plus the Python helpers register_dev_extension / unregister_dev_extension.
  • MigrationTable.ambiguous_bare_names added. Each entry is {name, candidates: [list of canonical IDs]} and registers a bare class name that exists in 2+ bundles. The deserializer now surfaces component-name-ambiguous (with the candidate targets) for any bare name listed here, instead of falling through to the generic component-not-found-with-hint. Seeded with the canonical regression cases (MergeDataComponent, SplitTextComponent, SubFlowComponent). check_bare_names.py now 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 .py under src/backend/base/langflow/api/** and src/lfx/src/lfx/**; a new file that mounts an APIRouter(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-scope marker.
  • 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.py now compares ambiguous_bare_names alongside entries. A marker may not be removed once published, and its candidates list may only grow -- shrinking it would silently regress flows from component-name-ambiguous to component-not-found-with-hint.
  • Router-trust guard now resolves dotted attribute references in include_router and decorators. include_router(child.api.router, prefix="/extensions") after import child.api (and the import child.api as alias; alias.router shape) are caught -- not just from child.api import router as child_router. The parser flattens any Name/Attribute chain, and the resolver walks imports of either kind (from M import N and import 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 Y anchors at the package itself (level=1 -> pkg); inside a regular module pkg.foo it anchors at the parent package (level=1 -> pkg). The arithmetic differs because __init__.py's file module IS the package, while pkg/foo.py's file module is pkg.foo. The resolver tracks is_package and decrements level by one for __init__.py files 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. DiscoveredExtension records emitted from discover_installed_extensions / discover_seed_extensions now run the same resolve-and-relative_to containment check that validate_extension performs. A symlinked bundles[0].path or a symlinked seed subdirectory that escapes the extension root is now rejected with path-escape before reaching the loader, instead of slipping through to exec_module(). The shared primitive lives at lfx.extension._paths.is_within; every walker (loader, validator, seed discovery, inline-bundle discovery) uses the same function and the same SKIP_DIR_NAMES.
    • --execute-imports env allowlist. The validator's --execute-imports subprocess now inherits an explicit allowlist (PATH, LANG, LC_*, SYSTEMROOT, TMPDIR, TZ, Python locale + encoding vars) instead of denylisting only LANGFLOW_*/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_io now flags exec, eval, __import__, compile as top-level primitives and importlib.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_modules now builds the staging->prod rename map before any sys.modules mutation, snapshots popped old modules into a recovery map, and restores them on any mid-swap exception. The length-mismatch tripwire on zip(strict=True) no longer leaves the prod namespace shredded. A new typed code, reload-class-retag-failed, is appended to ReloadResult.warnings when cls.__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_extensions now detects two distributions with different canonical names but identical bundle.name (which would silently clobber each other at _lfx_ext.official.<name>.*) and emits a typed duplicate-bundle-name error on the loser, dropping its components. BundleRegistry.install_bundle additionally logs a WARNING when an existing record is replaced by a record from a different source_path (catches collisions the upstream precedence resolver missed).
    • Reload endpoint off event loop. POST /api/v1/extensions/{id}/bundles/{name}/reload now invokes reload_bundle via asyncio.to_thread so 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-milestone is renamed to the stable multi-bundle-unsupported. The old code is retained in ERROR_CODES as a deprecated alias for one milestone for log scrapers. Three new codes are added to ERROR_CODES: duplicate-bundle-name (see above), reload-class-retag-failed (see above), and reload-transport-error (CLI-side connectivity failure, previously misreported as reload-source-missing).
    • Discovery preserves "unreadable" vs "absent" distinction. _pyproject_declares_extension now propagates OSError so a permission failure on a pyproject that might declare an extension surfaces as manifest-unreadable instead of being silently dropped as "no extension here".
    • Dev registry corruption is logged. _read_state now 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 next langflow run.
    • Entry-point predicate avoids module-level side effects. _entry_point_loads_to_component now consults importlib.util.find_spec first and only falls through to ep.load() when the spec lookup is insufficient. The except BaseException was narrowed to except Exception so SystemExit / KeyboardInterrupt are 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.modules surgery primitives moved to lfx.extension.reload_swap; load_installed_extensions / load_seed_extensions moved to lfx.extension.loader._startup. Both are re-exported from their previous import paths so external imports are unchanged.