Skip to content
CLI
Custom Tables

Custom Tables

The intellia tables command group manages custom tables in your Hutly organization — both the table schemas (defined in YAML and created from disk) and the rows stored inside them.

Table schemas are editable in place. intellia tables push diffs your YAML against the live table and applies the difference — add or remove a column, toggle required, change a default, or add a foreign key or unique constraint, and the next push carries it out. Changing a column’s type (or its Data Dictionary link) is the one change the diff can’t express: that still requires --force-recreate, which drops the table and recreates it from the YAML, destroying every row. Rows, by contrast, are fully mutable: create, update, delete, and bulk-import them with intellia tables rows.

Every table is created with a server-assigned, auto-increment id column. Never declare your own primary-key column — id, record_id, row_id, pk, primary_key, and <table_name>_id are all rejected. A _id suffix is for a foreign key pointing at another table (name it <parent_table>_id), not a substitute primary key.

All commands operate against the organization configured in your CLI context and require valid credentials. Every command accepts --json; JSON is the default machine-readable output for read commands, and the flag is a no-op elsewhere for scripting compatibility.

Schema commands

intellia tables list

List the custom tables in the current organization.

intellia tables list
intellia tables list --limit 50 --offset 50 --pretty
Option Description
-l, --limit <n> Max results (default: 20)
-o, --offset <n> Result offset (default: 0)
--pretty Pretty-print JSON output

intellia tables get

Fetch a table’s schema plus its first page of rows (up to 100) as a single bundle.

intellia tables get applicants
intellia tables get applicants --pretty
Option Description
--pretty Pretty-print JSON output

intellia tables describe

Fetch a table’s schema only, with no rows. Use get when you also want the first page of rows.

intellia tables describe applicants --pretty
Option Description
--pretty Pretty-print JSON output

intellia tables push

Create or update a custom table from a YAML file. Table names must contain only letters, numbers, and underscores.

push is declarative: if the table doesn’t exist yet, it’s created. If it does, the CLI asks the server for a change plan — the list of steps needed to make the live table match the YAML — and applies it. Steps that destroy data (dropping a column drops its values) are flagged; the CLI prompts for confirmation before applying any of them, and --dry-run prints the plan without applying it at all.

Pass --force-recreate to drop the existing table — this deletes all of its rows — and recreate it from the YAML instead of diffing. This is still the only way to change a column’s type or its Data Dictionary link, since the diff can’t express either.

intellia tables push tables/applicants.yaml
intellia tables push tables/applicants.yaml --dry-run
intellia tables push tables/applicants.yaml --force-recreate
intellia tables push tables/applicants.yaml --force-recreate --yes
intellia tables push tables/applicants.yaml --var region=vic
Option Description
--dry-run Print the change plan without applying it
--force-recreate If the table already exists, delete it (and all rows) and recreate from this YAML
-v, --var <key=value> Set a variable value (repeatable) for YAML preprocessing
-y, --yes Skip confirmation prompts

Table schema YAML

A table YAML document has a name, an optional description, and a non-empty fields array. Each field is either a plain column or a Data Dictionary–linked column.

name: applicants
description: Rental applicants captured during screening
fields:
  - name: full_name
    type: text
    required: true
    comments: Applicant's full legal name
  - name: monthly_income
    type: numeric
    required: false
    comments: Stated gross monthly income
  - name: screened
    type: boolean
    required: false
    default: false
    comments: Whether the applicant has been screened

Plain-column fields:

Key Description
name Column name (letters, numbers, underscores)
type One of text, numeric, boolean, timestamp, jsonb, reference
required Whether the column is mandatory
default Optional default value
comments Optional column description
fk (type: reference only) name of the custom table this column points at
display (with fk only) name of a column on the parent table to show instead of the raw id — pick a text column (see below)

Foreign keys point a reference column at another custom table’s id, in the same organization, ON DELETE RESTRICT — deleting a parent row a child still references is refused. display names the parent column shown in the UI instead of the raw id; a display without an fk is an error. display must be a text column: the row picker searches it with a case-insensitive text match, so a numeric or timestamp display column can’t be searched. This is enforced server-side — a schema update naming a display column that doesn’t exist on the parent table, or that exists but isn’t text, is rejected with a 400 naming the column and the table.

name: leases
fields:
  - name: property_id     # FK -> properties.id
    type: reference
    fk: properties
    display: address_line_1
    required: true
  - ddField: rent_amount
    required: true
unique:
  - [property_id, rent_amount]

Unique constraints live in a top-level unique array, each entry a list of one or more column names. Composite constraints are the common case once foreign keys exist — the example above means one rent value per property, and unique: [[property_id, inspection_date]] would mean one inspection per property per date.

Indexes live in a top-level indexes array, each entry a list of one or more column names, and speed up filtering and sorting on those columns:

indexes:
  - [item_id, warehouse_id]
  - [posting_date]

Every foreign-key (reference) column is indexed for you, so you never need to list one — listing it anyway is harmless and still yields a single index. The name is derived server-side (ix_<table>_<columns>); you never write it, and intellia tables pull writes back the indexes you declared, not the automatic foreign-key ones. Indexes are plain btrees: there is no partial (where) form, and uniqueness belongs in unique. An index is built inside the same locked transaction as the rest of a push, so adding one to a table with millions of rows blocks writes to that table for the duration of the build. An existing table gains its foreign-key indexes on its next schema push. The YAML is the whole truth for plain btree indexes: a push drops any it does not declare — the dry run lists the drop first — so a plain index made by hand in psql must be added to indexes to survive the next push. Partial, hash and INCLUDE indexes are outside the platform’s model: it neither creates nor drops them, and they do not satisfy a declared index.

Data Dictionary columns use the ddField shorthand, linking a column to a live Hutly Data Dictionary field by its canonical forms_fields.name. The server resolves and freezes the DD snapshot (label, type, enum values) at create time, so later DD edits don’t silently break the table.

name: lease_parties
fields:
  - ddField: tenant_full_name
    required: true
  - ddField: lease_start_date
    required: false
Key Description
ddField Canonical Data Dictionary field name to link
required Whether the column is mandatory
default Optional default value
comments Optional column description

Audit columns

Every table records who wrote each row and when. Four columns exist on every table and are written only by the platform:

Column Type Set on
created_at timestamp insert
created_by user id insert
updated_at timestamp insert and every update
updated_by user id insert and every update

The user id is the person behind the request: the signed-in user, the owner of an API key, the person who started a workflow run, or the person who activated a module for its seed rows. An agent run on a schedule or webhook is recorded as whoever set up that schedule or webhook. A workflow that a schedule or webhook starts directly records no person, so created_by and updated_by are null there. A workflow’s own hutly.tables calls from a sandbox reach the platform as the workflow owner and are recorded as that owner. Rows that existed before the columns were introduced keep nulls.

The names are reserved. A declaration that uses one is refused. On the REST API, the app gateway and the agent tools, an update may send a row back as it was read: created_at and created_by are accepted when they match the row’s own values, and updated_at and updated_by are ignored because the update stamps them, so saving twice from the same copy works. An update that changes created_at or created_by, and any insert that sends one of the four (so intellia tables rows import too), is refused with Field <name> is server-assigned and cannot be written. Inside a transaction any write that sends one is refused. A table that already had its own column with one of these names keeps it; the platform does not record that column, and the column stays yours to read and write; intellia tables pull says so. Rename the column and the platform takes over.

Sorting and filtering on the audit columns works in the grid, the API and the rows list command.

intellia tables pull

Fetch a table’s schema and write it to a YAML file. Rows are not included — manage those with intellia tables rows. DD-linked columns round-trip as the ddField shorthand so a pulled file can be pushed back without invalidating the link.

intellia tables pull applicants
intellia tables pull applicants --output ./schemas/applicants.yaml
Option Description
-o, --output <file> Output file path (defaults to ./tables/<tableName>.yaml)

intellia tables delete

Permanently delete a custom table and all of its rows. This is irreversible. The CLI prompts for confirmation; pass --yes to skip it.

intellia tables delete applicants
intellia tables delete applicants --yes
Option Description
-y, --yes Skip confirmation prompts

Row commands

Rows live under intellia tables rows <verb> <tableName>.

intellia tables rows list

List rows in a table. Pass --filter and --order multiple times to build compound queries.

intellia tables rows list applicants
intellia tables rows list applicants --limit 50 --offset 0 --pretty
intellia tables rows list applicants \
  --filter "monthly_income:gte:5000:and" \
  --filter "screened:eq:true:and" \
  --order "monthly_income:desc"
Option Description
-l, --limit <n> Max results (default: 100)
-o, --offset <n> Result offset (default: 0)
-f, --filter <segment> Filter field:op:value:join — repeatable
--order <segment> Order field:asc or field:desc — repeatable
--pretty Pretty-print JSON output

Filter segments take the form field:op:value:join:

  • op is one of eq, ne, gt, lt, gte, lte, like, ilike, in, nin
  • join is and or or, combining the segment with the next one

Order segments take the form field:asc or field:desc; repeat for multi-key ordering.

intellia tables rows aggregate

Grouped totals, computed by the platform through a published app’s gateway. Pass the app with --app; the table must appear in that app’s approved manifest, and the command runs exactly the tables.aggregate call the app would make.

intellia tables rows aggregate intellia_erp_invoice \
  --app 8b2f6b1e-4c1d-4f0a-9a1e-3d2c1b0a9f8e \
  --filter "status:eq:issued:and" \
  --group party_id --group invoice_date:month \
  --metric sum:total_incl_tax:total --metric count:n \
  --order total:desc --tz Australia/Melbourne --pretty
Option Description
--app <artifactId> The published app whose approved manifest declares the table (required)
-f, --filter <segment> Filter field:op:value:join, as for rows list; repeatable
--group <key> field or field:day|week|month|quarter|year, at most 2; repeatable
--metric <spec> count:alias or sum|avg|min|max:field:alias, 1 to 10; repeatable
--order <segment> name:asc or name:desc; name is a group field or a metric alias
--tz <zone> IANA zone for bucketing created_at and updated_at (default UTC). Custom timestamp columns hold local time and are bucketed as stored
--pretty Pretty-print JSON output

The output is { "groups": [...], "capped": false }. Sums and extremes of numeric columns are decimal strings, avg has six places, count is a number, and a bucket key is the first day of its bucket. Answers stop at 1,000 groups (capped: true) and at 10 seconds (aggregate_timeout). Under intellia app serve the harness cannot relay aggregate; use this command against the published app instead.

intellia tables rows create

Insert a single row. Provide the column values as a JSON object that matches the table’s field schema, either inline or from a file.

intellia tables rows create applicants --data '{"full_name":"Sam Lee","monthly_income":6200,"screened":false}'
intellia tables rows create applicants --data-file ./row.json
Option Description
--data <json> Inline JSON object matching the table’s field schema
--data-file <path> Read row JSON from a file

Pass exactly one of --data or --data-file.

intellia tables rows update

Replace the columns of a single row, identified by its numeric id (a positive integer).

intellia tables rows update applicants 42 --data '{"screened":true}'
intellia tables rows update applicants 42 --data-file ./update.json
Option Description
--data <json> Inline JSON object with the new column values
--data-file <path> Read row JSON from a file

Pass exactly one of --data or --data-file.

intellia tables rows delete

Delete a single row by its numeric id. The CLI prompts for confirmation; pass --yes to skip it.

intellia tables rows delete applicants 42
intellia tables rows delete applicants 42 --yes
Option Description
-y, --yes Skip confirmation prompts

intellia tables rows import

Bulk-insert rows from a JSON file. The file must contain a JSON array of row objects. The server validates each row independently and reports per-row errors; partial success is allowed. When any rows fail, the command prints the result envelope and exits non-zero.

intellia tables rows import applicants ./applicants-seed.json
[
  { "full_name": "Sam Lee", "monthly_income": 6200, "screened": false },
  { "full_name": "Jo Park", "monthly_income": 4800, "screened": true }
]