references/runtime-rules.md
3,031 tokens · o200k_base · 12,561 bytes
Source excerpt starting at line 1.# Runtime Rules ## Table of Contents - Conditional queries- Cache invalidation- Query key conventions- `mutate` vs `mutateAsync`- Error handling- Streaming requests- UseRequestProcessor defaults ## Conditional Queries Use the `enabled` option to conditionally run queries. Langflow hooks pass through `options` to `UseRequestProcessor`, which passes them to `useQuery`. ```typescript// Pattern: Disable query when not authenticatedexport const useGetGlobalVariables: useQueryFunctionType< undefined, GlobalVariable[]> = (options?) => { const { query } = UseRequestProcessor() const isAuthenticated = useAuthStore((state) => state.isAuthenticated) const getGlobalVariablesFn = async (): Promise<GlobalVariable[]> => { if (!isAuthenticated) return [] const res = await api.get(`${getURL("VARIABLES")}/`) return res.data } return query(["useGetGlobalVariables"], getGlobalVariablesFn, { refetchOnWindowFocus: false, enabled: isAuthenticated && (options?.enabled ?? true), ...options, })}``` ```typescript// Pattern: Disable query when required param is missingexport const useGetFlow: useQueryFunctionType<{ id: string }, FlowResponse> = ( params, options?,) => { const { query } = UseRequestProcessor() const getFlowFn = async (): Promise<FlowResponse> => { const res = await api.get(`${getURL("FLOWS")}/${params.id}`) return res.data } return query(["useGetFlow", params.id], getFlowFn, { enabled: !!params.id && (options?.enabled ?? true), ...options, })}``` ```typescript// Pattern: Consumer disables query via optionsconst { data: flow } = useGetFlow( { id: flowId }, { enabled: showFlowDetails },)``` Rules:- Always check `options?.enabled ?? true` when combining with other conditions, so consumers can also disable the query.- Guard early in the query function when auth or required data might be missing.- Do not use non-null assertions on params to work around missing data; use `enabled` instead. ## Cache Invalidation Bind invalidation in the mutation hook definition. Components should only add UI feedback (toasts, navigation) in call-site callbacks, not decide which queries to invalidate. ### Invalidation via onSettled The `UseRequestProcessor.mutate()` wrapper already calls `queryClient.invalidateQueries({ queryKey: mutationKey })` in its default `onSettled`. For additional invalidation, extend `onSettled`: ```typescriptexport const usePostAddFlow: useMutationFunctionType< undefined, PostAddFlowPayload> = (options?) => { const { mutate, queryClient } = UseRequestProcessor() const myCollectionId = useFolderStore((state) => state.myCollectionId) const postAddFlowFn = async (payload: PostAddFlowPayload): Promise<any> => { const response = await api.post(`${getURL("FLOWS")}/`, payload) return response.data } return mutate(["usePostAddFlow"], postAddFlowFn, { onSettled: (response) => { if (response) { queryClient.refetchQueries({ queryKey: ["useGetRefreshFlowsQuery", { get_all: true, header_flows: true }], }) queryClient.refetchQueries({ queryKey: ["useGetFolder", response.folder_id ?? myCollectionId], }) } }, ...options, // Consumer options come LAST })}``` ### Invalidation Patterns ```typescript// Broad invalidation: all queries for a domainqueryClient.invalidateQueries({ queryKey: ["useGetFlows"] }) // Specific invalidation: single cache entryqueryClient.invalidateQueries({ queryKey: ["useGetFlow", flowId] }) // Refetch instead of invalidate when you need data immediatelyqueryClient.refetchQueries({ queryKey: ["useGetFolder", folderId] })``` ### Component-side callbacks are for UI only ```typescript// Component only adds UI behaviorconst { mutate: addFlow } = usePostAddFlow() const handleCreate = () => { addFlow(flowData, { onSuccess: (response) => { // UI-only: navigate, show toast navigate(`/flow/${response.id}`) setSuccessData({ title: "Flow created successfully" }) }, onError: (error) => { setErrorData({ title: "Failed to create flow", list: [error.message], }) }, })}``` ## Query Key Conventions ```typescript// Base key: hook name["useGetGlobalVariables"] // Parameterized key: hook name + params["useGetFlow", flowId]["useGetFolder", folderId] // Complex key: hook name + param object["useGetRefreshFlowsQuery", { get_all: true, header_flows: true }]["useGetMessages", { flowId, sessionId }]["useGetBuilds", { flowId }] // Mutation key: hook name (used for automatic invalidation by UseRequestProcessor)["usePostAddFlow"]["useDeleteMessages"]``` Rules:- First element is always the hook name as a string.- Keep mutation keys matching the hook name.- `UseRequestProcessor.mutate()` auto-invalidates the `mutationKey` on settled.- Additional invalidation targets must be explicitly added in `onSettled`. ## `mutate` vs `mutateAsync` Prefer `mutate` by default. Use `mutateAsync` only when Promise semantics are truly required. Rules:- Event handlers should call `mutate(...)` with `onSuccess` or `onError`.- Every `await mutateAsync(...)` must be wrapped in `try/catch`.- Do not use `mutateAsync` when callbacks already express the flow clearly. ```typescript// Default: use mutate with callbacksconst { mutate: deleteFlow } = useDeleteFlow() const handleDelete = () => { deleteFlow(flowId, { onSuccess: () => { navigate("/flows") setSuccessData({ title: "Flow deleted" }) }, onError: (error) => { setErrorData({ title: "Delete failed", list: [error.message] }) }, })} // Exception: Promise semantics needed for sequential operationsconst handleDuplicateAndOpen = async () => { try { const newFlow = await duplicateFlow.mutateAsync(flowData) await renameFlow.mutateAsync({ id: newFlow.id, name: `${flowData.name} (copy)` }) navigate(`/flow/${newFlow.id}`) } catch (error) { setErrorData({ title: "Failed to duplicate flow", list: [error instanceof Error ? error.message : "Unknown error"], }) }}``` ## Error Handling ### API Interceptor Handles Auth Errors The `ApiInterceptor` in `controllers/API/api.tsx` automatically handles:- **401 Unauthorized**: Attempts token refresh via `useRefreshAccessToken`, then retries the request.- **403 Forbidden**: Same auth error flow as 401.- **After 3+ auth errors**: Logs the user out automatically.- **500 errors**: Clears build vertex state in the flow store. You do not need to handle 401/403 errors in individual hooks or components. ### Mutation Error Handling Handle errors at the call site for user-facing feedback: ```typescriptconst { mutate: saveFlow } = useSaveFlow() const handleSave = () => { saveFlow(flowData, { onError: (error) => { setErrorData({ title: "Failed to save flow", list: [error.response?.data?.detail ?? error.message], }) }, })}``` ### Query Error Handling For queries, React Query's retry logic (5 retries with exponential backoff via `UseRequestProcessor`) handles transient failures. For permanent errors, use `onError` in options or error boundaries: ```typescriptconst { data, error, isError } = useGetFlow( { id: flowId }, { retry: false, // Override default retry for known-missing resources onError: (error) => { if (error.response?.status === 404) { navigate("/flows") } }, },)``` ## Streaming Requests Langflow uses `performStreamingRequest()` from `controllers/API/api.tsx` for build and chat streaming. This uses the browser `fetch` API (not Axios) with Server-Sent Events (SSE) parsing. ### Streaming Architecture ```typescriptimport { performStreamingRequest } from "@/controllers/API/api" const buildController = new AbortController() await performStreamingRequest({ method: "POST", url: `${baseURL}/api/v1/build/${flowId}/flow`, body: { inputs, files }, buildController, onData: async (event) => { // Process individual SSE events // Return true to continue, false to abort return true }, onDataBatch: async (events) => { // Process batch of events from a single chunk (more efficient) // Return true to continue, false to abort return true }, onError: (statusCode) => { // Handle HTTP error status }, onNetworkError: (error) => { // Handle network-level errors },})``` ### Streaming vs REST Decision | Operation | Method ||-----------|--------|| Build flow | `performStreamingRequest()` with SSE || Chat interaction | `performStreamingRequest()` with SSE || CRUD operations (flows, folders, variables) | Axios `api` instance via query/mutation hooks || File upload/download | Axios `api` instance || Auth operations | Axios `api` instance | ### Aborting Streams Use `AbortController` to cancel streaming operations: ```typescriptconst buildController = useRef(new AbortController()) const handleStopBuild = () => { buildController.current.abort() buildController.current = new AbortController()}``` ## UseRequestProcessor Defaults The `UseRequestProcessor` hook in `controllers/API/services/request-processor.ts` provides: ### Query Defaults ```typescript{ retry: 5, retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), // 1s, 2s, 4s, 8s, 16s (capped at 30s)}``` ### Mutation Defaults The actual `mutate()` implementation in `request-processor.ts`: ```typescriptfunction mutate(mutationKey, mutationFn, options = {}) { return useMutation({ mutationKey, mutationFn, onSettled: (data, error, variables, context) => { queryClient.invalidateQueries({ queryKey: mutationKey }); options.onSettled && options.onSettled(data, error, variables, context); }, ...options, // Spreads AFTER the wrapper onSettled retry: options.retry ?? 3, // Comes AFTER the spread retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), });}``` **How onSettled actually works (important subtlety):** 1. `UseRequestProcessor.mutate()` defines a wrapper `onSettled` that (a) auto-invalidates the `mutationKey`, then (b) calls the hook's `options.onSettled`.2. **However**, the `...options` spread comes AFTER the wrapper, so if the hook's `options` object contains its own `onSettled`, it **overrides** the wrapper. This means the auto-invalidation is effectively **skipped** when hooks provide their own `onSettled`.3. In practice, most hooks DO provide their own `onSettled` with specific refetch/invalidation logic, so the auto-invalidation of the mutation key rarely runs.4. This is fine because invalidating a mutation key is usually unnecessary — mutations don't have cached entries like queries do. **Hook convention: place custom `onSettled` BEFORE `...options`:** ```typescriptmutate(["usePostAddFlow"], fn, { onSettled: () => { queryClient.refetchQueries(...) }, // Hook-specific invalidation retry: false, // Override default retry if needed ...options, // Consumer options come LAST (can override onSettled, retry, etc.)})``` ### When to Use `retry: false` Override the default `retry: 3` with `retry: false` for mutations where retrying would cause problems: - **Create operations** that are not idempotent (duplicate creation risk)- **Update operations** on global variables or settings (stale data risk)- **Delete operations** where the resource may already be gone ```typescript// Example: POST that creates a resource (not safe to retry)const mutation = mutate(["usePostGlobalVariables"], postFn, { onSettled: () => { queryClient.refetchQueries({ queryKey: ["useGetGlobalVariables"] }) }, retry: false, ...options,})``` ## Polling Patterns For queries that need periodic refreshing (build status, messages): ```typescriptexport const useGetMessagesPolling: useQueryFunctionType< { flowId: string; sessionId: string }, Message[]> = (params, options?) => { const { query } = UseRequestProcessor() const getMessagesFn = async (): Promise<Message[]> => { const res = await api.get( `${getURL("MESSAGES")}/?flow_id=${params.flowId}&session_id=${params.sessionId}`, ) return res.data } return query( ["useGetMessagesPolling", params.flowId, params.sessionId], getMessagesFn, { refetchInterval: 3000, // Poll every 3 seconds refetchIntervalInBackground: false, ...options, }, )}```
Referenced from SKILL.md
Source excerpt starting at line 43.- Read `references/query-patterns.md` for hook structure, naming, URL patterns, and query/mutation call-site shape. - Read `references/runtime-rules.md` for conditional queries, invalidation, error handling, and streaming. - Read both references when a task spans hook creation and runtime behavior.
Source excerpt starting at line 69.- Use `references/query-patterns.md` for hook structure, naming conventions, directory layout, and anti-patterns.- Use `references/runtime-rules.md` for conditional queries, invalidation, `mutate` versus `mutateAsync`, error handling, and streaming requests.