Skip to content
loom
Esc
↑↓navigate↵open⌘Jpreview
On this page

Stores and connectors

Stores save conversations so they survive refreshes and restarts. Connectors give an agent safe access to your data.

Loom keeps two jobs separate:

  • A store saves threads and messages.
  • A connector exposes an external system to the agent as tools.

Both are optional imports. Anything you do not import is never bundled.

Stores

A store saves every thread when a reply finishes. useAgent loads it again on the next page load, so a refresh or a server restart picks up the conversation where it left off.

SQLite

sqliteStore() saves threads in a SQLite file. It uses Node’s built-in node:sqlite, so there is nothing to install.

import { defineAgent } from "useloom";
import { sqliteStore } from "useloom/sqlite";

export default defineAgent({
  model,
  store: sqliteStore(), // .loom/loom.db
});
Option Default
path .loom/loom.db Database file, created if missing. sqliteStore("data/chat.db") is shorthand.
table loom_threads Table for threads, created if missing.
database An open database to use instead of path: node:sqlite’s DatabaseSync or a better-sqlite3 instance.

Requirements and limits:

  • Node 22.13 or newer, for node:sqlite. On older Node, pass a better-sqlite3 database as database.
  • Local disk. The file lives on the machine running your app. That suits development, a VPS, or a container with a volume. On serverless hosts the filesystem is temporary, so threads would not survive between requests there.
  • Shut down cleanly with await store.close() if your server handles shutdown signals.

In memory

memoryStore() keeps threads in memory: fine for tests, but threads are lost on restart.

import { defineAgent, memoryStore } from "useloom";

defineAgent({ model, store: memoryStore() });

Loading threads

With a store, GET /api/agents/<name>?threadId=<id> returns a saved thread, and useAgent calls it for you. When the agent has an auth hook, threads are scoped to the signed-in user. Without auth, the thread id is what keeps a conversation private, so Loom generates 128-bit random ids.

Supabase connector

supabaseTools() gives an agent read access to the tables you list, and optionally write access.

import { createServerClient } from "@supabase/ssr";
import { defineAgent } from "useloom";
import { supabaseTools } from "useloom/supabase";

export default defineAgent({
  model,
  connectors: [
    supabaseTools({
      client: ({ request }) => createServerClient(url, anonKey, { cookies: cookiesFrom(request) }),
      tables: {
        orders: { description: "Customer orders", columns: ["id", "status", "total", "created_at"] },
        products: { columns: ["id", "name", "price"] },
        tickets: { write: true },
      },
    }),
  ],
});

The agent gets these tools:

Tool Available for Does
supabaseQuery every listed table Select columns, filter, order, and limit rows
supabaseInsert tables with write: true Insert up to 20 rows, after the user approves
supabaseUpdate tables with write: true Update rows matching at least one filter, after the user approves

The model writes these queries, so the connector is conservative:

  • Your client, your user. client runs per request. Create it with the signed-in user’s session so your row level security policies apply. Loom never creates a client itself; a service-role client bypasses RLS and is only used if you pass one.
  • Allowlisted tables. The agent sees only the tables you name.
  • Plain columns only. Column names must be simple identifiers, and must be in columns when you set it. Relation embeds, JSON paths, and casts are refused, so a query cannot reach a table you did not list.
  • Writes need approval. Each insert and update waits for the user to approve it. Pass approveWrites: false to turn that off.
  • Bounded. Queries return at most maxRows rows (default 50). Updates without a filter are refused.
  • Errors go to the model. A database or RLS error comes back as { error } so the agent can explain it, instead of failing the request.

Install @supabase/supabase-js (or @supabase/ssr) yourself; Loom does not bundle it.

Your own connectors

A connector is a named set of tools. Use connector() to package your own:

import { connector, tool } from "useloom";

export const billing = connector("billing", {
  getInvoice: tool({ description: "...", input, run }),
});

Tool names must be unique across an agent’s tools/, agent.ts, and connectors. Loom throws at startup when two collide.

Was this page helpful?