frontend-query-mutation Skill

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.

Install
npx skills add 'https://github.com/langflow-ai/langflow/tree/main/.agents/skills/frontend-query-mutation'
Download bundle ↓

references/query-patterns.md

references/query-patterns.mdBrowse 3 files
View on GitHub
← Back to SKILL.md

Query and Mutation Patterns

Table of Contents

  • Intent
  • Directory structure
  • Query hook structure
  • Mutation hook structure
  • URL constants
  • Query keys
  • Type signatures
  • Anti-patterns

Intent

  • Keep API hooks in controllers/API/queries/{domain}/ organized by domain.
  • Use UseRequestProcessor for consistent behavior across all hooks.
  • Use the shared api Axios instance for all REST calls.
  • Type hooks with useQueryFunctionType or useMutationFunctionType from types/api/.

Directory Structure

controllers/API/
  api.tsx                          # Axios instance, interceptors, streaming
  helpers/
    constants.ts                   # URL constants and getURL()
  services/
    request-processor.ts           # UseRequestProcessor hook
  queries/
    flows/
      use-get-flow.ts
      use-get-refresh-flows-query.ts
      use-post-add-flow.ts
      use-delete-delete-flows.ts
      use-get-download-flows.ts
      index.ts
    folders/
      use-get-folder.ts
      use-get-folders.ts
      use-post-folders.ts
      use-patch-folders.ts
      use-delete-folders.ts
      index.ts
    variables/
      use-get-global-variables.ts
      use-post-global-variables.ts
      use-patch-global-variables.ts
      index.ts
    auth/
      use-get-autologin.ts
      use-post-login-user.ts
      use-post-logout.ts
      use-post-refresh-access.ts
      index.ts
    messages/
      use-get-messages.ts
      use-get-messages-polling.ts
      use-delete-messages.ts
      index.ts

Naming Convention

  • Queries: use-get-{resource}.ts (e.g., use-get-flow.ts, use-get-global-variables.ts)
  • Create mutations: use-post-{resource}.ts (e.g., use-post-add-flow.ts)
  • Update mutations: use-patch-{resource}.ts (e.g., use-patch-global-variables.ts)
  • Delete mutations: use-delete-{resource}.ts (e.g., use-delete-messages.ts)
  • Other mutations: use-{action}-{resource}.ts (e.g., use-rename-session.ts)

Query Hook Structure

Every query hook follows this structure:

import type { UseQueryResult } from "@tanstack/react-query"
import type { useQueryFunctionType } from "@/types/api"
import { api } from "../../api"
import { getURL } from "../../helpers/constants"
import { UseRequestProcessor } from "../../services/request-processor"

// 1. Define the response type (or import from types/)
interface GlobalVariable {
  id: string
  name: string
  value: string
  type: string
}

// 2. Export the hook typed with useQueryFunctionType
export const useGetGlobalVariables: useQueryFunctionType<
  undefined,
  GlobalVariable[]
> = (options?) => {
  // 3. Get query helper from UseRequestProcessor
  const { query } = UseRequestProcessor()

  // 4. Define the query function using the api Axios instance
  const getGlobalVariablesFn = async (): Promise<GlobalVariable[]> => {
    const res = await api.get(`${getURL("VARIABLES")}/`)
    return res.data
  }

  // 5. Return the query result
  const queryResult: UseQueryResult<GlobalVariable[], Error> = query(
    ["useGetGlobalVariables"],
    getGlobalVariablesFn,
    {
      refetchOnWindowFocus: false,
      ...options,
    },
  )

  return queryResult
}

Query Hook with Parameters

When a query takes parameters, use the first type argument:

export const useGetFolder: useQueryFunctionType<
  { id: string },
  FolderResponse
> = (params, options?) => {
  const { query } = UseRequestProcessor()

  const getFolderFn = async (): Promise<FolderResponse> => {
    const res = await api.get(`${getURL("FOLDERS")}/${params.id}`)
    return res.data
  }

  return query(["useGetFolder", params.id], getFolderFn, {
    ...options,
  })
}

Important: Some "get" Hooks Use Mutation

In the Langflow codebase, some hooks named use-get-* (e.g., useGetFlow) are actually mutation hooks typed with useMutationFunctionType. This happens when the "get" operation is triggered imperatively (on demand) rather than declaratively (on mount/re-render). Check the actual type signature before following the query pattern — if a hook uses mutate from UseRequestProcessor, follow the Mutation Hook Structure below instead.

Query Hook with Store Updates

Many Langflow queries update Zustand stores as a side effect within the query function:

export const useGetGlobalVariables: useQueryFunctionType<
  undefined,
  GlobalVariable[]
> = (options?) => {
  const { query } = UseRequestProcessor()

  // Access store setters
  const setGlobalVariablesEntries = useGlobalVariablesStore(
    (state) => state.setGlobalVariablesEntries,
  )

  const getGlobalVariablesFn = async (): Promise<GlobalVariable[]> => {
    const res = await api.get(`${getURL("VARIABLES")}/`)
    // Update store as side effect of fetching
    setGlobalVariablesEntries(res.data.map((entry) => entry.name))
    return res.data
  }

  return query(["useGetGlobalVariables"], getGlobalVariablesFn, {
    refetchOnWindowFocus: false,
    ...options,
  })
}

Mutation Hook Structure

Every mutation hook follows this structure:

import type { UseMutationResult } from "@tanstack/react-query"
import type { useMutationFunctionType } from "@/types/api"
import { api } from "../../api"
import { getURL } from "../../helpers/constants"
import { UseRequestProcessor } from "../../services/request-processor"

// 1. Define the payload type
interface PostAddFlowPayload {
  name: string
  data: ReactFlowJsonObject
  description: string
  folder_id: string
}

// 2. Export the hook typed with useMutationFunctionType
export const usePostAddFlow: useMutationFunctionType<
  undefined,       // Params (undefined if none)
  PostAddFlowPayload  // Variables (mutation payload)
> = (options?) => {
  // 3. Get mutate helper and queryClient from UseRequestProcessor
  const { mutate, queryClient } = UseRequestProcessor()

  // 4. Define the mutation function using the api Axios instance
  const postAddFlowFn = async (payload: PostAddFlowPayload): Promise<any> => {
    const response = await api.post(`${getURL("FLOWS")}/`, {
      name: payload.name,
      data: payload.data,
      description: payload.description,
      folder_id: payload.folder_id || null,
    })
    return response.data
  }

  // 5. Return the mutation with cache invalidation in onSettled
  // NOTE: Place hook-specific options (onSettled, retry) BEFORE ...options
  // so consumers can override them if needed. This matches the codebase convention.
  const mutation: UseMutationResult<any, any, PostAddFlowPayload> = mutate(
    ["usePostAddFlow"],
    postAddFlowFn,
    {
      onSettled: (response) => {
        if (response) {
          // Invalidate related queries so they refetch
          queryClient.refetchQueries({
            queryKey: ["useGetRefreshFlowsQuery", { get_all: true, header_flows: true }],
          })
          queryClient.refetchQueries({
            queryKey: ["useGetFolder", response.folder_id],
          })
        }
      },
      ...options,  // Consumer options come LAST (can override onSettled, retry, etc.)
    },
  )

  return mutation
}

Delete Mutation Example

export const useDeleteMessages: useMutationFunctionType<undefined, string[]> = (
  options?,
) => {
  const { mutate, queryClient } = UseRequestProcessor()

  const deleteMessagesFn = async (messageIds: string[]): Promise<void> => {
    await api.delete(`${getURL("MESSAGES")}/`, {
      data: messageIds,
    })
  }

  const mutation = mutate(["useDeleteMessages"], deleteMessagesFn, {
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ["useGetMessages"] })
    },
    ...options,
  })

  return mutation
}

Patch Mutation Example

export const usePatchGlobalVariables: useMutationFunctionType<
  undefined,
  { id: string; name: string; value: string; type: string }
> = (options?) => {
  const { mutate, queryClient } = UseRequestProcessor()

  const patchGlobalVariablesFn = async (payload: {
    id: string
    name: string
    value: string
    type: string
  }): Promise<GlobalVariable> => {
    const response = await api.patch(
      `${getURL("VARIABLES")}/${payload.id}`,
      payload,
    )
    return response.data
  }

  const mutation = mutate(["usePatchGlobalVariables"], patchGlobalVariablesFn, {
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ["useGetGlobalVariables"] })
    },
    retry: false,
    ...options,
  })

  return mutation
}

URL Constants

All API paths are defined in controllers/API/helpers/constants.ts:

export const URLs = {
  FLOWS: "flows",
  FOLDERS: "projects",
  VARIABLES: "variables",
  MESSAGES: "monitor/messages",
  BUILDS: "monitor/builds",
  API_KEY: "api_key",
  FILES: "files",
  // ...
} as const

Use getURL() to construct full API paths:

import { getURL } from "../../helpers/constants"

// Basic: /api/v1/flows
const url = getURL("FLOWS")

// With params: /api/v1/flows/{flowId}
const url = `${getURL("FLOWS")}/${flowId}`

// With v2 flag: /api/v2/flows
const url = getURL("FLOWS", {}, true)

When adding new endpoints, add the constant to URLs first, then use getURL() in the hook.

Query Keys

Query keys are arrays that uniquely identify cached data. Langflow conventions:

// Hook name as key (no params)
["useGetGlobalVariables"]

// Hook name + params for cache isolation
["useGetFlow", flowId]
["useGetFolder", folderId]

// Hook name + object params for complex queries
["useGetRefreshFlowsQuery", { get_all: true, header_flows: true }]
["useGetMessages", { flowId, sessionId, page }]

Rules:

  • The first element is always the hook name as a string.
  • Additional elements are parameters that differentiate cache entries.
  • Use objects for multi-parameter queries.
  • Keep keys consistent so invalidation works correctly.

Type Signatures

useQueryFunctionType

Used for query hooks. Defined in types/api/index.ts:

// Without params: useQueryFunctionType<undefined, ResponseType>
// The hook signature becomes: (options?) => UseQueryResult<ResponseType>

// With params: useQueryFunctionType<ParamsType, ResponseType>
// The hook signature becomes: (params, options?) => UseQueryResult<ResponseType>

useMutationFunctionType

Used for mutation hooks:

// Without params: useMutationFunctionType<undefined, VariablesType>
// The hook signature becomes: (options?) => UseMutationResult<Data, Error, Variables>

// With params: useMutationFunctionType<ParamsType, VariablesType>
// The hook signature becomes: (params, options?) => UseMutationResult<Data, Error, Variables>

Anti-Patterns

Do Not Bypass UseRequestProcessor

// Do not call useQuery/useMutation directly
const result = useQuery({
  queryKey: ["flows"],
  queryFn: () => api.get("/api/v1/flows"),
})

// Use UseRequestProcessor for consistent retry and error handling
const { query } = UseRequestProcessor()
const result = query(["useGetFlows"], () => api.get(getURL("FLOWS")))

Do Not Bypass the api Instance

// Do not use raw axios or fetch for REST calls
const result = await axios.get("/api/v1/flows")
const result = await fetch("/api/v1/flows")

// Use the shared api instance (has interceptors for auth, headers, error handling)
const result = await api.get(`${getURL("FLOWS")}/`)

Do Not Hardcode URLs

// Do not hardcode API paths
const result = await api.get("/api/v1/variables/")

// Use getURL() helper
const result = await api.get(`${getURL("VARIABLES")}/`)

Do Not Duplicate Query Keys

// Do not use different key strings for the same query
query(["getFlows"], fetchFn)       // in hook A
query(["useGetFlows"], fetchFn)    // in hook B
query(["flows-list"], fetchFn)     // in hook C

// Use one canonical key matching the hook name
query(["useGetFlows"], fetchFn)

Do Not Invalidate from Components

// Do not put invalidation logic in components
const Component = () => {
  const queryClient = useQueryClient()
  const { mutate } = useDeleteFlow()

  const handleDelete = () => {
    mutate(flowId, {
      onSuccess: () => {
        // Avoid: invalidation knowledge leaks into component
        queryClient.invalidateQueries({ queryKey: ["useGetFlows"] })
        queryClient.invalidateQueries({ queryKey: ["useGetFolder"] })
      },
    })
  }
}

// Put invalidation in the mutation hook's onSettled
export const useDeleteFlow = (options?) => {
  const { mutate, queryClient } = UseRequestProcessor()

  return mutate(["useDeleteFlow"], deleteFlowFn, {
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ["useGetFlows"] })
      queryClient.invalidateQueries({ queryKey: ["useGetFolder"] })
    },
    ...options,  // Consumer options come LAST
  })
}

Do Not Create Thin Wrapper Hooks

// Do not create hooks that just re-export an existing query hook
const useFlows = () => {
  return useGetRefreshFlowsQuery({ get_all: true })
}

// Import the existing hook directly where needed
import { useGetRefreshFlowsQuery } from "@/controllers/API/queries/flows"
Referenced from SKILL.md