---
title: "Rules for coding agents"
description: "The condensed set of rules and anti-patterns for writing Rayfin code — read this before generating entities, queries, permissions, or deployment commands."
url: https://rayfin.ai/docs/reference/agent-rules
markdown_url: https://rayfin.ai/docs/reference/agent-rules.md
section: reference
product: Rayfin
sdk_version: 1.34.0
cli_version: 1.33.2
last_updated: 2026-08-29T23:37:34-07:00
source: reference/agent-rules.mdx
---

# Rules for coding agents

> The condensed set of rules and anti-patterns for writing Rayfin code — read this before generating entities, queries, permissions, or deployment commands.

This is the short version of everything on this site that changes whether generated Rayfin
code works. If you are an agent and you read only one page, read this one.

Full detail lives in [Known limitations](/docs/reference/known-limitations),
[Modeling entities](/docs/data/modeling), and [Permissions](/docs/data/permissions).

## Platform [#platform]

* Rayfin uses **TC39 Stage 3 decorators**. Never enable `experimentalDecorators` or
  `emitDecoratorMetadata`.
* Include `ESNext.Decorators` in the tsconfig `lib` array.
* Rayfin has one deployment target: a managed **Fabric app** (MSSQL only).
* Prefer `npm create @microsoft/rayfin@latest` for new projects — it generates a correct
  tsconfig and schema boilerplate.

## Data modeling [#data-modeling]

* Define entities with `@entity()` in `rayfin/data/`, and register each one in
  `rayfin/data/schema.ts` as `type AppSchema = { Todo: Todo }`.
* Every field needs exactly one type decorator: `@uuid`, `@text`, `@int`, `@decimal`,
  `@boolean`, `@date`, `@email`, `@set`.
* Fields are **required by default**. For a nullable field use `{ optional: true }` and
  `?` together.
* `@text()`'s length option is `max`, not `maxLength` — `@text({ max: 200 })`. Always set
  it on MSSQL; see
  [Known limitations](/docs/reference/known-limitations#text-without-max-breaks-graphql-schema-generation-on-mssql).
* Use `@one(() => Target)` with a lazy arrow function for relationships. Rayfin
  auto-generates the foreign key column, named `{property}_id` — see
  [Known limitations](/docs/reference/known-limitations#foreign-keys-must-use-the-property_id-naming-convention).
* Use `import`, not `import type`, for entity classes referenced inside `@one()` / `@many()`
  arrow functions — the decorator needs the runtime class value.
* A foreign key column referencing another entity (`{property}_id`) must be `@uuid()` to
  match the primary key type. An auth-derived field such as `user_id` from `claims.sub` is
  `@text()`, not a foreign key.
* Many-to-many is not supported. Use an explicit join entity with two `@one()` fields. See
  [Known limitations](/docs/reference/known-limitations#many-to-many-relationships-are-not-supported).

## Security [#security]

* **Every entity needs an explicit permission decorator** (`@role`, `@anonymous`,
  `@authenticated`). Omitting one silently applies `authenticated: *` — full CRUD for any
  signed-in user — which is almost never what you want in production.
* Add a policy for row-level filtering on user-scoped data:
  `policy: (claims, item) => claims.sub.eq(item.user_id)`.
* Use `exclude` in role options to hide sensitive fields, e.g. `exclude: ['secret']`.
* Publishable keys (`pk-*`) are safe in client-side code. Never expose service secrets or
  connection strings.
* Keep `allowedRedirectUris` in `rayfin.yml` scoped to your app's own origins.
* Fabric SSO (Entra ID) is the only supported authentication method. Its popup flow works
  from any registered origin, including a local Vite dev server (`http://localhost:5173`);
  only the embedded (iframe) flow requires running inside the Fabric portal itself.

## Querying [#querying]

* The query chain is `.select()` → `.where()` → `.orderBy()` → `.execute()`.
* Fetch a single record with `client.data.Entity.findById('uuid')` — there is no
  `findByPk`.
* Filter by foreign key columns using `{property}_id` (`customer_id`), not a dot-path
  (`customer.id`). Dot-paths are for `.select()` only.
* Sort directions are lowercase: `'asc'` or `'desc'`.
* Use `.first(n).executePaginated()` with `.after(endCursor)` for pagination. Collection
  queries return a single page otherwise; the default page size is 100.
* `count()` does not exist on the fluent client. `count` is an
  [aggregation](/docs/data/aggregations) operation and, like `sum`/`avg`/`min`/`max`,
  accepts **numeric fields only** — so it counts non-null numeric values, not rows. For a
  row count, select minimal fields and use `results.length`. See
  [Known limitations](/docs/reference/known-limitations#count-counts-numeric-values-not-rows).
* Grouped aggregation cannot be combined with `.select()`, `.orderBy()`, `.first()` or
  `.after()` — Data API Builder rejects `groupBy` alongside `items`. Sort the returned array
  in TypeScript instead.

## Auth [#auth]

* The session change callback is `onSessionChange&#x60;. &#x2A;*`onAuthStateChange` does not
  exist.** See
  [Known limitations](/docs/reference/known-limitations#the-callback-is-onsessionchange-not-onauthstatechange).
* Session objects are opaque. Gate UI on `isAuthenticated` or the presence of a `user`
  property — do not introspect the session.
* After enabling or disabling auth in `rayfin.yml`, restart the backend so the updated
  endpoints are exposed.

## Configuration [#configuration]

* Always declare `services.auth` and `services.data` explicitly in `rayfin.yml`, even as
  `enabled: false`. The CLI reads those keys without guarding and does not apply defaults.
* If `services.data.enabled` is `true`, `dialect` is required — `mssql` is the only
  supported value. See
  [Known limitations](/docs/reference/known-limitations#dialect-is-required-when-the-data-module-is-enabled).

## Connectors [#connectors]

Connectors reach data that already exists in Fabric. They are in private preview and behind
a feature flag — read [Connectors](/docs/connectors) before writing any connector code.

* The `rayfin connector` command group is not registered until the project sets
  `services.connectors.enabled: true` in `rayfin.yml`, has a non-empty `connectors:` block,
  or runs with `RAYFIN_FEATURE_FLAGS=connectors`.
* The `connector add` flag is `--type`, not `--connector`. The five type literals are
  `fabric-sqlanalytics`, `fabric-warehouse`, `fabric-sqldatabase`, `fabric-semanticmodel`,
  and `kusto`. &#x2A;*There is no `fabric-sql`.**
* `connector add` scaffolds files but installs nothing. Run the version-pinned
  `npm install` it prints, verbatim — never drop the version.
* `connector add` does **not** emit entity `.ts` files. Generate them yourself from
  `rayfin/connectors/<name>/metadata.json`, following
  [Generating entity files](/docs/connectors/entity-generation). `metadata.json` is the only
  source of truth for primary keys and relationships — absent metadata means keyless, not
  permission to infer a key from column names.
* In `rayfin/connectors/<name>/schema.ts`, import and re-export entity classes with
  `import type` / `export type` only. A value import ships decorated classes into the browser
  bundle; the build and deploy both succeed and the deployed page renders blank.
* `GraphQLBackedConnector` is the marker for all three Category A types. Per-type names like
  `FabricWarehouse` do not exist.
* Import `ConnectorsRayfinClient` from `@microsoft/rayfin-client/experimental`, never the
  stable `@microsoft/rayfin-client` entry.
* Category B connectors need the runtime map as the client's second constructor argument —
  `kusto()` injects cluster routing and `fabricSemanticModel()` decodes the response. Omit it
  and calls do not work.
* `auth.type` is lowercase, `delegated` or `application`. `application` is rejected on
  `fabric-semanticmodel` and `kusto`. See
  [Connector authentication](/docs/connectors/auth).

## Deployment [#deployment]

* `rayfin up` is the canonical command for "deploy this change", "apply this schema
  change", or "push my entity update". It builds and deploys the static app **and** applies
  pending schema migrations in one step.
* `rayfin up db apply` is a narrow advanced subcommand that applies schema only. Use it
  when you explicitly want to skip the static deploy.
* After adding or changing an entity, verify with `rayfin up status`. A deploy can report
  success while a newly added entity is not yet readable.
* `--force` permits destructive changes (drop table, drop column, alter type). Never use it
  without review.
* When a user asks you to "build and deploy" or "make it live", run the workflow —
  `rayfin login` → `rayfin up` → `rayfin up status` — rather than printing steps for them
  to run.

## Anti-patterns [#anti-patterns]

| Don't                                        | Do                                                                                                 |
| -------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| Raw `fetch()` or hand-built GraphQL for data | `client.data.<Entity>` — typed queries with automatic auth                                         |
| Entities with no permission decorator        | An explicit `@anonymous()`, `@authenticated()`, or `@role()`                                       |
| `@text()` with no `max` on MSSQL             | `@text({ max: N })` — `NVARCHAR(MAX)` breaks GraphQL schema generation                             |
| `@text()` for a foreign key column           | `@uuid()`, to match the referenced primary key                                                     |
| `onAuthStateChange`                          | `onSessionChange`                                                                                  |
| `findByPk`                                   | `findById`                                                                                         |
| Guessing at an API surface                   | Fetch the relevant `<route>.md` page, or use the [MCP server](/docs/reference/cli/docs#mcp-server) |
