Skip to Content
ToolsUsers

Users

The users tool (ctx.USER.getUser) is a built-in user store that lives at the organization level — every loop in your org reads and writes the same user records. Use it to carry context across multiple runs of the same loop, or across different loops, without having to pipe state through trigger payloads.

A user record has two fields:

FieldTypeWhat it’s for
metadataRecord<string, unknown>Structured facts about the user — name, phone, locale, last-known clinic site, etc.
memorystring (markdown)An append-only running narrative — one short line per remember(...) call.

The user is keyed by a caller-supplied external ID (a string you control: patientId, phoneNumber, email — anything stable for the user you’re tracking). The row is created the moment you call ctx.USER.getUser({ id }) — that call upserts the row and returns a handle whose .id is the platform-assigned UUID. The metadata/memory methods all assume the row exists and throw if it’s ever deleted out from under you.

Tool API

ctx.USER.getUser returns a User handle. All calls must be made inside a step.do callback (or directly in onChatMessage for agent loops) — ctx only exists there. See Step context for the surrounding API.

type User = { /** * The platform-assigned UUID for this user. Stable across runs; pass this * to bindings (e.g. `ctx.OLLIE.chat`) when you need a canonical identifier. * This is *not* the external id you passed in — the external id is the * caller-facing key the handle was opened with. */ readonly id: string; /** Replace this user's metadata. */ setMetadata(metadata: Record<string, unknown>): Promise<void>; /** Read this user's metadata. */ getMetadata<T = Record<string, unknown>>(): Promise<T>; /** * Append a one-line memory entry. The platform owns the bullet/newline * formatting so loop code can pass the bare line. */ remember(line: string): Promise<void>; /** Read this user's full memory markdown. */ getMemory(): Promise<string>; /** This user's completed facial scans across every loop in the org, newest first. */ getScans(): Promise<FacialScanRef[]>; /** This user's most recent completed scan, or null if they have none. */ getLatestScan(): Promise<FacialScanRef | null>; /** Count of completed facial scans — cheaper than getScans().length for gating. */ getScanCount(): Promise<number>; };
MethodDescription
setMetadataReplace the entire metadata object. Throws if the row was deleted out from under you. Read first and merge yourself to preserve other keys.
getMetadataRead the current metadata. Throws if the row no longer exists.
rememberAppend one line to the user’s running memory markdown. The platform formats each entry as a - bullet on its own line.
getMemoryRead the current memory markdown. Throws if the row no longer exists.
getScansList completed facial scans across every loop in the org, newest first. Each entry is a { loopId, scanSessionId, createdAt } pointer.
getLatestScanThe user’s most recent completed scan, or null.
getScanCountCount of completed scans — use to gate flows (e.g. pass status: "limit_reached" to FACIAL_SCAN.create).

setMetadata is a replace, not a merge

This is the one footgun. setMetadata({ name: "Alice" }) replaces the whole metadata object — any keys you had before are gone. If you want to update one field while preserving the rest, read first and merge:

const existing = await user.getMetadata<{ name?: string; phone?: string }>(); await user.setMetadata({ ...existing, phone: "+27…" });

We chose replace over merge because it keeps the write semantics obvious — what you pass is what’s on disk afterwards. Loops that want merge behavior can build it on top.

Examples

Carry context across runs of the same loop

The patient triggers your loop multiple times — first to onboard, later to run a follow-up scan. Use the user store so the second run already knows who they are.

await step.do("Identify patient", async ({ ctx }) => { const user = await ctx.USER.getUser({ id: event.body.patientId }); // First-run path: write everything we learned from the intake. if (event.body.source === "onboarding") { await user.setMetadata({ name: event.body.name, phone: event.body.phone, clinicSiteId: event.body.clinicSiteId, }); await user.remember("Onboarded via clinic microsite"); return; } // Follow-up runs: the metadata is already there; just append to memory. await user.remember(`Returned for follow-up on ${new Date().toISOString().slice(0, 10)}`); });

Use memory to feed prompts in an agent loop

getMemory() returns markdown that you can paste straight into an agent’s system prompt or message context. Each call adds one new line, so the loop builds up a usable narrative over time without you maintaining a structured history.

const user = await ctx.USER.getUser({ id: phoneNumber }); const memory = await user.getMemory(); const stream = await ctx.OLLIE.chat({ thinking: "medium", messages: [ { role: "system", parts: [ { type: "text", text: `What we know about this patient so far:\n\n${memory || "(no prior notes)"}`, }, ], }, ...messages, ], }); return stream.toUIMessageStreamResponse();

Cross-loop handoffs

Because the store is org-wide, a record written by your onboarding loop is readable from your follow-up-scan loop with the same id — no API plumbing between loops required.

// In your onboarding loop: const user = await ctx.USER.getUser({ id: patientId }); await user.setMetadata({ enrolledAt: Date.now(), program: "cardio-12wk" }); // Later, in your follow-up-scan loop: const user = await ctx.USER.getUser({ id: patientId }); const { program } = await user.getMetadata<{ program?: string }>(); if (program === "cardio-12wk") { // …branch the flow… }

Gate scans by user history

Use getScanCount() to enforce a per-user scan cap before provisioning:

await step.do("Provision scan", async ({ ctx }) => { const user = await ctx.USER.getUser({ id: event.body.patientId }); const count = await user.getScanCount(); await ctx.FACIAL_SCAN.create({ user, metadata: { patientId: event.body.patientId }, status: count >= 3 ? "limit_reached" : "ready", }); });

See Facial scan → Assign a scan to a user for how user assignment feeds the cross-loop scan index.

Inspecting users in the dashboard

Open any loop in the dashboard, go to Storage → Users, and you’ll see every user record in the org with its current metadata and memory. The list is shared — every loop’s storage page reads from the same store.

Last updated on