---
title: "Data tables"
url: "/data-tables"
description: "Structured storage your agents can read from and write to — fields, personal data handling, and per-agent permissions."
productArea: data-tables
audience: ["business-user", "admin"]
tags: ["data-tables"]
lastReviewed: 2026-08-25
---

# Data tables (/data-tables)





A data table is structured storage your agent can read from and write to during a conversation
— like a spreadsheet the agent can use. Each table has a name, a permanent slug, typed fields
(columns) and records (rows).

Common uses: leads, support tickets, orders, bookings, and anything else an agent needs to
capture or look up.

## Creating a table [#creating-a-table]

Select **Data Tables** in the sidebar, then create a table and add fields.
Then open the table's
**Permissions** tab and grant your agent access — an agent cannot see a table until you do.

## Field types [#field-types]

| Group                    | Types                                                                                        |
| ------------------------ | -------------------------------------------------------------------------------------------- |
| Basic                    | `text`, `long_text`, `number`, `currency`, `date`, `datetime`, `boolean`, `json`, `url`      |
| Selection                | `single_select`, `multi_select`, `status`                                                    |
| Identity / personal data | `email`, `phone`, `name`, `nric`, `address`, `whatsapp_id`, `telegram_id`, `telegram_handle` |
| System                   | `auto_increment`, `uuid`, `contact_id`                                                       |
| Structural               | `relation` (links to another table), `asset_id` (references an uploaded file)                |

<Callout type="warn">
  A field's **slug** and **type** are permanent once created — they are the key its values are
  stored under. The table's display name, description, icon and colour can change at any time; the
  table's slug cannot.
</Callout>

## Personal data and encryption [#personal-data-and-encryption]

The identity field types above are treated as personal data and are automatically redacted from
audit logs, whether or not the field is encrypted.

A subset additionally supports **field-level encryption**: `text`, `long_text`, `email`,
`phone`, `url`, `name`, `nric`, `address`, `whatsapp_id`. Note that `telegram_id` and
`telegram_handle` are personal data but cannot be encrypted.

When a field is created with encryption on:

* Values are encrypted at rest and never returned in plaintext by default. Dashboard users and
  agents both see the masked form (`j***@x.com`, `+65***4567`); the real value comes back only
  through an explicit, audited reveal.
* `text`, `long_text` and `url` have no masked form, so an agent does not see those fields at
  all. It can still write to them.
* **The choice is permanent.** Encryption cannot be switched on or off after the field exists.

### Finding an encrypted field later [#finding-an-encrypted-field-later]

An encrypted field is still findable, but only in the ways you allow. The field editor asks
**Finding this field later** and offers three answers:

| Choice                 | What a search can find                                           |
| ---------------------- | ---------------------------------------------------------------- |
| **Can't be searched**  | Nothing. You can still test whether a record has a value at all  |
| **Exact matches only** | The complete value — a whole email address, a whole phone number |
| **Match any word**     | The complete value, or any single word of it                     |

Each field type arrives on the right answer, so you can usually leave this alone:

* `name` → **Match any word**, so searching "Sufiyan" finds "Mohammad Sufiyan Rahmat"
* `email`, `phone`, `whatsapp_id`, `nric`, `text` → **Exact matches only**
* `long_text`, `url`, `address` → **Can't be searched**

**Match any word** is offered on `name` and `text` only — the types whose values are made of
words. It matches whole words, not fragments: "sufi" does not find "sufiyan", and there is no
fuzzy matching, so "Sufyan" does not find "Sufiyan" either.

Exact matching ignores capitalisation and formatting, so `User@Example.com` finds the record
stored as `user@example.com`, and `+65 9123 4567` finds `+6591234567`.

Two things are never available on an encrypted field, whatever you choose: **sorting** by it, and
**range filters** on it (before/after, greater/less than). Like encryption itself, the choice is
permanent.

## Permissions [#permissions]

Access is granted per agent, per table, as four gates: **read**, **create**, **update**,
**delete**. Finer control — which fields an agent can see, whether it can search all rows or
only its own, export limits — is set by an admin on the table's **Permissions** tab. It cannot
be changed from a conversation.

Granting a gate is all an agent needs to use it. A read grant lets it find and open records, a
create grant lets it add them, and so on — you do not have to give the agent a data
[skill](/skills-and-tools) as well. Add a skill when you want to shape *how* it uses the table;
add it for schema changes, exports and bulk imports, which are never granted this way.

An agent with any read access can also inspect the table's structure — field names, types, which
fields are required — so it knows what a valid record looks like before it tries to save one. This
never exposes the contents of a record, and the **Allow schema inspection** checkbox lets you turn
it off per table.

A field an agent cannot read is also a field it cannot search. Hiding a field therefore hides it
completely: the agent cannot use a search to confirm what a record holds in a column it is not
allowed to see.

### Access modes [#access-modes]

Read, write and delete each have an access mode:

| Mode                        | Who it is for                                                                                                 |
| --------------------------- | ------------------------------------------------------------------------------------------------------------- |
| **Self** (own records only) | Customer-facing agents — each end user only sees rows belonging to them. Requires at least one row scope rule |
| **Scoped**                  | Internal agents limited to an admin-defined slice, e.g. one branch                                            |
| **Table**                   | Broad row access for trusted internal agents, still subject to field limits and per-query caps                |
| **Analytics** (read only)   | Aggregate and count queries only; individual rows are never returned                                          |

### Row scope rules [#row-scope-rules]

A scope rule ties rows to the person currently chatting by comparing a field against the live
identity of the conversation — contact ID, WhatsApp ID, Telegram ID or handle, user ID.

* `contact_id` equals the current **contact ID** — matches rows created for this contact.
* `whatsapp_number` equals the current **WhatsApp ID** — matches imported rows keyed by the
  customer's number, including rows that existed before they ever chatted.

Scope rules are enforced on the server and **fail closed**: if the required identity is not
available in that conversation, access is denied rather than opened up.

<Callout type="info">
  In dashboard test chat the identity is the logged-in operator's own contact, so a WhatsApp-keyed
  rule only resolves there if that contact has a linked WhatsApp number.
</Callout>

### Scoping through a related table [#scoping-through-a-related-table]

When a child table links to a parent via a `relation` field — for example
`policies.client_id → clients` — the child can be scoped *through* the parent, so you do not
have to copy the customer's identity onto every child row.

Read the rule as: &#x2A;allow a child row when its relation points at a parent row owned by the
person currently chatting.* A customer asking "show my policies" has their client record found
by WhatsApp number, and only policies linked to that client are returned.

Constraints:

* The relation must be a **single-value** relation. Multi-value relations cannot be used for
  scoping, and the dashboard rejects invalid configurations when you save.
* When the person matches exactly one parent record, the relation is **stamped automatically**
  on create — the agent cannot point a row at someone else's parent record.
* Matching no parent record returns zero rows, not an error.
* Matching several parent records means the agent must specify which one when creating.

### Revealing an encrypted value [#revealing-an-encrypted-value]

A mask is enough to recognise a record and talk about it, but not enough to phone anyone. When
an agent genuinely needs the real value — to call a lead, email a quote, or confirm two records
are the same person — it can reveal it, and both of these must be true:

1. The agent has the **Reveal Encrypted Field** tool, or the **Data Tables: Reveal PII**
   [skill](/reference/skills-catalog) that carries it.
2. The table's access settings have **Allow reveal of encrypted fields** ticked for that agent.

The second switch is off on every table and every access preset until you tick it. With one of
the two missing, the agent tells the person the value is available in the dashboard and stops.

A reveal returns the real values for **one record** at a time, and every field it returns is
written to the table's audit log with the agent that asked. Fields you have read-blocked stay
blocked here — asking for the real value is not a way around a restriction you already set.

Revealing is for **showing** a value, not for moving it. The agent can read it out to you, but
it cannot pass it on: not into a second record, not into a message it sends, not into a
connected app. Asking it to do any of those gets a refusal rather than a silent failure, so if
a value needs to end up somewhere else, put it there yourself. The agent does not receive the
value either — it writes its reply around a placeholder that KlicForge fills in afterwards, so
what reaches the model provider is the placeholder. See
[AI providers](/organization/security-and-privacy#ai-providers).

<Callout type="warn">
  Do not give this to a public widget agent, a customer-facing WhatsApp or Telegram agent, or any
  agent on a Self-mode table. Masking is what lets those agents be useful without holding real
  personal data. It is for internal operator agents — sales follow-up, support triage, back-office
  lookups.
</Callout>

## Adding records [#adding-records]

Records can be created by an agent during a conversation, added manually in the dashboard, or
brought in from a file — an agent can preview and import records from an attached spreadsheet.

### When a write is rejected [#when-a-write-is-rejected]

A record that fails validation is reported back to the agent field by field: which field, why, and
for a choice field, the values it accepts. An agent can then ask the contact one specific question
rather than guessing. If it keeps failing the same write, it stops after three attempts and says
what it could not do instead of retrying until it runs out of steps.

Two things make rejections less likely in the first place: required fields and formats are visible
to the agent before it writes, and fields the agent is not permitted to see are never named in an
error. How much of the field list an error names depends on **Allow schema inspection**: with it
on, a rejection lists every valid field slug; with it off, the agent is told to ask for the
structure instead. This applies to single-record writes and to importing many records at once —
a batch import now validates every record before writing, the same as a single one, instead of
accepting a malformed record and only failing later.

One rejection cannot be recovered from in the conversation: a choice field with no options set
accepts no value at all. The agent is told the field needs options rather than being handed a value
to guess at, and it will not create a choice field that way itself — add the options on the table's
Fields tab. See [field types](/reference/data-field-types).

## Protecting personal data from the agent [#protecting-personal-data-from-the-agent]

Blocking a field prevents both reading and writing it. If you want an agent to *capture*
personal data but never read it back — the right default for a public-facing agent — restrict
it on read only. Fields like contact name, email, phone and messaging IDs should be read-blocked
on any agent the public can talk to.

A read-blocked field cannot be revealed either, even on a table where reveal is allowed.

## Related pages [#related-pages]

* [Tools](/skills-and-tools/tools)
* [Contacts](/contacts)
* [Guardrails](/guardrails)
* [Security and privacy](/organization/security-and-privacy)
