DEV Community

Cover image for Inside AdaptTable’s AI layer: discovery, context, and capabilities
Orwa Mahmoud
Orwa Mahmoud

Posted on

Inside AdaptTable’s AI layer: discovery, context, and capabilities

In Part 3, I introduced the optional AI layer in AdaptTable v3. This article goes deeper into how to connect it, how an agent discovers the table’s capabilities, how context is selected, and how an application can extend that capability set without building a separate AI tool layer for the table.

For developers building an agentic CRM, dashboard, admin panel, or SaaS application, the starting point is the normal table configuration. Connect the optional AI layer and supply your model or agent connection. AdaptTable supplies the supported table capabilities and their execution path, so you do not have to implement each table action again as a separate agent tool.

One configuration for the React data table and the agent

Consider an orders table. Its columns define order numbers, statuses, customers, and amounts. Its features enable filtering, sorting, grouping, and editing. Its callbacks connect changes to your application.

A person can operate that table through its controls. An agent can operate it through the capabilities exposed by its AI session.

For example, a connected agent can translate:

Show pending orders, group them by customer, and sort by amount.

into the supported filtering, grouping, and sorting actions on that table. Those actions reuse the configured table behavior.

A read-only report and an editable admin screen therefore expose different capabilities, even when they use the same library.

If editing is disabled, there are no editing capabilities or instructions taking up context. The same goes for other disabled features. You spend context on the capabilities enabled on that table, not everything AdaptTable supports.

AdaptTable’s AI capabilities include search, filtering, sorting, pagination, grouping, aggregation configuration, selection, column management, pinning, saved views, exports, and configured write operations. Existing row and bulk actions can also contribute capabilities. See the AI capabilities guide for details.

The table configuration is the shared starting point for the interface and the agent.

The live session is the connection point

The architecture separates the configured AdaptTable instance, the live agent session, and the integration used to connect your model or agent:

Your model or agent
        ↕
Choose an integration path
├── AdaptTable HTTP protocol
│   └── JSON response or streamed SSE
├── AI SDK adapter
│   └── AI SDK tool/message stream format
├── AG-UI adapter
│   └── AG-UI events, state, and tool calls
└── Custom agent integration
    └── Your agent maps its actions to the AgentSession
        ↕
Live AgentSession
├── catalog()   → discover available capabilities
├── describe()  → load detailed guidance when needed
└── execute()   → run AdaptTable actions
        ↕
Your configured AdaptTable
├── Features and columns
├── Current state and data source
└── Application callbacks
Enter fullscreen mode Exit fullscreen mode

AdaptTable supports several ways to connect a model or agent to the same live AgentSession: the AdaptTable HTTP protocol, the AI SDK adapter, the AG-UI adapter, or a custom integration that maps your own agent to the session.

Each option changes how the agent connection is wired, while capability discovery and execution still go through the same AgentSession.

The connections are bidirectional: table context goes to the agent, proposed actions come back, and execution results can be returned when the workflow needs them.

@adapttable/ai supplies the provider-neutral session and executor. @adapttable/ai-react connects them to the live React table.

Where the session comes from

In React, tableAgent(...) is added to the table’s features prop. Its bridge.attach callback supplies the live session.

For an existing React AdaptTable instance, first receive that session in component state:

import { useState } from "react";
import type { AgentSession } from "@adapttable/ai";

// Inside your existing table component:
const [session, setSession] = useState<AgentSession | null>(null);
Enter fullscreen mode Exit fullscreen mode

Then add the AI feature alongside the table features you already use. This example keeps an existing filtering feature:

import { tableAgent } from "@adapttable/ai-react";

// In your existing component's JSX:
<DataTable
  data={rows}
  columns={columns}
  rowKey="id"
  features={[
    filters([]),
    tableAgent({
      tableId: "orders",
      bridge: { attach: setSession },
    }),
  ]}
/>
Enter fullscreen mode Exit fullscreen mode

Here, DataTable, filters, rows, and columns belong to the existing table setup. Keep its other feature entries too.

Adding tableAgent(...) to this table attaches the integration. tableId: "orders" assigns a stable identity used in the AI contract; it does not look up an HTML element. You assign that identity here rather than defining a matching DOM ID elsewhere.

The session received by setSession is what the assistant or custom agent integration uses. Your existing data, columns, and callbacks remain in place. AI access settings can further restrict what the session exposes, as we’ll see below.

The AI setup guide contains the complete assistant wiring. The important architectural boundary is that the agent connects to a live table session.

Capability discovery and progressive disclosure

The session separates discovering an operation, understanding it, and executing it.

Session method What it provides
catalog() Available capability keys and short summaries.
describe(key) Detailed instructions, accepted arguments, and result schema for a capability.
execute(...) Execution through the table integration, with a structured result.
Available capability catalog
├── view.setPage     → Change the page
├── view.setFilters  → Apply filters
└── edit.cells       → Edit cell values
              │
User: "Mark order ORD-1042 as reviewed"
              │
              ▼
Is the edit.cells guide in context?
              │
        ┌─────┴─────┐
       yes          no
        │            │
        │            ▼
        │     describe("edit.cells")
        │     (arguments, row references,
        │      write guidance)
        │            │
        └─────┬──────┘
              ▼
Agent constructs the edit action
Enter fullscreen mode Exit fullscreen mode

catalog() is derived from the live AdaptTable instance. Its enabled features, columns, source capabilities, configured actions and callbacks, and AI access settings determine which capabilities appear. If that configuration changes, the catalog reflects what the table can currently support and permit.

The distinction between catalog() and describe(...) becomes useful when an AdaptTable instance exposes many capabilities.

catalog() gives the agent every capability currently available on that AdaptTable instance, with its key and a short summary. The agent therefore knows what it can do without needing the full instructions for every operation in its context.

The catalog does not need to carry every capability’s full instructions. Each available capability remains visible by key and short summary, while its detailed guide and input schema can be supplied separately.

If the user asks for an operation whose detailed guidance is not already in the model’s context, the agent already knows that capability exists and knows its key from the catalog. It can request that key’s guidance, and the integration retrieves it through describe(...).

This separation keeps capability discovery lightweight without hiding available operations from the agent.

The same general pattern appears in Agent Skills, where a skill’s name and description support discovery and its full instructions are loaded when relevant. AdaptTable applies that pattern to capability guides and schemas; the analogy does not mean table capabilities are SKILL.md files.

Custom capabilities use the same session

The catalog is not limited to AdaptTable’s built-in operations. A developer can expose application-specific logic to the agent by registering a custom capability with its own:

  • key — the stable capability identifier, such as orders.archive.
  • summary — the short description that appears in catalog().
  • guide — the detailed instructions the agent receives through describe(...).
  • input schema — the arguments the agent is allowed to send.
  • output schema — the structured result the capability returns.
  • isEnabled rule — whether the capability is available for the current table state and policy.
  • execute handler — the application code that actually performs the operation.

For example, an orders application can expose an orders.archive operation:

import type { AgentCapabilityDefinition } from "@adapttable/ai";

const archiveInput = {
  type: "object",
  properties: {
    rowKey: { type: "string" },
  },
  required: ["rowKey"],
  additionalProperties: false,
} as const;

const archiveOutput = {
  type: "object",
  properties: {
    archived: { type: "boolean" },
  },
  required: ["archived"],
  additionalProperties: false,
} as const;

const archive: AgentCapabilityDefinition = {
  key: "orders.archive",
  summary: "Archive an order.",
  kind: "write",
  guide: {
    guide: "Archive an inactive order by its stable row key.",
    input: archiveInput,
    output: archiveOutput,
  },
  isEnabled: (observation) =>
    observation.writePolicy === "allow",
  execute: async (_context, args) => {
    await archiveOrder((args as { rowKey: string }).rowKey);
    return { archived: true };
  },
};
Enter fullscreen mode Exit fullscreen mode

Register it on the same tableAgent(...):

tableAgent({
  tableId: "orders",
  capabilities: [archive],
  commit: "immediate",
  approval: "writes",
  bridge: { attach: setSession },
});
Enter fullscreen mode Exit fullscreen mode

This archive handler performs the operation directly and does not support staging. Here, commit: "immediate" applies to all AI writes on this table, while approval: "writes" keeps human approval enabled. Approved writes execute without waiting for a separate Save step.

When enabled, orders.archive appears in catalog() like any other capability. describe("orders.archive") exposes its detailed guide and schemas, and execution still goes through the live session.

Because this capability is declared as a write, it also enters the same governance path as built-in write capabilities: write policy, commit compatibility, approval when required, revision and permission checks, and only then the application handler.

Custom capabilities therefore extend what the agent can do without creating a second execution architecture beside the table.

Full and compact context

AdaptTable offers two profiles for deciding how much capability guidance is sent to the model up front.

full is the default. It has no soft token budget unless the application supplies one. Enabled capability guides and input schemas, together with the available column descriptions, are therefore included up front rather than intentionally deferred for compactness.

compact uses a soft token budget. The default is 1,000 estimated tokens, and the application can override it with tokenBudget. The budget is a selection target, not permission to cut a schema or guide in half.

How the context builder selects what travels up front

Both profiles use the same context builder. What changes is the budget and therefore how much detail is selected before discovery is needed.

The builder keeps the permitted contract intact while deciding how much descriptive detail travels with it.

Available capability guide
            │
            ▼
Required up front?
            │
      ┌─────┴─────┐
     yes          no
      │            │
      │            ▼
      │      Selection order
      │      ├── priority
      │      └── remaining capabilities
      │            │
      │            ▼
      │      Fits soft token budget?
      │        ┌───┴───┐
      │       yes      no
      │        │        │
      │        │        ▼
      │        │   Defer guide + input schema
      │        │   reason: budget
      │        │
      └────┬───┘
           ▼
Would attaching guide + input schema
exceed the 128,000-byte hard limit?
           │
      ┌────┴────┐
     no        yes
      │          │
      ▼          ▼
Attach guide   Defer guide + input schema
+ input schema reason: hard-limit

In every case:
capability key + summary remain discoverable
Enter fullscreen mode Exit fullscreen mode

The selection rules are explicit:

  1. include marks a capability guide as required up front. It may take the context over the soft token target; only the hard capability-context size limit can still defer it.

  2. Compact mode also treats its built-in common operations as required up front: filtering, sorting, search, pagination, row reads, grouping, and aggregation configuration.

  3. Guides loaded through discovery are remembered for the same connection and contract version. If the context is built again under that same contract, those previously requested guides are considered first and treated as required against the soft budget, avoiding another discovery round for the same instructions.

  4. priority is the application’s ordering preference for the remaining guides. It moves those guides earlier in selection, but does not guarantee that they will fit.

  5. After that, the remaining capabilities are considered in deterministic registry order.

  6. Optional guides are included only when their complete guide and input schema fit within the remaining soft token budget. If they do not fit, the guide is deferred while the capability key and short summary remain available for discovery.

The selector handles required guides before optional ones:

Required guides
├── previously requested through discovery
├── built-in common compact operations
└── guides named in `include`
        │
        ▼
Optional guides
├── application `priority`
└── remaining capabilities in deterministic registry order
        │
        ▼
Fit optional guides within
the remaining soft token budget
Enter fullscreen mode Exit fullscreen mode

include and priority therefore serve different purposes: include makes a guide required against the soft budget, while priority only changes how early an optional guide is considered.

The soft budget is token-based. Without a tokenizer supplied by the application, AdaptTable estimates tokens; an application can provide its own estimateTokens implementation when it wants model-specific counting.

Capability guide attachment also has a separate hard ceiling of 128,000 UTF-8 bytes. A guide is atomic: if attaching its guide and input schema would cross that ceiling, the guide is deferred with a hard-limit reason rather than truncated, even if it would otherwise be required up front.

This means the application can force important guides up front with include, influence which optional guides are most likely to fit with priority, and control the soft target with tokenBudget

For example:

import { assistantHttpTransport } from "@adapttable/ai/http";

const transport = assistantHttpTransport({
  endpoint: "/api/agent",
  context: {
    profile: "compact",
    tokenBudget: 1600,
    include: ["edit.cells"],
    priority: ["export.run"],
  },
});
Enter fullscreen mode Exit fullscreen mode

Here, edit.cells is required up front, while export.run is only considered earlier than the normal remaining capabilities.

The HTTP discovery path can retrieve multiple missing guides together and cache them for the connection and contract version. It does not require a separate discovery exchange for every capability.

Deferred guidance does not mean a disabled capability. The operation remains available; only its detailed instructions were left out of the initial context.

The trade-off is between more initial context and additional discovery exchanges. The context-selection implementation exposes the budget and selection information so the application can log it, feed it into observability, debug selection behavior, and tune the context strategy.

Neither profile grants additional permissions. Context selection decides how much to explain, not which actions the agent is allowed to perform.

What the agent knows about the table

There are three different kinds of information involved: the table’s contract, its current view, and its row data.

Agent context
├── Contract
│   ├── Available capabilities
│   ├── Column metadata
│   ├── Access settings and limits
│   └── Write / approval / commit policies
│
├── Current view
│   ├── Page and page size
│   ├── Search and sorting
│   ├── Grouping
│   └── Filter state
│
└── Row data
    └── Read only when the task needs it,
        through bounded rows.read
Enter fullscreen mode Exit fullscreen mode

The contract: what this table supports

The contract describes available capabilities, column metadata, access settings, limits, and execution policies.

For the orders table, it can explain which columns are writable, which filters exist, and which operations are supported. Column descriptions can also communicate business meaning, units, or storage conventions that are not obvious from the column type alone.

amount: {
  type: "number",
  ai: {
    description: "Order total stored in cents.",
  },
}
Enter fullscreen mode Exit fullscreen mode

The model can then interpret 12500 as $125.00 instead of assuming the stored value is already in dollars.

Column descriptions and examples can be deferred to save context without removing the column itself from the contract.

The contract changes when something structural changes: for example, the available capabilities, relevant column metadata, or execution policy.

The view: where the table is now

The view describes the current page, sorting, filters, grouping, and other relevant state.

A user changing the page does not necessarily change the capability contract. Keeping these separate lets an integration update the current view without treating every interaction as a completely different set of tools.

This keeps the agent grounded in the same current view the user is working with: page, search, sorting, grouping, and filters are sent from the live table state. If that view changes while the agent is reasoning, the view revision lets execution detect that the plan is stale instead of applying it against a different table state.

That distinction also matters for HTTP delivery: a relatively stable contract can be reused while the current view still travels fresh with each turn.

Row data: what the task actually needs to read

Publishing the capability catalog does not automatically send the whole dataset to the model.

When the task needs cell values, rows.read provides bounded reads under the table’s readable-column and source-scope rules. An application can also explicitly opt columns into limited example sampling; that is a separate data read, not an automatic dump of table rows.

This matters with server-side pagination. Reading one loaded page does not mean the agent has inspected every matching record in the database. Broader reads depend on what the configured data source supports.

The agent context documentation describes the contract/view separation, column guidance, and optional sampling.

AI access can differ from the table interface

An operation may be useful to a person using the table but inappropriate for the agent on that screen.

For example, the orders table might retain its export and delete controls while excluding those capabilities from AI. It might also display internal notes to the user without exposing their cell values through the AI session.

These options go in the same tableAgent(...) configuration:

// Access-setting excerpt from the existing tableAgent entry:
tableAgent({
  tableId: "orders",
  bridge: { attach: setSession },
  excludeCapabilities: ["export.run", "rows.delete"],
  columns: {
    internalNotes: {
      readable: false,
      writable: false,
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

excludeCapabilities removes those keys from the agent’s catalog and execution path. The table’s normal controls are unaffected.

The column settings prevent the AI session from reading or editing internalNotes cell values.

For an individual row or bulk action, ai: false on its definition excludes that action from AI while preserving its normal table behavior.

For filters, ai: false excludes that specific filter from AI entirely, so the agent does not know about or use it.

const rowActions = [
  {
    key: "refund",
    label: "Refund",
    ai: false,
    onClick: refundOrder,
  },
];

const filters = [
  {
    key: "internalFlag",
    label: "Internal flag",
    ai: false,
  },
];
Enter fullscreen mode Exit fullscreen mode

Here, refund is still available to the user but not to AI, and internalFlag is not exposed to the agent at all.

To remove filtering entirely instead of excluding individual filters, use:

tableAgent({
  tableId: "orders",
  excludeCapabilities: ["view.setFilters"],
});
Enter fullscreen mode Exit fullscreen mode

These access controls are separate from approval and saving. Availability answers whether the agent can use an operation. Approval answers whether a person must confirm it. Commit behavior determines what happens after it is accepted.

Where this leaves the architecture

The result is one configured AdaptTable instance that can expose a live AI contract without duplicating the table into a second set of hand-written tools.

The session tells an agent what the table can do. The catalog keeps those capabilities discoverable. Detailed guidance can be supplied progressively. The context builder makes the upfront selection explicit. And applications can add their own governed capabilities when the built-in operations are not enough.

Try the live AI demo or explore the source on GitHub.

The next article follows the other half of the path: what happens after the agent decides to act — revisions, idempotency, approvals, staged and immediate writes, execution receipts, HTTP contract pinning, streaming, and existing agent stacks.

Next in Building AdaptTable: From AI intent to safe table execution in AdaptTable.

Top comments (0)