Building a Langflow custom component that actually loads
Written against Langflow 1.11.4. Component names, input types and loader
behaviour move between minor versions, so if you are on a different version,
check with the langflow-1-11-docs skill rather than trusting this.
For anything about how Langflow works generally — component reference, the
API, environment variables, MCP, deployment — use the langflow-1-11-docs
skill. This skill is only about the narrow path from "I wrote a .py" to "the
Agent called my action and got the right answer".
0. The shape of the problem
Almost every hour lost here goes to one of five things, and four of them fail silently. Check them in this order, because the cheap checks rule out the expensive ones.
| Symptom | Cause | Section |
|---|---|---|
| Component missing from the menu entirely | a non-top-level import, or a missing __init__.py |
§1, §2 |
Edited the .py, browser shows the old behaviour |
Langflow loads components at startup | §5 |
| Component loads, has no inputs or wrong inputs | tool_mode on an input type that does not support it |
§4 |
| Agent has the tool but never calls it | the tool name or description is not what you think | §3 |
| Agent calls the wrong action of the right component | N actions sharing one description | §3 |
1. Layout
custom_components/
mytools/ <- the category. This name becomes the sidebar heading.
__init__.py <- REQUIRED. Empty is fine.
market_data.py
LANGFLOW_COMPONENTS_PATH points at custom_components/. Two rules:
__init__.pyis mandatory. Without it nothing in the folder loads and there is no error message — not in the UI, not in the logs. If a whole category is missing, check this first.- Maximum two levels deep.
custom_components/a/b/c.pyis not scanned.
The category directory name is what students see as a sidebar section, and it is
also part of the component's registry key (ext:mytools:MarketData@extra),
which matters if you ever build flow JSON by hand.
2. Imports must be flat, unconditional, and top-level
This is the one that costs the most time, so it is first.
Langflow does not import your file. It parses it into a syntax tree, executes
only the module-level import and from ... import statements, and then
executes the class body against whatever namespace that produced. So this works:
from lfx.custom import Component
from lfx.io import MessageTextInput, Output
from lfx.schema import Data
and every one of these fails:
try: # a `try` statement is not an Import node.
import yfinance as yf # It never executes. `yf` is undefined
except ImportError: # when the class body runs.
yf = None
if TYPE_CHECKING: # same problem: not a top-level Import
from lfx.schema import Data
def run(self):
import pandas as pd # function-local: fine at call time,
... # but NOT available in the class body
The failure mode is that the component vanishes from the menu and leaves one line in the container log. Nothing appears in the UI. So:
docker compose logs langflow | grep -i "could not build template"
should print nothing. If it prints your class name, you have an import problem.
A function-local import is legal Python and works at call time — it is only a
problem if the class body needs the name (a default value, a type annotation
evaluated at definition time, a Field(...) argument). When in doubt, hoist it.
3. The contract that decides whether an Agent uses your component
Get this wrong and everything looks correct: the component loads, the flow saves, the Agent shows your tool in its tool list — and it never calls it, or calls the wrong action.
class MarketData(Component):
display_name = "Market Data" # human label on the canvas. The LLM
# never sees this.
description = "Market data lookups." # fallback tool description only
outputs = [
Output(
name="quote",
display_name="Quote",
method="price_quote", # <-- THIS is the tool name the LLM sees
info=( # <-- THIS is the tool description
"Latest trade price and previous close for one ticker. "
"Use for 'what is X trading at' questions. Returns a single "
"row; for a date range use the history action instead."
),
),
]
def price_quote(self) -> Data:
...
Three consequences, all of which surprise people:
The tool name is the output's
methodname, notdisplay_name. Renaming a method renames the tool the model is choosing from. That is a prompt change, not a refactor — an Agent that was working can stop working.The tool description is
Output(info=...). If you omitinfo, Langflow falls back to the component's singledescription, so a component with six actions gives the Agent six identically-described tools and it picks at random. This is the single most common reason "my agent ignores my tool". EveryOutputneeds its owninfo, and theinfoshould say when not to use that action, because that is what distinguishes it from its siblings.The args schema is built once from every
tool_mode=Trueinput and shared by every action. So each argument's description is repeated in the model's context once per action. An input namedtickerwith a 300-characterinfoon a six-action component costs you 1,800 characters of context on every turn, before the Agent reads anything. Keep argument text short and put the long explanation in the action'sinfo, which is paid once.
Write info for a reader who cannot see your code and has to pick between your
action and five siblings. "Gets price data" fails that test.
4. Inputs and tool mode
from lfx.io import MessageTextInput, IntInput, DropdownInput, Output
inputs = [
MessageTextInput(
name="ticker",
display_name="Ticker",
info="One ticker symbol, uppercase, e.g. AAPL.",
tool_mode=True, # the Agent may fill this in
required=True,
),
IntInput(name="window", display_name="Window (days)", value=90, tool_mode=True),
]
tool_mode=True is what promotes an input into the Agent-facing args schema.
Not every input type supports it, and an unsupported one is dropped from the
schema silently — the component still loads, the Agent just never passes that
argument and you get the default. tests/verify_components.py catches this;
your eyes will not. If you need an input type this skill does not list, confirm
it supports tool mode with the langflow-1-11-docs skill first.
If your component is a pipeline stage rather than a tool — it takes another
component's output and hands on a transformed one — it should not be offered to
an Agent at all. Set agent_tool = False on the class to opt out.
5. The edit → test → restart loop
Editing the .py and reloading the browser does nothing. Langflow loads
custom components at process startup, so the old class object is still
registered. This is true of any Python change, not just adding or renaming a
file.
# 1. Structure check. Replays Langflow's loader and its tool-name derivation, so
# it catches the silent failures above without starting anything. Runs on the
# host - it stubs out lfx and needs no Langflow install.
python3 tests/verify_components.py custom_components/mytools
# 2. Reload. ~30s. NOT a rebuild: `docker compose build` is only needed when the
# Dockerfile changes, and rebuilding to pick up a .py edit wastes minutes.
docker compose restart langflow
# 3. Wait for healthy before you refresh the browser.
docker compose ps # STATUS must read (healthy)
# 4. Confirm it registered, rather than hunting the sidebar by eye.
curl -s --compressed http://localhost:7860/api/v1/all \
| python3 -c "import json,sys; print([k for k in json.load(sys.stdin) if 'mytools' in k])"
If your course materials include tests/verify_components.py, run it: it is the
only check that catches §2 and §3 without a running Langflow. If you do not have
it, skip to step 2 and rely on step 4 plus the log grep — you lose the
tool-name and description checks, so read §3 carefully instead.
Step 4 is worth doing every time. "I do not see it in the menu" and "it is not registered" are different problems with different fixes, and the sidebar is long.
If docker is not found, or the container never reaches (healthy), that is a
different problem — use the langflow-env-doctor skill.
6. Wiring it to an Agent
In the UI: open the component, turn on Tool Mode, and connect its output to the Agent's Tools port. The component collapses to a single Toolset output and Langflow generates one entry per action.
Then check the Agent actually has what you think:
- Each action should show its own description, not the component's. If you see the same sentence repeated N times, go back to §3.2.
- Disable actions the Agent does not need for this flow. Every enabled action's description is in the context on every turn, so an unused action is a permanent tax and one more wrong choice the model can make.
If the Agent still will not call the tool, the fastest discriminator is to ask it directly in the Playground: "list the tools you have, with their descriptions." What it reports is the ground truth about names and descriptions, and it is usually not what you expected.
7. Known non-issue
Langflow logs exactly one error-level OpenAI 400 per model per container
process, saying function tools with reasoning_effort are not supported. This
is by design — Langflow discovers the model's capabilities by trying, reading
the error text, and retrying with different settings. It is not your component.
Only worry if it repeats many times within one container run.