Elizon Docs
Features & SpecsPortable definitions

Data Tables

The portable definition of a Data Table — columns, never rows — and every way a Package can use one.

This page is the authoring reference. What a Data Table is, and what an installing operator sees, is Data Tables and Data Tables in Packages.

A Package carries a Data Table's shape — its name and its ordered typed columns — and never a row. Every install provisions the table empty, in the project the Package is installed into, and the Workflows and Agents the Package ships are what fill it.

There is no field anywhere in a table definition that can hold a cell, so a Package cannot carry customer records by accident. A rows: key is refused rather than ignored.

tables/request-log.yaml
displayName: Request log
columns:
  - name: Request
    type: string
  - name: Answer
    type: string
onUninstall: keep
FieldMeaning
displayNameThe table's name in the installing project. Unique there, compared without regard to case.
columnsOrdered. The array order is the column order. May be empty if a Workflow adds the columns at run time.
columns[].nameThe key a row's values are written under. Not id, createdAt or updatedAt — those are system fields.
columns[].typeOne of boolean, date, number, string.
onUninstallkeep (the default) or delete. See Uninstall below.

No column ids: a stored column's id belongs to the project that created it, and the installing project mints its own.

What can use a table

Four things in a Package can use a table it declares. Each one names the table the same way, by its manifest-local name dataTables.<name>, never by an id — and validate and build refuse a name the manifest's dataTables: does not declare.

ConsumerWhere the reference goesWhat it can do
A Workflow's Data Table nodedata.tableEvery table and row operation. See Data Table node.
An Agent's tool bindingtools[].tableRead and look up rows; add and change them only where a write is bound. Never delete. See Agent tool bindings.
The Agent's skill scriptstools[].config.expose_to_scriptsRead the whole table as a file. See Skill scripts.
A Workflow's Code nodedata.tableInputs[].tableRead matching rows as a file. See Code node.

The install creates every table before the Agents and Workflows that name it, and rewrites each reference into the id of the table it just created.

Data Table node

A Data Table node points at the table by manifest-local name, never by id:

workflows/handle-request.yaml
- id: record
  type: data_table
  position: { x: 560, y: 0 }
  data:
    resource: row
    operation: insert
    table: dataTables.request-log
    tableName: Request log
    mapping: manual
    mappingRows:
      - id: r1
        column: Request
        value: '{{ trigger.body.request }}'
        valueMode: expression

validate resolves that reference against what the manifest declares and reports an undeclared one as UNRESOLVED_LOCAL_REFERENCE; build refuses a malformed table definition before writing a .pkg. Neither check waits for an install. tableName is the canvas label and is refreshed from the installed table's own name — which may differ, because the installer can rename objects at install.

Every field of the node's data travels except tableId, which becomes table. The node offers these operations (the node reference has every parameter):

resourceoperation
tablecreate, list, rename, delete, addColumn, removeColumn
rowinsert, get, update, delete, upsert, bulkUpsert, exists

A create names the table it makes in tableName and carries table: null. That table is made at run time, not by the install: it is not one of the Package's declared tables, so onUninstall does not apply to it.

The key rule

A node that matches rows needs a key — a condition that names a column:

  • upsert always needs at least one. Its conditions are the key it matches on, and applyToAllRows does not excuse it.
  • update and delete need one, or applyToAllRows: true to say every row is meant.
  • bulkUpsert matches on keyColumns (at least one column name) instead of conditions.
  • insert, get and exists need none.

An upsert, update or delete that breaks the rule does not stop validate or build: they report it as the warning DATA_TABLE_NODE_KEY_MISSING, naming the node. The installed Workflow cannot be published or run until the node has its key, so fix it before you build. A bulkUpsert is not checked there: an empty keyColumns passes validate, build and publish, and is refused only when the node runs.

workflows/handle-request.yaml
- id: remember
  type: data_table
  position: { x: 560, y: 0 }
  data:
    resource: row
    operation: upsert
    table: dataTables.request-log
    tableName: Request log
    mapping: manual
    mappingRows:
      - id: r1
        column: Answer
        value: '{{ upstream.respond.output.text }}'
        valueMode: expression
    # The key: the row whose Request equals this one is updated, or one is added.
    conditions:
      - id: k1
        column: Request
        operator: eq
        value: '{{ trigger.body.request }}'
        valueMode: expression
    conditionMatch: all
workflows/refresh-prices.yaml
- id: load
  type: data_table
  position: { x: 560, y: 0 }
  data:
    resource: row
    operation: bulkUpsert
    table: dataTables.price-list
    tableName: Price list
    keyColumns: [Sku]
    rowsSourceMode: file
    rowsFileExpression: '{{ upstream.convert.output.files.0 }}'

Conditions and operators

Conditions take the operators eq, neq, gt, gte, lt, lte, isEmpty, isNotEmpty, contains and startsWith. contains and startsWith apply to string columns only, ignore case and treat the value as plain text. The full per-type matrix is on the node reference.

workflows/find-requests.yaml
- id: find
  type: data_table
  position: { x: 280, y: 0 }
  data:
    resource: row
    operation: get
    table: dataTables.request-log
    tableName: Request log
    conditions:
      - id: c1
        column: Request
        operator: contains
        value: invoice
        valueMode: fixed
    conditionMatch: all
    limit: 50

Agent tool bindings

An Agent reaches a table through a tool binding in its tools: list. Each binding is one table and one action: bind the same table once per action the Agent should have.

agents/registrar.yaml
tools:
  # The reads.
  - { table: dataTables.request-log, actionId: rows.get, enabled: true, config: {} }
  - { table: dataTables.request-log, actionId: rows.exists, enabled: true, config: {} }
  - { table: dataTables.request-log, actionId: rows.lookup, enabled: true, config: {} }
  # The writes: each one is a separate decision.
  - { table: dataTables.request-log, actionId: rows.insert, enabled: true, config: {} }
  - { table: dataTables.request-log, actionId: rows.update, enabled: true, config: {} }
  - table: dataTables.request-log
    actionId: rows.upsert
    enabled: false
    config:
      description_override: Records the answer to a request, adding the row if it is new.
actionIdWhat the Agent can doBound per call
rows.getRead rows with conditions, ordering and paging.At most 50 rows returned
rows.existsAsk whether any row matches, and how many.—
rows.lookupGive one key column and a list of values; get the matching rows per value.50 values, 5 rows per value
rows.insertAdd new rows. Writes data.10 rows
rows.updateChange the rows that match conditions. Writes data.50 rows touched
rows.upsertChange the rows that match, or add one when none do. Writes data.50 rows touched

There is no delete action. A definition that names one — or anything else outside these six — is refused by build, refused again at install, and refused a third time when the Agent calls it. To delete rows, use a Workflow with a Data Table node.

A write happens only through a binding for exactly that table and that action; a read binding never lets the Agent write. enabled: false travels as written, so the installed Agent has the binding switched off until the operator turns it on.

config takes three keys, all optional: description_override (replaces the tool's description; the sentence that says a write changes data cannot be removed), param_overrides (pins a parameter the model would otherwise choose) and expose_to_scripts (below). What the model sees, and how each action answers, is on Letting an Agent read and write a table.

Skill scripts

expose_to_scripts: true on a table's binding also makes the table available to the Agent's skill scripts: the rows are mounted in the script's workspace as data/<table-slug>.jsonl, one JSON row per line. Only an enabled binding with expose_to_scripts: true mounts the file: a binding that travels as enabled: false gives the scripts nothing until the operator turns it on.

The slug comes from the installed table's name, not from the manifest name: the name in lower case with every run of other characters turned into - ("Price list" is data/price-list.jsonl), and a second table with the same slug gets -2. The installer may rename the table, so a script should find its file in data/ rather than hard-code the slug.

agents/registrar.yaml
tools:
  - table: dataTables.price-list
    actionId: rows.get
    enabled: true
    config:
      expose_to_scripts: true

A script reads the whole table, where the model sees at most a page — which is why the switch is off unless you set it. The file is a read-only snapshot, taken the first time a script runs in a turn; a script cannot write the table. A table larger than the Code node's input budget (50 MB by default) fails the script run and names the table. The details are in Letting skill scripts read a table.

Code node

A Code node reads tables as files through tableInputs. Each entry names the table, the file stem it is mounted under and, optionally, which rows to read:

workflows/count-requests.yaml
- id: count
  type: code
  position: { x: 840, y: 0 }
  data:
    language: python
    code: |
      import json

      def main(input, files):
          table = next(f for f in files if f["name"].endswith("-log.jsonl"))
          with open(table["path"]) as fh:
              rows = [json.loads(line) for line in fh if line.strip()]
          return {"rows": len(rows)}
    inputExpression: ''
    fileExpressions: []
    tableInputs:
      - name: log
        table: dataTables.request-log
        where:
          match: all
          conditions:
            - { column: Answer, operator: isNotEmpty }
    timeoutSeconds: 30
FieldMeaning
nameThe file stem: letters, digits, - and _, up to 64 characters. The file is /workspace/in/<n>-<name>.jsonl, where <n> is the file's position among all input files, counting from 1.
tabledataTables.<name>. The install replaces it with the id of the table it created.
whereOptional. match (all or any) and conditions, with the node's operators. Values are literal, not expressions.

A node takes at most 5 table inputs. The rows count against the node's input-file budget (50 MB by default); a table that does not fit fails the node and names it. The Code node cannot write the table: write the rows to a file and follow it with a Data Table node set to bulkUpsert from that file. See Reading a Data Table.

Limits

A table the Package declares is an ordinary table in the installing project, and every platform limit applies to it. The install creates it through the same checks Studio uses, so an install into a project already holding the maximum number of tables is refused, and rolls back.

Each limit is a deployment setting; these are the defaults.

LimitDefaultSetting
Tables per project50DATA_TABLES_MAX_TABLES_PER_PROJECT
Columns per table50DATA_TABLES_MAX_COLUMNS_PER_TABLE
Rows per table100,000DATA_TABLES_MAX_ROWS_PER_TABLE
Row size64 KBDATA_TABLES_MAX_ROW_BYTES
Rows per read1,000DATA_TABLES_MAX_ROWS_PER_READ
Rows per write20,000DATA_TABLES_MAX_ROWS_PER_WRITE
Source file size50 MBDATA_TABLES_MAX_SOURCE_FILE_BYTES

Rows per read is a ceiling, not a refusal: a read that asks for more is clamped. Rows per write bounds one insert or bulkUpsert, and Source file size the file it reads rows from. An Agent's calls have their own, smaller bounds (see Agent tool bindings). Design a register that may outgrow 100,000 rows to page, archive or summarise rather than to hold everything.

Compliance

A table the Package ships is a place the operator's records accumulate, so it counts toward the Package's own declaration:

  • what it holds belongs in compliance.dataCategories — a register of counterparties and dates is personal data whatever the Workflow writing it is called,
  • how long it holds it belongs in compliance.retention — a table kept on uninstall is retained until the operator deletes it.

See compliance.

Install

Data Tables live in a project, so a Package that declares one cannot be installed at the Organization level. npx @elizon/cli install says so before its first prompt rather than after its last one; pass --project=<project-id>, or choose a project in the install wizard.

A name already taken in the target project is refused as PORTABLE_IDENTIFIER_COLLISION. Rename the table at install, or reuse the project's existing table instead of creating a second one.

An install that fails part way removes the tables it created — they are empty by construction, so there is nothing to lose.

Uninstall

onUninstall says what should happen to the table, and therefore to the operator's rows, when the Package is uninstalled:

  • keep (the default) — the table is not offered for removal. It stays as an ordinary Data Table the operator owns; they can delete it themselves in Studio whenever they like. This is the default because by then what is in it is their data rather than your structure. A request to remove it anyway is ignored.
  • delete — the table is offered for removal alongside the Package's other objects. It is still only removed if the operator explicitly asks for it: the policy decides whether removal is on the table at all, never whether it happens unasked. Removing it takes its rows with it. The one exception is a table that is the Package's own entryPoint — uninstalling a Package always takes its entry point down, unasked, so a register you want the operator to keep must declare keep rather than rely on the prompt.

Hiding an installation removes nothing, so no policy applies to it.

Update

A newer version of the Package may add columns to a table it already installed; the table and its rows survive and the columns are added in place. A table is never replaced by an update, whatever its definition now says.

Removing a column, renaming one, or changing one's type is refused in v1, because each of those destroys or silently reinterprets rows the operator already has. A rename cannot be told apart from a removal — the artifact carries no column ids — so both are reported the same way. The whole update is refused before anything is written; bump the version with the column restored, or ship the change as a new Package.

The comparison is against the table as it stands, not against your previous version. A column the operator added in Studio, or one a Workflow added with the Data Table node's addColumn, is one the installed table has, so a new version that does not declare it is refused as a removal. While the Package is installed the operator cannot rename the table's columns in Studio either, for the same reason.

A table the new version no longer declares is not deleted. The update leaves it standing as an ordinary resource the operator owns.

The install, uninstall and update behaviour on this page is pinned by the gateway's package-data-tables.integration.test.ts and package-data-tables-install-and-run.integration.test.ts suites, which run it against a real database.