Skip to main content

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
Previous
Infinite Queries

Stay in the loop. Sign up for our newsletter.

We care about your data. We'll never share your email.

Powered by Directive. This signup uses a Directive module with facts, derivations, constraints, and resolvers – zero useState, zero useEffect. Read how it works

Directive - Constraint-Driven Runtime for TypeScript | AI Guardrails & State Management