.agents/skills/frontend-code-review/references/business-logic.md
Update this file when adding, editing, or removing Business Logic rules so the catalog remains accurate.
IsUrgent: True Category: Business Logic
File path pattern of node components: src/frontend/src/CustomNodes/GenericNode/ and sub-components.
GenericNode and its child components render inside the @xyflow/react canvas, which has its own React context. These components may also render in contexts where certain store providers are not available (e.g., preview panels, template views). Directly importing and calling Zustand store hooks that depend on a specific provider context can cause blank screens or runtime errors when the component mounts outside that provider.
Use the store hooks that are available globally (like useFlowStore, useTypesStore) or pass data as props from the parent that has provider access. If imperative store access is needed, use the store's getState() method which does not require a provider.
// Wrong -- may fail outside provider tree
import { useSpecialContext } from "@/contexts/specialContext";
function NodeField() {
const { config } = useSpecialContext(); // crashes if provider missing
return <div>{config.label}</div>;
}
// Right -- use props or global store
function NodeField({ config }: { config: FieldConfig }) {
return <div>{config.label}</div>;
}
flowStore for current flow stateIsUrgent: True Category: Business Logic
The current flow's state (nodes, edges, flow metadata) is managed through useFlowStore (located in src/frontend/src/stores/flowStore.ts). Always use this store for reading and modifying the active flow. Do not create parallel state tracking for flow data, and do not cache flow state in component-local state unless there is a specific performance reason (document it if so).
// Wrong -- local state duplicating flow data
const [nodes, setNodes] = useState(initialNodes);
// Right -- use the flow store
const nodes = useFlowStore((state) => state.nodes);
const setNodes = useFlowStore((state) => state.setNodes);
IsUrgent: True Category: Business Logic
The Axios HTTP client in src/frontend/src/controllers/API/ is configured with interceptors that handle authentication headers, token refresh, retry logic, and error normalization. Do not duplicate any of this logic in individual API call sites or components. Use the configured Axios instance (via the API controller helpers) for all backend requests. Do not create new Axios instances or use raw fetch() for API calls to the Langflow backend.
// Wrong -- raw fetch with manual auth
const response = await fetch("/api/v1/flows", {
headers: { Authorization: `Bearer ${token}` },
});
// Wrong -- new Axios instance bypassing interceptors
const client = axios.create({ baseURL: "/api/v1" });
const response = await client.get("/flows");
// Right -- use the project's API utilities
import { api } from "@/controllers/API/api";
const response = await api.get("/flows");
IsUrgent: False Category: Business Logic
Interactive components must include data-testid attributes following the project's established naming conventions. This is critical for E2E test stability. The primary patterns are:
popover-anchor-input-{name} (generated via StrRenderComponent -> InputComponent -> CustomInputPopover)edit-fields-button, playground-btn-flow-iosidebar-nav-{item}, sidebar-add-sticky-note-buttontitle-{display_name}, handle-{id}When adding new interactive elements, follow the existing patterns. When modifying elements that have data-testid, preserve the attribute and its value to avoid breaking E2E tests.
// Wrong -- missing data-testid on interactive element
<button onClick={handleSave}>Save</button>
// Right -- includes data-testid
<button data-testid="save-flow-button" onClick={handleSave}>Save</button>
load_from_db and value patternIsUrgent: True Category: Business Logic
Template fields in starter projects use the load_from_db + value pattern to reference global variables. When load_from_db: true and value is set to a key like "OPENAI_API_KEY", the field displays a badge (via InputGlobalComponent) instead of a text input. The actual input element (<input>) does not exist in the DOM when a global variable is selected.
This has implications for:
load_from_db is true and a global variable is set.getByTestId("popover-anchor-input-api_key") will not find an element when a global variable badge is displayed.load_from_db: true with global variable values.When writing components that interact with template fields, check the load_from_db state:
// Wrong -- assumes input always exists
const apiKeyInput = data.node.template.api_key;
// Directly rendering an <input> without checking load_from_db
// Right -- handle both states
if (field.load_from_db && field.value) {
// Render global variable badge
return <GlobalVariableBadge name={field.value} />;
}
// Render normal input
return <InputComponent value={field.value} onChange={handleChange} />;
enableInspectPanel before accessing panel elementsIsUrgent: True Category: Business Logic
The inspection panel (added in the panel migration from the old edit modal pattern) must be explicitly enabled before its elements are accessible. In E2E tests and in code that interacts with the panel programmatically:
enableInspectPanel(page) before attempting to access edit-fields-button or other panel controls.edit-fields-button to toggle field editing.openAdvancedOptions(page) / closeAdvancedOptions(page) pattern is deprecated.editNode flag is always false; -edit suffix testids no longer exist.This rule is especially important when writing or modifying E2E tests that interact with node configuration.
IsUrgent: False Category: Business Logic
Node fields have different visibility rules depending on where they are rendered. Understanding these rules is critical when modifying field display logic:
isCanvasVisible() in src/frontend/src/CustomNodes/helpers/parameter-filtering.ts.shouldRenderInspectionPanelField() in the same file.showNode flag: Defaults to true. When set to false, fields receive the hidden CSS class rather than being removed from the DOM.When adding new fields or modifying visibility logic, ensure both canvas and inspection panel rendering paths are considered.
// Wrong -- only checking one visibility context
if (!field.advanced) {
return <FieldComponent />;
}
// Right -- check the appropriate visibility function for the context
import { isCanvasVisible, shouldRenderInspectionPanelField } from "@/CustomNodes/helpers/parameter-filtering";
// In canvas context
if (isCanvasVisible(field)) {
return <FieldComponent />;
}
// In inspection panel context
if (shouldRenderInspectionPanelField(field)) {
return <FieldComponent />;
}
IsUrgent: False Category: Business Logic
Langflow has a customization layer in src/frontend/src/customization/ that allows overriding default behaviors and components. When adding new features that may need to be customized (e.g., branding, feature flags, custom components), use the customization layer rather than hardcoding values. Check src/frontend/src/customization/ for existing customization points before adding new ones.
IsUrgent: False Category: Business Logic
Forms that require validation should use React Hook Form with Zod schemas. Do not implement manual form state tracking or validation logic when React Hook Form can handle it. Define Zod schemas for form data types and use zodResolver to integrate them with React Hook Form.
// Wrong -- manual form state and validation
const [name, setName] = useState("");
const [error, setError] = useState("");
const handleSubmit = () => {
if (!name) {
setError("Name is required");
return;
}
// submit
};
// Right -- React Hook Form + Zod
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const schema = z.object({
name: z.string().min(1, "Name is required"),
});
type FormData = z.infer<typeof schema>;
function MyForm() {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(schema),
});
const onSubmit = (data: FormData) => {
// submit
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("name")} />
{errors.name && <span>{errors.name.message}</span>}
</form>
);
}