# AGENTS.md — `@alvera-ai/platform-sdk` corpus

This corpus documents the platform's resource surface. It ships
inside the npm package and is read directly via `node_modules`
filesystem walk. Consumers include downstream agents writing
code in CLIs, React apps, integration test suites, and code
generators.

> **Wiring this into your project:** run `npx @alvera-ai/platform-sdk
> llm-export` from your project root to add a pointer to this corpus in
> your `AGENTS.md` (and a `@AGENTS.md` import in `CLAUDE.md`). Idempotent.

## ⚠ This file is a LOOKUP ONLY. Read the per-kind MD before emitting code.

`AGENTS.md` (this file) is a **DIRECTORY**. It points you to
per-kind MDs (`tools.md`, `workflows.md`, `data_sources.md`,
etc.). It is **NOT** a schema reference, and you **CANNOT**
ground any wire-level claim from this file alone.

### Mandatory protocol before emitting any code that touches a kind

1. **Locate the kind** in the Resources section below.
2. **Open the kind's MD** (`<kind>.md`).
3. **Read §1 (Wire shape) and §2 (Rules the type cannot encode)**
   at minimum.
4. **Every claim you emit** about wire shape, polymorphism,
   constraints, enum values, or required fields **MUST be
   grounded in §1 or §2 of the kind's MD**. If you cannot
   point at the section, re-read before emitting.

### Failure mode this prevents

The one-line summaries in the Resources section are
**navigation labels**, not schemas. Example:

> The index line `workflows.md — Filter + decision + action;
> standard + agent-driven variants` **cannot** tell you whether
> `variant` is a body field. It is not. `workflows.md` §1 shows
> the variant is **structural** — an agent-driven workflow simply
> has one or more AI agents *nested* on its body (the
> `workflow_ai_agents` array) — **not** a top-level `variant` discriminator.

An agent that paraphrases the index into a wire-level claim is
**hallucinating**. The index summary is too compressed to encode
polymorphism, constraints, enum values, or required fields.

### Rule of thumb

Treat index summaries like file names in a directory listing:
useful for finding the right file, useless as a substitute for
opening it. If you find yourself emitting a claim about a kind's
wire shape without having opened that kind's MD in the current
session, **stop and open it now**.

### You are NOT entitled to conclude "the platform cannot do X"

This corpus is your window onto the platform, not its floor plan.
**Absence from these guides is not evidence the platform lacks a
capability** — it usually just means no use case had yet forced that
capability into the docs. The platform is deliberately generic; the
same primitives back many verticals, and each new build surfaces a
fresh batch of capabilities the corpus never had a reason to spell out.

So when a build seems to need something you can't find here, the honest
output is a **problem statement to the platform team** ("I need to do
X; the corpus doesn't document a way — does one exist?"), **not a
verdict** ("the platform can't do X"). A wrong "can't" is expensive: it
sends an agent to build the wrong thing or to file a request for a
feature that already ships. Real examples that were *all* in the
platform but missing from the corpus until a single build forced them
in: SMS quiet-hours send windows, pausing a scheduled campaign by
editing an action's `runtime_filter`, scheduling on the action rather
than the run, per-recipient tracked-link minting, and per-variant
`GROUP BY decision_key` analytics. Each was momentarily mistaken for a
platform gap. Don't repeat that — escalate the question, don't rule.

---

## If you know MCP — vocabulary mapping

The platform's resources don't implement MCP wire format, but
the conceptual roles map cleanly onto MCP terms. If you've
landed here from an MCP background, this table tells you which
corpus page documents the surface you already know:

| MCP concept           | Alvera resource              | Corpus page                                   |
|-----------------------|------------------------------|-----------------------------------------------|
| Host                  | The platform itself          | This corpus + `account_management.md`         |
| Client                | Data Activation Client       | `data_activation_clients.md`                  |
| Server                | Agentic Workflow             | `workflows.md`                                |
| Resources             | Datasets + generic tables    | `generic_tables.md`, `mdm.md`                |
| Prompts               | Interoperability Contracts   | `interoperability_contracts.md`, `templates.md` |
| Sampling              | Dataset event triggers       | `data_activation_clients.md` §6.4 logs        |
| Tools                 | Tool Calls (polymorphic body)| `tools.md`                                    |
| Roots                 | Datalake boundaries          | `datalakes.md`                                |
| Human-in-the-loop     | Authenticated links          | `authenticated_links.md`                      |
| Sandboxed execution   | Liquid + SQL fragment lanes  | `ai_sandbox.md`                               |

A few caveats on the mapping:

- **Capability negotiation is configuration, not runtime
  handshake.** Tools declare their `intent` and `tool_body_type`
  at create time; data activation clients declare their bound
  contracts and tool call shape; workflows declare their event
  dataset and decision keys. The platform doesn't negotiate
  these per session — they're fixed by the resource records.
- **Sampling is lossless, not subscribed.** Dataset events
  surface through database triggers and per-tenant work queues
  rather than an MCP-style sampling subscription. Consumers
  observe outcomes through the log subresource on the data
  activation client or the workflow, not through a streaming
  channel.
- **Roots are the tenancy boundary, full stop.** Cross-datalake
  reads and writes are physically blocked at the connection
  layer; a session scoped to one datalake can never reach
  another tenant's data even if the calling code addresses a
  different slug.

The rest of this preamble covers the actual SDK auth and client
construction.

## SDK auth + client construction

The SDK is a strict-TypeScript REST client for the Alvera
platform. Two-step auth + client construction — and there are **two**
session minters, not one (see `account_management.md` for the full
treatment):

    import {
      createBootstrapSession,   // tenantless / root Bearer
      createSession,            // tenant-scoped Bearer
      createIsolatedPlatformApi,
      type PlatformApi,
    } from '@alvera-ai/platform-sdk'

    // 1a. tenantless / root — POST /api/v1/admin/bootstrap-session.
    //     Keyless by structural necessity (it mints the FIRST Bearer of an
    //     environment, before any tenant key exists). Returned tenant + role
    //     are always null. Dev / integration-test route — absent from prod.
    const bootstrap = await createBootstrapSession({ baseUrl, email, password })

    // 1b. tenant-scoped — the canonical app session. tenantSlug AND apiKey
    //     are BOTH required; sign-in 401s ("X-API-Key required to sign in to
    //     a tenant") without a resolvable X-API-Key.
    const session = await createSession({
      baseUrl,
      email,
      password,
      tenantSlug,    // REQUIRED
      apiKey,        // REQUIRED — the tenant's publishable key
    })

    // 2. build a typed client
    const api: PlatformApi = createIsolatedPlatformApi({
      baseUrl,
      sessionToken: session.sessionToken,
      apiKey,        // rides as X-API-Key on every request
    })

**Two credentials travel together on every request** (Firebase's
"API key + ID token" pairing, not an either/or): the session token
(`Authorization: Bearer …`) is what the platform authenticates and
authorizes as — Bearer always wins identity; the **publishable API
key** (`X-API-Key: …`) identifies the client and carries the
origin policy — a `public_api` key declares `allowed_origins`, and
a browser request's `Origin` is checked against the key that
resolves. A Bearer session *inherits the origin policy of the key
that minted it*. Where the key comes from: a tenant admin mints it
in the platform's API-keys page (Settings → API Keys), or
programmatically via `api.admin.createTenantApiKey(tenantSlug,
{ name, data_access_mode })` (platform-admin-gated).

**A CORS preflight is NOT proof the key admits your origin.** A
browser `OPTIONS` preflight carries no `X-API-Key` (only the method
+ `Access-Control-Request-Headers`), so the server cannot resolve a
key and answers permissively — EVERY origin gets a friendly
`access-control-allow-origin` echo. The enforced policy runs on the
actual authenticated request, against the resolved key's
`allowed_origins`. Never verify origins with a preflight — verify
with a real signed-in round-trip. Triage for a failing browser
sign-in: (1) console says the key env var is missing → your `.env`
didn't load (restart the dev server after editing it); (2) the REAL
call is blocked by CORS (no ACAO on it) → the key's
`allowed_origins` doesn't include your dev origin — mint/patch the
key, and don't trust the earlier green preflight; (3) a `401` with
NO CORS error → the request PASSED CORS; the fault is the
credentials or the key not resolving, not origins.

Sessions come in three scopes, minted by two functions:
- **root** — Alvera root admin; user signup + confirmation. Minted by
  `createBootstrapSession` (a tenantless Bearer that belongs to a
  platform-admin user).
- **tenantless** — authenticated user with no tenant chosen yet;
  used once to create a tenant via `api.tenants.create(...)`. Also
  minted by `createBootstrapSession`.
- **tenant-scoped** — the canonical Bearer for tenant operations,
  minted by `createSession` (**requires both `tenantSlug` and a real
  tenant API key** — the login 401s with `X-API-Key required to sign
  in to a tenant` otherwise).

**Build apps tenant-scoped, and only tenant-scoped.** The root and
tenantless scopes exist for the platform's own UI and for
admin/bootstrap flows (signup, confirmation, tenant creation,
test-suite setup) — they are NOT app-building surfaces; an app that
holds a root or tenantless Bearer is misdesigned. An app
authenticates against exactly one tenant with that tenant's
publishable key. Likewise, the host behind an action's
`link_prefix` resolves its links against one tenant, on that tenant's
publishable key (see `authenticated_links.md`).

Consumers holding multiple concurrent clients in the same file
(e.g. integration tests that need root + tenantless + tenant-
scoped APIs concurrently) MUST use `createIsolatedPlatformApi`
rather than the singleton `createPlatformApi`. The singleton
mutates a shared client; the most-recent construction call
clobbers earlier instances' auth.

## Using this corpus: building an app vs. writing a test

The `api.*` call sequences in this corpus are framework-agnostic — the
**same** code powers all of these:

- **A React / CopilotKit app** — call `api.workflows.create(...)`,
  `api.dataActivationClients.ingest(...)`, etc. from an event handler, a
  server action, or a CopilotKit action; render the typed response.
- **A backend / Bun / Node script** — drive the client straight through.
- **A vitest / integration test** — thread the same calls through a test
  closure, asserting on each typed response.

What changes between them is only *where you put the calls* (a component
handler vs. a test body), never the calls themselves. The cookbooks under
`cookbook/` read as test-style sequences because that is how they are
validated, but every step is a plain `api.*` call you can lift into an app
unchanged. Capabilities are not documented separately — each lives as
numbered steps inside the cookbook whose use case motivates it.

## Resources

```
   INFRASTRUCTURE  (stood up before data flows)
   ──────────────
   datalakes.md                  Storage layer; the raw lake and
                                  its tokenized + redacted copies.
   data_sources.md               External ingestion endpoints.
   tools.md                      Authenticated connections for
                                  action execution.
   templates.md                  Platform-shipped Liquid templates
                                  consumed by other resources.
   ai_agents.md                  LLM-backed workers.
   action_status_updaters.md     Polling reconcilers.
   mdm.md                        Entity resolution + identity
                                  verification across domains.
   ai_sandbox.md                 Three-layer safety contract
                                  bounding every AI-authored
                                  fragment: compliance gates,
                                  Liquid sandbox, SQL boundary.

   DATA ACTIVATION  (how data flows in)
   ───────────────
   generic_tables.md            Schema-on-write tables.
   advanced_migrations.md       Hand-written DDL for one lake —
                                  indexes, foreign keys, anything
                                  past the CREATE TABLE / ADD COLUMN
                                  the platform writes for itself.
   interoperability_contracts.md Liquid template mappings.
   data_activation_clients.md    Ingestion pipelines — both the
                                  binding row (CRUD) and the
                                  runtime verbs (.ingest, .ingestFile,
                                  .runManually, logs, dataset search).

   WORKFLOWS  (composition on top)
   ─────────
   workflows.md                  Filter + decision + action;
                                  standard + agent-driven variants;
                                  §8 watching a run.
   messages.md                   The messages DATASET — what went
                                  out, delivery status, engagement.
   action_logs.md                The action_logs DATASET — per-
                                  action outcomes, batch_id joins,
                                  audience export.
   authenticated_links.md        Every URL a message carries, as
                                  <link_prefix>/<token>; the host
                                  behind the prefix resolves taps
                                  and reports the page.
```

Axes are mostly disjoint but a few resources cross. AI Agents
anchor INFRASTRUCTURE and appear in DATA ACTIVATION (they
tokenize during ingestion) and WORKFLOWS (workflows invoke
them).

Each resource is exposed via a TypeScript namespace on the
typed client. The SDK namespace name follows TS conventions
(camelCase); the corpus MD filename matches the platform's
wire name (snake_case):

    SDK namespace                       Corpus MD
    ────────────────────                ───────────────────────────
    api.datalakes                       datalakes.md
    api.dataSources                     data_sources.md
    api.tools                           tools.md
    api.aiAgents                        ai_agents.md
    api.actionStatusUpdaters            action_status_updaters.md
    api.mdm                             mdm.md
    api.genericTables                   generic_tables.md
    api.advancedMigrations              advanced_migrations.md
    api.interoperabilityContracts       interoperability_contracts.md
    api.dataActivationClients           data_activation_clients.md
    api.workflows                       workflows.md
    api.authenticatedLinks              authenticated_links.md

Two corpus pages document DATASETS, not namespaces — read
through `api.datasets` with SINGULAR dataset types (the plural
forms are the SQL table names used by `executeSql`):

    dataset type 'message'    (table messages)      messages.md
    dataset type 'action_log' (table action_logs)   action_logs.md

## Cookbooks (Golden Path scenarios)

`.agent/cookbook/<slug>.md` files are pure-markdown scenario
stories — one business outcome each, told as a numbered API-call
sequence that the cookbook validator runs, step by step, against a
live platform. Agents read these directly
from the filesystem at
`node_modules/@alvera-ai/platform-sdk/.agent/cookbook/`; there
is no SDK function and no CLI verb that returns cookbook bytes
at runtime.

Each cookbook structure — front matter, then these top-level `#`
sections, each exactly once and in this order. The validator refuses a
cookbook that is missing one, repeats one, reorders them, leaves one
empty, or adds a top-level section that is not on this list:

- **Front matter** — `title`, `summary`, `use_case`, `slug`,
  `status`. `use_case` is one
  of the four surfaces — `organic-marketing`,
  `subscription-saas`, `payments-compliance`,
  `primary-care-feedback` — and there is exactly one cookbook per use
  case. Three are at `cookbook/<use-case>.md`; the fourth is at
  `cookbook/primary-care.md`, because `use_case` keeps the use case's
  full name and the filename does not. Read `use_case` from the front
  matter rather than deriving it from the filename.
- **Problem** — the business-outcome statement in domain terms,
  sourced from what the walk's steps actually do (not from
  customer-narrative documentation).
- **Composition** — table of resources provisioned, mapped to
  the audience skill that owns each (setup / build / compose).
- **Walkthrough** — numbered `## NNN — <step>` subsections, each
  carrying one fenced TypeScript block. State threads forward across subsections via
  plain JavaScript variables in the validator's generated
  `describe` closure. A numbered step anywhere else in the file
  is refused.
- **Outcome** — author's prose summary of what the scenario
  produces.
- **Gotchas** — one numbered list of the mistakes the walk exists
  to keep you from making. Each item opens with a bold one-sentence
  rule, says why it bites, and names the step that proves it as
  `§NNN`. The validator refuses an item that names no step, or a
  step the cookbook does not have — and every step runs against a
  live platform — so a gotcha cannot outlive the behaviour it warns
  about.
- **See also** — links to relevant per-resource reference MDs.

### Every cookbook is self-reliant, and repeats what it needs

A cookbook stands up **its own** tenant, datalake and resources in
its own numbered steps. There is no shared setup file, no setup
group and no bootstrap phase: everything the walk depends on is
in the walk.

That means the auth + tenant + datalake preamble is **duplicated
across all four cookbooks, deliberately.** This corpus is read by
someone — or something — that opens one file and works through it,
and for that reader a shared setup is not a saving. It is a second
file to find, a second convention to learn, and a hidden dependency
on steps they never see. Four documents repeating a preamble is the
cheaper trade, and it is the trade this corpus makes. Do not
re-factor it out.

Two consequences worth stating plainly:

- **Write for idempotence, not just for a clean run.** Use stable
  names and create-or-reuse: look the resource up, create it only
  if it is absent, and assert the end state either way. A step that
  mints a fresh random name works once and litters forever —
  nothing in the product reclaims an abandoned datalake. Ask of
  every creating step: what does this do on run two?
- **Label numbering is local and clean.** Each cookbook numbers its
  own steps `§001…§00N` from the top, and a failure names the file
  and the step directly.

### Vendored fixtures (Liquid templates, CSV bodies)

End-to-end cookbooks walk the full data-activation chain
(data source → interoperability contract → Data Activation
Client → CSV ingest), which requires Liquid templates that
map inbound rows into the platform's upsert shapes. Inlining
those templates verbatim inside every cookbook markdown
bloats each recipe to several hundred lines of Liquid noise
that obscures the cookbook's lesson. The corpus ships them
as **vendored fixtures** instead.

- Fixtures live under
  `.agent/cookbook/_fixtures/<industry>/`. The leading
  underscore marks the directory as fragment infrastructure
  (not a cookbook); the validator's discovery walk skips it.
  Each `<industry>/` subdirectory carries a `README.md`
  listing every template and which cookbook consumes it.
- Cookbook code loads fixtures via the
  `COOKBOOK_FIXTURES_DIR` environment variable:
  ```typescript
  import fs from 'node:fs'
  import path from 'node:path'

  const LE_TEMPLATE = fs.readFileSync(
    path.join(
      process.env.COOKBOOK_FIXTURES_DIR!,
      'organic-marketing/_lead_submissions_foundation_legal_entity.liquid',
    ),
    'utf8',
  )
  ```
  Same env-var pattern as the `ALVERA_BASE_URL` /
  `ALVERA_ROOT_EMAIL` / `ALVERA_ROOT_PASSWORD` credentials
  the generated specs already consume.
  `make validate-cookbook` exports
  `COOKBOOK_FIXTURES_DIR=<absolute path to _fixtures>` to the
  spawned `bun test` process; any operator running cookbook
  code directly from a Bun script must set the same variable.
- The fixtures ship with the `@alvera-ai/platform-sdk` npm
  package via the `files: [.agent]` field, so a consumer
  who installed the package finds the templates at
  `node_modules/@alvera-ai/platform-sdk/.agent/cookbook/_fixtures/`.
- An agent reading the corpus discovers vendored templates by
  directory walk:
  `_fixtures/` → `<industry>/` → `README.md` lists every
  template plus the cookbooks that depend on it. No need to
  read cookbook code first to know what fixtures exist.

### The four cookbooks

`cookbook/` holds **four files, one per use case**, and nothing else that
runs. Each is self-reliant: it stands up its own tenant, datalake and
resources in its own numbered steps and shares nothing with its siblings.
A reader reads one file end to end and needs no second one. The
duplicated bootstrap across them is deliberate — four repeated preambles
are cheaper than one shared setup every reader must go and understand
first.

There is no separate per-capability file. Bulk upload, REST fetch, team
invitations, action status updaters, paginated pollers, text-to-SQL and
agent file-invocation each live as numbered steps inside the cookbook
whose use case motivates them, because a capability shown without a
reason to use it is the thing readers skip.

All four are verified at dev time by `make validate-cookbook` at the
repo root; every numbered step is one test.

<!-- BEGIN:cookbook-index -->

- [organic-marketing](./cookbook/organic-marketing.md) — score inbound
  leads into four bands with an LLM agent, schedule a birthday greeting a
  year out with a pure-Liquid trigger, and run an A/B campaign whose
  suppression gates and split live in the workflow. Ends with delivery
  reconciliation and a natural-language read of the lake. One raw
  datalake.
- [payments-compliance](./cookbook/payments-compliance.md) —
  disambiguate gray-zone sanctions screenings with an LLM agent that
  never sees who was screened, and fire a KYC notification on account
  activation. Ends by reading one row through raw, tokenized and redacted
  to show what each reader gets. Three datalakes.
- [subscription-saas](./cookbook/subscription-saas.md) — one customers
  table and three workflows over it: a welcome SMS gated on
  reachability, a dunning reminder gated on reachability AND KYC, and an
  LLM agent banding accounts by priority. Then the same table filled
  three more ways — inline, bulk CSV, REST fetch — and a teammate invited
  onto the tenant. One raw datalake.
- [primary-care](./cookbook/primary-care.md) — a review request that
  only reaches patients whose visit actually happened, an LLM agent
  triaging inbound messages off the tokenized projection, and a vision
  agent reading a scanned document. Three datalakes.

<!-- END:cookbook-index -->

This index is mirrored into the managed block `alvera llm-export` writes
to consumer `AGENTS.md` files — `buildManagedBlock`, duplicated in both
the SDK and CLI packages and kept in sync by hand. **Both suites now
assert every cookbook named in the block exists on disk, and that every
cookbook on disk is named.** They previously pinned names against the
block string alone, which is how the index went on advertising four
deleted files without a single test failing.

## Utility namespaces

Cross-resource utilities not documented in this corpus:

    api.admin         signUp + confirmUser + createTenantApiKey
                       (dev / integration-test fixtures)
    api.sessions      verify; top-level createBootstrapSession /
                       createSession / revokeSession mint + revoke Bearers
    api.tenants       create + tenant lifecycle
    api.datasets      cross-resource search / metadata /
                       user-saved searches
    api.templates     system-template discovery + per-tenant
                       metadata

## Resource page structure

Each per-resource MD (e.g. `tools.md`) follows a consistent
6-section structure: Wire shape, Rules the type cannot encode,
Field ownership, Error envelopes, Lifecycle, Gotchas.

## Common sections (apply to all resources)

These sibling files cover patterns that apply uniformly across
all resources. Per-resource MDs reference them by name instead
of restating:

| Topic                                         | File             |
|-----------------------------------------------|------------------|
| Error envelope shape                          | `errors.md`      |
| Mutations (`update()` replaces whole; exceptions) | `mutations.md` |
| Async + readiness                             | `async.md`       |
| Debugging (HTTP interceptor + safe redaction) | `debugging.md`   |
| Type naming + server-derived fields           | `type_naming.md` |
| Local mock surfaces (WireMock / LocalStack)   | `mock-services.md` |

`mock-services.md` answers the local-development questions the other
pages assume away: how the platform reaches mocked third-party APIs on
a laptop, the `endpoint_url` override on the sender tools, and why End
User Messaging points at WireMock rather than LocalStack. It is fetched
from the platform-metadata channel, not authored here.

## Just getting started?

In your consumer repo, run:

```
npx @alvera-ai/platform-sdk llm-export
```

This writes a managed block into your `AGENTS.md` pointing back
at this corpus, and an `@AGENTS.md` import into `CLAUDE.md` for
Claude Code compatibility. Re-running replaces only the managed
block — content outside the markers is preserved.

## Base path

All resources are scoped under a tenant + datalake:

```
/api/v1/tenants/{tenant_slug}/datalakes/{datalake_slug}/<resource>
```

The `{tenant_slug}` comes from your authenticated session's
tenant; the `{datalake_slug}` comes from the response of
`api.datalakes.create(tenantSlug, body)` (server-derived; never
pre-compute client-side — see `type_naming.md` "Server-derived
fields auto-excluded from Writable").
