Files
inkreach-teable-cli/.agents/skills/inkreach-teable/guides/tables-fields.md
T
yeuimu 035384131c feat(view-preset): shared view presets with fork-on-edit semantics
Lands the view-preset / server-persisted personal-layer feature that has
been running in production, plus the hotfix for "the same preset shows
different filter configs to different users" (物流问题-已出轨迹 on
tblgsJTXhqwiDpsjeL5 / viwyszJmLQNOLhA9sh5).

Root cause of the divergence:
- usePersonalViewSync implicitly flushed any overlay edit by a
  view|update user INTO the shared preset row (last-writer-wins). Overlays
  are stale snapshots seeded at apply time, so one user's seed silently
  erased colleagues' filter changes (2026-09-17 02:55 the preset lost the
  流转角色=售后 condition), and active overlays never converged because
  preset updates do not propagate.

Fix (semantics: presets are shared definitions, edits are personal):
- fork-on-edit: the first user edit strips the seed markers from the
  overlay (toolbar switches to the personal config); edits only ever
  write the user's personal row
- shared presets change ONLY through the new explicit, confirmed
  "Update to current config" row action (canManage users)
- un-edited preset overlays auto-follow redefinitions: applyPreset stamps
  a presetVersion (preset.lastModifiedTime); when the preset list
  refetches with a newer row the overlay re-seeds. Edited overlays carry
  no markers and are never clobbered. Legacy rows without a version
  re-seed exactly once
- exits strip both seed markers from the persisted personal row

Also lands the pending WIP shipped with the deployed build: select-choice
visibility constraint, personal-view server sync + proxy, grid async
records scope/request tracking, CLI resource display, preset i18n (10
locales), view_preset migrations, docs/skill updates, .zcode gitignore.

Verified end-to-end on the live instance (headless chromium, real
session): apply -> edit flips toolbar to Personal with ZERO shared-row
PUTs; explicit update PUTs the shared row; API-side redefinition
auto-reseeds an un-edited overlay on the next list refetch.
2026-09-17 04:07:30 +00:00

18 KiB

Tables, Fields, Records & Views

Complete guide for building table structures and working with data via the inkreach-teable CLI.

Quick Start

Minimal walkthrough — create a table with fields in one shot, then insert a record:

# 1. Create a "Tasks" table with 4 fields
inkreach-teable table create --name "Tasks" --base-id bseXXX \
  --fields '[
    {"name":"Title","type":"singleLineText"},
    {"name":"Status","type":"singleSelect","options":{"choices":["Todo","Doing","Done"]}},
    {"name":"Due Date","type":"date"},
    {"name":"Est Hours","type":"number","options":{"formatting":{"precision":1}}}
  ]'

# 2. Insert a record
inkreach-teable record create --table-id tblXXX \
  --fields '{"Title":"Ship sign-up flow","Status":"Doing","Due Date":"2025-06-15","Est Hours":3.5}'

# 3. Verify
inkreach-teable record get --table-id tblXXX

Tip

: table create --fields creates the table AND all fields atomically. For existing tables, use field create.


Tables

Create

inkreach-teable table create --name "<name>" [--base-id <id>] \
  [--fields '<json>'] [--description <desc>]
Flag Required Description
--name yes Display name (unique within base)
--base-id no Defaults to stored config
--fields no Array of field objects (see below)
--description no Optional description

--fields format: JSON array of {name, type, [options], [description]} objects. Types accept full values or aliases (sel → singleSelect); per-type option shapes and inference rules: field-types.md.

Compact shorthand (in --fields only): "name:type" string format:

inkreach-teable table create --name "Projects" --fields '["Name:text","Budget:num","Active:check"]'

With select options: "Status:sel:Todo,Doing,Done", "Tags:mul:Bug,Feature,UI"

Update & Delete

inkreach-teable table update <tableId> --name "<newName>" [--description <d>]
inkreach-teable table delete <tableId>
Operation Idempotent? Notes
update yes Only changes name/description
delete DESTRUCTIVE Deletes all records + fields; irreversible

List & Get

inkreach-teable table list [--base-id <id>]
inkreach-teable table get <tableId>

table get returns dbTableName — the internal SQL table name used in sql queries (e.g., SELECT * FROM "bseXXX"."tbl_abc").


Fields

Create

inkreach-teable field create --table-id <tid> --name "<name>" \
  --type <type> [--options '<json>'] [--description <d>]
Flag Required Notes
--table-id yes Target table
--name yes Field display name
--type no Full type name or alias; omitted → inferred from --options shape or --name pattern (rules: field-types.md § Smart Inference)
--options depends on type JSON object; select choices accept plain strings — see field-types.md § Option Shapes
--description no Optional

Name resolution: for link fields, use foreignTableName instead of foreignTableId — the CLI resolves it automatically:

inkreach-teable field create --table-id tblXXX --name "Project" --type link \
  --options '{"foreignTableName":"Projects","relationship":"manyOne"}'

Update & Delete

inkreach-teable field update <tableId> <fieldIdOrName> [--name "<n>"] [--options '<json>']
inkreach-teable field delete <tableId> <fieldId>
Detail Rule
--name only Uses the PATCH update API (name/description only)
--options Rewrites options via the convert API (PUT …/field/:fid/convert) — the PATCH update API strips options by design. CLI auto-fetches the field's current type; omitted keys are replaced whole, not merged
--name + --options Both applied via one convert call
field delete Irreversible — all cell data in that field is lost

Inspecting fields & constraints

field get and field visible-choices accept fieldId or field name (the CLI resolves names automatically). Human mode renders select choices with their conditions — no --json post-processing needed:

# One-field overview: which fields carry conditional choices
inkreach-teable field list tblXXX --with-options
# ID    NAME  TYPE          OPTIONS
# ...   处理反馈 singleSelect  21 choices · 19 conditional
# followed by per-choice listings: name [color] + "when: <condition summary>"

# Single field by name — choices expanded with human-readable conditions
inkreach-teable field get tblXXX 处理反馈
# choices (21 · 19 conditional):
#   待工厂补发 [orange]
#     when: 问题类型 is 物流问题-超时未揽收 OR 问题类型 is 物流问题-丢件 …

# What a specific record may currently select
inkreach-teable field visible-choices tblXXX recXXX 处理反馈

Select-choice dependency (visibilityFilter) — 2026-08

Single/multiple-select choices may carry a visibilityFilter making them visible only when another select field matches:

{"choices":[{"id":"choA","name":"墨盒","visibilityFilter":{
  "filterSet":[{"fieldId":"fldDeviceType","operator":"is","value":"choPrinter"}]}}]}
  • Read config: field get <tableId> <fieldIdOrName> → options.choices[].visibilityFilter (rendered human-readable in default mode; raw JSON with --json)
  • Survey a whole table: field list <tableId> --with-options shows a constraint count per field plus per-choice conditions
  • Write config: field update --options (convert API). Backend rejects self-reference (Self-reference in visibilityFilter is not allowed) and dependency cycles (Dependency cycle detected…) with 400 — enforced on both V1 and V2 paths since fix 26ed1f732
  • Evaluate for a record: field visible-choices <tableId> <recordId> <fieldIdOrName> → choices currently selectable
  • Write-time enforcement: writing a record value that fails its visibility condition → 400 Option "X" does not meet the current row conditions and cannot be selected.

Records

Get (read)

# All records (paginated)
inkreach-teable record get --table-id <tid> [--filter '<json>'] [--projection '<json>'] \
  [--page-size <n>] [--page <n>] [--search <kw>]

# Single record by ID
inkreach-teable record get-one <recordId>
Flag Description
--filter Filter expression: {"fieldId":{"operator":"value"}}
--projection JSON array of field IDs to return: '["fldXXX","fldYYY"]'
--page-size Records per page (default 100)
--search Keyword search: "keyword" (auto-wraps to {"value":"keyword"})
--view-id Fetch in view order with view's filter/sort/group applied

Filter operators by field type:

Field Type Supported Operators
singleLineText longText url email phone is isNot contains doesNotContain isEmpty isNotEmpty
number currency percent duration rating is isNot isGreater isGreaterEqual isLess isLessEqual isEmpty isNotEmpty
checkbox is
date createdTime lastModifiedTime is isNot isWithIn isBefore isAfter isOnOrBefore isOnOrAfter isEmpty isNotEmpty
singleSelect (single) is isNot isAnyOf isNoneOf isEmpty isNotEmpty
singleSelect (multi) hasAnyOf hasAllOf isExactly isNotExactly hasNoneOf isEmpty isNotEmpty
multipleSelect hasAnyOf hasAllOf isExactly isNotExactly hasNoneOf isEmpty isNotEmpty
user createdBy lastModifiedBy (single) is isNot isAnyOf isNoneOf isEmpty isNotEmpty
user createdBy lastModifiedBy (multi) hasAnyOf hasAllOf isExactly hasNoneOf isNotExactly isEmpty isNotEmpty
link (single) is isNot isAnyOf isNoneOf contains doesNotContain isEmpty isNotEmpty
link (multi) hasAnyOf hasAllOf isExactly hasNoneOf isNotExactly contains doesNotContain isEmpty isNotEmpty
attachment isEmpty isNotEmpty

⚠️ isAnyOf only works for select/link/user fields, NOT for text/number fields. For text fields with multiple values, use or conjunction with multiple is rules:

{"conjunction":"or","filterSet":[
  {"fieldId":"fldX","operator":"is","value":"val1"},
  {"fieldId":"fldX","operator":"is","value":"val2"}
]}

Date sub-operators (with is / isNot / isWithIn / isBefore etc.):

Operator Description
today tomorrow yesterday Relative day
currentWeek lastWeek nextWeekPeriod Relative week
currentMonth lastMonth nextMonthPeriod Relative month
currentYear lastYear nextYearPeriod Relative year
oneWeekAgo oneWeekFromNow ±1 week
oneMonthAgo oneMonthFromNow ±1 month
daysAgo daysFromNow ±N days (set value)
exactDate Specific date: "2025-06-15"
exactDateTime Specific datetime: "2025-06-15T09:00:00.000Z"
pastWeek pastMonth pastYear Last week/month/year (for isWithIn)
nextWeek nextMonth nextYear Next week/month/year (for isWithIn)
pastNumberOfDays nextNumberOfDays ±N days (for isWithIn, set value)

Conjunction: "and" (all must match) or "or" (any matches). Filter sets can nest for complex logic.

Create

# Single record
inkreach-teable record create --table-id <tid> --fields '<json>'

# Batch (up to 2000)
inkreach-teable record create --table-id <tid> --records '<json>'

--fields format (single): {"FieldName":"value","Another":"value"} — field names resolve automatically.

Value semantics:

Value Meaning
"string" Normal value
null Clear the cell
"" (empty string) Skip (no change; update only)

Important

: "" is skip, not clear. Use null to empty a cell. For checkbox: true = checked, null = unchecked (NOT false).

--typecast: auto-converts human-readable values to internal IDs. Essential for user and link fields:

# Without typecast — must use IDs
inkreach-teable record create --table-id tblXXX --fields '{"Assignee":{"id":"usrXXX","title":"Alice"}}'

# With typecast — use display names
inkreach-teable record create --table-id tblXXX --fields '{"Assignee":"Alice"}' --typecast

--records format (batch): {"header":["Field1","Field2"],"records":[["val1","val2"],["val3","val4"]]}

Update

inkreach-teable record update --table-id <tid> --records '<json>'

--records format: [{"id":"recXXX","fields":{"FieldName":"newValue"}}] — array of {id, fields} objects. Batch up to 2000.

Record reordering: pass --order with viewId, anchorId, position:

inkreach-teable record update --table-id tblXXX \
  --records '[{"id":"recXXX","fields":{}}]' \
  --order '{"viewId":"viwXXX","anchorId":"recYYY","position":"before"}'

Delete

inkreach-teable record delete --table-id <tid> --record-ids "recA,recB,recC"

Max 1000 IDs per call. Irreversible.


Views

List & Get

inkreach-teable view list <tableId>
inkreach-teable view get <viewId>

Create

inkreach-teable view create --table-id <tid> --name "<name>" [--type <t>]
--type Description
grid Default spreadsheet view
kanban Board (requires a singleSelect/link grouping field)
form Form input view
gallery Card gallery
calendar Calendar (requires a date field for grouping)

Update (configure filters, sorts, grouping)

inkreach-teable view update <viewId> \
  [--name "<n>"] \
  [--filter '<json>'] \
  [--sort '<json>'] \
  [--group '<json>'] \
  [--column-meta '<json>']

Filters: {"conjunction":"and","filterSet":[{"fieldId":"fldXXX","operator":"is","value":"Done"}]}

Sorts: [{"fieldId":"fldXXX","order":"asc"}]

Group: [{"fieldId":"fldXXX","order":"asc"}]

Column meta: controls visibility, width, order of fields in the view.

Delete

inkreach-teable view delete <viewId>

Deleting a view does NOT delete the table or its data — only the view configuration.


View Presets

Presets are view-scoped, named bundles of view configuration (filter/sort/group, optionally options/columnMeta), stored server-side and shared with all collaborators — each view owns its own preset list. Users apply them from the toolbar preset dropdown (labeled by current state: 公共 / 个人 / active preset name; the old "个人" toggle button was merged into this dropdown). Applying seeds the user's personal-view overlay (persisted server-side per user per view), so operators can work in different presets simultaneously without touching the collaborative view. Managing presets requires table-level view-update permission.

Typical use

One-click screening configs for operators, e.g. "pending orders" vs "in-progress orders" on an order table, without granting view-edit permission.

Commands

# preset screening pending orders (bound to one view)
inkreach-teable preset create --table-id tblXxx --view-id viwXxx --name "待处理订单" \
  --filter '{"conjunction":"and","filterSet":[{"fieldId":"fldStatus","operator":"is","value":"待处理"}]}'

# preset for in-progress orders, newest first
inkreach-teable preset create --table-id tblXxx --view-id viwXxx --name "正在处理的订单" \
  --filter '{"conjunction":"and","filterSet":[{"fieldId":"fldStatus","operator":"is","value":"处理中"}]}' \
  --sort '{"sortObjs":[{"fieldId":"fldCreated","order":"desc"}]}'

inkreach-teable preset list tblXxx --view-id viwXxx
inkreach-teable preset update <presetId> --table-id tblXxx --name "新名字"   # or replace --filter/--sort/...
inkreach-teable preset delete <presetId> --table-id tblXxx

Semantics

  • Config keys you do NOT pass stay unset: on apply, the preset keeps the view's current value for those keys (a filter-only preset does not touch the sort).
  • Keys you DO pass replace that aspect on apply.
  • --column-meta snapshots field visibility/order — avoid unless needed: fields added later stay hidden while that preset is active (stale snapshot).
  • Deleting a preset does not touch views or records; users applying it fall back to the public config on next switch.
  • REST equivalents: GET/POST /api/table/:tableId/view-preset (list accepts ?viewId=; create requires viewId), PUT/DELETE /api/table/:tableId/view-preset/:presetId (writes need view-update permission).
  • Per-user personal config: GET/PUT/DELETE /api/table/:tableId/view-preset/personal?viewId= (view-read permission; PUT body {config, enabled}) — this is what the UI "个人配置" entry persists.

Value Formats Quick Reference

Field Type Value format Notes
singleLineText / longText "string"
number 42 or 3.14 Integer or float
checkbox true / null NOT false for unchecked
date "2025-06-15" or "2025-06-15T09:00:00.000Z" ISO 8601; "now" maps to current time
singleSelect "Option Name" (string)
multipleSelect ["Tag A","Tag B"] (array of strings)
rating 3 Integer 1..max
attachment [{"name":"f.png","token":"xxx"}] Always array of objects; update replaces all
user {"id":"usrXXX","title":"Alice"} With --typecast: just "Alice" or "alice@example.com"
link [{"id":"recXXX"}] With --typecast: just "Linked Record Name"
formula / rollup / lookup Read-only Cannot be set via record create/update
autoNumber / createdBy / createdTime / lastModifiedTime / lastModifiedBy Auto-populated Cannot be set manually
button {"count":0} Read-only; incremented by button clicks

Common Patterns

User intent Correct approach Wrong approach
Create a table with fields table create --fields '[...]' — one call Multiple field create calls (slower, more error-prone)
Use display names for linked records record create --fields '{"Project":"Acme"}' --typecast Manually looking up recXXX IDs
Use email to assign user record create --typecast --fields '{"Assignee":"alice@co.com"}' Looking up usrXXX IDs
Clear a cell null in the fields object "" (which means skip!)
Update one field, leave others --records '[{"id":"recXXX","fields":{"Status":"Done"}}]' Re-sending all fields (unnecessary, slower)
Fetch only 2 columns record get --projection '["fldXXX","fldYYY"]' Fetching all fields (wastes bandwidth)
Find records by keyword record get --search "keyword" record get then filtering manually
Add a select field with colored options --options '{"choices":["A","B"]}' — colors auto Manually specifying color for every choice
Make a link field to another table --type link --options '{"foreignTableName":"Target","relationship":"manyOne"}' Using IDs + manual lookup
Reorder records in a view record update --order '{"viewId":...,"anchorId":...,"position":"before"}' Deleting and re-creating records
View data in kanban board view create --type kanban then view update --group Creating special columns
Set datetime in a date field "2025-06-15T14:30:00.000Z" "2025-06-15 14:30" (non-ISO; may be rejected)
Update attachments Always pass the full array (replaces all) Trying to add/remove individual attachments
Batch insert 500 records One record create --records call 500 separate record create calls