# The eight primitives

Everything in the framework is one of these. `x g resource <name>` scaffolds a whole feature slice — an entity + repo, a policy, an errors module, **two** actions (`create-*`, `archive-*`), a live query, a job, an app route, plus service, UI and i18n files, 25 in all — each with a test that **passes on the first run**. Not one of each: the slice emits no `mutator` and no `task`. Those have their own generators.

```
entity    — a table + its domain type + invariants
policy    — an authz rule, evaluated in every surface
action    — a mutation or command (server-authoritative)
mutator   — an action with an optimistic local twin (offline/realtime)
query     — a read; optionally live (subscribable)
job       — durable background work, optionally multi-step
route     — a URL + render mode + metadata + offline strategy
task      — a scheduled trigger (cron) that enqueues jobs
```

| Primitive | Declared in | Generator | Deep page |
|---|---|---|---|
| `entity` | `<feature>/entity.ts` | `x g entity <name>` | [Entities and migrations](/entities-and-migrations) |
| `policy` | `<feature>/policy.ts` | `x g policy <name>` | [Policies and authz](/policies-and-authz) |
| `action` | `api/` or `<feature>/actions/<name>.ts` | `x g action <name>` | [Actions](/actions) |
| `mutator` | `<feature>/actions/<name>.ts` | `x g mutator <name>` | [Realtime](/realtime) |
| `query` | `<feature>/queries/<name>.ts`, or `live/<name>.ts` with `--live` | `x g query <name> --feature <slice>` | [Queries and live queries](/queries-and-live-queries) |
| `job` | `<feature>/jobs/<name>.ts` | `x g job <name>` | [Jobs and workflows](/jobs-and-workflows) |
| `route` | a route folder's `config` export | `x g route <path>` | [Routes and render modes](/routes-and-render-modes) |
| `task` | `<feature>/tasks/<name>.ts`, plus the job it enqueues | `x g task <name>` | [Scheduled tasks](/scheduled-tasks) |

Every one is a **function returning a value**, so an app encodes a house rule by wrapping one and exporting its own factory — `tenantEntity`, `auditedMutator` — with the registry, the manifest, the projections, admin and MCP treating the result identically. That is how the framework extends itself too (`llm()` returns an `action`, `backfill()` returns a `job`), and it is why there is no plugin API → [Building your own base](/building-your-own-base).

## Every primitive projects itself

A primitive's surfaces are methods **on the primitive**, never free functions taking it. `publishPost.openapi()`, not `toOpenApiOperation(publishPost)`. (The MCP tool is `@ultimat3/mcp`'s one projection, `toolFrom(publishPost)`: a tier-3 primitive cannot hold a tier-4 projection.) Every declared field is lifted onto it, and the declaration object — the `handle`, the `sql`, the `server` — is not reachable from app code at all.

| Primitive | Its surface |
|---|---|
| `entity` | `$`-sigil members: `posts.$view([...])`, `posts.$parse(row)`, `posts.$assert(row)`. An entity *is* its columns, so the sigil keeps the namespace clear — `posts.name` is a column, `posts.$name` is the entity |
| `policy` | one signature, `({ actor, input, row, ctx })`, identical on every surface. `row` is required and nullable, never smuggled through `input` |
| `action` | `.as()` `.openapi()` `.client()` `.job()` `.contract()` `.describe()`, plus `.input` `.output` `.policy` `.mcp`. `handle` is not among them |
| `mutator` | everything `action` has, plus `.local()` `.server()` `.conflict` `.describeMutator()` |
| `query` | `.as()` `.live()` `.client()` `.describe()`, plus `.input` `.policy` `.cache` `.mcp` `.isLive`. `sql` is not among them |
| `job` | `.enqueue()` `.as()` `.describe()`, plus `.parse()` and `.idempotencyKeyFor()` |
| `route` | a normalized descriptor rather than methods — `meta()` always awaits, `budget` is always an object. A route declares no behaviour to project; `describePages()` is the one route list |
| `task` | `.entries()` `.enqueue()` `.describe()` |

The declaration lives in a private store inside the package that runs it, and nothing exports a reader for it. So there is one execution path and one authz path structurally, not by convention: a hand-rolled look-alike carrying the right `kind` is `X_ACTION_FOREIGN` / `X_QUERY_FOREIGN`, never a registered primitive. Registration stamps the export name **in place**, so `import { publishPost }` is the object that projects once the app has booted — there is no second, differently-named twin to remember.

Each primitive's deep page carries the full member table.

## One import per authoring file

`t` is re-exported from every package that hosts a declaration factory. Import it from the package you declare in; never reach past that package to `@ultimat3/schema`. It is the **same object**, not a copy — `configureSchemaProvider()` still swaps the provider under it — and each package pins that with an identity assertion in its `index.test.ts`.

| Import `t` from | When the file declares |
|---|---|
| `@ultimat3/entity` | an `entity` |
| `@ultimat3/action` | an `action` or a `mutator` |
| `@ultimat3/query` | a `query` |
| `@ultimat3/jobs` | a `job` or a `task` |
| `@ultimat3/mail` | a `defineMail` template |
| `@ultimat3/mcp` | a `defineAppMcp` hand-written tool |
| `@ultimat3/ai` | an `llm` — a factory over `action`, so it carries `t` too |
| `@ultimat3/schema` | nothing — a plain view-schema module, where schema *is* the one import |

`policy` and `route` declare no schema, so neither package re-exports `t`: a policy decides from `({ actor, input, row, ctx })` and a route from its path.

## `entity`

A table + its domain type + invariants. The single source of the DB schema, the TS type, and the parse boundary.

| Aspect | Rule |
|---|---|
| Projects to | Postgres table, domain type, migration, repo (`postgresRepo`), admin screen, seed factory |
| Owns | column types, defaults, invariants, tenant column |
| Never | business logic, I/O, HTTP awareness, policy decisions |

## `policy`

An authz rule, evaluated in every surface.

```ts
policy: can('post:publish', ({ input, actor }) => ownsPost(actor, input.postId))
```

| Aspect | Rule |
|---|---|
| Projects to | HTTP guard, live-query row filter, job actor check, MCP tool gate, admin visibility |
| Owns | the yes/no and the denial reason |
| Never | mutate, query outside the declared repos, or return partial data (filter in the `query`) |

## `action`

A mutation or command, server-authoritative. Declared once, projected six ways.

```ts
export const publishPost = action({
  input:  t.object({ postId: t.uuid, notify: t.boolean.default(true) }),
  output: PostView,
  policy: can('post:publish', ({ input, actor }) => ownsPost(actor, input.postId)),
  cache:  { invalidates: [tag.post, tag.feed] },
  mcp:    { expose: true, description: 'Publish a draft post' },
  async handle({ input, ctx }) {
    const post = await ctx.posts.publish(input.postId);
    if (input.notify) await notifySubscribers.enqueue({ postId: post.id, orgId: post.orgId });
    return post;
  },
});
```

| Aspect | Rule |
|---|---|
| Projects to | HTTP route, OpenAPI operation, typed client function, job handle, MCP tool, contract test |
| Owns | input/output contract, its policy, what it invalidates, MCP exposure |
| Never | read headers or cookies, render, authorize inside `handle`, do slow work inline |

## `mutator`

An action with an optimistic local twin. `local` runs client-side against the local store; `server` is authoritative. Same input, same name, both halves in one file.

```ts
export const likePost = mutator({
  // REQUIRED (`X_MUTATOR_NOT_IDEMPOTENT`): a replay under the same Idempotency-Key answers the
  // first result instead of running `server` again.
  idempotent: true,
  // Convergent, not incremental: `local` replays on every rebase, so applying it N times has to
  // equal applying it once — `likedByMe` is what makes the second application a no-op.
  local(tx, { postId }) {
    tx.posts.update(postId, (p) =>
      p.likedByMe ? {} : { likedByMe: true, likeCount: p.likeCount + 1 });
  },
  async server(ctx, { postId }) { return ctx.posts.like(postId); },
  conflict: 'server-wins', // | 'last-write-wins' | custom(merge)
});
```

| Aspect | Rule |
|---|---|
| Projects to | everything `action` does, plus a local-store transaction and a rebase entry |
| Owns | conflict strategy |
| Never | let `local` do I/O, randomness, or `Date.now()` — it must be replayable |

## `query`

A read; optionally live (subscribable).

```ts
export const liveFeed = query({
  input: t.object({ orgId: t.uuid }),
  policy: can('feed:read'),
  live: true,
  sql: ({ orgId }) => db.posts.where({ orgId }).orderBy('createdAt').limit(50),
});
```

| Aspect | Rule |
|---|---|
| Projects to | HTTP GET, typed client hook, live subscription (`live: true`), cache entry with tags, MCP read tool |
| Owns | shape + row-level filtering |
| Never | write, enqueue, or send mail. `live: true` requires a deterministic, bounded `sql` |

## `job`

Durable background work, optionally multi-step. `idempotencyKey` is **required by the type**.

```ts
export const onboardOrg = job({
  input: t.object({ orgId: t.uuid }),
  tenant: ({ orgId }) => orgId,               // the org this run acts as
  idempotencyKey: ({ orgId }) => `onboard:${orgId}`,   // REQUIRED by the type
  retry: { attempts: 5, backoff: 'exponential' },
  async run({ input, step, ctx }) {
    const org = await step.run('provision', () => ctx.orgs.provision(input.orgId));
    await step.run('welcome-email', () => ctx.mail.send(welcomeEmail, org));
    await step.sleep('3d');
    await step.run('nudge', () => ctx.mail.send(nudgeEmail, org));
  },
});
```

| Aspect | Rule |
|---|---|
| Projects to | queue row, per-step persistence, retry schedule, dashboard entry, MCP `jobs.inspect` tool |
| Owns | retries, steps, concurrency class |
| Never | assume it runs once — assume at-least-once. Durable business state lives in your tables, never only in the payload |
| Factories over it | `backfill()`, `rollup()`, `refreshView()` ([Rollups](/jobs-and-workflows#rollups-and-materialized-views)), `consumer()` (a `job` plus its `.supervisor` `task` — [Consumers](/jobs-and-workflows#consumers--consumer)), `purge()`, `exportRows()`, `importRows()`, `notifier()`, `webhook()` (outbound) and `webhookReceiver()` (inbound: a `job` plus the plain route that verifies, enqueues, then acks) — [Jobs and workflows](/jobs-and-workflows#inbound-webhooks) |

## `route`

A URL + render mode + metadata + offline strategy.

```ts
export const config = defineRoute({
  render:     'isr',                  // static | isr | ssr | stream
  revalidate: { tags: [tag.post] },
  prerender:  () => db.posts.slugs(),
  offline:    'precache',             // precache | runtime | network-only
  hydrate:    'visible',              // idle | visible | interaction | never
  budget:     { js: '40kb' },
  meta:       ({ post }) => ({ title: post.title, description: post.excerpt,
                               og: { image: post.cover }, ld: ld.Article(post) }),
});
```

| Aspect | Rule |
|---|---|
| Projects to | router entry, prerender list, `sw.js` precache/runtime rule, sitemap + RSS row, `<head>` + JSON-LD, per-route budget check |
| Owns | render mode, hydration timing, metadata, offline strategy |
| Never | touch the DB directly, hold business logic, or omit `meta.description` in `site/` — that is a build error, `X_SEO_META_MISSING` — the same code for a missing title, with `cause` naming the field |

Render modes: `static` (built once), `isr` (static + background regen), `ssr` (per-request), `stream` (shell flushed instantly, holes streamed — **default for app pages**). Four, declared once in `@ultimat3/core` as `RENDER_MODES`; `spa` was the fifth until 6.0.0. Table in [Routes and render modes](/routes-and-render-modes).

## `task`

A scheduled trigger. Enqueues jobs; never does work itself.

```ts
export const nightlyDigest = task({
  cron: '0 3 * * *',
  tz: 'UTC',
  enqueue: () => [[sendDigest, {}]],
});
```

| Aspect | Rule |
|---|---|
| Projects to | scheduler entry (lease-row leader, `x_scheduler_leader`), next-run introspection, a `tasks` row in `x.manifest.json` |
| Owns | cron expression + explicit `tz` |
| Never | contain a handler body. If it does work, it is a `job` |

## Composition

```
entity ──> policy ──> action ──> job ──> task
   │          │         │
   │          └──> query (live) ──> route
   └──> mutator (action + local twin)
```

Feature slicing puts a feature's primitives in one folder, not one layer per app:

```
apps/web/app/<feature>/{entity,repo,service,actions,live,jobs,policy,ui}.ts
```

Every primitive emits its test beside it, and that test **passes on the first run** — a generator that emits a `TODO` has moved the work, not done it. What it pins is the distant invariant the primitive owns: a policy denial, an idempotency key, a budget. Extend it as the feature grows ([Testing](/testing)). Every primitive appears in `x.manifest.json`, so `x manifest --json` and the MCP `manifest.read` tool describe the whole app as data.

## The rule

**If a feature doesn't fit a primitive, it doesn't ship.** No ninth primitive, no escape-hatch `mode:` option, no plugin API in 1.0. Removing an alternative is a feature — ambiguity is the tax agents pay. Source: [`docs/idea/02-primitives.md`](https://github.com/developerz-ai/ultimate/blob/main/docs/idea/02-primitives.md).
