frontend-query-mutation

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗

Guide for implementing Langflow frontend query and mutation patterns with Axios and TanStack React Query v5. Trigger when creating or updating API hooks in controllers/API/queries, consuming UseRequestProcessor in components, deciding whether to use useQuery or useMutation, handling conditional queries, cache invalidation, mutation error handling, or migrating legacy API calls to the query hook pattern.

.agents/skills/frontend-query-mutation/SKILL.md

Download bundle ↓
main · 595cd723 bundle filesScanned 2026-09-15

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