Data Fetching
•5 min read
Convenience API
The convenience layer reduces setup from 4 steps to 1. Bound handles eliminate the need to pass system.facts to every imperative call.
createQuerySystem
One call to create a fully wired system. Accepts queries, mutations, subscriptions, and infinite queries inline.
import { createQuerySystem } from "@directive-run/query";
const app = createQuerySystem({
facts: { userId: "", ticker: "" },
queries: {
user: {
key: (f) => f.userId ? { userId: f.userId } : null,
fetcher: async (p, signal) => {
const res = await fetch(`/api/users/${p.userId}`, { signal });
return res.json();
},
tags: ["users"],
refetchAfter: 30_000,
},
},
mutations: {
updateUser: {
mutator: async (vars, signal) => {
const res = await fetch(`/api/users/${vars.id}`, {
method: "PATCH",
body: JSON.stringify(vars),
signal,
});
return res.json();
},
invalidates: ["users"],
},
},
subscriptions: {
prices: {
key: (f) => f.ticker ? { ticker: f.ticker } : null,
subscribe: (params, { onData, onError, signal }) => {
const ws = new WebSocket(`wss://api.example.com/${params.ticker}`);
ws.onmessage = (e) => onData(JSON.parse(e.data));
ws.onerror = () => onError(new Error("Connection lost"));
signal.addEventListener("abort", () => ws.close());
return () => ws.close();
},
},
},
infiniteQueries: {
feed: {
key: (f) => f.userId ? { userId: f.userId } : null,
fetcher: async (p, signal) => api.getFeed(p.userId, p.pageParam),
getNextPageParam: (lastPage) => lastPage.nextCursor,
initialPageParam: null,
},
},
// Optional
plugins: [],
history: { maxSnapshots: 50 },
autoStart: true,
});
Bound Handles
No more passing system.facts. Every query, mutation, subscription, and infinite query gets a bound handle.
// Queries
app.queries.user.refetch();
app.queries.user.invalidate();
app.queries.user.cancel();
app.queries.user.setData(newData);
// Mutations
app.mutations.updateUser.mutate({ id: "42", name: "New" });
const result = await app.mutations.updateUser.mutateAsync({ id: "42" });
app.mutations.updateUser.reset();
// Subscriptions
app.subscriptions.prices.setData({ price: 200 });
// Infinite Queries
app.infiniteQueries.feed.fetchNextPage();
app.infiniteQueries.feed.fetchPreviousPage();
app.infiniteQueries.feed.refetch();
// Debug
app.explain("user");
Options
autoStart
By default, the system starts immediately. Set autoStart: false for SSR or deferred initialization.
const app = createQuerySystem({
facts: { userId: "" },
queries: { user: { key: ..., fetcher: ... } },
autoStart: false, // call app.start() manually
});
// SSR: initialize without starting
// Client: hydrate then start
app.start();
Module Config Pass-Through
createQuerySystem accepts all standard module config options:
const app = createQuerySystem({
facts: { userId: "" },
queries: { ... },
// Standard module config
derive: {
isReady: (facts) => !!facts.userId,
},
events: {
setUser: (facts, { id }) => { facts.userId = id; },
},
effects: { ... },
constraints: { ... },
resolvers: { ... },
// System config
plugins: [loggingPlugin()],
history: { maxSnapshots: 100 },
trace: true,
initialFacts: { userId: "default" },
});
createQueryModule
For multi-module systems. Returns a standard ModuleDef that works with createSystem.
import { createSystem, t } from "@directive-run/core";
import { createQueryModule, createQuery, createMutation } from "@directive-run/query";
const dataModule = createQueryModule("data", [
createQuery({ name: "user", key: ..., fetcher: ... }),
createMutation({ name: "updateUser", mutator: ..., invalidateTags: ["users"] }),
], {
schema: { facts: { userId: t.string() } },
init: (f) => { f.userId = ""; },
});
// Single-module
const system = createSystem({ module: dataModule });
// Multi-module
const system = createSystem({
modules: {
data: dataModule,
auth: authModule,
ui: uiModule,
},
});
system.start();
// Namespaced access
system.facts.data.userId = "42";
system.read("data.user");
With a Multi-Module System
Other modules can react to query state changes via crossModuleDeps:
import { createModule, t } from "@directive-run/core";
const uiModule = createModule("ui", {
schema: { facts: { banner: t.string() } },
crossModuleDeps: { data: dataSchema },
init: (facts) => { facts.banner = ""; },
effects: {
onUserLoaded: {
run: (facts) => {
const userState = facts.data._q_user_state;
if (userState?.isSuccess) {
facts.self.banner = `Welcome, ${userState.data.name}!`;
}
},
},
},
});
With AI Single-Agent Orchestrator
Use createQuerySystem alongside @directive-run/ai:
import { createQuerySystem } from "@directive-run/query";
const app = createQuerySystem({
facts: { prompt: "", context: "" },
queries: {
context: {
key: (f) => f.prompt ? { prompt: f.prompt } : null,
fetcher: async (p) => {
// Fetch relevant documents for RAG
const res = await fetch(`/api/search?q=${p.prompt}`);
return res.json();
},
},
},
subscriptions: {
agent: {
key: (f) => f.prompt && f.context ? { prompt: f.prompt, context: f.context } : null,
subscribe: (params, { onData, signal }) => {
// Stream AI response using the fetched context
streamAgent(params, { onToken: (t) => onData((prev) => (prev || "") + t), signal });
},
},
},
});
app.facts.prompt = "Summarize the quarterly report";
// 1. "context" query fetches relevant documents
// 2. When context is ready, "agent" subscription starts streaming
With AI Multi-Agent Orchestrator
For multi-agent systems, use createQueryModule to compose data fetching with agent modules:
import { createQueryModule, createQuery } from "@directive-run/query";
import { createMultiAgentOrchestrator } from "@directive-run/ai";
// Data module handles all API fetching
const dataModule = createQueryModule("data", [
createQuery({ name: "documents", key: ..., fetcher: ... }),
createQuery({ name: "userHistory", key: ..., fetcher: ... }),
], { schema: ..., init: ... });
// Agent orchestrator as its own module
const agentSystem = createMultiAgentOrchestrator({
agents: { researcher, writer, reviewer },
modules: { data: dataModule }, // agents can read query state
});
// Agents access fetched data via cross-module facts
// data.documents and data.userHistory are available to all agents

