PAMbaseDocs
Guide

Headless agent.

What you're building

A server-side agent with no UI: it reads the user's memory on a loop, does work, and writes results back. The payoff is automation that runs unattended — no chat surface, no front end — while still building on the same portable memory every other PAMbase app shares.

Prerequisites

The connect flow still happens once: the user authorizes in the hub, you receive a short-lived code, and you exchange it for a long-lived connection token stored server-side. You hold an OAuth client id and secret, and the read/write scopes (one permission each) your agent needs.

Scopes to request

ScopeWhy
memory:read:*read whatever the agent reasons over
memory:write:generalpersist results and observations

Narrow these to the specific namespaces your agent uses. See permissions.

Connect once, then run headless

Even with no UI, exchange the authorization code for a connection_token after the user authorizes in the hub. The code is single-use and expires in 10 minutes; the token lasts about a year.

typescript
import { exchangeCodeForToken } from "@pambase/sdk";
const { connection_token, ai_id, scopes } = await exchangeCodeForToken({
apiBaseUrl: process.env.PAMBASE_API_URL!,
code: req.query.code, // single-use, expires in 10 min
clientId: process.env.PAMBASE_CLIENT_ID!,
clientSecret: process.env.PAMBASE_CLIENT_SECRET!,
});
// → { connection_token: "ct_…", ai_id: "ai_…", scopes: ["memory:read:*", "memory:write:general"] }
await vault.put(`pambase:${ai_id}`, connection_token); // store securely — never in code or logs

What to write

Persist results as durable memory — plain natural-language content in the scope you name. No LLM required to write.

typescript
await pambase.remember({
content: "Flagged 3 stale invoices for review.",
kind: "event",
scope: "general",
});
// res → { accepted: true, memoryIds: ["mem_…"] }

How to read

Pure reads: recall for ranked, relevant memories, or getContext for a selected bundle.

typescript
const { memories } = await pambase.recall({ query: "open tasks", limit: 20 });
// memories → [{ id, type, scope, content, importance, occurredAt, sourceApp? }, …]

Putting it together — a cron loop

Run one tick per connected user on a schedule. Read, act, write, and queue the next run. If a call returns 401 the connection token was revoked — there is no refresh, so you must re-run the connect flow.

agent.ts
import { PAMbaseApp } from "@pambase/sdk";
async function tick(aiId: string) {
const token = await vault.get(`pambase:${aiId}`);
const pambase = new PAMbaseApp({ baseUrl: process.env.PAMBASE_API_URL!, connectionToken: token });
try {
const { memories } = await pambase.recall({ query: "open tasks", limit: 20 });
const result = await doWork(memories);
await pambase.remember({ content: result, kind: "event", scope: "general" });
await pambase.schedule({ fireAt: nextRunISO(), kind: "tick" }); // → { id, fireAt }
} catch (e) {
if (e.status === 401) await markReauthNeeded(aiId); // token revoked → re-run connect flow
}
}
// cron: every hour, for each connected user
setInterval(() => connectedAiIds().forEach(tick), 60 * 60_000);
Store the token securely; re-auth on 401

The connection_token is long-lived and grants full access for that user — keep it in a secrets vault, never in code or logs (see security). There is no refresh token: on a 401, stop the loop and re-run the connect flow for a fresh token.

Next