refactor: rename skill 'inkreach-teable-ops' -> 'inkreach-teable'

Rename the skill directory and update all references (SKILL.md frontmatter
name, AGENTS.md, design doc, install.sh). Git tracks this as a rename so
history is preserved.
This commit is contained in:
yeuimu
2026-08-05 10:35:04 +08:00
parent 3810c33d69
commit d3790ab856
12 changed files with 2027 additions and 4 deletions
+155
View File
@@ -0,0 +1,155 @@
---
name: inkreach-teable
description: >-
Operate a deployed inkreach-teable instance (a Teable fork with a self-built
automation engine). Configure tables, fields, records, views, and automations
(8 triggers: recordCreated/recordUpdated/recordCondition/recordCreatedOrUpdated/
formSubmitted/scheduledTime/buttonClick/webhook; 6 actions: script/sendEmail/
httpRequest/createRecord/updateRecord/getRecords), plus secrets and execution
history. Trigger when the user wants to manage tables/fields/records/views, build
or configure automations/workflows, set up button-field triggers, schedule jobs,
create webhooks, inspect run history, or troubleshoot a deployed inkreach-teable
instance — even if they don't say "inkreach-teable" but reference Teable-style IDs
(bseXXX, tblXXX, fldXXX, recXXX, viwXXX, wflXXX) or ask to configure tables and
automation on the local/deployed instance. Also covers dev/deploy tasks (build,
restart systemd services, known gotchas). Uses the inkreach-teable CLI, which targets
the project's OWN automation contract (NOT the official teable CLI).
---
# inkreach-teable operations
This skill operates a deployed inkreach-teable instance via the `inkreach-teable` CLI.
Respond in the user's language. Keep answers concise and action-oriented.
> **Critical**: inkreach-teable's automation engine is **self-built and differs from
> official teable**. The official `teable` CLI does NOT work for automations here.
> Always use the `inkreach-teable` CLI. See [Key Constraints](#3-key-constraints) for the
> exact key-name differences.
## 1. Prerequisites & Constraints
- All operations use the `inkreach-teable` CLI. Install via `bash scripts/install.sh` (relative to this skill dir) if `inkreach-teable` is not found.
- **Endpoint + token**: configure once with `inkreach-teable auth login --endpoint <url> --token <PAT>`. Default endpoint is `http://localhost:3000`. Verify with `inkreach-teable auth status`.
- **Getting a PAT**: `inkreach-teable auth token create --name my-cli` (returns the full token **only once**). Scopes default to automation+table+record read/write. Or create via the web UI (Settings → Personal Access Tokens).
- **`--base-id`**: omit if a default was set via `auth login`; CLI uses it automatically. Ask user only if a command fails with MISSING_BASE_ID.
- **`--json` mode**: add `--json` to any command for raw JSON output (for agent parsing).
- **Unfamiliar commands**: `inkreach-teable <cmd> --help` as a fallback.
- **Read the relevant guide before acting**: after entering a module, read the doc(s) marked "Required" in the module map.
## 2. Module & Command Navigation
### 2.1 Module Map
| Module | What it solves | Entry commands | Guide to read |
|--------|---------------|----------------|---------------|
| Tables & Fields | Create/modify table structure | `table create`, `field create` | [tables-fields.md](guides/tables-fields.md), [field-types.md](api-reference/field-types.md) |
| Records | Read/write row data | `record get`, `record create/update/delete` | [cli-reference.md § record](guides/cli-reference.md#base--table--field--record--view) |
| Views | Persistent filtered/sorted perspectives | `view create/update` | [cli-reference.md § view](guides/cli-reference.md#base--table--field--record--view) |
| Automation | Event-driven workflows | `workflow *`, `trigger set`, `action add` | [automation-guide.md](guides/automation-guide.md), [trigger-types.md](api-reference/trigger-types.md), [action-types.md](api-reference/action-types.md) |
| Secrets | Store API keys for script actions | `secret set/list/delete` | [automation-guide.md § Secrets](guides/automation-guide.md#secrets) |
| Execution History | Inspect run results / rerun failures | `runs list/get/rerun` | [cli-reference.md § runs](guides/cli-reference.md#runs) |
| Manual Trigger | Test by firing button-click/webhook | `fire button-click`, `fire webhook` | [automation-guide.md § Testing](guides/automation-guide.md#testing--triggering) |
| SQL | Read-only analytics, JOINs, aggregation | `sql <sql>` | [cli-reference.md § sql](guides/cli-reference.md#sql) |
| Dev/Deploy | Build, restart services, known gotchas | (systemd / pnpm build) | [development.md](guides/development.md) |
### 2.2 Routing Rules
> **Before executing**: read the documents marked "Required" in the guide for the module.
| User intent | Correct module | Do NOT do this |
|-------------|---------------|----------------|
| Configure an automation | Automation: `workflow create` → `trigger set` → `action add` → `activate` | Manually editing DB rows |
| React to new records | Trigger `recordCreated` | Polling loop in a script |
| React to field changes | Trigger `recordUpdated` with `--field-ids` | `recordCondition` (ignores fields) |
| React to a condition matching | Trigger `recordCondition` (NOT recordMatchesConditions) | Official-teable key names |
| Button click starts a workflow | Trigger `buttonClick` + a Button-type field | Any other trigger type |
| Receive external HTTP | Trigger `webhook`; fire via `fire webhook` | Expecting auth (backend ignores it) |
| Schedule recurring job | Trigger `scheduledTime` (only type+interval work) | Passing triggerTime/weekdays (backend ignores) |
| Per-row AI / transformation | Action `script` reading `triggerInput` | Manual row-by-row via record API |
| Send notification | Action `sendEmail` or `httpRequest` | — |
| Read records for later writes | `record get` (returns record IDs) | `sql` (no record IDs returned) |
| Analytics / aggregation / JOIN | `sql` with GROUP BY | Fetch all records + compute in code |
| Store an API key for a script | `secret set` → read in script via `process.env.NAME` | Hardcoding in script code |
| Inspect why a workflow didn't run | `runs list --status failed` → `runs get` | Guessing; re-activating blindly |
### 2.3 Quick Syntax
```bash
# Configure access (once)
inkreach-teable auth login --endpoint http://localhost:3000 --token <PAT>
# Create a table
inkreach-teable table create --name "Tasks" --base-id bseXXX \
--fields '[{"name":"Title","type":"singleLineText"},{"name":"Status","type":"singleSelect","options":{"choices":["Todo","Done"]}}]'
# Create a workflow + set buttonClick trigger + add a script action + activate
inkreach-teable workflow create --name "On Go click" --base-id bseXXX
inkreach-teable trigger set wflXXX --type buttonClick --table-id tblXXX --field-ids '["fldXXX"]'
inkreach-teable action add wflXXX --type script --code 'return triggerInput.record.fields'
inkreach-teable workflow activate wflXXX
# Test by firing the button
inkreach-teable fire button-click tblXXX recXXX fldXXX
inkreach-teable runs list wflXXX
```
For complete syntax and all options, read [cli-reference.md](guides/cli-reference.md).
## 3. Key Constraints
These differ from official teable and from the (stale) `docs/automation/triggers.md`. **Use these exact values:**
1. **Filter key is `conditions`** (NOT `filter`). Pass via `trigger set --conditions '<json>'`. The JSON shape is `{conjunction:"and"|"or", filterSet:[{fieldId, operator, value}]}`.
2. **Watch-fields key is `fieldIds`** (NOT `watchFieldIds`). For `recordUpdated`/`buttonClick`. `[]` means watch all fields.
3. **Trigger category is `recordCondition`** (NOT `recordMatchesConditions`). It triggers on create+update when the record matches conditions.
4. **`scheduledTime` backend honors only `timing.type` + `timing.interval`**. `triggerTime`, `weekdays`, `daysOfMonth`, `starting`, `ending`, `tz` are all IGNORED by the backend. `weeks` type is hardcoded to every Sunday. Don't configure fields the backend won't use.
5. **Webhook real route is `/api/webhook/{baseId}/{workflowId}`** (no `/base/` or `/workflow/` path segments), and `authorization` config is UI-only — **backend does NOT validate it**. Fire via `inkreach-teable fire webhook <baseId> <wfId>`.
6. **Node updates use whole-graph replacement** (`PUT /node`). CLI commands (`trigger set`, `action add/update/delete`) handle GET→modify→PUT internally — always use them, never hand-edit partial graphs.
7. **`activate` creates a snapshot**: changes to nodes only take effect after re-activating. `hasDraft: true` means unactivated changes exist.
8. **`workflow delete` is a SOFT delete** (cascades to base_node, auto-token, trigger unregister). Test workflows should be `deactivate`d, not deleted.
9. **Secrets values are write-only** — `secret list` never returns the value. Scripts read them via `process.env.<NAME>`.
10. **PAT format is `teable_{id}_{encryptedSign}`** — create via `auth token create`, the full string is returned **once**.
11. **Script sandbox has NO `fs`/`eval`/`Function`**, but has `triggerInput`, `console`, `fetch`, `process.env` (= secrets + `AUTOMATION_TOKEN` + `PUBLIC_ORIGIN`), and a controlled `require`. Use `triggerInput._accessToken` + `triggerInput._apiBaseUrl` to call back into the instance API.
## 4. Execution Rules
### 4.1 Standard Order
1. **Confirm context** — identify target base/table/workflow (`--base-id`, IDs in user's message).
2. **Read before write** — `table get` / `field list` / `workflow get` to confirm current state.
3. **Execute changes** — create/update via CLI commands.
4. **Verify** — re-read; for automations, `activate` then `runs list` after a test fire.
### 4.2 Critical Rules (with reasoning)
1. **Use `inkreach-teable` CLI, never `teable` CLI for automations** — official CLI uses incompatible contract (filter/watchFieldIds/recordMatchesConditions) and will 404 or corrupt config.
2. **Read [trigger-types.md](api-reference/trigger-types.md) before configuring a trigger** — contains exact config keys per trigger; guessing leads to silent misfires.
3. **Build the full chain then activate** — `workflow create` → `trigger set` → `action add` → `activate`. Editing nodes after activation requires re-`activate` to snapshot.
4. **For `buttonClick`, ensure the field is a Button field bound to this workflow** — create via `field create --type button` with `options.workflowId`, or it won't render/click. Verify `workflow.isActive` (the field options capture a stale `isActive` snapshot at creation).
5. **Read [action-types.md](api-reference/action-types.md) before writing scripts** — covers sandbox globals, variable references `{{nodeIndex.path}}`, and the `_accessToken`/`_apiBaseUrl` back-channel.
## 5. Common Errors & Recovery
| Symptom | Cause | Fix |
|---------|-------|-----|
| `MISSING_BASE_ID` | No base id resolved | Pass `--base-id`, or `auth login --base-id <id>` |
| `401` / "access denied" | Token invalid/expired or lacks scope | `auth status`; recreate PAT with needed scopes |
| `403` | Base role lacks permission | User must grant role (owner/creator) in web UI |
| Workflow never fires | Not activated, or trigger misconfigured | `workflow get` → check `isActive`; verify trigger type + tableId/fieldIds |
| `scheduledTime` fires at wrong time | Backend ignores most timing fields | Only `timing.type`+`interval` work; use cron-friendly intervals |
| button-click returns 400 "not active" | Workflow deactivated since field created | `workflow activate <id>`, or field options are stale |
| `runs list` shows `failed` | Action error | `runs get <wfId> <execId>` → read `nodeResults[].error` |
| Secret not readable in script | Wrong env var name | Scripts use `process.env.<NAME>` where NAME = secret name |
## 6. API Reference Index
Files in `api-reference/`, read when you need exact config formats:
- **[trigger-types.md](api-reference/trigger-types.md)** — all 8 triggers: type string, config keys, output variables.
- **[action-types.md](api-reference/action-types.md)** — all 6 actions: config keys, sandbox globals, variable references.
- **[field-types.md](api-reference/field-types.md)** — Teable field types, aliases, and option shapes (incl. Button field with `workflowId`).
## 7. Instance Access
Endpoint, service status, and credential-acquisition steps: see [instance-access.md](guides/instance-access.md).
For dev/deploy (build, restart systemd, known gotchas): see [development.md](guides/development.md).
@@ -0,0 +1,246 @@
# Action Types Reference
All 6 actions of the inkreach-teable automation engine. Actions run in topological order after the trigger; each receives the merged upstream output.
An action node:
```json
{
"id": "node-2",
"type": "action",
"category": "<one of the 6 types below>",
"config": { ... },
"name": null
}
```
Use `inkreach-teable action add <wfId> --type <T> ...` (don't hand-build the JSON).
---
## Sandbox Globals (all script actions)
Inside a `script` action, these are available:
| Global | Description |
|--------|-------------|
| `triggerInput` | Merged output of trigger + all upstream actions. Contains `_accessToken`, `_apiBaseUrl`, `_baseId`, `_workflowId`, `_nodeId`, `_secrets`, `_publicOrigin` |
| `console` | Captured to execution logs (`logs.stdout` / `logs.stderr`) |
| `fetch` | Standard fetch (30s timeout recommended) |
| `process.env` | Secrets (by name) + `AUTOMATION_TOKEN` + `PUBLIC_ORIGIN` |
| `require` | Controlled — allows whitelisted npm deps declared via `dependencies` |
**NOT available**: `fs`, `eval`, `Function`, `process` (only `process.env`), `child_process`.
**Back-channel API access**: inside scripts, call the instance's own API:
```js
const res = await fetch(triggerInput._apiBaseUrl + '/api/table/tblXXX/record', {
headers: { Authorization: 'Bearer ' + triggerInput._accessToken }
});
```
`_accessToken` is a 1-hour JWT auto-created on activation (scope: table|read + record|read/create/update).
---
## 1. script — Run JavaScript
```bash
inkreach-teable action add <wfId> --type script \
--code 'return { doubled: triggerInput.record.fields.Amount * 2 }'
# or from a file
inkreach-teable action add <wfId> --type script --code-file ./my-script.js --timeout 120000
```
**Config:**
```json
{
"code": "// JS code",
"timeout": 60000,
"dependencies": [ { "name": "axios", "version": "1.6.0" } ]
}
```
- `timeout`: default 60000 (120000 in batch mode).
- `dependencies`: optional npm packages; `require('axios')` works in-sandbox.
**Output:**
```json
{ "result": "<return value>", "logs": { "stdout": "...", "stderr": "..." }, "executionTime": 123 }
```
In batch mode (`triggerInput.records` array):
```js
const records = triggerInput.records || [triggerInput.record];
const results = await Promise.allSettled(records.map(async (r) => {
// per-record work
}));
return { ok: results.filter(r => r.status === 'fulfilled').length };
```
---
## 2. sendEmail — Send an email
```bash
inkreach-teable action add <wfId> --type sendEmail \
--to user@example.com --subject 'Alert' --body '<p>Check this</p>'
```
**Config:**
```json
{
"to": "user@example.com",
"subject": "Alert",
"body": "<html>...</html>",
"transporterName": "automation"
}
```
- `transporterName`: `automation` (default) or `notify`. Requires SMTP configured on the server.
**Output:** `{ "sent": true, "to": "..." }`
Variable reference: `--body '{{1.record.fields.Name}}'` injects upstream data.
---
## 3. httpRequest — Call an external API
```bash
inkreach-teable action add <wfId> --type httpRequest \
--url https://api.example.com/webhook --method POST \
--headers '{"X-Api-Key":"xxx"}' --req-body '{"event":"test"}'
```
**Config:**
```json
{
"url": "https://...",
"method": "POST",
"headers": { "X-Api-Key": "xxx" },
"body": { ... }
}
```
- `method`: GET (default) | POST | PUT | PATCH | DELETE.
- 30s hard timeout.
- GET/HEAD do not send a body.
**Output:** `{ "statusCode": 200, "headers": {...}, "body": <parsed> }`
- Non-2xx still returns output but the node status is `failed`.
---
## 4. createRecord — Create records
```bash
inkreach-teable action add <wfId> --type createRecord \
--target-table tblXXX \
--records '[{"fields":{"Name":"New","Source":"{{1.record.id}}"}}]'
```
**Config:**
```json
{
"tableId": "tblXXX",
"fieldKeyType": "id",
"records": [ { "fields": { "fldName": "value" } } ]
}
```
- `fieldKeyType`: `id` (default) or `name`.
**Output:** `{ "records": [ { "id": "recXXX", "fields": {...} } ] }`
---
## 5. updateRecord — Update records
```bash
inkreach-teable action add <wfId> --type updateRecord \
--target-table tblXXX \
--records '[{"id":"recXXX","fields":{"Status":"Done"}}]'
```
**Config:**
```json
{
"tableId": "tblXXX",
"fieldKeyType": "id",
"records": [ { "id": "recXXX", "fields": { "fldStatus": "Done" } } ]
}
```
- Each record **must** include `id` (recordId).
**Output:** `{ "records": [ ... ] }`
---
## 6. getRecords — Query records
```bash
inkreach-teable action add <wfId> --type getRecords \
--target-table tblXXX \
--query '{"pageSize":100,"filter":{"conjunction":"and","filterSet":[]}}'
```
**Config:**
```json
{
"tableId": "tblXXX",
"query": {
"pageSize": 100,
"page": 1,
"filter": { "conjunction": "and", "filterSet": [...] },
"orderBy": "fldXXX",
"search": "keyword"
}
}
```
- `search` string is auto-normalized to the `search[]` tuple format the API expects.
**Output:** `{ "records": [...], "total": 123 }`
---
## Variable References
In any action's **string** config field, use `{{nodeIndex.path}}` to inject upstream output:
| Reference | Resolves to |
|-----------|-------------|
| `{{1.record.id}}` | trigger record id (trigger = node 1) |
| `{{1.record.fields.fldStatus}}` | trigger record's Status field |
| `{{2.result}}` | script action's return value (node 2) |
| `{{3.records}}` | getRecords output array (node 3) |
| `{{2.statusCode}}` | httpRequest status (node 2) |
**Node index** = topological execution order (trigger is always 1, then actions in chain order).
Script actions can also read everything via `triggerInput` (the merged upstream output):
```js
const prev = triggerInput; // has all upstream fields merged
```
## Common Action Patterns
### Call OpenAI with a secret
```js
// secret named OPENAI_KEY must be set first
const res = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: { Authorization: 'Bearer ' + process.env.OPENAI_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: triggerInput.record.fields.Prompt }]
})
});
const data = await res.json();
return { reply: data.choices[0].message.content };
```
### Conditional logic
```js
const status = triggerInput.record.fields.Status;
if (status === 'Done') {
// call API
return { skipped: false };
}
return { skipped: true };
```
(There's no dedicated "if" node — do branching inside a script.)
@@ -0,0 +1,389 @@
# Field Types Reference
Complete reference of all Teable field types, aliases, option shapes, and smart inference rules.
---
## Type Overview
| `--type` value | Display name | Purpose | Requires `--options` |
|---------------|-------------|---------|---------------------|
| `singleLineText` | Single Line Text | Short text (titles, names, IDs) | No |
| `longText` | Long Text | Rich text, descriptions, notes | No |
| `number` | Number | Integers or decimals | No (defaults to integer) |
| `singleSelect` | Single Select | Pick one from fixed options | Yes (`choices`) |
| `multipleSelect` | Multiple Select | Pick many (tags/labels) | Yes (`choices`) |
| `date` | Date | Date or datetime | No |
| `checkbox` | Checkbox | Yes/no toggle | No |
| `rating` | Rating | Star/numeric rating 1..10 | No (defaults: max=5, star, yellow) |
| `attachment` | Attachment | Files, images, documents | No |
| `user` | User | Person/collaborator reference | No |
| `link` | Link | Reference records in another table | Yes (`foreignTableId`/`foreignTableName`) |
| `lookup` | Lookup | Mirror a field from linked record | Yes (`lookupOptions`) |
| `rollup` | Rollup | Aggregate linked records (SUM, COUNT, etc.) | Yes (expression + `lookupOptions`) |
| `conditionalRollup` | Conditional Rollup | Aggregate from ANY table (no link required) | Yes (expression + `lookupOptions` + conditions) |
| `formula` | Formula | Per-row computation | Yes (`expression`) |
| `createdTime` | Created Time | Auto-set when record created | No |
| `lastModifiedTime` | Last Modified Time | Auto-updated on any change | No |
| `createdBy` | Created By | Auto-set to creator's user | No |
| `lastModifiedBy` | Last Modified By | Auto-updated to last editor | No |
| `autoNumber` | Auto Number | Auto-incrementing integer | No |
| `button` | Button | Click-to-trigger automation | Yes (`workflow`; see Button section) |
### Date vs DateTime
There is no separate `dateTime` type. Use `date` and control the time component via `--options`:
```bash
# Date-only
--options '{"formatting":{"date":"YYYY-MM-DD","time":"None","timeZone":"UTC"}}'
# DateTime (with time)
--options '{"formatting":{"date":"YYYY-MM-DD","time":"HH:mm","timeZone":"America/New_York"}}'
```
| `time` value | Behavior |
|-------------|----------|
| `"None"` | Date only (no time picker) |
| `"HH:mm"` | 24-hour time |
| `"hh:mm A"` | 12-hour time with AM/PM |
---
## Type Aliases
When using `field create --type <alias>` or `table create --fields` compact format:
| Alias | Full type |
|-------|-----------|
| `text`, `str` | `singleLineText` |
| `long` | `longText` |
| `num`, `int` | `number` |
| `check`, `bool` | `checkbox` |
| `sel`, `select` | `singleSelect` |
| `multi`, `tags` | `multipleSelect` |
| `date` | `date` |
| `rate`, `star` | `rating` |
| `user`, `person` | `user` |
| `file`, `attach` | `attachment` |
| `link`, `rel` | `link` |
| `rollup` | `rollup` |
| `condRollup` | `conditionalRollup` |
| `formula` | `formula` |
| `auto` | `autoNumber` |
| `created` | `createdTime` |
| `modified` | `lastModifiedTime` |
| `createdBy` | `createdBy` |
| `modifiedBy` | `lastModifiedBy` |
| `button` | `button` |
**`table create --fields` compact format** also supports inline select options:
| Format | Meaning |
|--------|---------|
| `"Name:text"` | Single line text field |
| `"Amount:num"` | Number field |
| `"Status:sel:Todo,Doing,Done"` | Single select with choices |
| `"Tags:mul:Bug,Feature,UI"` | Multi-select with choices |
| `"Done:check"` | Checkbox |
| `"Score:rate"` | Rating (defaults) |
---
## Option Shapes by Type
### SingleSelect / MultipleSelect
```bash
# Simple — strings only, colors auto-assigned
inkreach-teable field create --table-id tblXXX --name "Status" --type singleSelect \
--options '{"choices":["Todo","Doing","Done"]}'
# With explicit colors
inkreach-teable field create --table-id tblXXX --name "Priority" --type singleSelect \
--options '{"choices":[{"name":"High","color":"redBright"},{"name":"Low","color":"gray"}]}'
# With default value
inkreach-teable field create --table-id tblXXX --name "Status" --type singleSelect \
--options '{"choices":["Todo","Done"],"defaultValue":"Todo"}'
```
| Option key | Type | Required | Notes |
|-----------|------|----------|-------|
| `choices` | `string[]` or `{name,color}[]` | yes | Colors auto-assigned from palette if omitted |
| `defaultValue` | `string` (singleSelect) / `string[]` (multipleSelect) | no | Must match a choice name |
| `preventAutoNewOptions` | `boolean` | no | Disable adding choices from cell editing |
**Available colors**: `blue`, `cyan`, `gray`, `green`, `orange`, `pink`, `purple`, `red`, `teal`, `yellow` — each with `Light2`, `Light1`, `Bright`, (base), `Dark1` variants. When colors are auto-assigned, the system cycles through a built-in palette.
### Number
```bash
# Integer (default — no options needed)
inkreach-teable field create --table-id tblXXX --name "Count" --type number
# Decimal with 2 places
inkreach-teable field create --table-id tblXXX --name "Price" --type number \
--options '{"formatting":{"precision":2}}'
# With default value
inkreach-teable field create --table-id tblXXX --name "Quantity" --type number \
--options '{"defaultValue":1}'
```
| Option key | Type | Default | Notes |
|-----------|------|---------|-------|
| `formatting.precision` | `number` | `0` | Decimal places (0 = integer) |
| `formatting.type` | `"decimal"` / `"integer"` | `"decimal"` | Number type |
| `showAs` | object | — | Display as percentage, currency, etc. |
| `defaultValue` | `number` | — | Numeric default for new records |
### Rating
```bash
# Quick: default (max=5, star icon, yellow)
inkreach-teable field create --table-id tblXXX --name "Score" --type rating
# Custom
inkreach-teable field create --table-id tblXXX --name "Priority" --type rating \
--options '{"max":3,"icon":"flame","color":"redBright"}'
# Max 10
inkreach-teable field create --table-id tblXXX --name "NPS" --type rating \
--options '{"max":10,"icon":"star","color":"tealBright"}'
```
| Option key | Type | Default | Notes |
|-----------|------|---------|-------|
| `max` | `number` | `5` | 1–10 |
| `icon` | `"star"` / `"moon"` / `"sun"` / `"zap"` / `"flame"` / `"heart"` / `"apple"` / `"thumb-up"` | `"star"` | Icon to display |
| `color` | `"yellowBright"` / `"redBright"` / `"tealBright"` | `"yellowBright"` | Icon fill color |
### Date
```bash
# Date only (default)
inkreach-teable field create --table-id tblXXX --name "Due" --type date
# Date with time
inkreach-teable field create --table-id tblXXX --name "Appointment" --type date \
--options '{"formatting":{"date":"YYYY-MM-DD","time":"HH:mm","timeZone":"Asia/Shanghai"}}'
# Auto-fill with "now" for new records
inkreach-teable field create --table-id tblXXX --name "Created At" --type date \
--options '{"defaultValue":"now"}'
```
| Option key | Type | Default | Notes |
|-----------|------|---------|-------|
| `formatting.date` | `string` | `"YYYY-MM-DD"` | Preset: `M/D/YYYY`, `D/M/YYYY`, `YYYY/MM/DD`, `YYYY-MM-DD`, `YYYY-MM`, `MM-DD`, `YYYY`, `MM`, `DD` |
| `formatting.time` | `"None"` / `"HH:mm"` / `"hh:mm A"` | `"None"` | Time picker if not `"None"` |
| `formatting.timeZone` | `string` | System local | IANA timezone name |
| `defaultValue` | `"now"` | — | Auto-fills current time on new records |
### Link
```bash
# Using table name (preferred)
inkreach-teable field create --table-id tblXXX --name "Project" --type link \
--options '{"foreignTableName":"Projects","relationship":"manyOne"}'
# Using table ID
inkreach-teable field create --table-id tblXXX --name "Project" --type link \
--options '{"foreignTableId":"tblXXX","relationship":"manyOne"}'
# Many-to-many
inkreach-teable field create --table-id tblXXX --name "Tags" --type link \
--options '{"foreignTableName":"Tags","relationship":"manyMany"}'
```
| Option key | Type | Required | Notes |
|-----------|------|----------|-------|
| `foreignTableName` | `string` | * | Resolved to `foreignTableId` by CLI; preferred over ID |
| `foreignTableId` | `string` | * | Direct ID; use `foreignTableName` instead |
| `relationship` | `"oneOne"` / `"oneMany"` / `"manyOne"` / `"manyMany"` | yes | Direction is from THIS table to the FOREIGN table |
| `isOneWay` | `boolean` | no | No symmetric field in foreign table |
| `filterByViewId` | `string` | no | Limit selectable records to a view |
| `visibleFieldIds` | `string[]` | no | Fields shown in linked record picker |
**Relationship quick reference**:
| Relationship | Meaning | Example |
|-------------|---------|---------|
| `manyOne` | Many records here → one record there | Tasks → Project |
| `oneMany` | One record here → many records there | Project → Tasks |
| `oneOne` | One-to-one | User ↔ Profile |
| `manyMany` | Many-to-many | Tasks ↔ Tags |
> **Name resolution**: use `foreignTableName` instead of `foreignTableId` — the CLI resolves it. Preferred over manual ID lookups.
### Formula
```bash
inkreach-teable field create --table-id tblXXX --name "Full Name" --type formula \
--options '{"expression":"{fldFirstName} & \" \" & {fldLastName}"}'
inkreach-teable field create --table-id tblXXX --name "Total" --type formula \
--options '{"expression":"{fldQuantity} * {fldPrice}"}'
```
| Option key | Type | Required | Notes |
|-----------|------|----------|-------|
| `expression` | `string` | yes | Formula referencing fields by ID: `{fldXXX}` |
| `timeZone` | `string` | no | For date functions |
| `formatting` | object | no | Formatting for result (number/date) |
**Field references**: use `{fldXXX}` with field IDs. Get IDs from `field list <tableId>`.
### Rollup
```bash
# Requires an existing link field + a target field in the linked table
inkreach-teable field create --table-id tblXXX --name "Total Hours" --type rollup \
--options '{"expression":"sum({values})"}' \
--lookup-options '{"linkFieldName":"Tasks","lookupFieldName":"Hours","foreignTableId":"tblTasks"}'
```
| Required | Meaning |
|----------|---------|
| `--options.expression` | Aggregation: `sum({values})`, `count({values})`, `average({values})`, `max({values})`, `min({values})`, `concatenate({values})`, `unique({values})`, `countDistinct({values})` |
| `--lookup-options.linkFieldName` | The link field in THIS table pointing to the foreign table |
| `--lookup-options.lookupFieldName` | The field in the FOREIGN table to aggregate |
| `--lookup-options.foreignTableId` | The foreign table (typically same as the link field's target) |
**Name resolution**: `linkFieldName` and `lookupFieldName` accept field display names — CLI resolves them to IDs.
### Lookup
```bash
# Mirror a field from a linked record
inkreach-teable field create --table-id tblXXX --name "Project Name" --type singleLineText \
--is-lookup \
--lookup-options '{"linkFieldName":"Project","lookupFieldName":"Name","foreignTableId":"tblProjects"}'
```
Lookup uses the base type of the field being mirrored (here copied as `singleLineText`), with `--is-lookup` and `--lookup-options`.
### ConditionalRollup
```bash
# Aggregate from ANY table without a link field
inkreach-teable field create --table-id tblXXX --name "Active Tasks" --type conditionalRollup \
--options '{"expression":"sum({values})"}' \
--lookup-options '{"lookupFieldName":"Estimate","foreignTableId":"tblTasks"}' \
--conditions '{"conjunction":"and","filterSet":[{"fieldId":"fldStatus","operator":"isNot","value":"Done"}]}'
```
### Button
```bash
# Create a button field bound to a workflow
inkreach-teable field create --table-id tblXXX --name "Approve" --type button \
--options '{"label":"Approve","color":"blue","workflow":{"id":"wflXXX","name":"Approval Flow"}}'
# With click limit
inkreach-teable field create --table-id tblXXX --name "Run Once" --type button \
--options '{"label":"Run","color":"teal","maxCount":1,"workflow":{"id":"wflXXX"}}'
# With confirmation dialog
inkreach-teable field create --table-id tblXXX --name "Delete" --type button \
--options '{"label":"Delete","color":"red","workflow":{"id":"wflXXX"},"confirm":{"title":"Are you sure?","description":"This cannot be undone","confirmText":"Yes, delete"}}'
```
| Option key | Type | Required | Default | Notes |
|-----------|------|----------|---------|-------|
| `label` | `string` | no | `"Button"` | Display text on the button |
| `color` | Color enum | no | `"teal"` | Button color |
| `workflow.id` | `string` | **yes** | — | Workflow ID triggered on click |
| `workflow.name` | `string` | no | — | Workflow display name (informational) |
| `workflow.isActive` | `boolean` | no | — | Captured at creation time; reflects whether workflow was active |
| `maxCount` | `number` | no | — | Max clicks per record; empty = unlimited |
| `resetCount` | `boolean` | no | — | Reset click count behavior |
| `confirm.title` | `string` | no | — | Confirmation dialog title |
| `confirm.description` | `string` | no | — | Confirmation dialog body |
| `confirm.confirmText` | `string` | no | — | Confirm button label |
**Available button colors**: `blueLight2`, `blueLight1`, `blueBright`, `blue`, `blueDark1`, `cyan`, `cyanBright`, `cyanDark1`, `gray`, `grayBright`, `grayDark1`, `green`, `greenBright`, `greenDark1`, `orange`, `orangeBright`, `orangeDark1`, `pink`, `pinkBright`, `pinkDark1`, `purple`, `purpleBright`, `purpleDark1`, `red`, `redBright`, `redDark1`, `teal`, `tealBright`, `tealDark1`, `yellow`, `yellowBright`, `yellowDark1`.
**Key behaviors**:
1. **Workflow binding**: button clicks fire the `buttonClick` trigger of the bound workflow. The workflow MUST be active (`inkreach-teable workflow activate wflXXX`) for the button to be clickable.
2. **`workflow.isActive` snapshot**: captured when the field is created. If the workflow is deactivated later, the field's `isActive` goes stale — the button won't be clickable. Resolve by updating the field options or re-activating the workflow.
3. **Billing**: in this self-hosted CE fork, button fields are **always available** — billing gate is lifted.
4. **Cell value**: read-only `{"count": N}` tracking click count. Automatically incremented by the backend.
5. **Click limit**: when `maxCount` is set and count reaches it, button becomes disabled.
---
## Smart Inference
### Type inference from `--name`
When `--type` is omitted, the CLI infers the type from the field name:
| Name pattern | Inferred type |
|-------------|---------------|
| `status`, `priority`, `category`, `type`, `stage`, `level` | `singleSelect` |
| `tag`, `label` | `multipleSelect` |
| `description`, `notes`, `comment`, `content`, `body` | `longText` |
| `count`, `amount`, `price`, `quantity`, `total`, `number`, `age` | `number` |
| `rating`, `score`, `stars` | `rating` |
| `is*`, `has*`, `active`, `enabled`, `done`, `completed` | `checkbox` |
| `assignee`, `owner`, `user` | `user` |
| `attachment`, `file`, `image`, `photo`, `document` | `attachment` |
| `email`, `url` | `singleLineText` |
| `created time`, `created` (without "by") | `createdTime` |
| `last modified`, `updated` (without "by") | `lastModifiedTime` |
| `created by`, `author` | `createdBy` |
| `modified by`, `updated by` | `lastModifiedBy` |
### Options inference
| If you pass | Type inferred | Notes |
|-------------|--------------|-------|
| `--options '{"choices":["A","B"]}'` (no `--type`) | `singleSelect` | Colors auto-assigned |
| `--options '{"max":5}'` (no `--type`) | `rating` | Icon `star`, color `yellowBright` defaults applied |
| `--options '{"foreignTableName":"X"}'` (no `--type`) | `link` | |
| `--options '{"expression":"..."}'` (no `--type`) | `formula` | Unless combined with `foreignTableId` + conditions → `conditionalRollup` |
| `--options '{"workflow":{"id":"wflXXX"}}'` (no `--type`) | `button` | |
| No `--type`, no `--options` | Depot on `--name` pattern | Falls back to `singleLineText` if no match |
### Options defaults
| Type | Defaults applied when omitted |
|------|------------------------------|
| `rating` | `{"max":5,"icon":"star","color":"yellowBright"}` |
| `number` | `{"formatting":{"precision":0,"type":"decimal"}}` |
| `date` | `{"formatting":{"date":"YYYY-MM-DD","time":"None","timeZone":"<system>"}}` |
| `button` | `{"label":"Button","color":"teal"}` |
| `singleSelect` / `multipleSelect` | Choices auto-colored if no `color` in choice objects |
---
## Typecast Reference
`--typecast` converts human-readable values to internal IDs on `record create`/`record update`.
| Field type | Without typecast | With typecast |
|-----------|-----------------|---------------|
| `user` | `{"id":"usrXXX","title":"Alice"}` | `"Alice"` or `"alice@example.com"` |
| `link` (single) | `{"id":"recXXX"}` or `[{"id":"recXXX"}]` | `"Record Display Name"` |
| `link` (multi) | `[{"id":"recA"},{"id":"recB"}]` | `["Record A","Record B"]` |
**When to use**: always use `--typecast` when creating/updating records with user or link fields — it eliminates manual ID lookups.
---
## Field Type Selection Guide
| Data shape | Use | Not |
|-----------|-----|-----|
| Fixed options (status, priority) | `singleSelect` | `singleLineText` (no color/filter) |
| Multiple tags per record | `multipleSelect` | `singleSelect` (only one value) |
| Numbers with decimals | `number` with `precision` | `singleLineText` (can't calculate) |
| 1–10 ratings | `rating` | `number` (no star UI) |
| Yes/no flags | `checkbox` | `number` 0/1 (worse UX) |
| Dates/times | `date` with time formatting | `singleLineText` (can't sort/filter) |
| Reference another table | `link` | `singleSelect` (hardcoded list; breaks when added) |
| Auto-increment IDs | `autoNumber` | Manual numbering |
| Trigger an action | `button` | Any other type |
@@ -0,0 +1,236 @@
# Trigger Types Reference
All 8 triggers of the inkreach-teable automation engine. **Use these exact config keys** — they differ from official teable and from the (stale) `docs/automation/triggers.md`.
Every trigger node has this shape (stored in `workflow_node`):
```json
{
"id": "node-1",
"type": "trigger",
"category": "<one of the 8 types below>",
"config": { ... },
"name": null
}
```
> You don't build this JSON by hand — use `inkreach-teable trigger set <wfId> --type <T> ...` which constructs it correctly.
---
## 1. recordCreated — When a record is created
```bash
inkreach-teable trigger set <wfId> --type recordCreated --table-id tblXXX \
[--conditions '<filter json>'] \
[--batch '{"enabled":true,"batchWindow":2000,"batchSize":100}']
```
**Config:**
```json
{
"tableId": "tblXXX",
"conditions": null,
"batch": { "enabled": false, "batchWindow": 2000, "batchSize": 100 }
}
```
**Output variables** (available to downstream actions):
- `record.id`, `record.fields.<fieldId>`
- `user` (who created)
- `tableId`
---
## 2. recordUpdated — When a record is updated
```bash
inkreach-teable trigger set <wfId> --type recordUpdated --table-id tblXXX \
--field-ids '["fldXXX"]' \
[--batch '{...}']
```
**Config:**
```json
{
"tableId": "tblXXX",
"fieldIds": ["fldXXX"],
"batch": { ... }
}
```
- `fieldIds: []` (empty) = watch **all** fields (including future-created ones).
- ⚠️ **Key is `fieldIds`, NOT `watchFieldIds`.**
- ⚠️ recordUpdated **ignores `conditions`** — use `recordCondition` if you need filtering.
**Output:** `record.id`, `record.fields.*`, `record.oldFields.*` (pre-update values), `user`, `tableId`, `changedFieldIds`.
---
## 3. recordCondition — When a record matches conditions
```bash
inkreach-teable trigger set <wfId> --type recordCondition --table-id tblXXX \
--conditions '{"conjunction":"and","filterSet":[{"fieldId":"fldStatus","operator":"is","value":"Done"}]}'
```
**Config:**
```json
{
"tableId": "tblXXX",
"conditions": { "conjunction": "and", "filterSet": [ ... ] },
"batch": { ... }
}
```
- ⚠️ **Category is `recordCondition`, NOT `recordMatchesConditions`.**
- `conditions` is **required**.
- Fires on create AND update when the record transitions to matching.
**conditions structure** (same as view filters):
```json
{
"conjunction": "and" | "or",
"filterSet": [
{ "fieldId": "fldXXX", "operator": "is", "value": "Done" },
{ "fieldId": "fldNum", "operator": "isGreater", "value": 100 }
]
}
```
Common operators: `is`, `isNot`, `isEmpty`, `isNotEmpty`, `isAnyOf`, `isNoneOf`, `contains`, `isGreater`, `isLess`, `isAfter`, `isBefore`.
**Output:** `record.id`, `record.fields.*`, `record.oldFields.*`, `user`, `tableId`.
---
## 4. recordCreatedOrUpdated — When a record is created or updated
```bash
inkreach-teable trigger set <wfId> --type recordCreatedOrUpdated --table-id tblXXX \
[--conditions '<filter json>'] \
[--batch '{...}']
```
**Config:**
```json
{ "tableId": "tblXXX", "conditions": null, "batch": { ... } }
```
- Ignores `fieldIds` (watches all fields).
- `conditions` optional.
**Output:** same as recordCreated + `record.oldFields.*` on updates.
---
## 5. formSubmitted — When a form is submitted
```bash
inkreach-teable trigger set <wfId> --type formSubmitted --table-id tblXXX --form-id viwXXX
```
**Config:**
```json
{ "tableId": "tblXXX", "formId": "viwXXX" }
```
- `formId` is the **form view's id** (a view of type `form`).
**Output:** `record.id`, `record.fields.*`, `user`, `tableId`, `formId`.
---
## 6. scheduledTime — On a schedule
```bash
inkreach-teable trigger set <wfId> --type scheduledTime \
--schedule '{"timing":{"type":"minutes","interval":5}}'
```
**Config (only these fields are honored):**
```json
{ "timing": { "type": "minutes", "interval": 5 } }
```
| `timing.type` | `interval` range | Meaning |
|---------------|------------------|---------|
| `minutes` | 1–60 | Every N minutes |
| `hours` | 1–24 | Every N hours |
| `days` | 1–N | Every N days |
| `months` | 1–N | Every N months (uses a fixed day) |
⚠️ **Backend limitations (known gotchas):**
- `triggerTime`, `weekdays`, `daysOfMonth`, `starting`, `ending`, `tz` are **all IGNORED**.
- `weeks` type is hardcoded to every Sunday (ignores `weekdays`).
- `OneTime` type is unsupported → falls back to every 60s.
- The scheduler is in-process `setInterval`; **multi-instance deployments fire duplicates**.
If you need precise scheduling, set `interval` to a friendly value. Do NOT pass the extra fields — they give false confidence.
**Output:** none useful (no record context); typically used with a `getRecords` + `script` action chain.
---
## 7. buttonClick — When a button field is clicked
```bash
inkreach-teable trigger set <wfId> --type buttonClick --table-id tblXXX --field-ids '["fldGo"]'
```
**Config:**
```json
{ "tableId": "tblXXX", "fieldIds": ["fldGo"] }
```
- The field **must be a Button-type field** whose `options.workflowId` points to this workflow.
- Create it: `inkreach-teable field create --table-id tblXXX --name "Go" --type button --options '{"workflowId":"wflXXX","label":"Go"}'`
**Triggering:** the click comes from `POST /api/table/{tableId}/record/{recordId}/{fieldId}/button-click` (the web UI calls this; CLI tests via `fire button-click`).
**Output:** `record.id`, `record.fields.*`, `fieldId`, `user`, `runId`, `name`, `tableId`, `workflowId`.
⚠️ The Button field's `options` captures a stale `isActive` snapshot at creation. After deactivating, the API validates the DB `isActive` instead — so a deactivated workflow returns 400 "not active" on click. Re-`activate` to fix.
---
## 8. webhook — On external HTTP POST
```bash
inkreach-teable trigger set <wfId> --type webhook
```
**Config:**
```json
{ "authorization": { "type": "none" } }
```
⚠️ **`authorization` is UI-only — the backend does NOT validate it.** Anyone with the URL can trigger it. Don't rely on it for security.
**Real webhook URL:**
```
POST http://<endpoint>/api/webhook/{baseId}/{workflowId}
```
- ⚠️ The route is `/api/webhook/{baseId}/{workflowId}` — **no** `/base/` or `/workflow/` segments (the frontend UI shows a different, WRONG URL).
- Test: `inkreach-teable fire webhook <baseId> <wfId> --body '{"any":"json"}'`
**Output:** `headers` (request headers), `body` (parsed JSON body), `method`, `baseId`, `webhookId`.
---
## Batch Processing
For `recordCreated`/`recordUpdated`/`recordCondition`/`recordCreatedOrUpdated`, add `--batch`:
```json
{ "enabled": true, "batchWindow": 2000, "batchSize": 100 }
```
- Events within `batchWindow` ms (up to `batchSize`) are merged into ONE run.
- The script receives `triggerInput.records` (array) instead of `triggerInput.record` (single).
- Use `Promise.allSettled` in scripts to handle the array concurrently.
```bash
inkreach-teable trigger set wflXXX --type recordCreated --table-id tblXXX \
--batch '{"enabled":true,"batchWindow":2000,"batchSize":100}'
inkreach-teable action add wflXXX --type script --code '
const records = triggerInput.records || [triggerInput.record];
const results = await Promise.allSettled(records.map(async (r) => {
// process each record
}));
return { processed: results.length };
'
```
@@ -0,0 +1,191 @@
# Automation Guide
Automations (workflows) are event-driven: a **trigger** fires, then **actions** run in topological order. inkreach-teable's automation engine is self-built — use the `inkreach-teable` CLI (not official `teable`).
> **Required reading before configuring triggers**: [trigger-types.md](../api-reference/trigger-types.md)
> **Required reading before writing script actions**: [action-types.md](../api-reference/action-types.md)
## Mental Model
```
[Trigger] → [Action 1] → [Action 2] → ... → [triggerEnd]
```
- A workflow has **exactly one trigger node** (type `trigger`) and N action nodes (type `action`).
- Nodes connect via **edges** (linear chain; each node has at most one upstream — no parallel branches).
- **Activating** freezes a snapshot; execution runs against the snapshot, not the draft.
- `hasDraft: true` = unactivated changes exist; re-`activate` to apply.
## Available Commands
| Command | Purpose |
|---------|---------|
| `workflow list` | List all workflows in the base |
| `workflow get <id>` | Detail (nodes + edges + config) |
| `workflow create --name <n>` | Create (auto-adds a recordCreated trigger) |
| `workflow update <id> [--name] [--description]` | Rename |
| `workflow delete <id>` | Soft-delete |
| `workflow activate <id>` | Snapshot + register trigger + create auto-token |
| `workflow deactivate <id>` | Unregister trigger |
| `workflow test <id>` | Mock-input test run (no trigger, no queue) |
| `trigger set <id> --type <T> ...` | Set trigger type + config |
| `action add <id> --type <T> ...` | Add an action node |
| `action update <id> <nodeId> ...` | Update an action node |
| `action delete <id> <nodeId>` | Remove an action node |
| `secret set/list/delete` | Manage script secrets |
| `runs list/get/rerun` | Execution history |
| `fire button-click/webhook` | Manually trigger for testing |
## Trigger Selection
| User scenario | `--type` | Key options |
|--------------|----------|-------------|
| React to new records | `recordCreated` | `--table-id` |
| React to record changes | `recordUpdated` | `--table-id`, `--field-ids` (empty = all) |
| React when a condition matches | `recordCondition` | `--table-id`, `--conditions` (required) |
| React to create OR update | `recordCreatedOrUpdated` | `--table-id`, `--conditions` (optional) |
| Form submission | `formSubmitted` | `--table-id`, `--form-id` |
| Schedule (only type+interval work) | `scheduledTime` | `--schedule '{"timing":{"type":"minutes","interval":5}}'` |
| Button field click | `buttonClick` | `--table-id`, `--field-ids` |
| External HTTP POST | `webhook` | (none; auth backend-ignored) |
**Batch processing** (record triggers only): add `--batch '{"enabled":true,"batchWindow":2000,"batchSize":100}'` to merge events within a window into a single run. The script receives an array of records.
Full config details: [trigger-types.md](../api-reference/trigger-types.md).
## Action Selection
| Goal | `--type` | Key options |
|------|----------|-------------|
| Run JS code (transform, call APIs, compute) | `script` | `--code '<js>'` or `--code-file <path>`, `--timeout <ms>` |
| Send email | `sendEmail` | `--to`, `--subject`, `--body <html>` |
| Call external HTTP | `httpRequest` | `--url`, `--method`, `--headers <json>`, `--req-body <json>` |
| Create records | `createRecord` | `--target-table <id>`, `--records <json>` |
| Update records | `updateRecord` | `--target-table <id>`, `--records <json>` |
| Query records | `getRecords` | `--target-table <id>`, `--query <json>` |
Full config details: [action-types.md](../api-reference/action-types.md).
## Creation Workflow
```bash
# 1. Gather IDs
inkreach-teable table get tblXXX # confirm table
inkreach-teable field list tblXXX # find field ids for triggers
# 2. Create workflow
inkreach-teable workflow create --name "My automation"
# → returns wflXXX
# 3. Set trigger
inkreach-teable trigger set wflXXX --type recordUpdated \
--table-id tblXXX --field-ids '["fldStatus"]'
# 4. Add action(s)
inkreach-teable action add wflXXX --type script \
--code 'return { status: triggerInput.record.fields.Status }'
# 5. (Optional) add secrets the script needs
inkreach-teable secret set wflXXX --name API_KEY --value sk_xxx
# 6. Activate
inkreach-teable workflow activate wflXXX
# → returns snapshotId
# 7. Test
inkreach-teable fire button-click tblXXX recXXX fldXXX # for buttonClick
# or just perform the triggering action (create/update a record, POST webhook...)
inkreach-teable runs list wflXXX
```
## Editing an Existing Workflow
```bash
# Change trigger config
inkreach-teable trigger set wflXXX --type buttonClick --table-id tblXXX --field-ids '["fldNew"]'
# Add another action
inkreach-teable action add wflXXX --type sendEmail --to a@b.com --subject Done --body '<p>ok</p>'
# Remove an action
inkreach-teable action delete wflXXX node-12345
# RE-ACTIVATE to snapshot the changes
inkreach-teable workflow activate wflXXX
```
`trigger set` / `action add/update/delete` internally do GET→modify→PUT the whole graph, so they're safe. But you **must re-activate** for changes to take effect at runtime.
## Secrets
Scripts often need API keys. Never hardcode — use secrets.
```bash
inkreach-teable secret set wflXXX --name OPENAI_KEY --value sk-xxx --description "for AI calls"
inkreach-teable secret list wflXXX # names only — value never returned
inkreach-teable secret update wflXXX secXXX --value sk-new
inkreach-teable secret delete wflXXX secXXX
```
In a script action, read via `process.env.OPENAI_KEY`. Secrets are AES-encrypted at rest.
## Testing / Triggering
Most triggers fire from real events (record create/update, form submit, schedule). Two triggers are manually fireable for testing:
```bash
# Button field click
inkreach-teable fire button-click <tableId> <recordId> <fieldId>
# Webhook (no auth needed)
inkreach-teable fire webhook <baseId> <workflowId> --body '{"event":"test"}'
```
For other triggers, test by performing the real action (create/update a record, submit the form). Then inspect:
```bash
inkreach-teable runs list wflXXX --status failed
inkreach-teable runs get wflXXX <executionId>
# rerun a failed execution
inkreach-teable runs rerun wflXXX <executionId>
```
## Variable References (cross-node data)
An action can reference upstream nodes' output in string fields via `{{nodeIndex.path}}`:
- `{{1.record.id}}` — trigger record's id (trigger = node 1)
- `{{1.record.fields.Status}}` — trigger record's Status field
- `{{2.records}}` — output of the first action (node 2)
Node index follows **topological execution order** (trigger is always 1). Script actions can also read the merged upstream output directly via `triggerInput`.
## Common Patterns
### Notify on status change
```bash
inkreach-teable workflow create --name "Status→email"
inkreach-teable trigger set wflXXX --type recordUpdated --table-id tblXXX --field-ids '["fldStatus"]'
inkreach-teable action add wflXXX --type sendEmail \
--to ops@x.com --subject 'Status changed' \
--body '{{1.record.fields.Status}}'
inkreach-teable workflow activate wflXXX
```
### Daily digest
```bash
inkreach-teable trigger set wflXXX --type scheduledTime \
--schedule '{"timing":{"type":"hours","interval":24}}'
```
### Button → script → update another table
```bash
inkreach-teable trigger set wflXXX --type buttonClick --table-id tblA --field-ids '["fldGo"]'
inkreach-teable action add wflXXX --type script --code '
const r = triggerInput.record;
// call back into the instance API using the auto-injected token
const res = await fetch(triggerInput._apiBaseUrl + "/api/table/tblB/record", {
method: "POST",
headers: { Authorization: "Bearer " + triggerInput._accessToken, "Content-Type": "application/json" },
body: JSON.stringify({ fields: { Source: r.id } })
});
return { created: await res.json() };
'
```
@@ -0,0 +1,182 @@
# CLI Reference
Complete command reference for `inkreach-teable`. Global options apply to all commands.
## Global Options
| Option | Description |
|--------|-------------|
| `--endpoint <url>` | Backend URL (default `http://localhost:3000`) |
| `--token <pat>` | Personal Access Token |
| `--base-id <id>` | Default base id |
| `--json` | Output raw JSON (for agent parsing) |
| `--debug` | Print HTTP requests/responses to stderr |
| `-v, --version` | Show version |
Config precedence: global flag > env (`INKREACH_TEABLE_ENDPOINT`/`INKREACH_TEABLE_TOKEN`/`INKREACH_TEABLE_BASE_ID`) > `~/.inkreach-teable/config.json`.
## auth
```bash
inkreach-teable auth status # verify token + show user
inkreach-teable auth login --endpoint <url> --token <PAT> [--base-id <id>]
inkreach-teable auth token list
inkreach-teable auth token create --name <n> [--scopes <csv>] [--base-ids <csv>] [--full-access] [--expires <date>]
inkreach-teable auth token delete <id>
```
- `auth token create` returns the full token **only once**. Default scopes: `automation|read,automation|create,automation|update,automation|delete,table|read,record|read,record|create,record|update`.
## base / table / field / record / view
These resources use the official-teable-compatible API. Full guide: [tables-fields.md](tables-fields.md).
```bash
# base
inkreach-teable base list
inkreach-teable base get <baseId>
# table
inkreach-teable table list [--base-id <id>]
inkreach-teable table get <tableId>
inkreach-teable table create --name <n> [--base-id <id>] [--fields '<json>']
inkreach-teable table update <tableId> --name <n>
inkreach-teable table delete <tableId>
# field
inkreach-teable field list <tableId>
inkreach-teable field get <fieldId>
inkreach-teable field create --table-id <tid> --name <n> --type <t> [--options '<json>']
inkreach-teable field update <fieldId> [--name <n>] [--options '<json>']
inkreach-teable field delete <fieldId>
# record
inkreach-teable record get --table-id <tid> [--filter '<json>'] [--projection '<json>'] [--page-size <n>] [--page <n>] [--search <kw>]
inkreach-teable record get-one <recordId>
inkreach-teable record create --table-id <tid> --fields '<json>' # single
inkreach-teable record create --table-id <tid> --records '<json>' # batch
inkreach-teable record update --table-id <tid> --records '<json>'
inkreach-teable record delete --table-id <tid> --record-ids <csv>
# view
inkreach-teable view list <tableId>
inkreach-teable view get <viewId>
inkreach-teable view create --table-id <tid> --name <n> [--type <t>]
inkreach-teable view update <viewId> [--name <n>]
inkreach-teable view delete <viewId>
```
## workflow
```bash
inkreach-teable workflow list [--base-id <id>]
inkreach-teable workflow get <workflowId> [--base-id <id>]
inkreach-teable workflow create --name <n> [--description <d>] [--base-id <id>]
inkreach-teable workflow update <workflowId> [--name <n>] [--description <d>] [--base-id <id>]
inkreach-teable workflow delete <workflowId> [--base-id <id>]
inkreach-teable workflow activate <workflowId> [--base-id <id>]
inkreach-teable workflow deactivate <workflowId> [--base-id <id>]
inkreach-teable workflow test <workflowId> [--base-id <id>]
```
Full automation flow: [automation-guide.md](automation-guide.md).
## trigger
```bash
inkreach-teable trigger set <workflowId> --type <T> [options] [--base-id <id>]
```
| `--type` | Required options |
|----------|------------------|
| `recordCreated` | `--table-id` |
| `recordUpdated` | `--table-id`, `--field-ids '<json>'` |
| `recordCondition` | `--table-id`, `--conditions '<json>'` |
| `recordCreatedOrUpdated` | `--table-id` |
| `formSubmitted` | `--table-id`, `--form-id <id>` |
| `scheduledTime` | `--schedule '{"timing":{"type":"minutes","interval":5}}'` |
| `buttonClick` | `--table-id`, `--field-ids '<json>'` |
| `webhook` | (none) |
Common optional: `--batch '<json>'` (record triggers only).
Details: [trigger-types.md](../api-reference/trigger-types.md).
## action
```bash
inkreach-teable action add <workflowId> --type <T> [options] [--base-id <id>]
inkreach-teable action update <workflowId> <nodeId> [--config '<json>'] [--code <js>] [--code-file <path>] [--name <n>]
inkreach-teable action delete <workflowId> <nodeId> [--base-id <id>]
```
Per-type options:
| `--type` | Options |
|----------|---------|
| `script` | `--code <js>` / `--code-file <path>`, `--timeout <ms>` |
| `sendEmail` | `--to <email>`, `--subject <s>`, `--body <html>` |
| `httpRequest` | `--url <u>`, `--method <m>`, `--headers <json>`, `--req-body <json>` |
| `createRecord` | `--target-table <id>`, `--records <json>` |
| `updateRecord` | `--target-table <id>`, `--records <json>` |
| `getRecords` | `--target-table <id>`, `--query <json>` |
Details: [action-types.md](../api-reference/action-types.md).
## secret
```bash
inkreach-teable secret list <workflowId> [--base-id <id>]
inkreach-teable secret set <workflowId> --name <n> --value <v> [--description <d>] [--base-id <id>]
inkreach-teable secret update <workflowId> <secretId> [--name <n>] [--value <v>] [--description <d>] [--base-id <id>]
inkreach-teable secret delete <workflowId> <secretId> [--base-id <id>]
```
Values are write-only — `list` never returns them. Scripts read via `process.env.<NAME>`.
## runs
```bash
inkreach-teable runs list <workflowId> [--status failed|succeeded|running] [--base-id <id>]
inkreach-teable runs get <workflowId> <executionId> [--base-id <id>]
inkreach-teable runs rerun <workflowId> <executionId> [--base-id <id>]
```
`rerun` only works on `failed` executions (backend validates).
## fire
```bash
inkreach-teable fire button-click <tableId> <recordId> <fieldId>
inkreach-teable fire webhook <baseId> <workflowId> [--body '<json>'] [--method <m>]
```
Manually trigger for testing. Webhook needs no auth (backend is `@Public`).
## sql
```bash
inkreach-teable sql '<SELECT ...>' [--base-id <id>]
```
Read-only. Must use `dbTableName`/`dbFieldName` (from `table get`/`field get`), double-quote identifiers, add `LIMIT 100`.
```bash
inkreach-teable sql 'SELECT "name" FROM "bseXXX"."tbl_abc" LIMIT 10'
```
## Output Modes
- **Human (default)**: tables / key-values, colored where supported.
- **`--json`**: compact raw JSON on stdout. Errors in JSON mode:
```json
{ "error": { "status": 404, "code": "NOT_FOUND", "message": "..." } }
```
(exit code 1)
## Error Troubleshooting
1. `inkreach-teable auth status` — token valid? endpoint reachable?
2. Verify IDs: `table get`, `field get`, `workflow get` (typo'd IDs → 404).
3. Check permissions: token scopes + base role. `403` → role too low.
4. `--debug` to see the raw HTTP request/response.
@@ -0,0 +1,121 @@
# Development & Deployment
How to build, restart, and debug this inkreach-teable instance. Covers the
dual-process architecture, known gotchas, and test infrastructure. For daily
operations (tables/automation), use the `inkreach-teable` CLI instead — this guide is
for changing the source code itself.
## Architecture
Two **separate production processes** managed by systemd user services. Do NOT
use `pnpm dev` as a long-running service — the embedded Next.js dev server
inside NestJS is unstable and will hang or OOM-crash.
| Service | Command | Port | Mode |
|---------|---------|------|------|
| `teable-backend.service` | `node dist/index.js` | `:3000` | `NODE_ENV=production`, `BACKEND_SKIP_NEXT_START=true` |
| `teable-frontend.service` | `next start -p 3002 -H 0.0.0.0` | `:3002` | `NODE_ENV=production` |
Users access `:3002`; the backend `:3000` serves the API only.
## Build & Restart
### Backend (no hot reload)
After editing anything under `apps/nestjs-backend/src/`:
```bash
cd apps/nestjs-backend && pnpm build # webpack, ~18s
systemctl --user restart teable-backend.service
```
`dist/` from `pnpm dev:swc` is hot-update fragments, NOT a valid artifact — always `nest build`.
### Frontend
```bash
pnpm -r -F './packages/**' build # build dependent packages first
pnpm --filter @teable/app build-fast
systemctl --user restart teable-frontend.service
```
Browser cache is aggressive — hard-refresh (Ctrl+Shift+R) after a rebuild.
### Recover from OOM crash
If `:3000` is unreachable but systemd says `active`:
```bash
systemctl --user stop teable-backend.service
pkill -9 -f 'dist/index.js'
systemctl --user start teable-backend.service
```
## Known Gotchas (from real debugging)
### Node v24 fetch drops the Authorization header
The global `fetch()` on Node v24 (undici) drops the `Authorization` request
header in certain cases (redirects, proxies). **Use `node:http`/`node:https`**
directly for any backend API call, or the `axios`-style clients. This is why
the `inkreach-teable` CLI uses `node:http` instead of `fetch`.
### Script sandbox has no `process` global
In automation script actions, `typeof process === 'undefined'`. Access secrets
via `process.env.NAME` (this is allowed — only `process.env` is exposed, not
`process` itself). Use `triggerInput._secrets`, `triggerInput._accessToken`,
`triggerInput._apiBaseUrl` for back-channel API calls. No `fs`, `eval`,
`Function`, or `child_process`.
### scheduledTime backend is a simplified implementation
The backend `scheduled-time.trigger.ts` only honors `timing.type` + `timing.interval`.
It ignores `triggerTime`, `weekdays`, `daysOfMonth`, `starting`, `ending`, `tz`.
`weeks` type is hardcoded to every Sunday. `OneTime` falls back to every 60s.
The scheduler is in-process `setInterval` — multi-instance deployments fire
duplicates. See [trigger-types.md § scheduledTime](../api-reference/trigger-types.md#6-scheduledtime--on-a-schedule).
### Webhook route mismatch
Frontend UI shows `/api/webhook/base/{baseId}/workflow/{workflowId}` but the
backend's real route is `/api/webhook/{baseId}/{workflowId}` (no `/base/` or
`/workflow/` segments). The CLI uses the real route. Webhook `authorization`
config is UI-only — backend does NOT validate it.
### Button field options capture stale isActive
A Button field's `options.workflow.isActive` is a snapshot from creation time.
After deactivating a workflow, the API validates the DB `is_active` instead —
a click returns 400 "not active". Re-`activate` the workflow, or recreate the
field. Always verify DB state, not field options.
### base_node pollution (sidebar perf)
Workflows create entries in the `base_node` table, which feeds the sidebar
navigation tree. Hundreds of test workflows → thousands of DOM nodes → jank.
The sidebar is now virtualized (`@tanstack/react-virtual`), but avoid
generating throwaway workflows; `deactivate` instead of `delete` to keep them
queryable.
### Key-name differences from official teable
The self-built automation uses `conditions` (not `filter`), `fieldIds` (not
`watchFieldIds`), category `recordCondition` (not `recordMatchesConditions`).
The `inkreach-teable` CLI handles these correctly; never use the official `teable`
CLI for automations. See [SKILL.md § Key Constraints](../SKILL.md#3-key-constraints).
## Test Infrastructure
Automation E2E tests live in `scripts/automation-test/`:
```bash
node scripts/automation-test/run-all.js # 59 cases: 8 triggers + 6 actions + cross-features
node scripts/stress-test/stress-test.js # concurrency stress test
```
Lib framework: `scripts/automation-test/lib.js`. Uses Node `http` (not fetch)
due to the auth-header bug. Test workflows are **deactivated, not deleted**
(retention policy) — the base has ~600 historical test workflows.
## Useful DB Queries
```bash
# Count workflows by active state
PGPASSWORD=teable psql -h localhost -U teable -d teable -c \
"SELECT is_active, count(*) FROM workflow WHERE deleted_time IS NULL GROUP BY is_active;"
# Find button fields and their bound workflows
PGPASSWORD=teable psql -h localhost -U teable -d teable -c \
"SELECT id, name, options->'workflow'->>'id' AS wf_id, options->'workflow'->>'isActive' AS active
FROM field WHERE type='button';"
# Recent executions
PGPASSWORD=teable psql -h localhost -U teable -d teable -c \
"SELECT id, workflow_id, status, started_at FROM workflow_execution
ORDER BY started_at DESC LIMIT 10;"
```
@@ -0,0 +1,87 @@
# Instance Access
How to reach the deployed inkreach-teable instance, acquire credentials, and
verify health. This file describes the **reference deployment** — adapt
endpoint/ports for your own deployment.
## Connection
| Service | URL / Port | Mode | Purpose |
|---------|-----------|------|---------|
| Backend API | `http://localhost:3000` | production (`NODE_ENV=production`) | REST API, automation engine, websocket |
| Frontend | `http://localhost:3002` | production | End-user web UI (Next.js) |
| PostgreSQL | `localhost:5432` | — | Database (db: `teable`, user: `teable`) |
| Redis | `localhost:6379` | — | BullMQ queue for automation runs, distributed lock |
- The frontend reads `SOCKET_PORT=3000` from `.env` to point its websocket at the backend. Don't let it fall back to `3001`.
- End users browse `:3002`. The backend `:3000` is the API only (`BACKEND_SKIP_NEXT_START=true` disables the embedded Next server).
## Health Checks
```bash
# Backend API up?
curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/api/auth/signin -X POST \
-H 'Content-Type: application/json' -d '{}' # expect 401 (endpoint exists, bad creds)
# Frontend up?
curl -s -o /dev/null -w '%{http_code}' http://localhost:3002 # expect 307 or 200
# Ports listening?
ss -tlnp | grep -E ':3000|:3002|:5432|:6379'
```
If `:3000` is unreachable but systemd shows `active`, the worker likely OOM-crashed while the `pnpm` parent stayed alive:
```bash
systemctl --user stop teable-backend.service
pkill -9 -f 'dist/index.js'
systemctl --user start teable-backend.service
```
## Acquiring a PAT (Personal Access Token)
The CLI authenticates with a PAT (`teable_{id}_{encryptedSign}`). Two ways to get one:
### Option A — via the CLI (needs a session cookie or existing token)
```bash
inkreach-teable auth token create --name my-cli \
--scopes automation|read,automation|create,automation|update,automation|delete,table|read,record|read,record|create,record|update \
--base-ids bseXXX --expires 2099-12-31
# → prints full token ONCE
```
Note: `auth token list/create` may require a **session** (browser login) rather than a PAT — if you get 403, use Option B.
### Option B — via the web UI (recommended for the first token)
1. Log in to `http://localhost:3002` as an admin.
2. User menu → **Settings** → **Personal Access Tokens**.
3. Create a token with scopes: `automation|*`, `table|*`, `record|*`, `field|*`, `view|*` as needed. Limit to the base if desired.
4. Copy the `teable_xxx_yyy` string.
### Configure the CLI
```bash
inkreach-teable auth login --endpoint http://localhost:3000 --token <PAT> --base-id bseXXX
inkreach-teable auth status # verify
```
## Reference Credentials (local dev)
> Never commit real secrets. The reference dev instance uses:
> - Admin: `hr_admin@xingchen.tech` / (ask maintainer) — for browser login only.
> - Base: `bseeZR7yiDNeMPCa4CN` (人事薪资管理) — the test base with ~600 automations.
> - Test table: `tblzPOXQvUruQosKNP5` (has button fields for automation testing).
For CLI use, **always prefer a PAT** over the admin password.
## Permissions
- PAT scope ∩ base role = effective permission. To manage automations you need `automation|read/create/update/delete` scopes **and** a role of editor+ on the base.
- Roles: owner (all), creator (all on the base), editor (read/create/update), commenter (read), viewer (read).
- Workflows auto-create an internal token on activation (name `workflow:{id}`, scope limited to table/record read+write) — used by action nodes, not for CLI auth.
## Troubleshooting Access
| Symptom | Fix |
|---------|-----|
| `401 Unauthorized` | Token wrong/expired → recreate PAT, `auth login` again |
| `403 Forbidden` | Token lacks scope, or base role too low → widen scopes / raise role |
| `404 Base not found` | Wrong base id, or endpoint points to a different instance → `auth status`, check `--base-id` |
| Connection refused | Backend down → health check, restart service (see [development.md](development.md)) |
@@ -0,0 +1,327 @@
# 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:
```bash
# 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
```bash
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. Allowed types and aliases:
| Shorthand | Full type | Shorthand | Full type |
|-----------|-----------|-----------|-----------|
| `text`, `str` | `singleLineText` | `long` | `longText` |
| `num`, `int` | `number` | `check`, `bool` | `checkbox` |
| `sel`, `select` | `singleSelect` | `multi`, `tags` | `multipleSelect` |
| `date` | `date` | `rate`, `star` | `rating` |
| `user`, `person` | `user` | `file`, `attach` | `attachment` |
| `link`, `rel` | `link` | `rollup` | `rollup` |
| `formula` | `formula` | `auto` | `autoNumber` |
| `created` | `createdTime` | `modified` | `lastModifiedTime` |
| `createdBy` | `createdBy` | `modifiedBy` | `lastModifiedBy` |
| `button` | `button` | `condRollup` | `conditionalRollup` |
**Compact shorthand** (in `--fields` only): `"name:type"` string format:
```bash
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
```bash
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
```bash
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
```bash
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` | yes | Full type name or alias (see [field-types.md](../api-reference/field-types.md)) |
| `--options` | depends on type | JSON object; see type-specific shapes below |
| `--description` | no | Optional |
**Type inference from name**: when `--type` is omitted, the CLI infers from the field name:
| Name pattern | Inferred type | Name pattern | Inferred type |
|-------------|---------------|-------------|---------------|
| `status`, `priority`, `stage`, `category`, `type`, `level` | `singleSelect` | `tag`, `label` | `multipleSelect` |
| `description`, `notes`, `comment`, `content`, `body` | `longText` | `count`, `amount`, `price`, `quantity`, `total`, `age` | `number` |
| `rating`, `score`, `stars` | `rating` | `is*`, `has*`, `active`, `enabled`, `done`, `completed` | `checkbox` |
| `assignee`, `owner`, `user` | `user` | `attachment`, `file`, `image`, `photo`, `document` | `attachment` |
| `email` | `singleLineText` | `url`, `link` (without table context) | `singleLineText` |
| `created time` / `created` | `createdTime` | `last modified` / `updated` | `lastModifiedTime` |
| `created by`, `author` | `createdBy` | `modified by`, `updated by` | `lastModifiedBy` |
**Options by type** (most common):
| Type | Required options | Shape |
|------|-----------------|-------|
| `singleSelect` / `multipleSelect` | `choices` | `{"choices":["A","B","C"]}` or `{"choices":[{"name":"A","color":"blue"}]}` — colors auto-assigned if omitted |
| `number` | none (defaults integer) | `{"formatting":{"precision":2}}` for decimal |
| `rating` | none (defaults max=5, star, yellow) | `{"max":5,"icon":"star","color":"yellowBright"}` |
| `date` | none | `{"formatting":{"date":"YYYY-MM-DD","time":"None","timeZone":"UTC"}}` |
| `link` | `foreignTableId` / `foreignTableName` | `{"foreignTableId":"tblXXX","relationship":"manyOne"}` |
| `formula` | `expression` | `{"expression":"{fieldId}+{otherFieldId}"}` |
| `rollup` | via `--lookup-options` | See [field-types.md](../api-reference/field-types.md) |
| `button` | `workflow` | `{"label":"Click","color":"blue","workflow":{"id":"wflXXX"}}` |
**Name resolution**: for `link` fields, use `foreignTableName` instead of `foreignTableId` — the CLI resolves it automatically:
```bash
inkreach-teable field create --table-id tblXXX --name "Project" --type link \
--options '{"foreignTableName":"Projects","relationship":"manyOne"}'
```
### Update & Delete
```bash
inkreach-teable field update <fieldId> [--name "<n>"] [--options '<json>']
inkreach-teable field delete <fieldId>
```
| Detail | Rule |
|--------|------|
| `--name` only | Convenience; for other props use `--options` |
| `--options` merge | Partial updates; omitted keys are preserved |
| `field delete` | Irreversible — all cell data in that field is lost |
---
## Records
### Get (read)
```bash
# 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 |
### Create
```bash
# 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:
```bash
# 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
```bash
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`:
```bash
inkreach-teable record update --table-id tblXXX \
--records '[{"id":"recXXX","fields":{}}]' \
--order '{"viewId":"viwXXX","anchorId":"recYYY","position":"before"}'
```
### Delete
```bash
inkreach-teable record delete --table-id <tid> --record-ids "recA,recB,recC"
```
Max 1000 IDs per call. Irreversible.
---
## Views
### List & Get
```bash
inkreach-teable view list <tableId>
inkreach-teable view get <viewId>
```
### Create
```bash
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)
```bash
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
```bash
inkreach-teable view delete <viewId>
```
Deleting a view does NOT delete the table or its data — only the view configuration.
---
## 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 |
@@ -0,0 +1,16 @@
{
"version": "0.1.0",
"organization": "inkreach",
"authors": ["inkreach"],
"tags": ["database", "teable", "no-code", "automation", "self-hosted"],
"abstract": "Operate a deployed inkreach-teable instance — configure tables, fields, records, views, and the self-built automation engine (8 triggers + 6 actions), plus development/deployment knowledge. Uses the inkreach-teable CLI which targets the project's own automation contract (not official teable CLI compatible).",
"engines": {
"inkreachTeableCli": ">=0.1"
},
"references": [
{
"title": "inkreach-teable",
"url": "https://github.com/yeuimu/teable"
}
]
}
@@ -0,0 +1,73 @@
#!/bin/bash
# Install/upgrade the inkreach-teable CLI from the local monorepo source.
# Run from this skill's directory. Builds the CLI package and links it globally.
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Resolve the project root: skill lives at <root>/.agents/skills/inkreach-teable/
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
CLI_DIR="$PROJECT_ROOT/packages/inkreach-cli"
echo "=== inkreach-teable CLI installation ==="
# Check Node.js
if ! command -v node &> /dev/null; then
echo "ERROR: Node.js is not installed. Install Node.js >= 18 first."
exit 1
fi
NODE_VERSION=$(node -v | sed 's/v//' | cut -d. -f1)
if [ "$NODE_VERSION" -lt 18 ]; then
echo "ERROR: Node.js >= 18 required, found v$(node -v)"
exit 1
fi
echo "✓ Node.js $(node -v)"
# Check CLI source exists
if [ ! -d "$CLI_DIR" ]; then
echo "ERROR: CLI source not found at $CLI_DIR"
echo " Ensure the inkreach-teable repo is checked out at $PROJECT_ROOT"
exit 1
fi
# Build
echo "Building CLI from $CLI_DIR ..."
cd "$CLI_DIR"
if command -v pnpm &> /dev/null; then
pnpm install --prefer-offline 2>/dev/null || true
pnpm build
else
echo "ERROR: pnpm not found. Install pnpm first: npm i -g pnpm"
exit 1
fi
# Link globally. Prefer pnpm; fall back to npm link; finally create a wrapper.
echo "Linking 'inkreach-teable' command globally ..."
LINKED=0
if command -v pnpm &> /dev/null && pnpm link --global 2>/dev/null; then
LINKED=1
elif command -v npm &> /dev/null && npm link 2>/dev/null; then
LINKED=1
fi
# Ensure a usable wrapper exists on PATH even if global bin isn't configured.
BIN_DIR="${PNPM_GLOBAL_BIN:-$(npm bin -g 2>/dev/null):-${HOME}/.local/bin}"
mkdir -p "$HOME/.local/bin"
cat > "$HOME/.local/bin/inkreach-teable" <<WRAPPER
#!/bin/bash
exec node "$CLI_DIR/dist/bin.js" "\$@"
WRAPPER
chmod +x "$HOME/.local/bin/inkreach-teable"
# Verify
echo ""
echo "=== Verification ==="
if command -v inkreach-teable &> /dev/null; then
echo "✓ inkreach-teable CLI installed ($(inkreach-teable --version 2>/dev/null || echo 'unknown'))"
echo ""
echo "Next: configure endpoint + token:"
echo " inkreach-teable auth login --endpoint http://localhost:3000 --token <PAT>"
echo "Or create a PAT first:"
echo " inkreach-teable auth token create --name my-cli"
else
echo "⚠ 'inkreach-teable' not on PATH. Add pnpm/npm global bin to your PATH, or run directly:"
echo " node $CLI_DIR/dist/bin.js <command>"
fi
+4 -4
View File
@@ -1,4 +1,4 @@
# 设计:inkreach-cli + inkreach-teable-ops skill
# 设计:inkreach-cli + inkreach-teable skill
> 日期:2026-08-07
> 分支:develop
@@ -24,7 +24,7 @@ inkreach-teable(本项目)自研了一套自动化(workflow)引擎,与
## 2. 目标
1. **`inkreach-teable` CLI**(`packages/inkreach-cli/`):Node.js 命令行工具,封装本项目全部 REST API,重点是自研自动化契约。
2. **`inkreach-teable-ops` skill**(`.agents/skills/inkreach-teable-ops/`):教 Agent 何时用哪个 CLI 命令、配置表格与自动化的正确姿势、本项目的部署/开发约定。
2. **`inkreach-teable` skill**(`.agents/skills/inkreach-teable/`):教 Agent 何时用哪个 CLI 命令、配置表格与自动化的正确姿势、本项目的部署/开发约定。
3. **端到端验证**:在本地实例真实跑通"建表 → 建自动化 → 配 buttonClick 触发器 → 激活 → 查执行历史"全链路。
## 3. CLI 设计(`packages/inkreach-cli/`)
@@ -120,7 +120,7 @@ SQL(只读):
7. **`--json` 模式**:所有命令支持,输出原始 JSON,Agent 可解析。默认是人类可读表格。
8. **错误处理**:非 2xx 响应解析 `{error:{code,message}}`,人类模式打印友好错误,`--json` 模式打印原始错误体。
## 4. Skill 设计(`.agents/skills/inkreach-teable-ops/`)
## 4. Skill 设计(`.agents/skills/inkreach-teable/`)
### 4.1 SKILL.md(入口)
@@ -138,7 +138,7 @@ YAML frontmatter(name + 极详细的 description 触发条件)+ 以下章节
### 4.2 目录结构
```
.agents/skills/inkreach-teable-ops/
.agents/skills/inkreach-teable/
├── SKILL.md 入口
├── metadata.json 版本元数据
├── scripts/