references/runtime-rules.md
3,031 tokens · o200k_base · 12,561 bytes
Runtime Rules
Table of Contents
- Conditional queries
- Cache invalidation
- Query key conventions
mutatevsmutateAsync- 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.
// Pattern: Disable query when not authenticated
export 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,
})
}
// Pattern: Disable query when required param is missing
export 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,
})
}
// Pattern: Consumer disables query via options
const { data: flow } = useGetFlow(
{ id: flowId },
{ enabled: showFlowDetails },
)
Rules:
- Always check
options?.enabled ?? truewhen 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
enabledinstead.
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:
export 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
// Broad invalidation: all queries for a domain
queryClient.invalidateQueries({ queryKey: ["useGetFlows"] })
// Specific invalidation: single cache entry
queryClient.invalidateQueries({ queryKey: ["useGetFlow", flowId] })
// Refetch instead of invalidate when you need data immediately
queryClient.refetchQueries({ queryKey: ["useGetFolder", folderId] })
Component-side callbacks are for UI only
// Component only adds UI behavior
const { 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
// 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 themutationKeyon 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(...)withonSuccessoronError. - Every
await mutateAsync(...)must be wrapped intry/catch. - Do not use
mutateAsyncwhen callbacks already express the flow clearly.
// Default: use mutate with callbacks
const { 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 operations
const 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:
const { 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:
const { 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
import { 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:
const 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
{
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:
function 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):
UseRequestProcessor.mutate()defines a wrapperonSettledthat (a) auto-invalidates themutationKey, then (b) calls the hook'soptions.onSettled.- However, the
...optionsspread comes AFTER the wrapper, so if the hook'soptionsobject contains its ownonSettled, it overrides the wrapper. This means the auto-invalidation is effectively skipped when hooks provide their ownonSettled. - In practice, most hooks DO provide their own
onSettledwith specific refetch/invalidation logic, so the auto-invalidation of the mutation key rarely runs. - 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:
mutate(["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
// 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):
export 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.