# Langflow Component Build

> Write a custom Python component for a local Langflow 1.11.x deployment, or add an action to one you were given, and get it to actually appear in the component menu and be callable by an Agent. Use this whenever a component does not show up in the sidebar, shows up but has no inputs, loads but the Agent never calls its tool, or the Agent calls the wrong action of the right tool; and whenever adding a method, renaming a method, editing an Output(info=...) string, adding a tool_mode input, or setting up a category folder under custom_components/. Also use it before running tests/verify_components.py or restarting the container after a Python edit. Four of the failure modes here are silent - no error, no traceback, just a component that is missing or an Agent that answers from memory.

- Skill: `x1linwang/langflow-component-build` (Agent Skill)
- Install (CLI): `npx skillmds@latest add x1linwang/langflow-component-build`
- Raw SKILL.md: https://api.skillmd.com/api/skills/x1linwang/langflow-component-build/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: x1linwang (https://skillmd.com/u/x1linwang)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/x1linwang/langflow-component-build

---


# 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__.py` is 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.py` is 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:

```python
from lfx.custom import Component
from lfx.io import MessageTextInput, Output
from lfx.schema import Data
```

and every one of these fails:

```python
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:

```bash
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.

```python
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:

1. **The tool name is the output's `method` name, not `display_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.

2. **The tool description is `Output(info=...)`.** If you omit `info`, Langflow
   falls back to the component's single `description`, 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".
   Every `Output` needs its own `info`, and the `info` should say when *not* to
   use that action, because that is what distinguishes it from its siblings.

3. **The args schema is built once from every `tool_mode=True` input and shared
   by every action.** So each argument's description is repeated in the model's
   context once per action. An input named `ticker` with a 300-character `info`
   on 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's `info`, 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

```python
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.

```bash
# 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.

