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 asdatabase. - 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.
clientruns 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
columnswhen 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: falseto turn that off. - Bounded. Queries return at most
maxRowsrows (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.