Every function you hand FlowPanel — a widget query, a field's `defaultValue`, an
action's `run`, an adapter method — receives a context object. They all extend
`RequestContext`, so `session`, `role` and `scope` are always available.

## RequestContext

**RequestContext** — Everything the runtime knows about the caller. Every other context extends it.
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `requestId` | `string` | no | Stable correlation id generated once per request. |
| `req` | `object` | yes | The incoming request. Built from the real headers, even during page renders. |
| `session` | `Session \| null` | yes | Result of `AuthConfig.session` for this request. |
| `role` | `string` | yes | Result of `AuthConfig.role` for this session. |
| `scope` | `Scope` | yes | Tenant scope resolved by the admin-wide `scope`. Null when unscoped. |
| `ip` | `string \| null` | yes | Caller IP from `x-forwarded-for`, when the proxy sets it. |
| `userAgent` | `string \| null` | yes |  |

`req` is built from the real incoming headers even during a server render, so
cookie- and header-based auth works identically in pages and API routes.

## QueryContext

Base context for reads. Widget queries and `options` resolvers receive this.

**QueryContext** — Base context for reads. Widget queries and option resolvers receive this.
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `db` | `Db` | yes |  |
| `unsafe` | `object` | no | Explicit escape hatch for trusted application callbacks. |
| `dateRange` | `object` | yes | Active date range — the dashboard picker, or all-time elsewhere. |
| `searchParams` | `object` | yes | Query string of the page being rendered. |
| `signal` | `object` | yes | Aborted when the request is cancelled. Pass it to long queries. |
| `select` | `array` | no | Explicit adapter projection. Omitted preserves the legacy full-row read;
a non-empty list returns exactly those fields; `[]` returns no values.
For `get`, an empty selection yields `{}` when an in-scope row exists or
`null` when it does not. For `list`, it yields a fresh `{}` for each row
on the requested page while retaining scoped `total`, `page`, and
`pageSize`. Adapters must not substitute a primary key or full row. |
| `boundScope` | `object` | no | Opaque request-bound tenant policy. |
| `applyScope` | `function` | no |  |
| `scopeRequired` | `boolean` | no |  |
| `requestId` | `string` | no | Stable correlation id generated once per request. |
| `req` | `object` | yes | The incoming request. Built from the real headers, even during page renders. |
| `session` | `Session \| null` | yes | Result of `AuthConfig.session` for this request. |
| `role` | `string` | yes | Result of `AuthConfig.role` for this session. |
| `scope` | `Scope` | yes | Tenant scope resolved by the admin-wide `scope`. Null when unscoped. |
| `ip` | `string \| null` | yes | Caller IP from `x-forwarded-for`, when the proxy sets it. |
| `userAgent` | `string \| null` | yes |  |

## ListQueryContext

What an adapter's `list` receives — everything needed to build one page of rows.

**ListQueryContext**
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `filters` | `Record<string, unknown>` | yes | Filter values keyed by declared field name. |
| `sort` | `object \| null` | yes | Active sort, or null when the list is unsorted. |
| `page` | `number` | yes | 1-based page number. |
| `pageSize` | `number` | yes |  |
| `search` | `string` | yes | Current search term. Empty string when the box is blank. |
| `searchFields` | `array` | no | Allowlist of columns `search` is OR-ed across, taken verbatim from `resource.options.search`. |
| `softDelete` | `object` | no | Set when the resource declares `delete.softDelete` — deleted rows are filtered out. |
| `includeDeleted` | `boolean` | no | Include soft-deleted rows, for the "show deleted" view. |
| `db` | `Db` | yes |  |
| `unsafe` | `object` | no | Explicit escape hatch for trusted application callbacks. |
| `dateRange` | `object` | yes | Active date range — the dashboard picker, or all-time elsewhere. |
| `searchParams` | `object` | yes | Query string of the page being rendered. |
| `signal` | `object` | yes | Aborted when the request is cancelled. Pass it to long queries. |
| `select` | `array` | no | Explicit adapter projection. Omitted preserves the legacy full-row read;
a non-empty list returns exactly those fields; `[]` returns no values.
For `get`, an empty selection yields `{}` when an in-scope row exists or
`null` when it does not. For `list`, it yields a fresh `{}` for each row
on the requested page while retaining scoped `total`, `page`, and
`pageSize`. Adapters must not substitute a primary key or full row. |
| `boundScope` | `object` | no | Opaque request-bound tenant policy. |
| `applyScope` | `function` | no |  |
| `scopeRequired` | `boolean` | no |  |
| `requestId` | `string` | no | Stable correlation id generated once per request. |
| `req` | `object` | yes | The incoming request. Built from the real headers, even during page renders. |
| `session` | `Session \| null` | yes | Result of `AuthConfig.session` for this request. |
| `role` | `string` | yes | Result of `AuthConfig.role` for this session. |
| `scope` | `Scope` | yes | Tenant scope resolved by the admin-wide `scope`. Null when unscoped. |
| `ip` | `string \| null` | yes | Caller IP from `x-forwarded-for`, when the proxy sets it. |
| `userAgent` | `string \| null` | yes |  |

`searchFields` is an allowlist taken verbatim from `resource.options.search`. An
adapter must never search columns outside it: that list is what keeps the search
box from becoming a way to probe columns the operator cannot otherwise see.

## ItemQueryContext

**ItemQueryContext** — Read context for a single row.
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `string` | yes | Primary key of the requested row, as it appeared in the URL. |
| `db` | `Db` | yes |  |
| `unsafe` | `object` | no | Explicit escape hatch for trusted application callbacks. |
| `dateRange` | `object` | yes | Active date range — the dashboard picker, or all-time elsewhere. |
| `searchParams` | `object` | yes | Query string of the page being rendered. |
| `signal` | `object` | yes | Aborted when the request is cancelled. Pass it to long queries. |
| `select` | `array` | no | Explicit adapter projection. Omitted preserves the legacy full-row read;
a non-empty list returns exactly those fields; `[]` returns no values.
For `get`, an empty selection yields `{}` when an in-scope row exists or
`null` when it does not. For `list`, it yields a fresh `{}` for each row
on the requested page while retaining scoped `total`, `page`, and
`pageSize`. Adapters must not substitute a primary key or full row. |
| `boundScope` | `object` | no | Opaque request-bound tenant policy. |
| `applyScope` | `function` | no |  |
| `scopeRequired` | `boolean` | no |  |
| `requestId` | `string` | no | Stable correlation id generated once per request. |
| `req` | `object` | yes | The incoming request. Built from the real headers, even during page renders. |
| `session` | `Session \| null` | yes | Result of `AuthConfig.session` for this request. |
| `role` | `string` | yes | Result of `AuthConfig.role` for this session. |
| `scope` | `Scope` | yes | Tenant scope resolved by the admin-wide `scope`. Null when unscoped. |
| `ip` | `string \| null` | yes | Caller IP from `x-forwarded-for`, when the proxy sets it. |
| `userAgent` | `string \| null` | yes |  |

## MutationContext

What an adapter's `create`, `update`, `delete` and `restore` receive.

**MutationContext** — Context for writes. Adapters receive this for create / update / delete / restore.
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `db` | `Db` | yes | Your database client, exactly as handed to the adapter. |
| `input` | `Partial<Row>` | yes | Values to write, already coerced and validated. |
| `id` | `string` | no | Target row id. Absent on create. |
| `softDelete` | `object` | no | Set when the resource declares `delete.softDelete` — delete stamps the column. |
| `boundScope` | `object` | no | Opaque request-bound tenant policy. |
| `applyScope` | `function` | no |  |
| `scopeRequired` | `boolean` | no |  |
| `requestId` | `string` | no | Stable correlation id generated once per request. |
| `req` | `object` | yes | The incoming request. Built from the real headers, even during page renders. |
| `session` | `Session \| null` | yes | Result of `AuthConfig.session` for this request. |
| `role` | `string` | yes | Result of `AuthConfig.role` for this session. |
| `scope` | `Scope` | yes | Tenant scope resolved by the admin-wide `scope`. Null when unscoped. |
| `ip` | `string \| null` | yes | Caller IP from `x-forwarded-for`, when the proxy sets it. |
| `userAgent` | `string \| null` | yes |  |

## ActionContext

What every action's `run` receives.

**ActionContext** — What every action's `run` receives as its last argument.
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `db` | `Db` | yes |  |
| `unsafe` | `object` | no | Explicit escape hatch for trusted action implementations. |
| `actorId` | `string \| null` | yes | Caller identity — same derivation the audit trail uses (`auth.userId`, then `session.id`, then `session.user.id`). |
| `publish` | `function` | yes | Push a realtime message to every connected admin. |
| `requestId` | `string` | no | Stable correlation id generated once per request. |
| `req` | `object` | yes | The incoming request. Built from the real headers, even during page renders. |
| `session` | `Session \| null` | yes | Result of `AuthConfig.session` for this request. |
| `role` | `string` | yes | Result of `AuthConfig.role` for this session. |
| `scope` | `Scope` | yes | Tenant scope resolved by the admin-wide `scope`. Null when unscoped. |
| `ip` | `string \| null` | yes | Caller IP from `x-forwarded-for`, when the proxy sets it. |
| `userAgent` | `string \| null` | yes |  |

## Filter values

Most filters arrive in `ctx.filters` as plain scalars. Two kinds arrive
structured, and ship with runtime guards so you can narrow `unknown` safely.

### FilterRangeValue

Produced by `daterange` and `numeric-range` filters.

**FilterRangeValue** — Structured decode of a `numeric-range` / `daterange` `FilterDef` value.
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `op` | `"range"` | yes |  |
| `gte` | `object \| number` | no |  |
| `lte` | `object \| number` | no |  |

### FilterInValue

Produced by `multiselect` filters — match any of `values`.

**FilterInValue** — Structured decode of a `multiselect` `FilterDef` value: match ANY of `values`.
| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `op` | `"in"` | yes |  |
| `values` | `array` | yes |  |

### Narrowing them

```ts excerpt
import { isFilterInValue, isFilterRangeValue } from "@flowpanel/kit";

for (const [field, value] of Object.entries(ctx.filters)) {
  if (isFilterRangeValue(value)) {
    // value.gte / value.lte
  } else if (isFilterInValue(value)) {
    // value.values
  } else {
    // a scalar
  }
}
```

## Raw SQL and counts

A widget query's [`WidgetContext`](/docs/reference/widgets#widgetcontext) — and a
detail tab's `DetailTabContext` — carries two reads the ORM cannot express
comfortably: `ctx.sql` and `ctx.count`.

### ctx.sql

`ctx.sql` runs exactly the statement you write. It applies no tenant scope, no
access rule and no soft-delete filter — add those predicates yourself, or use
`ctx.count` and resource reads, which do.

```ts excerpt
metric("New signups", async (ctx) => {
  const [row] = await ctx.sql<{ n: number }>`
    select count(*)::int as n
    from users
    where created_at >= ${ctx.dateRange.from}
  `;
  return row?.n ?? 0;
});
```

In a multi-tenant admin that means writing the predicate in the statement:
`where tenant_id = ${ctx.session?.tenantId}`. A resource read — `ctx.count`, a
table widget's `resource`, a related tab — is the scoped primitive; a raw
statement is not one.

`ctx.sql` is a tagged template. Every interpolated value is bound as a parameter,
so `${filter}` from a search box can never change the statement — there is no
string-concatenating form of this API, on purpose. A `Date` binds as an ISO-8601
string and a `bigint` as decimal text, so the same value works on Postgres, MySQL
and SQLite. An array is a single value to the driver, and each adapter binds one
its own way — see [the adapter reference](/docs/reference/adapters#raw-sql) before
writing an `IN` list.

Results come back as plain objects. Because a free-form statement carries no
column types to consult, every string shaped exactly like a zoneless
`YYYY-MM-DD HH:mm:ss` (with an optional `T` separator and fractional seconds) is
returned as a UTC `Date` — that is what the drivers returning them mean. A text
column of your own holding such a string is parsed too. Cast it in the query, or,
when `select *` gives you no place to cast, turn the pass off for the whole
adapter with `parseDates: false` — `drizzleAdapter({ db, schema, parseDates: false })`
or `prismaAdapter({ prisma, provider, parseDates: false })`. It defaults to `true`.

`ctx.sql` throws when the configured adapter implements no `sql` capability. The
error names the adapter kind; query through `ctx.db` instead.

### ctx.count

`ctx.count(resource, where)` counts rows of a registered resource through the
same role, access and tenant-scope checks as any other cross-resource read, so a
resource the current session may not read counts `0` rather than leaking a total.
`where` takes the filter shapes `ctx.filters` uses, and soft-deleted rows are
excluded unless `where` says otherwise. A filter on a field the role may not read
counts `0`, and so does an `undefined` filter value — a missing value narrows to
nothing rather than widening to every row.

```ts excerpt
stat("Open tickets", (ctx) => ctx.count("tickets", { status: "open" }));
```

## Request-scoped access

When you cannot thread a context through — a helper several calls deep, say —
read it from AsyncLocalStorage instead of reaching for a global.

```ts excerpt
import { getRequestContext, tryGetRequestContext } from "@flowpanel/kit";

const ctx = getRequestContext();      // throws outside a request
const maybe = tryGetRequestContext(); // null outside a request
```

`runWithRequestContext(ctx, fn)` establishes the scope; FlowPanel already wraps
every adapter call and every action `run` in it. This ambient state lives in
AsyncLocalStorage so concurrent requests do not share it.
